firebase-tools 9.19.0 → 9.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +1 -3
  2. package/lib/api.js +1 -0
  3. package/lib/commands/crashlytics-symbols-upload.js +146 -0
  4. package/lib/commands/ext-configure.js +2 -0
  5. package/lib/commands/ext-install.js +23 -2
  6. package/lib/commands/ext-uninstall.js +6 -0
  7. package/lib/commands/ext-update.js +9 -2
  8. package/lib/commands/functions-config-export.js +115 -0
  9. package/lib/commands/functions-delete.js +44 -35
  10. package/lib/commands/functions-list.js +1 -1
  11. package/lib/commands/index.js +8 -0
  12. package/lib/deploy/functions/backend.js +13 -4
  13. package/lib/deploy/functions/prepare.js +3 -0
  14. package/lib/deploy/functions/runtimes/node/parseTriggers.js +13 -1
  15. package/lib/deploy/functions/triggerRegionHelper.js +32 -0
  16. package/lib/downloadUtils.js +37 -0
  17. package/lib/emulator/auth/apiSpec.js +513 -1
  18. package/lib/emulator/auth/handlers.js +4 -3
  19. package/lib/emulator/auth/operations.js +144 -14
  20. package/lib/emulator/auth/server.js +10 -13
  21. package/lib/emulator/auth/state.js +132 -13
  22. package/lib/emulator/download.js +2 -31
  23. package/lib/emulator/functionsEmulatorRuntime.js +29 -7
  24. package/lib/extensions/askUserForConsent.js +14 -1
  25. package/lib/extensions/askUserForParam.js +72 -3
  26. package/lib/extensions/extensionsApi.js +1 -0
  27. package/lib/extensions/extensionsHelper.js +1 -0
  28. package/lib/extensions/paramHelper.js +3 -2
  29. package/lib/extensions/secretsUtils.js +58 -0
  30. package/lib/functional.js +8 -1
  31. package/lib/functions/env.js +10 -4
  32. package/lib/functions/runtimeConfigExport.js +137 -0
  33. package/lib/gcp/cloudfunctions.js +4 -2
  34. package/lib/gcp/cloudfunctionsv2.js +6 -0
  35. package/lib/gcp/secretManager.js +111 -0
  36. package/lib/gcp/storage.js +16 -0
  37. package/lib/previews.js +1 -1
  38. package/lib/requireInteractive.js +12 -0
  39. package/package.json +1 -1
@@ -283,7 +283,13 @@ class ProjectState {
283
283
  createRefreshTokenFor(userInfo, provider, { extraClaims = {}, secondFactor, } = {}) {
284
284
  const localId = userInfo.localId;
285
285
  const refreshToken = utils_1.randomBase64UrlStr(204);
286
- this.refreshTokens.set(refreshToken, { localId, provider, extraClaims, secondFactor });
286
+ this.refreshTokens.set(refreshToken, {
287
+ localId,
288
+ provider,
289
+ extraClaims,
290
+ secondFactor,
291
+ tenantId: userInfo.tenantId,
292
+ });
287
293
  let refreshTokens = this.refreshTokensForLocalId.get(localId);
288
294
  if (!refreshTokens) {
289
295
  refreshTokens = new Set();
@@ -416,9 +422,9 @@ exports.ProjectState = ProjectState;
416
422
  class AgentProjectState extends ProjectState {
417
423
  constructor(projectId) {
418
424
  super(projectId);
419
- this.tenantForTenantId = new Map();
420
425
  this._oneAccountPerEmail = true;
421
426
  this._usageMode = UsageMode.DEFAULT;
427
+ this.tenantProjectForTenantId = new Map();
422
428
  this._authCloudFunction = new cloudFunctions_1.AuthCloudFunction(this.projectId);
423
429
  }
424
430
  get authCloudFunction() {
@@ -436,27 +442,74 @@ class AgentProjectState extends ProjectState {
436
442
  set usageMode(usageMode) {
437
443
  this._usageMode = usageMode;
438
444
  }
439
- getTenant() {
440
- throw new errors_1.NotImplementedError("getTenant is not implemented yet.");
445
+ get allowPasswordSignup() {
446
+ return true;
447
+ }
448
+ get disableAuth() {
449
+ return false;
450
+ }
451
+ get mfaConfig() {
452
+ return { state: "ENABLED", enabledProviders: ["PHONE_SMS"] };
453
+ }
454
+ get enableAnonymousUser() {
455
+ return true;
441
456
  }
442
- listTenants() {
443
- throw new errors_1.NotImplementedError("listTenants is not implemented yet.");
457
+ get enableEmailLinkSignin() {
458
+ return true;
444
459
  }
445
- createTenant() {
446
- throw new errors_1.NotImplementedError("createTenant is not implemented yet.");
460
+ getTenantProject(tenantId) {
461
+ if (!this.tenantProjectForTenantId.has(tenantId)) {
462
+ this.createTenantWithTenantId(tenantId, { tenantId });
463
+ }
464
+ return this.tenantProjectForTenantId.get(tenantId);
447
465
  }
448
- updateTenant() {
449
- throw new errors_1.NotImplementedError("updateTenant is not implemented yet.");
466
+ listTenants(startToken) {
467
+ const tenantProjects = [];
468
+ for (const tenantProject of this.tenantProjectForTenantId.values()) {
469
+ if (!startToken || tenantProject.tenantId > startToken) {
470
+ tenantProjects.push(tenantProject);
471
+ }
472
+ }
473
+ tenantProjects.sort((a, b) => {
474
+ if (a.tenantId < b.tenantId) {
475
+ return -1;
476
+ }
477
+ else if (a.tenantId > b.tenantId) {
478
+ return 1;
479
+ }
480
+ return 0;
481
+ });
482
+ return tenantProjects.map((tenantProject) => tenantProject.tenantConfig);
483
+ }
484
+ createTenant(tenant) {
485
+ for (let i = 0; i < 10; i++) {
486
+ const tenantId = utils_1.randomId(28);
487
+ const createdTenant = this.createTenantWithTenantId(tenantId, tenant);
488
+ if (createdTenant) {
489
+ return createdTenant;
490
+ }
491
+ }
492
+ throw new Error("Could not generate a random unique tenantId after 10 tries");
493
+ }
494
+ createTenantWithTenantId(tenantId, tenant) {
495
+ if (this.tenantProjectForTenantId.has(tenantId)) {
496
+ return undefined;
497
+ }
498
+ tenant.name = `projects/${this.projectId}/tenants/${tenantId}`;
499
+ tenant.tenantId = tenantId;
500
+ this.tenantProjectForTenantId.set(tenantId, new TenantProjectState(this.projectId, tenantId, tenant, this));
501
+ return tenant;
450
502
  }
451
- deleteTenant() {
452
- throw new errors_1.NotImplementedError("deleteTenant is not implemented yet.");
503
+ deleteTenant(tenantId) {
504
+ this.tenantProjectForTenantId.delete(tenantId);
453
505
  }
454
506
  }
455
507
  exports.AgentProjectState = AgentProjectState;
456
508
  class TenantProjectState extends ProjectState {
457
- constructor(projectId, tenantId, parentProject) {
509
+ constructor(projectId, tenantId, _tenantConfig, parentProject) {
458
510
  super(projectId);
459
511
  this.tenantId = tenantId;
512
+ this._tenantConfig = _tenantConfig;
460
513
  this.parentProject = parentProject;
461
514
  }
462
515
  get oneAccountPerEmail() {
@@ -468,6 +521,72 @@ class TenantProjectState extends ProjectState {
468
521
  get usageMode() {
469
522
  return this.parentProject.usageMode;
470
523
  }
524
+ get tenantConfig() {
525
+ return this._tenantConfig;
526
+ }
527
+ get allowPasswordSignup() {
528
+ var _a;
529
+ return (_a = this._tenantConfig.allowPasswordSignup) !== null && _a !== void 0 ? _a : true;
530
+ }
531
+ get disableAuth() {
532
+ var _a;
533
+ return (_a = this._tenantConfig.disableAuth) !== null && _a !== void 0 ? _a : false;
534
+ }
535
+ get mfaConfig() {
536
+ var _a;
537
+ return ((_a = this._tenantConfig.mfaConfig) !== null && _a !== void 0 ? _a : {
538
+ state: "ENABLED",
539
+ enabledProviders: ["PHONE_SMS"],
540
+ });
541
+ }
542
+ get enableAnonymousUser() {
543
+ var _a;
544
+ return (_a = this._tenantConfig.enableAnonymousUser) !== null && _a !== void 0 ? _a : true;
545
+ }
546
+ get enableEmailLinkSignin() {
547
+ var _a;
548
+ return (_a = this._tenantConfig.enableEmailLinkSignin) !== null && _a !== void 0 ? _a : true;
549
+ }
550
+ delete() {
551
+ this.parentProject.deleteTenant(this.tenantId);
552
+ }
553
+ updateTenant(update, updateMask) {
554
+ if (!updateMask) {
555
+ this._tenantConfig = Object.assign(Object.assign({}, update), { tenantId: this.tenantId, name: this.tenantConfig.name });
556
+ return this.tenantConfig;
557
+ }
558
+ const paths = updateMask.split(",");
559
+ for (const path of paths) {
560
+ const fields = path.split(".");
561
+ let updateField = update;
562
+ let existingField = this._tenantConfig;
563
+ let field;
564
+ for (let i = 0; i < fields.length - 1; i++) {
565
+ field = fields[i];
566
+ if (updateField[field] == null) {
567
+ console.warn(`Unable to find field '${field}' in update '${updateField}`);
568
+ break;
569
+ }
570
+ if (Array.isArray(updateField[field]) ||
571
+ Object(updateField[field]) !== updateField[field]) {
572
+ console.warn(`Field '${field}' is singular and cannot have sub-fields`);
573
+ break;
574
+ }
575
+ if (!existingField[field]) {
576
+ existingField[field] = {};
577
+ }
578
+ updateField = updateField[field];
579
+ existingField = existingField[field];
580
+ }
581
+ field = fields[fields.length - 1];
582
+ if (updateField[field] == null) {
583
+ console.warn(`Unable to find field '${field}' in update '${JSON.stringify(updateField)}`);
584
+ continue;
585
+ }
586
+ existingField[field] = updateField[field];
587
+ }
588
+ return this.tenantConfig;
589
+ }
471
590
  }
472
591
  exports.TenantProjectState = TenantProjectState;
473
592
  function getProviderEmailsForUser(user) {
@@ -1,23 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.downloadEmulator = void 0;
4
- const url_1 = require("url");
5
4
  const crypto = require("crypto");
6
5
  const fs = require("fs-extra");
7
6
  const path = require("path");
8
- const ProgressBar = require("progress");
9
7
  const tmp = require("tmp");
10
8
  const unzipper = require("unzipper");
11
- const apiv2_1 = require("../apiv2");
12
9
  const emulatorLogger_1 = require("./emulatorLogger");
13
10
  const error_1 = require("../error");
14
11
  const downloadableEmulators = require("./downloadableEmulators");
12
+ const downloadUtils = require("../downloadUtils");
15
13
  tmp.setGracefulCleanup();
16
14
  async function downloadEmulator(name) {
17
15
  const emulator = downloadableEmulators.getDownloadDetails(name);
18
16
  emulatorLogger_1.EmulatorLogger.forEmulator(name).logLabeled("BULLET", name, `downloading ${path.basename(emulator.downloadPath)}...`);
19
17
  fs.ensureDirSync(emulator.opts.cacheDir);
20
- const tmpfile = await downloadToTmp(emulator.opts.remoteUrl);
18
+ const tmpfile = await downloadUtils.downloadToTmp(emulator.opts.remoteUrl);
21
19
  if (!emulator.opts.skipChecksumAndSize) {
22
20
  await validateSize(tmpfile, emulator.opts.expectedSize);
23
21
  await validateChecksum(tmpfile, emulator.opts.expectedChecksum);
@@ -58,33 +56,6 @@ function removeOldFiles(name, emulator, removeAllVersions = false) {
58
56
  }
59
57
  }
60
58
  }
61
- async function downloadToTmp(remoteUrl) {
62
- const u = new url_1.URL(remoteUrl);
63
- const c = new apiv2_1.Client({ urlPrefix: u.origin, auth: false });
64
- const tmpfile = tmp.fileSync();
65
- const writeStream = fs.createWriteStream(tmpfile.name);
66
- const res = await c.request({
67
- method: "GET",
68
- path: u.pathname,
69
- queryParams: u.searchParams,
70
- responseType: "stream",
71
- resolveOnHTTPError: true,
72
- });
73
- if (res.status !== 200) {
74
- throw new error_1.FirebaseError(`download failed, status ${res.status}`, { exit: 1 });
75
- }
76
- const total = parseInt(res.response.headers.get("content-length") || "0", 10);
77
- const totalMb = Math.ceil(total / 1000000);
78
- const bar = new ProgressBar(`Progress: :bar (:percent of ${totalMb}MB)`, { total, head: ">" });
79
- res.body.on("data", (chunk) => {
80
- bar.tick(chunk.length);
81
- });
82
- await new Promise((resolve) => {
83
- writeStream.on("finish", resolve);
84
- res.body.pipe(writeStream);
85
- });
86
- return tmpfile.name;
87
- }
88
59
  function validateSize(filepath, expectedSize) {
89
60
  return new Promise((resolve, reject) => {
90
61
  const stat = fs.statSync(filepath);
@@ -259,13 +259,35 @@ async function initializeFirebaseFunctionsStubs(frb) {
259
259
  };
260
260
  const onCallInnerMethodName = "_onCallWithOptions";
261
261
  const onCallMethodOriginal = httpsProvider[onCallInnerMethodName];
262
- httpsProvider[onCallInnerMethodName] = (handler, opts) => {
263
- const wrapped = wrapCallableHandler(handler);
264
- const cf = onCallMethodOriginal(wrapped, opts);
265
- return cf;
266
- };
267
- httpsProvider.onCall = (handler) => {
268
- return httpsProvider[onCallInnerMethodName](handler, {});
262
+ if (onCallMethodOriginal.length === 3) {
263
+ httpsProvider[onCallInnerMethodName] = (opts, handler, deployOpts) => {
264
+ const wrapped = wrapCallableHandler(handler);
265
+ const cf = onCallMethodOriginal(opts, wrapped, deployOpts);
266
+ return cf;
267
+ };
268
+ }
269
+ else {
270
+ httpsProvider[onCallInnerMethodName] = (handler, opts) => {
271
+ const wrapped = wrapCallableHandler(handler);
272
+ const cf = onCallMethodOriginal(wrapped, opts);
273
+ return cf;
274
+ };
275
+ }
276
+ httpsProvider.onCall = function (optsOrHandler, handler) {
277
+ if (onCallMethodOriginal.length === 3) {
278
+ let opts;
279
+ if (arguments.length === 1) {
280
+ opts = {};
281
+ handler = optsOrHandler;
282
+ }
283
+ else {
284
+ opts = optsOrHandler;
285
+ }
286
+ return httpsProvider[onCallInnerMethodName](opts, handler, {});
287
+ }
288
+ else {
289
+ return httpsProvider[onCallInnerMethodName](optsOrHandler, {});
290
+ }
269
291
  };
270
292
  }
271
293
  function wrapCallableHandler(handler) {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.promptForPublisherTOS = exports.displayRoles = exports.retrieveRoleInfo = exports.formatDescription = void 0;
3
+ exports.promptForPublisherTOS = exports.displayApis = exports.displayRoles = exports.retrieveRoleInfo = exports.formatDescription = void 0;
4
4
  const _ = require("lodash");
5
5
  const clc = require("cli-color");
6
6
  const marked = require("marked");
@@ -35,6 +35,19 @@ async function displayRoles(extensionName, projectId, roles) {
35
35
  utils.logLabeledBullet(extensionsHelper_1.logPrefix, message);
36
36
  }
37
37
  exports.displayRoles = displayRoles;
38
+ function displayApis(extensionName, projectId, apis) {
39
+ if (!apis.length) {
40
+ return;
41
+ }
42
+ const question = `${clc.bold(extensionName)} will enable the following APIs for project ${clc.bold(projectId)}`;
43
+ const results = apis.map((api) => {
44
+ return `- ${api.apiName}: ${api.reason}`;
45
+ });
46
+ results.unshift(question);
47
+ const message = results.join("\n");
48
+ utils.logLabeledBullet(extensionsHelper_1.logPrefix, message);
49
+ }
50
+ exports.displayApis = displayApis;
38
51
  async function promptForPublisherTOS() {
39
52
  const termsOfServiceMsg = "By registering as a publisher, you confirm that you have read the Firebase Extensions Publisher Terms and Conditions (linked below) and you, on behalf of yourself and the organization you represent, agree to comply with it. Here is a brief summary of the highlights of our terms and conditions:\n" +
40
53
  " - You ensure extensions you publish comply with all laws and regulations; do not include any viruses, spyware, Trojan horses, or other malicious code; and do not violate any person’s rights, including intellectual property, privacy, and security rights.\n" +
@@ -5,11 +5,18 @@ const _ = require("lodash");
5
5
  const clc = require("cli-color");
6
6
  const marked = require("marked");
7
7
  const extensionsApi_1 = require("./extensionsApi");
8
+ const secretManagerApi = require("../gcp/secretManager");
9
+ const secretsUtils = require("./secretsUtils");
8
10
  const extensionsHelper_1 = require("./extensionsHelper");
9
11
  const utils_1 = require("./utils");
10
12
  const logger_1 = require("../logger");
11
13
  const prompt_1 = require("../prompt");
12
14
  const utils = require("../utils");
15
+ var SecretUpdateAction;
16
+ (function (SecretUpdateAction) {
17
+ SecretUpdateAction[SecretUpdateAction["LEAVE"] = 0] = "LEAVE";
18
+ SecretUpdateAction[SecretUpdateAction["SET_NEW"] = 1] = "SET_NEW";
19
+ })(SecretUpdateAction || (SecretUpdateAction = {}));
13
20
  function checkResponse(response, spec) {
14
21
  let valid = true;
15
22
  let responses;
@@ -48,7 +55,7 @@ function checkResponse(response, spec) {
48
55
  return valid;
49
56
  }
50
57
  exports.checkResponse = checkResponse;
51
- async function askForParam(paramSpec) {
58
+ async function askForParam(projectId, instanceId, paramSpec, reconfiguring) {
52
59
  let valid = false;
53
60
  let response = "";
54
61
  const description = paramSpec.description || "";
@@ -89,6 +96,11 @@ async function askForParam(paramSpec) {
89
96
  choices: utils_1.convertExtensionOptionToLabeledList(paramSpec.options),
90
97
  });
91
98
  break;
99
+ case extensionsApi_1.ParamType.SECRET:
100
+ response = reconfiguring
101
+ ? await promptReconfigureSecret(projectId, instanceId, paramSpec)
102
+ : await promptCreateSecret(projectId, instanceId, paramSpec);
103
+ break;
92
104
  default:
93
105
  response = await prompt_1.promptOnce({
94
106
  name: paramSpec.param,
@@ -102,6 +114,63 @@ async function askForParam(paramSpec) {
102
114
  return response;
103
115
  }
104
116
  exports.askForParam = askForParam;
117
+ async function promptReconfigureSecret(projectId, instanceId, paramSpec) {
118
+ const action = await prompt_1.promptOnce({
119
+ type: "list",
120
+ message: `Choose what you would like to do with this secret:`,
121
+ choices: [
122
+ { name: "Leave unchanged", value: SecretUpdateAction.LEAVE },
123
+ { name: "Set new value", value: SecretUpdateAction.SET_NEW },
124
+ ],
125
+ });
126
+ switch (action) {
127
+ case SecretUpdateAction.SET_NEW:
128
+ let secret;
129
+ let secretName;
130
+ if (paramSpec.default) {
131
+ secret = secretManagerApi.parseSecretResourceName(paramSpec.default);
132
+ secretName = secret.name;
133
+ }
134
+ else {
135
+ secretName = await generateSecretName(projectId, instanceId, paramSpec.param);
136
+ }
137
+ const secretValue = await prompt_1.promptOnce({
138
+ name: paramSpec.param,
139
+ type: "password",
140
+ message: `This secret will be stored in Cloud Secret Manager as ${secretName}.\nEnter new value for ${paramSpec.label.trim()}:`,
141
+ });
142
+ if (!secret) {
143
+ secret = await secretManagerApi.createSecret(projectId, secretName, secretsUtils.getSecretLabels(instanceId));
144
+ }
145
+ return addNewSecretVersion(projectId, instanceId, secret, paramSpec, secretValue);
146
+ case SecretUpdateAction.LEAVE:
147
+ default:
148
+ return paramSpec.default || "";
149
+ }
150
+ }
151
+ async function promptCreateSecret(projectId, instanceId, paramSpec) {
152
+ const secretName = await generateSecretName(projectId, instanceId, paramSpec.param);
153
+ const secretValue = await prompt_1.promptOnce({
154
+ name: paramSpec.param,
155
+ type: "password",
156
+ default: paramSpec.default,
157
+ message: `This secret will be stored in Cloud Secret Manager (https://cloud.google.com/secret-manager/pricing) as ${secretName} and managed by Firebase Extensions (Firebase Extensions Service Agent will be granted Secret Admin role on this secret).\nEnter a value for ${paramSpec.label.trim()}:`,
158
+ });
159
+ const secret = await secretManagerApi.createSecret(projectId, secretName, secretsUtils.getSecretLabels(instanceId));
160
+ return addNewSecretVersion(projectId, instanceId, secret, paramSpec, secretValue);
161
+ }
162
+ async function generateSecretName(projectId, instanceId, paramName) {
163
+ let secretName = `ext-${instanceId}-${paramName}`;
164
+ while (await secretManagerApi.secretExists(projectId, secretName)) {
165
+ secretName += `-${utils_1.getRandomString(3)}`;
166
+ }
167
+ return secretName;
168
+ }
169
+ async function addNewSecretVersion(projectId, instanceId, secret, paramSpec, secretValue) {
170
+ const version = await secretManagerApi.addVersion(secret, secretValue);
171
+ await secretsUtils.grantFirexServiceAgentSecretAdminRole(secret);
172
+ return `projects/${version.secret.projectId}/secrets/${version.secret.name}/versions/${version.versionId}`;
173
+ }
105
174
  function getInquirerDefault(options, def) {
106
175
  const defaultOption = _.find(options, (option) => {
107
176
  return option.value === def;
@@ -109,7 +178,7 @@ function getInquirerDefault(options, def) {
109
178
  return defaultOption ? defaultOption.label || defaultOption.value : "";
110
179
  }
111
180
  exports.getInquirerDefault = getInquirerDefault;
112
- async function ask(paramSpecs, firebaseProjectParams) {
181
+ async function ask(projectId, instanceId, paramSpecs, firebaseProjectParams, reconfiguring) {
113
182
  if (_.isEmpty(paramSpecs)) {
114
183
  logger_1.logger.debug("No params were specified for this extension.");
115
184
  return {};
@@ -119,7 +188,7 @@ async function ask(paramSpecs, firebaseProjectParams) {
119
188
  const result = {};
120
189
  const promises = _.map(substituted, (paramSpec) => {
121
190
  return async () => {
122
- result[paramSpec.param] = await askForParam(paramSpec);
191
+ result[paramSpec.param] = await askForParam(projectId, instanceId, paramSpec, reconfiguring);
123
192
  };
124
193
  });
125
194
  await promises.reduce((prev, cur) => prev.then(cur), Promise.resolve());
@@ -30,6 +30,7 @@ var ParamType;
30
30
  ParamType["STRING"] = "STRING";
31
31
  ParamType["SELECT"] = "SELECT";
32
32
  ParamType["MULTISELECT"] = "MULTISELECT";
33
+ ParamType["SECRET"] = "SECRET";
33
34
  })(ParamType = exports.ParamType || (exports.ParamType = {}));
34
35
  async function createInstanceHelper(projectId, instanceId, config) {
35
36
  const createRes = await api.request("POST", `/${VERSION}/projects/${projectId}/instances/`, {
@@ -33,6 +33,7 @@ var SpecParamType;
33
33
  SpecParamType["MULTISELECT"] = "multiSelect";
34
34
  SpecParamType["STRING"] = "string";
35
35
  SpecParamType["SELECTRESOURCE"] = "selectResource";
36
+ SpecParamType["SECRET"] = "secret";
36
37
  })(SpecParamType = exports.SpecParamType || (exports.SpecParamType = {}));
37
38
  var SourceOrigin;
38
39
  (function (SourceOrigin) {
@@ -48,7 +48,7 @@ async function getParams(args) {
48
48
  }
49
49
  else {
50
50
  const firebaseProjectParams = await extensionsHelper_1.getFirebaseProjectParams(args.projectId);
51
- params = await askUserForParam.ask(args.paramSpecs, firebaseProjectParams);
51
+ params = await askUserForParam.ask(args.projectId, args.instanceId, args.paramSpecs, firebaseProjectParams, !!args.reconfiguring);
52
52
  }
53
53
  track("Extension Params", _.isEmpty(params) ? "Not Present" : "Present", _.size(params));
54
54
  return params;
@@ -81,6 +81,7 @@ async function getParamsForUpdate(args) {
81
81
  newSpec: args.newSpec,
82
82
  currentParams: args.currentParams,
83
83
  projectId: args.projectId,
84
+ instanceId: args.instanceId,
84
85
  });
85
86
  }
86
87
  track("Extension Params", _.isEmpty(params) ? "Not Present" : "Present", _.size(params));
@@ -106,7 +107,7 @@ async function promptForNewParams(args) {
106
107
  if (paramsDiffAdditions.length) {
107
108
  logger_1.logger.info("To update this instance, configure the following new parameters:");
108
109
  for (const param of paramsDiffAdditions) {
109
- const chosenValue = await askUserForParam.askForParam(param);
110
+ const chosenValue = await askUserForParam.askForParam(args.projectId, args.instanceId, param, false);
110
111
  args.currentParams[param.param] = chosenValue;
111
112
  }
112
113
  }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prettySecretName = exports.getSecretLabels = exports.getManagedSecrets = exports.grantFirexServiceAgentSecretAdminRole = exports.usesSecrets = exports.ensureSecretManagerApiEnabled = void 0;
4
+ const getProjectNumber_1 = require("../getProjectNumber");
5
+ const utils = require("../utils");
6
+ const ensureApiEnabled_1 = require("../ensureApiEnabled");
7
+ const projectUtils_1 = require("../projectUtils");
8
+ const extensionsApi = require("./extensionsApi");
9
+ const secretManagerApi = require("../gcp/secretManager");
10
+ const logger_1 = require("../logger");
11
+ const SECRET_LABEL = "firebase-extensions-managed";
12
+ async function ensureSecretManagerApiEnabled(options) {
13
+ const projectId = projectUtils_1.needProjectId(options);
14
+ return await ensureApiEnabled_1.ensure(projectId, "secretmanager.googleapis.com", "extensions", options.markdown);
15
+ }
16
+ exports.ensureSecretManagerApiEnabled = ensureSecretManagerApiEnabled;
17
+ function usesSecrets(spec) {
18
+ return spec.params && !!spec.params.find((p) => p.type == extensionsApi.ParamType.SECRET);
19
+ }
20
+ exports.usesSecrets = usesSecrets;
21
+ async function grantFirexServiceAgentSecretAdminRole(secret) {
22
+ const projectNumber = await getProjectNumber_1.getProjectNumber({ projectId: secret.projectId });
23
+ const firexSaProjectId = utils.envOverride("FIREBASE_EXTENSIONS_SA_PROJECT_ID", "gcp-sa-firebasemods");
24
+ const saEmail = `service-${projectNumber}@${firexSaProjectId}.iam.gserviceaccount.com`;
25
+ return secretManagerApi.grantServiceAgentRole(secret, saEmail, "roles/secretmanager.admin");
26
+ }
27
+ exports.grantFirexServiceAgentSecretAdminRole = grantFirexServiceAgentSecretAdminRole;
28
+ async function getManagedSecrets(instance) {
29
+ return (await Promise.all(getActiveSecrets(instance).map(async (secretResourceName) => {
30
+ const secret = secretManagerApi.parseSecretResourceName(secretResourceName);
31
+ const labels = await secretManagerApi.getSecretLabels(secret.projectId, secret.name);
32
+ if (labels && labels[SECRET_LABEL]) {
33
+ return secretResourceName;
34
+ }
35
+ return Promise.resolve("");
36
+ }))).filter((secretId) => !!secretId);
37
+ }
38
+ exports.getManagedSecrets = getManagedSecrets;
39
+ function getActiveSecrets(instance) {
40
+ return instance.config.source.spec.params
41
+ .map((p) => p.type == extensionsApi.ParamType.SECRET && instance.config.params[p.param])
42
+ .filter((pv) => !!pv);
43
+ }
44
+ function getSecretLabels(instanceId) {
45
+ const labels = {};
46
+ labels[SECRET_LABEL] = instanceId;
47
+ return labels;
48
+ }
49
+ exports.getSecretLabels = getSecretLabels;
50
+ function prettySecretName(secretResourceName) {
51
+ const nameTokens = secretResourceName.split("/");
52
+ if (nameTokens.length != 4 && nameTokens.length != 6) {
53
+ logger_1.logger.debug(`unable to parse secret secretResourceName: ${secretResourceName}`);
54
+ return secretResourceName;
55
+ }
56
+ return nameTokens.slice(0, 4).join("/");
57
+ }
58
+ exports.prettySecretName = prettySecretName;
package/lib/functional.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.assertExhaustive = exports.zipIn = exports.zip = exports.reduceFlat = exports.flatten = exports.flattenArray = exports.flattenObject = void 0;
3
+ exports.partition = exports.assertExhaustive = exports.zipIn = exports.zip = exports.reduceFlat = exports.flatten = exports.flattenArray = exports.flattenObject = void 0;
4
4
  function* flattenObject(obj) {
5
5
  function* helper(path, obj) {
6
6
  for (const [k, v] of Object.entries(obj)) {
@@ -55,3 +55,10 @@ function assertExhaustive(val) {
55
55
  throw new Error(`Never has a value (${val}). This should be impossible`);
56
56
  }
57
57
  exports.assertExhaustive = assertExhaustive;
58
+ function partition(arr, callbackFn) {
59
+ return arr.reduce((acc, elem) => {
60
+ acc[callbackFn(elem) ? 0 : 1].push(elem);
61
+ return acc;
62
+ }, [[], []]);
63
+ }
64
+ exports.partition = partition;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loadFirebaseEnvs = exports.loadUserEnvs = exports.hasUserEnvs = exports.validateKey = exports.parse = void 0;
3
+ exports.loadFirebaseEnvs = exports.loadUserEnvs = exports.hasUserEnvs = exports.validateKey = exports.KeyValidationError = exports.parse = void 0;
4
4
  const clc = require("cli-color");
5
5
  const fs = require("fs");
6
6
  const path = require("path");
@@ -72,17 +72,23 @@ function parse(data) {
72
72
  }
73
73
  exports.parse = parse;
74
74
  class KeyValidationError extends Error {
75
+ constructor(key, message) {
76
+ super(`Failed to validate key ${key}: ${message}`);
77
+ this.key = key;
78
+ this.message = message;
79
+ }
75
80
  }
81
+ exports.KeyValidationError = KeyValidationError;
76
82
  function validateKey(key) {
77
83
  if (RESERVED_KEYS.includes(key)) {
78
- throw new KeyValidationError(`Key ${key} is reserved for internal use.`);
84
+ throw new KeyValidationError(key, `Key ${key} is reserved for internal use.`);
79
85
  }
80
86
  if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) {
81
- throw new KeyValidationError(`Key ${key} must start with an uppercase ASCII letter or underscore` +
87
+ throw new KeyValidationError(key, `Key ${key} must start with an uppercase ASCII letter or underscore` +
82
88
  ", and then consist of uppercase ASCII letters, digits, and underscores.");
83
89
  }
84
90
  if (key.startsWith("X_GOOGLE_") || key.startsWith("FIREBASE_")) {
85
- throw new KeyValidationError(`Key ${key} starts with a reserved prefix (X_GOOGLE_ or FIREBASE_)`);
91
+ throw new KeyValidationError(key, `Key ${key} starts with a reserved prefix (X_GOOGLE_ or FIREBASE_)`);
86
92
  }
87
93
  }
88
94
  exports.validateKey = validateKey;