firebase-tools 9.19.0 → 9.23.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 (101) hide show
  1. package/CHANGELOG.md +1 -3
  2. package/lib/api.js +3 -0
  3. package/lib/apiv2.js +7 -4
  4. package/lib/commands/crashlytics-symbols-upload.js +146 -0
  5. package/lib/commands/deploy.js +9 -1
  6. package/lib/commands/ext-configure.js +3 -1
  7. package/lib/commands/ext-dev-deprecate.js +63 -0
  8. package/lib/commands/ext-dev-undeprecate.js +56 -0
  9. package/lib/commands/ext-dev-unpublish.js +10 -3
  10. package/lib/commands/ext-export.js +44 -0
  11. package/lib/commands/ext-install.js +24 -3
  12. package/lib/commands/ext-uninstall.js +6 -0
  13. package/lib/commands/ext-update.js +10 -3
  14. package/lib/commands/functions-config-export.js +115 -0
  15. package/lib/commands/functions-delete.js +47 -25
  16. package/lib/commands/functions-list.js +12 -12
  17. package/lib/commands/index.js +9 -0
  18. package/lib/commands/init.js +3 -0
  19. package/lib/config.js +3 -2
  20. package/lib/deploy/extensions/args.js +2 -0
  21. package/lib/deploy/extensions/deploy.js +49 -0
  22. package/lib/deploy/extensions/deploymentSummary.js +52 -0
  23. package/lib/deploy/extensions/errors.js +31 -0
  24. package/lib/deploy/extensions/index.js +8 -0
  25. package/lib/deploy/extensions/params.js +39 -0
  26. package/lib/deploy/extensions/planner.js +94 -0
  27. package/lib/deploy/extensions/prepare.js +111 -0
  28. package/lib/deploy/extensions/release.js +43 -0
  29. package/lib/deploy/extensions/secrets.js +150 -0
  30. package/lib/deploy/extensions/tasks.js +98 -0
  31. package/lib/deploy/extensions/validate.js +17 -0
  32. package/lib/deploy/functions/backend.js +93 -115
  33. package/lib/deploy/functions/checkIam.js +8 -8
  34. package/lib/deploy/functions/containerCleaner.js +71 -14
  35. package/lib/deploy/functions/deploy.js +4 -10
  36. package/lib/deploy/functions/functionsDeployHelper.js +3 -68
  37. package/lib/deploy/functions/prepare.js +63 -27
  38. package/lib/deploy/functions/pricing.js +17 -17
  39. package/lib/deploy/functions/prompts.js +22 -21
  40. package/lib/deploy/functions/release/executor.js +39 -0
  41. package/lib/deploy/functions/release/fabricator.js +422 -0
  42. package/lib/deploy/functions/release/index.js +73 -0
  43. package/lib/deploy/functions/release/planner.js +162 -0
  44. package/lib/deploy/functions/release/reporter.js +165 -0
  45. package/lib/deploy/functions/release/sourceTokenScraper.js +28 -0
  46. package/lib/deploy/functions/release/timer.js +14 -0
  47. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +129 -126
  48. package/lib/deploy/functions/runtimes/node/parseTriggers.js +41 -45
  49. package/lib/deploy/functions/triggerRegionHelper.js +40 -0
  50. package/lib/deploy/functions/validate.js +1 -24
  51. package/lib/deploy/index.js +10 -1
  52. package/lib/downloadUtils.js +37 -0
  53. package/lib/emulator/auth/apiSpec.js +549 -6
  54. package/lib/emulator/auth/handlers.js +4 -3
  55. package/lib/emulator/auth/operations.js +154 -14
  56. package/lib/emulator/auth/server.js +26 -15
  57. package/lib/emulator/auth/state.js +151 -13
  58. package/lib/emulator/download.js +2 -31
  59. package/lib/emulator/downloadableEmulators.js +7 -7
  60. package/lib/emulator/functionsEmulator.js +18 -4
  61. package/lib/emulator/functionsEmulatorRuntime.js +29 -7
  62. package/lib/emulator/storage/cloudFunctions.js +37 -7
  63. package/lib/extensions/askUserForConsent.js +14 -1
  64. package/lib/extensions/askUserForParam.js +81 -4
  65. package/lib/extensions/checkProjectBilling.js +7 -7
  66. package/lib/extensions/export.js +107 -0
  67. package/lib/extensions/extensionsApi.js +104 -21
  68. package/lib/extensions/extensionsHelper.js +6 -2
  69. package/lib/extensions/listExtensions.js +16 -11
  70. package/lib/extensions/paramHelper.js +9 -6
  71. package/lib/extensions/provisioningHelper.js +16 -3
  72. package/lib/extensions/refs.js +9 -1
  73. package/lib/extensions/secretsUtils.js +59 -0
  74. package/lib/extensions/updateHelper.js +12 -2
  75. package/lib/extensions/versionHelper.js +14 -0
  76. package/lib/extensions/warnings.js +33 -1
  77. package/lib/functional.js +8 -1
  78. package/lib/functions/env.js +10 -4
  79. package/lib/functions/runtimeConfigExport.js +137 -0
  80. package/lib/gcp/artifactregistry.js +16 -0
  81. package/lib/gcp/cloudfunctions.js +20 -74
  82. package/lib/gcp/cloudfunctionsv2.js +12 -90
  83. package/lib/gcp/cloudscheduler.js +22 -16
  84. package/lib/gcp/cloudtasks.js +143 -0
  85. package/lib/gcp/docker.js +7 -1
  86. package/lib/gcp/proto.js +2 -2
  87. package/lib/gcp/pubsub.js +1 -9
  88. package/lib/gcp/secretManager.js +132 -0
  89. package/lib/gcp/storage.js +16 -0
  90. package/lib/projectUtils.js +10 -1
  91. package/lib/requireInteractive.js +12 -0
  92. package/lib/utils.js +30 -1
  93. package/package.json +5 -4
  94. package/schema/firebase-config.json +9 -0
  95. package/lib/deploy/functions/deploymentPlanner.js +0 -113
  96. package/lib/deploy/functions/deploymentTimer.js +0 -23
  97. package/lib/deploy/functions/errorHandler.js +0 -75
  98. package/lib/deploy/functions/release.js +0 -116
  99. package/lib/deploy/functions/tasks.js +0 -324
  100. package/lib/functions/listFunctions.js +0 -10
  101. package/lib/functionsDelete.js +0 -60
package/CHANGELOG.md CHANGED
@@ -1,3 +1 @@
1
- - `ext:dev:publish` and `ext:update` now support --force and --non-interactive flags.
2
- - Fixes issue where account specified by `login:use` was not being correctly loaded (#3759).
3
- - Fixes minor layout issues in Auth Emulator IDP sign-in page (#3774).
1
+ - `firebase deploy --only extensions` now supports project specifc .env files. When deploying to multiple projects, param values that are different between projects can be put in `extensions/${extensionInstanceId}.env.${projectIdOrAlias}` and common param values can be put in `extensions/${extensionInstanceId}.env`.
package/lib/api.js CHANGED
@@ -72,6 +72,7 @@ var api = {
72
72
  cloudbillingOrigin: utils.envOverride("FIREBASE_CLOUDBILLING_URL", "https://cloudbilling.googleapis.com"),
73
73
  cloudloggingOrigin: utils.envOverride("FIREBASE_CLOUDLOGGING_URL", "https://logging.googleapis.com"),
74
74
  containerRegistryDomain: utils.envOverride("CONTAINER_REGISTRY_DOMAIN", "gcr.io"),
75
+ artifactRegistryDomain: utils.envOverride("ARTIFACT_REGISTRY_DOMAIN", "https://artifactregistry.googleapis.com"),
75
76
  appDistributionOrigin: utils.envOverride("FIREBASE_APP_DISTRIBUTION_URL", "https://firebaseappdistribution.googleapis.com"),
76
77
  appengineOrigin: utils.envOverride("FIREBASE_APPENGINE_URL", "https://appengine.googleapis.com"),
77
78
  authOrigin: utils.envOverride("FIREBASE_AUTH_URL", "https://accounts.google.com"),
@@ -95,6 +96,7 @@ var api = {
95
96
  functionsUploadRegion: utils.envOverride("FIREBASE_FUNCTIONS_UPLOAD_REGION", "us-central1"),
96
97
  functionsDefaultRegion: utils.envOverride("FIREBASE_FUNCTIONS_DEFAULT_REGION", "us-central1"),
97
98
  cloudschedulerOrigin: utils.envOverride("FIREBASE_CLOUDSCHEDULER_URL", "https://cloudscheduler.googleapis.com"),
99
+ cloudTasksOrigin: utils.envOverride("FIREBASE_CLOUD_TAKS_URL", "https://cloudtasks.googleapis.com"),
98
100
  pubsubOrigin: utils.envOverride("FIREBASE_PUBSUB_URL", "https://pubsub.googleapis.com"),
99
101
  googleOrigin: utils.envOverride("FIREBASE_TOKEN_URL", utils.envOverride("FIREBASE_GOOGLE_URL", "https://www.googleapis.com")),
100
102
  hostingOrigin: utils.envOverride("FIREBASE_HOSTING_URL", "https://web.app"),
@@ -115,6 +117,7 @@ var api = {
115
117
  serviceUsageOrigin: utils.envOverride("FIREBASE_SERVICE_USAGE_URL", "https://serviceusage.googleapis.com"),
116
118
  githubOrigin: utils.envOverride("GITHUB_URL", "https://github.com"),
117
119
  githubApiOrigin: utils.envOverride("GITHUB_API_URL", "https://api.github.com"),
120
+ secretManagerOrigin: utils.envOverride("CLOUD_SECRET_MANAGER_URL", "https://secretmanager.googleapis.com"),
118
121
  githubClientId: utils.envOverride("GITHUB_CLIENT_ID", "89cf50f02ac6aaed3484"),
119
122
  githubClientSecret: utils.envOverride("GITHUB_CLIENT_SECRET", "3330d14abc895d9a74d5f17cd7a00711fa2c5bf0"),
120
123
  setRefreshToken: function (token) {
package/lib/apiv2.js CHANGED
@@ -118,8 +118,10 @@ class Client {
118
118
  reqOptions.headers.set("User-Agent", `FirebaseCLI/${CLI_VERSION}`);
119
119
  }
120
120
  reqOptions.headers.set("X-Client-Version", `FirebaseCLI/${CLI_VERSION}`);
121
- if (reqOptions.responseType === "json") {
122
- reqOptions.headers.set("Content-Type", "application/json");
121
+ if (!reqOptions.headers.has("Content-Type")) {
122
+ if (reqOptions.responseType === "json") {
123
+ reqOptions.headers.set("Content-Type", "application/json");
124
+ }
123
125
  }
124
126
  return reqOptions;
125
127
  }
@@ -217,11 +219,12 @@ class Client {
217
219
  }
218
220
  let body;
219
221
  if (options.responseType === "json") {
220
- if (res.status === 204) {
222
+ const text = await res.text();
223
+ if (!text.length) {
221
224
  body = undefined;
222
225
  }
223
226
  else {
224
- body = (await res.json());
227
+ body = JSON.parse(text);
225
228
  }
226
229
  }
227
230
  else if (options.responseType === "stream") {
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fs = require("fs-extra");
4
+ const os = require("os");
5
+ const path = require("path");
6
+ const spawn = require("cross-spawn");
7
+ const uuid = require("uuid");
8
+ const command_1 = require("../command");
9
+ const downloadUtils = require("../downloadUtils");
10
+ const error_1 = require("../error");
11
+ const logger_1 = require("../logger");
12
+ const rimraf = require("rimraf");
13
+ const utils = require("../utils");
14
+ var SymbolGenerator;
15
+ (function (SymbolGenerator) {
16
+ SymbolGenerator["breakpad"] = "breakpad";
17
+ SymbolGenerator["csym"] = "csym";
18
+ })(SymbolGenerator || (SymbolGenerator = {}));
19
+ const SYMBOL_CACHE_ROOT_DIR = process.env.FIREBASE_CRASHLYTICS_CACHE_PATH || os.tmpdir();
20
+ const JAR_CACHE_DIR = process.env.FIREBASE_CRASHLYTICS_BUILDTOOLS_PATH ||
21
+ path.join(os.homedir(), ".cache", "firebase", "crashlytics", "buildtools");
22
+ const JAR_VERSION = "2.8.0";
23
+ const JAR_URL = `https://dl.google.com/android/maven2/com/google/firebase/firebase-crashlytics-buildtools/${JAR_VERSION}/firebase-crashlytics-buildtools-${JAR_VERSION}.jar`;
24
+ exports.default = new command_1.Command("crashlytics:symbols:upload <symbolFiles...>")
25
+ .description("Upload symbols for native code, to symbolicate stack traces.")
26
+ .option("--app <appID>", "the app id of your Firebase app")
27
+ .option("--generator [breakpad|csym]", "the symbol generator being used, defaults to breakpad.")
28
+ .option("--dry-run", "generate symbols without uploading them")
29
+ .option("--debug", "print debug output and logging from the underlying uploader tool")
30
+ .action(async (symbolFiles, options) => {
31
+ const app = getGoogleAppID(options) || "";
32
+ const generator = getSymbolGenerator(options);
33
+ const dryRun = !!options.dryRun;
34
+ const debug = !!options.debug;
35
+ let jarFile = await downloadBuiltoolsJar();
36
+ if (process.env.LOCAL_JAR) {
37
+ jarFile = process.env.LOCAL_JAR;
38
+ }
39
+ const jarOptions = {
40
+ jarFile,
41
+ app,
42
+ generator,
43
+ cachePath: path.join(SYMBOL_CACHE_ROOT_DIR, `crashlytics-${uuid.v4()}`, "nativeSymbols", app.replace(/:/g, "-"), generator),
44
+ symbolFile: "",
45
+ generate: true,
46
+ };
47
+ for (const symbolFile of symbolFiles) {
48
+ utils.logBullet(`Generating symbols for ${symbolFile}`);
49
+ const generateArgs = buildArgs(Object.assign(Object.assign({}, jarOptions), { symbolFile }));
50
+ const output = runJar(generateArgs, debug);
51
+ if (output.length > 0) {
52
+ utils.logBullet(output);
53
+ }
54
+ else {
55
+ utils.logBullet(`Generated symbols for ${symbolFile}`);
56
+ utils.logBullet(`Output Path: ${jarOptions.cachePath}`);
57
+ }
58
+ }
59
+ if (dryRun) {
60
+ utils.logBullet("Skipping upload because --dry-run was passed");
61
+ return;
62
+ }
63
+ utils.logBullet(`Uploading all generated symbols`);
64
+ const uploadArgs = buildArgs(Object.assign(Object.assign({}, jarOptions), { generate: false }));
65
+ const output = runJar(uploadArgs, debug);
66
+ if (output.length > 0) {
67
+ utils.logBullet(output);
68
+ }
69
+ else {
70
+ utils.logBullet("Successfully uploaded all symbols");
71
+ }
72
+ });
73
+ function getGoogleAppID(options) {
74
+ if (!options.app) {
75
+ throw new error_1.FirebaseError("set the --app option to a valid Firebase app id and try again");
76
+ }
77
+ return options.app;
78
+ }
79
+ function getSymbolGenerator(options) {
80
+ if (!options.generator) {
81
+ return SymbolGenerator.breakpad;
82
+ }
83
+ if (!Object.values(SymbolGenerator).includes(options.generator)) {
84
+ throw new error_1.FirebaseError('--symbol-generator should be set to either "breakpad" or "csym"');
85
+ }
86
+ return options.generator;
87
+ }
88
+ async function downloadBuiltoolsJar() {
89
+ const jarPath = path.join(JAR_CACHE_DIR, `crashlytics-buildtools-${JAR_VERSION}.jar`);
90
+ if (fs.existsSync(jarPath)) {
91
+ logger_1.logger.debug(`Buildtools Jar already downloaded at ${jarPath}`);
92
+ return jarPath;
93
+ }
94
+ if (fs.existsSync(JAR_CACHE_DIR)) {
95
+ logger_1.logger.debug(`Deleting Jar cache at ${JAR_CACHE_DIR} because the CLI was run with a newer Jar version`);
96
+ rimraf.sync(JAR_CACHE_DIR);
97
+ }
98
+ utils.logBullet("Downloading buildtools.jar to " + jarPath);
99
+ utils.logBullet("For open source licenses used by this command, look in the META-INF directory in the buildtools.jar file");
100
+ const tmpfile = await downloadUtils.downloadToTmp(JAR_URL);
101
+ fs.mkdirSync(JAR_CACHE_DIR, { recursive: true });
102
+ fs.copySync(tmpfile, jarPath);
103
+ return jarPath;
104
+ }
105
+ function buildArgs(options) {
106
+ const baseArgs = [
107
+ "-jar",
108
+ options.jarFile,
109
+ `-symbolGenerator=${options.generator}`,
110
+ `-symbolFileCacheDir=${options.cachePath}`,
111
+ "-verbose",
112
+ ];
113
+ if (options.generate) {
114
+ return baseArgs.concat(["-generateNativeSymbols", `-unstrippedLibrary=${options.symbolFile}`]);
115
+ }
116
+ return baseArgs.concat([
117
+ "-uploadNativeSymbols",
118
+ `-googleAppId=${options.app}`,
119
+ ]);
120
+ }
121
+ function runJar(args, debug) {
122
+ var _a, _b, _c;
123
+ const outputs = spawn.sync("java", args, {
124
+ stdio: debug ? "inherit" : "pipe",
125
+ });
126
+ if (outputs.status || 0 > 0) {
127
+ if (!debug) {
128
+ utils.logWarning(((_a = outputs.stdout) === null || _a === void 0 ? void 0 : _a.toString()) || "An unknown error occurred");
129
+ }
130
+ throw new error_1.FirebaseError("Failed to upload symbols");
131
+ }
132
+ if (!debug) {
133
+ let logRegex = /(Generated symbol file.*$)/m;
134
+ let matched = (((_b = outputs.stdout) === null || _b === void 0 ? void 0 : _b.toString()) || "").match(logRegex);
135
+ if (matched) {
136
+ return matched[1];
137
+ }
138
+ logRegex = /(Crashlytics symbol file uploaded successfully.*$)/m;
139
+ matched = (((_c = outputs.stdout) === null || _c === void 0 ? void 0 : _c.toString()) || "").match(logRegex);
140
+ if (matched) {
141
+ return matched[1];
142
+ }
143
+ return "";
144
+ }
145
+ return "";
146
+ }
@@ -9,7 +9,15 @@ const deploy = require("../deploy");
9
9
  const requireConfig = require("../requireConfig");
10
10
  const { filterTargets } = require("../filterTargets");
11
11
  const { requireHostingSite } = require("../requireHostingSite");
12
- const VALID_TARGETS = ["database", "storage", "firestore", "functions", "hosting", "remoteconfig"];
12
+ const VALID_TARGETS = [
13
+ "database",
14
+ "storage",
15
+ "firestore",
16
+ "functions",
17
+ "hosting",
18
+ "remoteconfig",
19
+ "extensions",
20
+ ];
13
21
  const TARGET_PERMISSIONS = {
14
22
  database: ["firebasedatabase.instances.update"],
15
23
  hosting: ["firebasehosting.sites.update"],
@@ -52,6 +52,8 @@ exports.default = new command_1.Command("ext:configure <extensionInstanceId>")
52
52
  paramSpecs: paramSpecWithNewDefaults,
53
53
  nonInteractive: options.nonInteractive,
54
54
  paramsEnvPath: options.params,
55
+ instanceId,
56
+ reconfiguring: true,
55
57
  });
56
58
  if (immutableParams.length) {
57
59
  const plural = immutableParams.length > 1;
@@ -67,7 +69,7 @@ exports.default = new command_1.Command("ext:configure <extensionInstanceId>")
67
69
  ", uninstall the extension, then install a new instance of this extension.");
68
70
  }
69
71
  spinner.start();
70
- const res = await extensionsApi.configureInstance(projectId, instanceId, params);
72
+ const res = await extensionsApi.configureInstance({ projectId, instanceId, params });
71
73
  spinner.stop();
72
74
  utils.logLabeledSuccess(extensionsHelper_1.logPrefix, `successfully configured ${clc.bold(instanceId)}.`);
73
75
  utils.logLabeledBullet(extensionsHelper_1.logPrefix, marked(`You can view your reconfigured instance in the Firebase console: ${utils.consoleUrl(projectId, `/extensions/instances/${instanceId}?tab=config`)}`));
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const clc = require("cli-color");
4
+ const semver = require("semver");
5
+ const refs = require("../extensions/refs");
6
+ const utils = require("../utils");
7
+ const command_1 = require("../command");
8
+ const prompt_1 = require("../prompt");
9
+ const extensionsHelper_1 = require("../extensions/extensionsHelper");
10
+ const extensionsApi_1 = require("../extensions/extensionsApi");
11
+ const versionHelper_1 = require("../extensions/versionHelper");
12
+ const requireAuth_1 = require("../requireAuth");
13
+ const error_1 = require("../error");
14
+ exports.default = new command_1.Command("ext:dev:deprecate <extensionRef> <versionPredicate>")
15
+ .description("deprecate extension versions that match the version predicate")
16
+ .option("-m, --message <deprecationMessage>", "deprecation message")
17
+ .option("-f, --force", "override deprecation message for existing deprecated extension versions that match")
18
+ .before(requireAuth_1.requireAuth)
19
+ .before(extensionsHelper_1.ensureExtensionsApiEnabled)
20
+ .action(async (extensionRef, versionPredicate, options) => {
21
+ const { publisherId, extensionId, version } = refs.parse(extensionRef);
22
+ if (version) {
23
+ throw new error_1.FirebaseError(`The input extension reference must be of the format ${clc.bold("<publisherId>/<extensionId>")}. Version should be supplied in the version predicate argument.`);
24
+ }
25
+ if (!publisherId || !extensionId) {
26
+ throw new error_1.FirebaseError(`Error parsing publisher ID and extension ID from extension reference '${clc.bold(extensionRef)}'. Please use the format '${clc.bold("<publisherId>/<extensionId>")}'.`);
27
+ }
28
+ const { comparator, targetSemVer } = versionHelper_1.parseVersionPredicate(versionPredicate);
29
+ const filter = `id${comparator}"${targetSemVer}"`;
30
+ const extensionVersions = await extensionsApi_1.listExtensionVersions(extensionRef, filter);
31
+ const filteredExtensionVersions = extensionVersions
32
+ .sort((ev1, ev2) => {
33
+ return -semver.compare(ev1.spec.version, ev2.spec.version);
34
+ })
35
+ .filter((extensionVersion) => {
36
+ if (extensionVersion.state === "DEPRECATED" && !options.force) {
37
+ return false;
38
+ }
39
+ const message = extensionVersion.state === "DEPRECATED" ? ", will overwrite deprecation message" : "";
40
+ utils.logLabeledBullet(extensionVersion.ref, extensionVersion.state + message);
41
+ return true;
42
+ });
43
+ if (filteredExtensionVersions.length > 0) {
44
+ if (!options.force) {
45
+ const confirmMessage = "You are about to deprecate these extension version(s). Do you wish to continue?";
46
+ const consent = await prompt_1.promptOnce({
47
+ type: "confirm",
48
+ message: confirmMessage,
49
+ default: false,
50
+ });
51
+ if (!consent) {
52
+ throw new error_1.FirebaseError("Deprecation canceled.");
53
+ }
54
+ }
55
+ }
56
+ else {
57
+ throw new error_1.FirebaseError("No extension versions matched the version predicate.");
58
+ }
59
+ await utils.allSettled(filteredExtensionVersions.map(async (extensionVersion) => {
60
+ await extensionsApi_1.deprecateExtensionVersion(extensionVersion.ref, options.deprecationMessage);
61
+ }));
62
+ utils.logLabeledSuccess(extensionsHelper_1.logPrefix, "successfully deprecated extension version(s).");
63
+ });
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const clc = require("cli-color");
4
+ const semver = require("semver");
5
+ const refs = require("../extensions/refs");
6
+ const utils = require("../utils");
7
+ const command_1 = require("../command");
8
+ const prompt_1 = require("../prompt");
9
+ const extensionsHelper_1 = require("../extensions/extensionsHelper");
10
+ const extensionsApi_1 = require("../extensions/extensionsApi");
11
+ const versionHelper_1 = require("../extensions/versionHelper");
12
+ const requireAuth_1 = require("../requireAuth");
13
+ const error_1 = require("../error");
14
+ exports.default = new command_1.Command("ext:dev:undeprecate <extensionRef> <versionPredicate>")
15
+ .description("undeprecate extension versions that match the version predicate")
16
+ .before(requireAuth_1.requireAuth)
17
+ .before(extensionsHelper_1.ensureExtensionsApiEnabled)
18
+ .action(async (extensionRef, versionPredicate, options) => {
19
+ const { publisherId, extensionId, version } = refs.parse(extensionRef);
20
+ if (version) {
21
+ throw new error_1.FirebaseError(`The input extension reference must be of the format ${clc.bold("<publisherId>/<extensionId>")}. Version should be supplied in the version predicate argument.`);
22
+ }
23
+ if (!publisherId || !extensionId) {
24
+ throw new error_1.FirebaseError(`Error parsing publisher ID and extension ID from extension reference '${clc.bold(extensionRef)}'. Please use the format '${clc.bold("<publisherId>/<extensionId>")}'.`);
25
+ }
26
+ const { comparator, targetSemVer } = versionHelper_1.parseVersionPredicate(versionPredicate);
27
+ const filter = `id${comparator}"${targetSemVer}"`;
28
+ const extensionVersions = await extensionsApi_1.listExtensionVersions(extensionRef, filter);
29
+ extensionVersions
30
+ .sort((ev1, ev2) => {
31
+ return -semver.compare(ev1.spec.version, ev2.spec.version);
32
+ })
33
+ .forEach((extensionVersion) => {
34
+ utils.logLabeledBullet(extensionVersion.ref, extensionVersion.state);
35
+ });
36
+ if (extensionVersions.length > 0) {
37
+ if (!options.force) {
38
+ const confirmMessage = "You are about to undeprecate these extension version(s). Do you wish to continue?";
39
+ const consent = await prompt_1.promptOnce({
40
+ type: "confirm",
41
+ message: confirmMessage,
42
+ default: false,
43
+ });
44
+ if (!consent) {
45
+ throw new error_1.FirebaseError("Undeprecation canceled.");
46
+ }
47
+ }
48
+ }
49
+ else {
50
+ throw new error_1.FirebaseError("No extension versions matched the version predicate.");
51
+ }
52
+ await utils.allSettled(extensionVersions.map(async (extensionVersion) => {
53
+ await extensionsApi_1.undeprecateExtensionVersion(extensionVersion.ref);
54
+ }));
55
+ utils.logLabeledSuccess(extensionsHelper_1.logPrefix, "successfully undeprecated extension version(s).");
56
+ });
@@ -12,11 +12,12 @@ const error_1 = require("../error");
12
12
  const checkMinRequiredVersion_1 = require("../checkMinRequiredVersion");
13
13
  module.exports = new command_1.Command("ext:dev:unpublish <extensionRef>")
14
14
  .description("unpublish an extension")
15
+ .withForce()
15
16
  .help("use this command to unpublish an extension, and make it unavailable for developers to install or reconfigure. " +
16
17
  "Specify the extension you want to unpublish using the format '<publisherId>/<extensionId>.")
17
18
  .before(requireAuth_1.requireAuth)
18
19
  .before(checkMinRequiredVersion_1.checkMinRequiredVersion, "extDevMinVersion")
19
- .action(async (extensionRef) => {
20
+ .action(async (extensionRef, options) => {
20
21
  const { publisherId, extensionId, version } = refs.parse(extensionRef);
21
22
  utils.logLabeledWarning(extensionsHelper_1.logPrefix, "If you unpublish this extension, developers won't be able to install it. For developers who currently have this extension installed, it will continue to run and will appear as unpublished when listed in the Firebase console or Firebase CLI.");
22
23
  utils.logLabeledWarning("This is a permanent action", `Once unpublished, you may never use the extension name '${clc.bold(extensionId)}' again.`);
@@ -24,14 +25,20 @@ module.exports = new command_1.Command("ext:dev:unpublish <extensionRef>")
24
25
  throw new error_1.FirebaseError(`Unpublishing a single version is not currently supported. You can only unpublish ${clc.bold("ALL versions")} of an extension. To unpublish all versions, please remove the version from the reference.`);
25
26
  }
26
27
  await extensionsApi_1.getExtension(extensionRef);
27
- const consent = await comfirmUnpublish(publisherId, extensionId);
28
+ const consent = await comfirmUnpublish(publisherId, extensionId, options);
28
29
  if (!consent) {
29
30
  throw new error_1.FirebaseError("unpublishing cancelled.");
30
31
  }
31
32
  await extensionsApi_1.unpublishExtension(extensionRef);
32
33
  utils.logLabeledSuccess(extensionsHelper_1.logPrefix, "successfully unpublished all versions of this extension.");
33
34
  });
34
- async function comfirmUnpublish(publisherId, extensionId) {
35
+ async function comfirmUnpublish(publisherId, extensionId, options) {
36
+ if (options.nonInteractive && !options.force) {
37
+ throw new error_1.FirebaseError("Pass the --force flag to use this command in non-interactive mode");
38
+ }
39
+ if (options.nonInteractive && options.force) {
40
+ return true;
41
+ }
35
42
  const message = `You are about to unpublish ALL versions of ${clc.green(`${publisherId}/${extensionId}`)}.\nDo you wish to continue? `;
36
43
  return prompt_1.promptOnce({
37
44
  type: "confirm",
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const checkMinRequiredVersion_1 = require("../checkMinRequiredVersion");
4
+ const command_1 = require("../command");
5
+ const planner = require("../deploy/extensions/planner");
6
+ const export_1 = require("../extensions/export");
7
+ const extensionsHelper_1 = require("../extensions/extensionsHelper");
8
+ const functional_1 = require("../functional");
9
+ const getProjectNumber_1 = require("../getProjectNumber");
10
+ const logger_1 = require("../logger");
11
+ const projectUtils_1 = require("../projectUtils");
12
+ const prompt_1 = require("../prompt");
13
+ const requirePermissions_1 = require("../requirePermissions");
14
+ module.exports = new command_1.Command("ext:export")
15
+ .description("export all Extension instances installed on a project to a local Firebase directory")
16
+ .before(requirePermissions_1.requirePermissions, ["firebaseextensions.instances.list"])
17
+ .before(extensionsHelper_1.ensureExtensionsApiEnabled)
18
+ .before(checkMinRequiredVersion_1.checkMinRequiredVersion, "extMinVersion")
19
+ .withForce()
20
+ .action(async (options) => {
21
+ const projectId = projectUtils_1.needProjectId(options);
22
+ const projectNumber = await getProjectNumber_1.getProjectNumber(options);
23
+ const have = await Promise.all((await planner.have(projectId)).map(async (i) => {
24
+ const subbed = await export_1.setSecretParamsToLatest(i);
25
+ return export_1.parameterizeProject(projectId, projectNumber, subbed);
26
+ }));
27
+ if (have.length == 0) {
28
+ logger_1.logger.info(`No extension instances installed on ${projectId}, so there is nothing to export.`);
29
+ return;
30
+ }
31
+ const [withRef, withoutRef] = functional_1.partition(have, (s) => !!s.ref);
32
+ export_1.displayExportInfo(withRef, withoutRef);
33
+ if (!options.nonInteractive &&
34
+ !options.force &&
35
+ !(await prompt_1.promptOnce({
36
+ message: "Do you wish to add these Extension instances to firebase.json?",
37
+ type: "confirm",
38
+ default: true,
39
+ }))) {
40
+ logger_1.logger.info("Exiting. No changes made.");
41
+ return;
42
+ }
43
+ await export_1.writeFiles(withRef, options);
44
+ });
@@ -14,6 +14,7 @@ const command_1 = require("../command");
14
14
  const error_1 = require("../error");
15
15
  const projectUtils_1 = require("../projectUtils");
16
16
  const extensionsApi = require("../extensions/extensionsApi");
17
+ const secretsUtils = require("../extensions/secretsUtils");
17
18
  const provisioningHelper = require("../extensions/provisioningHelper");
18
19
  const refs = require("../extensions/refs");
19
20
  const warnings_1 = require("../extensions/warnings");
@@ -37,7 +38,8 @@ async function installExtension(options) {
37
38
  const spinner = ora.default();
38
39
  try {
39
40
  await provisioningHelper.checkProductsProvisioned(projectId, spec);
40
- if (spec.billingRequired) {
41
+ const usesSecrets = secretsUtils.usesSecrets(spec);
42
+ if (spec.billingRequired || usesSecrets) {
41
43
  const enabled = await cloudbilling_1.checkBillingEnabled(projectId);
42
44
  if (!enabled && nonInteractive) {
43
45
  throw new error_1.FirebaseError(`This extension requires the Blaze plan, but project ${projectId} is not on the Blaze plan. ` +
@@ -45,12 +47,29 @@ async function installExtension(options) {
45
47
  }
46
48
  else if (!enabled) {
47
49
  await billingMigrationHelper_1.displayNode10CreateBillingNotice(spec, false);
48
- await checkProjectBilling_1.enableBilling(projectId, spec.displayName || spec.name);
50
+ await checkProjectBilling_1.enableBilling(projectId);
49
51
  }
50
52
  else {
51
53
  await billingMigrationHelper_1.displayNode10CreateBillingNotice(spec, !nonInteractive);
52
54
  }
53
55
  }
56
+ const apis = spec.apis || [];
57
+ if (usesSecrets) {
58
+ apis.push({
59
+ apiName: "secretmanager.googleapis.com",
60
+ reason: `To access and manage secrets which are used by this extension. By using this product you agree to the terms and conditions of the following license: https://console.cloud.google.com/tos?id=cloud&project=${projectId}`,
61
+ });
62
+ }
63
+ if (apis.length) {
64
+ askUserForConsent.displayApis(spec.displayName || spec.name, projectId, apis);
65
+ const consented = await extensionsHelper_1.confirm({ nonInteractive, force, default: true });
66
+ if (!consented) {
67
+ throw new error_1.FirebaseError("Without explicit consent for the APIs listed, we cannot deploy this extension.");
68
+ }
69
+ }
70
+ if (usesSecrets) {
71
+ await secretsUtils.ensureSecretManagerApiEnabled(options);
72
+ }
54
73
  const roles = spec.roles ? spec.roles.map((role) => role.role) : [];
55
74
  if (roles.length) {
56
75
  await askUserForConsent.displayRoles(spec.displayName || spec.name, projectId, roles);
@@ -86,6 +105,7 @@ async function installExtension(options) {
86
105
  paramSpecs: spec.params,
87
106
  nonInteractive,
88
107
  paramsEnvPath,
108
+ instanceId,
89
109
  });
90
110
  spinner.text = "Installing your extension instance. This usually takes 3 to 5 minutes...";
91
111
  spinner.start();
@@ -106,6 +126,7 @@ async function installExtension(options) {
106
126
  paramSpecs: spec.params,
107
127
  nonInteractive,
108
128
  paramsEnvPath,
129
+ instanceId,
109
130
  });
110
131
  spinner.text = "Updating your extension instance. This usually takes 3 to 5 minutes...";
111
132
  spinner.start();
@@ -164,7 +185,7 @@ async function infoInstallByReference(extensionName) {
164
185
  }
165
186
  const extVersion = await extensionsApi.getExtensionVersion(extensionName);
166
187
  displayExtensionInfo_1.displayExtInfo(extensionName, ref.publisherId, extVersion.spec, true);
167
- warnings_1.displayWarningPrompts(ref.publisherId, extension.registryLaunchStage, extVersion);
188
+ await warnings_1.displayWarningPrompts(ref.publisherId, extension.registryLaunchStage, extVersion);
168
189
  return extVersion;
169
190
  }
170
191
  exports.default = new command_1.Command("ext:install [extensionName]")
@@ -10,6 +10,7 @@ const command_1 = require("../command");
10
10
  const error_1 = require("../error");
11
11
  const projectUtils_1 = require("../projectUtils");
12
12
  const extensionsApi = require("../extensions/extensionsApi");
13
+ const secretsUtils = require("../extensions/secretsUtils");
13
14
  const extensionsHelper_1 = require("../extensions/extensionsHelper");
14
15
  const prompt_1 = require("../prompt");
15
16
  const requirePermissions_1 = require("../requirePermissions");
@@ -51,11 +52,16 @@ exports.default = new command_1.Command("ext:uninstall <extensionInstanceId>")
51
52
  }
52
53
  if (!options.force) {
53
54
  const serviceAccountMessage = `Uninstalling deletes the service account used by this extension instance:\n${clc.bold(instance.serviceAccountEmail)}\n\n`;
55
+ const managedSecrets = await secretsUtils.getManagedSecrets(instance);
54
56
  const resourcesMessage = _.get(instance, "config.source.spec.resources", []).length
55
57
  ? "Uninstalling deletes all extension resources created for this extension instance:\n" +
56
58
  instance.config.source.spec.resources
57
59
  .map((resource) => clc.bold(`- ${extensionsHelper_1.resourceTypeToNiceName[resource.type] || resource.type}: ${resource.name} \n`))
58
60
  .join("") +
61
+ managedSecrets
62
+ .map(secretsUtils.prettySecretName)
63
+ .map((s) => clc.bold(`- Secret: ${s}\n`))
64
+ .join("") +
59
65
  "\n"
60
66
  : "";
61
67
  const artifactsMessage = `The following ${clc.bold("will not")} be deleted:\n` +
@@ -12,6 +12,7 @@ const billingMigrationHelper_1 = require("../extensions/billingMigrationHelper")
12
12
  const checkProjectBilling_1 = require("../extensions/checkProjectBilling");
13
13
  const cloudbilling_1 = require("../gcp/cloudbilling");
14
14
  const extensionsApi = require("../extensions/extensionsApi");
15
+ const secretsUtils = require("../extensions/secretsUtils");
15
16
  const provisioningHelper = require("../extensions/provisioningHelper");
16
17
  const extensionsHelper_1 = require("../extensions/extensionsHelper");
17
18
  const paramHelper = require("../extensions/paramHelper");
@@ -124,7 +125,8 @@ exports.default = new command_1.Command("ext:update <extensionInstanceId> [updat
124
125
  force: options.force,
125
126
  });
126
127
  await provisioningHelper.checkProductsProvisioned(projectId, newSpec);
127
- if (newSpec.billingRequired) {
128
+ const usesSecrets = secretsUtils.usesSecrets(newSpec);
129
+ if (newSpec.billingRequired || usesSecrets) {
128
130
  const enabled = await cloudbilling_1.checkBillingEnabled(projectId);
129
131
  billingMigrationHelper_1.displayNode10UpdateBillingNotice(existingSpec, newSpec);
130
132
  if (!(await extensionsHelper_1.confirm({
@@ -136,7 +138,7 @@ exports.default = new command_1.Command("ext:update <extensionInstanceId> [updat
136
138
  }
137
139
  if (!enabled) {
138
140
  if (!options.nonInteractive) {
139
- await checkProjectBilling_1.enableBilling(projectId, instanceId);
141
+ await checkProjectBilling_1.enableBilling(projectId);
140
142
  }
141
143
  else {
142
144
  throw new error_1.FirebaseError("The extension requires your project to be upgraded to the Blaze plan. " +
@@ -144,7 +146,11 @@ exports.default = new command_1.Command("ext:update <extensionInstanceId> [updat
144
146
  marked(`https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`));
145
147
  }
146
148
  }
149
+ if (usesSecrets) {
150
+ await secretsUtils.ensureSecretManagerApiEnabled(options);
151
+ }
147
152
  }
153
+ const oldParamValues = Object.assign({}, existingParams);
148
154
  const newParams = await paramHelper.getParamsForUpdate({
149
155
  spec: existingSpec,
150
156
  newSpec,
@@ -152,6 +158,7 @@ exports.default = new command_1.Command("ext:update <extensionInstanceId> [updat
152
158
  projectId,
153
159
  paramsEnvPath: options.params,
154
160
  nonInteractive: options.nonInteractive,
161
+ instanceId,
155
162
  });
156
163
  spinner.start();
157
164
  const updateOptions = {
@@ -164,7 +171,7 @@ exports.default = new command_1.Command("ext:update <extensionInstanceId> [updat
164
171
  else {
165
172
  updateOptions.source = newSource;
166
173
  }
167
- if (!_.isEqual(newParams, existingParams)) {
174
+ if (!_.isEqual(newParams, oldParamValues)) {
168
175
  updateOptions.params = newParams;
169
176
  }
170
177
  await updateHelper_1.update(updateOptions);