11import * as clc from "colorette" ;
2+ import * as semver from "semver" ;
23
34import * as functionsConfig from "../functionsConfig" ;
45import { Command } from "../command" ;
56import { FirebaseError } from "../error" ;
6- import { input } from "../prompt" ;
7+ import { input , confirm } from "../prompt" ;
78import { requirePermissions } from "../requirePermissions" ;
8- import { logBullet , logWarning , logSuccess } from "../utils" ;
9+ import { logBullet , logSuccess } from "../utils" ;
910import { requireConfig } from "../requireConfig" ;
1011import { ensureValidKey , ensureSecret } from "../functions/secrets" ;
11- import { addVersion , toSecretVersionResourceName } from "../gcp/secretManager" ;
12+ import { addVersion , listSecretVersions , toSecretVersionResourceName } from "../gcp/secretManager" ;
1213import { needProjectId } from "../projectUtils" ;
1314import { requireAuth } from "../requireAuth" ;
1415import { ensureApi } from "../gcp/secretManager" ;
@@ -29,6 +30,17 @@ const SECRET_MANAGER_PERMISSIONS = [
2930 "secretmanager.versions.add" ,
3031] ;
3132
33+ function maskConfigValues ( obj : any ) : any {
34+ if ( typeof obj === "object" && obj !== null && ! Array . isArray ( obj ) ) {
35+ const masked : Record < string , any > = { } ;
36+ for ( const [ key , value ] of Object . entries ( obj ) ) {
37+ masked [ key ] = maskConfigValues ( value ) ;
38+ }
39+ return masked ;
40+ }
41+ return "******" ;
42+ }
43+
3244export const command = new Command ( "functions:config:export" )
3345 . description ( "export environment config as a JSON secret to store in Cloud Secret Manager" )
3446 . option ( "--secret <name>" , "name of the secret to create (default: RUNTIME_CONFIG)" )
@@ -72,26 +84,48 @@ export const command = new Command("functions:config:export")
7284 // Display config in interactive mode
7385 if ( ! options . nonInteractive ) {
7486 logBullet ( clc . bold ( "Configuration to be exported:" ) ) ;
75- logWarning ( "This may contain sensitive data. Do not share this output." ) ;
76- console . log ( "" ) ;
77- console . log ( JSON . stringify ( configJson , null , 2 ) ) ;
87+ console . log ( JSON . stringify ( maskConfigValues ( configJson ) , null , 2 ) ) ;
7888 console . log ( "" ) ;
7989 }
8090
81- const defaultSecretName = "RUNTIME_CONFIG " ;
91+ const defaultSecretName = "FUNCTIONS_CONFIG_EXPORT " ;
8292 let secretName = options . secret as string ;
8393 if ( ! secretName ) {
84- secretName = await input ( {
85- message : "What would you like to name the new secret for your configuration?" ,
86- default : defaultSecretName ,
87- nonInteractive : options . nonInteractive ,
88- force : options . force ,
89- } ) ;
94+ if ( options . force ) {
95+ secretName = defaultSecretName ;
96+ } else {
97+ secretName = await input ( {
98+ message : "What would you like to name the new secret for your configuration?" ,
99+ default : defaultSecretName ,
100+ nonInteractive : options . nonInteractive ,
101+ } ) ;
102+ }
90103 }
91104
92105 const key = await ensureValidKey ( secretName , options ) ;
93106 await ensureSecret ( projectId , key , options ) ;
94107
108+ const versions = await listSecretVersions ( projectId , key ) ;
109+ const enabledVersions = versions . filter ( ( v ) => v . state === "ENABLED" ) ;
110+ enabledVersions . sort ( ( a , b ) => ( b . createTime || "" ) . localeCompare ( a . createTime || "" ) ) ;
111+ const latest = enabledVersions [ 0 ] ;
112+
113+ if ( latest ) {
114+ logBullet (
115+ `Secret ${ clc . bold ( key ) } already exists (latest version: ${ clc . bold ( latest . versionId ) } , created: ${ latest . createTime } ).` ,
116+ ) ;
117+ const proceed = await confirm ( {
118+ message : "Do you want to add a new version to this secret?" ,
119+ default : false ,
120+ nonInteractive : options . nonInteractive ,
121+ force : options . force ,
122+ } ) ;
123+ if ( ! proceed ) {
124+ return ;
125+ }
126+ console . log ( "" ) ;
127+ }
128+
95129 const secretValue = JSON . stringify ( configJson , null , 2 ) ;
96130
97131 // Check size limit (64KB)
@@ -111,32 +145,52 @@ export const command = new Command("functions:config:export")
111145 console . log ( "" ) ;
112146 logBullet ( clc . bold ( "To complete the migration, update your code:" ) ) ;
113147 console . log ( "" ) ;
114- console . log ( clc . gray ( " // Before:" ) ) ;
115- console . log ( clc . gray ( ` const functions = require('firebase-functions');` ) ) ;
116- console . log ( clc . gray ( ` ` ) ) ;
117- console . log ( clc . gray ( ` exports.myFunction = functions.https.onRequest((req, res) => {` ) ) ;
118- console . log ( clc . gray ( ` const apiKey = functions.config().service.key;` ) ) ;
119- console . log ( clc . gray ( ` // ...` ) ) ;
120- console . log ( clc . gray ( ` });` ) ) ;
121- console . log ( "" ) ;
122- console . log ( clc . gray ( " // After:" ) ) ;
123- console . log ( clc . gray ( ` const functions = require('firebase-functions');` ) ) ;
124- console . log ( clc . gray ( ` const { defineJsonSecret } = require('firebase-functions/params');` ) ) ;
125- console . log ( clc . gray ( ` ` ) ) ;
126- console . log ( clc . gray ( ` const config = defineJsonSecret("${ key } ");` ) ) ;
127- console . log ( clc . gray ( ` ` ) ) ;
128- console . log ( clc . gray ( ` exports.myFunction = functions` ) ) ;
129- console . log ( clc . gray ( ` .runWith({ secrets: [config] }) // Bind secret here` ) ) ;
130- console . log ( clc . gray ( ` .https.onRequest((req, res) => {` ) ) ;
131- console . log ( clc . gray ( ` const apiKey = config.value().service.key;` ) ) ;
132- console . log ( clc . gray ( ` // ...` ) ) ;
133- console . log ( clc . gray ( ` });` ) ) ;
134- console . log ( "" ) ;
135- logBullet (
136- clc . bold ( "Note: " ) +
137- "defineJsonSecret requires firebase-functions v6.6.0 or later. " +
138- "Update your package.json if needed." ,
148+ console . log (
149+ clc . gray ( ` // Before:
150+ const functions = require('firebase-functions');
151+
152+ exports.myFunction = functions.https.onRequest((req, res) => {
153+ const apiKey = functions.config().service.key;
154+ // ...
155+ });
156+
157+ // After:
158+ const functions = require('firebase-functions');
159+ const { defineJsonSecret } = require('firebase-functions/params');
160+
161+ const config = defineJsonSecret("${ key } ");
162+
163+ exports.myFunction = functions
164+ .runWith({ secrets: [config] }) // Bind secret here
165+ .https.onRequest((req, res) => {
166+ const apiKey = config.value().service.key;
167+ // ...
168+ });` ) ,
139169 ) ;
170+ console . log ( "" ) ;
171+
172+ // Try to detect the firebase-functions version to see if we need to warn about defineJsonSecret
173+ let sdkVersion : string | undefined ;
174+ try {
175+ const functionsConfig = options . config . get ( "functions" ) ;
176+ const source = Array . isArray ( functionsConfig )
177+ ? functionsConfig [ 0 ] ?. source
178+ : functionsConfig ?. source ;
179+ if ( source ) {
180+ const sourceDir = options . config . path ( source ) ;
181+ sdkVersion = getFunctionsSDKVersion ( sourceDir ) ;
182+ }
183+ } catch ( e ) {
184+ // ignore error, just show the warning if we can't detect the version
185+ }
186+
187+ if ( ! sdkVersion || semver . lt ( sdkVersion , "6.6.0" ) ) {
188+ logBullet (
189+ clc . bold ( "Note: " ) +
190+ "defineJsonSecret requires firebase-functions v6.6.0 or later. " +
191+ "Update your package.json if needed." ,
192+ ) ;
193+ }
140194 logBullet ( "Then deploy your functions:\n " + clc . bold ( "firebase deploy --only functions" ) ) ;
141195
142196 return secretName ;
0 commit comments