firebase-tools 10.1.5 → 10.2.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 (40) hide show
  1. package/lib/api.js +1 -0
  2. package/lib/auth.js +62 -25
  3. package/lib/commands/ext-configure.js +1 -0
  4. package/lib/commands/ext-install.js +1 -0
  5. package/lib/commands/ext-uninstall.js +1 -0
  6. package/lib/commands/ext-update.js +1 -0
  7. package/lib/commands/functions-secrets-access.js +17 -0
  8. package/lib/commands/functions-secrets-destroy.js +40 -0
  9. package/lib/commands/functions-secrets-get.js +21 -0
  10. package/lib/commands/functions-secrets-prune.js +50 -0
  11. package/lib/commands/functions-secrets-set.js +46 -0
  12. package/lib/commands/index.js +7 -3
  13. package/lib/commands/login.js +1 -1
  14. package/lib/deploy/functions/backend.js +9 -1
  15. package/lib/deploy/functions/ensure.js +112 -0
  16. package/lib/deploy/functions/ensureCloudBuildEnabled.js +0 -49
  17. package/lib/deploy/functions/prepare.js +12 -18
  18. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +1 -0
  19. package/lib/deploy/functions/runtimes/node/parseTriggers.js +12 -0
  20. package/lib/deploy/functions/validate.js +56 -1
  21. package/lib/emulator/controller.js +3 -1
  22. package/lib/emulator/emulatorLogger.js +7 -0
  23. package/lib/emulator/functionsEmulator.js +110 -78
  24. package/lib/emulator/functionsEmulatorRuntime.js +100 -83
  25. package/lib/emulator/functionsEmulatorShared.js +51 -1
  26. package/lib/emulator/functionsEmulatorShell.js +1 -2
  27. package/lib/emulator/functionsRuntimeWorker.js +1 -1
  28. package/lib/extensions/askUserForParam.js +1 -1
  29. package/lib/extensions/diagnose.js +56 -0
  30. package/lib/extensions/extensionsHelper.js +10 -1
  31. package/lib/extensions/secretsUtils.js +1 -1
  32. package/lib/functions/env.js +5 -7
  33. package/lib/functions/secrets.js +112 -0
  34. package/lib/gcp/cloudfunctions.js +2 -2
  35. package/lib/gcp/secretManager.js +128 -46
  36. package/lib/previews.js +1 -1
  37. package/lib/serve/functions.js +2 -2
  38. package/lib/utils.js +6 -1
  39. package/npm-shrinkwrap.json +2 -2
  40. package/package.json +1 -1
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pruneSecrets = exports.of = exports.ensureSecret = exports.ensureValidKey = exports.labels = exports.isFirebaseManaged = void 0;
4
+ const secretManager_1 = require("../gcp/secretManager");
5
+ const error_1 = require("../error");
6
+ const utils_1 = require("../utils");
7
+ const prompt_1 = require("../prompt");
8
+ const env_1 = require("./env");
9
+ const FIREBASE_MANGED = "firebase-managed";
10
+ function isFirebaseManaged(secret) {
11
+ return Object.keys(secret.labels || []).includes(FIREBASE_MANGED);
12
+ }
13
+ exports.isFirebaseManaged = isFirebaseManaged;
14
+ function labels() {
15
+ return { [FIREBASE_MANGED]: "true" };
16
+ }
17
+ exports.labels = labels;
18
+ function toUpperSnakeCase(key) {
19
+ return key
20
+ .replace("-", "_")
21
+ .replace(".", "_")
22
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
23
+ .toUpperCase();
24
+ }
25
+ async function ensureValidKey(key, options) {
26
+ const transformedKey = toUpperSnakeCase(key);
27
+ if (transformedKey !== key) {
28
+ if (options.force) {
29
+ throw new error_1.FirebaseError("Secret key must be in UPPER_SNAKE_CASE.");
30
+ }
31
+ (0, utils_1.logWarning)(`By convention, secret key must be in UPPER_SNAKE_CASE.`);
32
+ const confirm = await (0, prompt_1.promptOnce)({
33
+ name: "updateKey",
34
+ type: "confirm",
35
+ default: true,
36
+ message: `Would you like to use ${transformedKey} as key instead?`,
37
+ }, options);
38
+ if (!confirm) {
39
+ throw new error_1.FirebaseError("Secret key must be in UPPER_SNAKE_CASE.");
40
+ }
41
+ }
42
+ try {
43
+ (0, env_1.validateKey)(transformedKey);
44
+ }
45
+ catch (err) {
46
+ throw new error_1.FirebaseError(`Invalid secret key ${transformedKey}`, { children: [err] });
47
+ }
48
+ return transformedKey;
49
+ }
50
+ exports.ensureValidKey = ensureValidKey;
51
+ async function ensureSecret(projectId, name, options) {
52
+ try {
53
+ const secret = await (0, secretManager_1.getSecret)(projectId, name);
54
+ if (!isFirebaseManaged(secret)) {
55
+ if (!options.force) {
56
+ (0, utils_1.logWarning)("Your secret is not managed by Firebase. " +
57
+ "Firebase managed secrets are automatically pruned to reduce your monthly cost for using Secret Manager. ");
58
+ const confirm = await (0, prompt_1.promptOnce)({
59
+ name: "updateLabels",
60
+ type: "confirm",
61
+ default: true,
62
+ message: `Would you like to have your secret ${secret.name} managed by Firebase?`,
63
+ }, options);
64
+ if (confirm) {
65
+ return (0, secretManager_1.patchSecret)(projectId, secret.name, Object.assign(Object.assign({}, secret.labels), labels()));
66
+ }
67
+ }
68
+ }
69
+ return secret;
70
+ }
71
+ catch (err) {
72
+ if (err.status !== 404) {
73
+ throw err;
74
+ }
75
+ }
76
+ return await (0, secretManager_1.createSecret)(projectId, name, labels());
77
+ }
78
+ exports.ensureSecret = ensureSecret;
79
+ function of(endpoints) {
80
+ return endpoints.reduce((envs, endpoint) => [...envs, ...(endpoint.secretEnvironmentVariables || [])], []);
81
+ }
82
+ exports.of = of;
83
+ async function pruneSecrets(projectInfo, endpoints) {
84
+ const { projectId, projectNumber } = projectInfo;
85
+ const pruneKey = (name, version) => `${name}@${version}`;
86
+ const prunedSecrets = new Set();
87
+ const haveSecrets = await (0, secretManager_1.listSecrets)(projectId, `labels.${FIREBASE_MANGED}=true`);
88
+ for (const secret of haveSecrets) {
89
+ const versions = await (0, secretManager_1.listSecretVersions)(projectId, secret.name, `state: ENABLED`);
90
+ for (const version of versions) {
91
+ prunedSecrets.add(pruneKey(secret.name, version.versionId));
92
+ }
93
+ }
94
+ const sevs = of(endpoints).filter((sev) => sev.projectId === projectId || sev.projectId === projectNumber);
95
+ for (const sev of sevs) {
96
+ let name = sev.secret;
97
+ if (name.includes("/")) {
98
+ const secret = (0, secretManager_1.parseSecretResourceName)(name);
99
+ name = secret.name;
100
+ }
101
+ let version = sev.version;
102
+ if (version === "latest") {
103
+ const resolved = await (0, secretManager_1.getSecretVersion)(projectId, name, version);
104
+ version = resolved.versionId;
105
+ }
106
+ prunedSecrets.delete(pruneKey(name, version));
107
+ }
108
+ return Array.from(prunedSecrets)
109
+ .map((key) => key.split("@"))
110
+ .map(([secret, version]) => ({ projectId, version, secret, key: secret }));
111
+ }
112
+ exports.pruneSecrets = pruneSecrets;
@@ -240,7 +240,7 @@ function endpointFromFunction(gcfFunction) {
240
240
  if (uri) {
241
241
  endpoint.uri = uri;
242
242
  }
243
- proto.copyIfPresent(endpoint, gcfFunction, "serviceAccountEmail", "availableMemoryMb", "timeout", "minInstances", "maxInstances", "vpcConnector", "vpcConnectorEgressSettings", "ingressSettings", "labels", "environmentVariables", "sourceUploadUrl");
243
+ proto.copyIfPresent(endpoint, gcfFunction, "serviceAccountEmail", "availableMemoryMb", "timeout", "minInstances", "maxInstances", "vpcConnector", "vpcConnectorEgressSettings", "ingressSettings", "labels", "environmentVariables", "secretEnvironmentVariables", "sourceUploadUrl");
244
244
  return endpoint;
245
245
  }
246
246
  exports.endpointFromFunction = endpointFromFunction;
@@ -283,7 +283,7 @@ function functionFromEndpoint(endpoint, sourceUploadUrl) {
283
283
  else {
284
284
  gcfFunction.httpsTrigger = {};
285
285
  }
286
- proto.copyIfPresent(gcfFunction, endpoint, "serviceAccountEmail", "timeout", "availableMemoryMb", "minInstances", "maxInstances", "vpcConnector", "vpcConnectorEgressSettings", "ingressSettings", "environmentVariables");
286
+ proto.copyIfPresent(gcfFunction, endpoint, "serviceAccountEmail", "timeout", "availableMemoryMb", "minInstances", "maxInstances", "vpcConnector", "vpcConnectorEgressSettings", "ingressSettings", "environmentVariables", "secretEnvironmentVariables");
287
287
  return gcfFunction;
288
288
  }
289
289
  exports.functionFromEndpoint = functionFromEndpoint;
@@ -1,30 +1,88 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.grantServiceAgentRole = exports.addVersion = exports.createSecret = exports.toSecretVersionResourceName = exports.parseSecretVersionResourceName = exports.parseSecretResourceName = exports.secretExists = exports.getSecretVersion = exports.getSecret = exports.listSecrets = exports.secretManagerConsoleUri = void 0;
3
+ exports.ensureServiceAgentRole = exports.setIamPolicy = exports.getIamPolicy = exports.addVersion = exports.deleteSecret = exports.patchSecret = exports.createSecret = exports.toSecretVersionResourceName = exports.parseSecretVersionResourceName = exports.parseSecretResourceName = exports.secretExists = exports.destroySecretVersion = exports.accessSecretVersion = exports.getSecretVersion = exports.listSecretVersions = exports.listSecrets = exports.getSecret = exports.secretManagerConsoleUri = void 0;
4
4
  const utils_1 = require("../utils");
5
- const api_1 = require("../api");
5
+ const error_1 = require("../error");
6
6
  const apiv2_1 = require("../apiv2");
7
+ const api_1 = require("../api");
8
+ const SECRET_NAME_REGEX = new RegExp("projects\\/" +
9
+ "(?<project>(?:\\d+)|(?:[A-Za-z]+[A-Za-z\\d-]*[A-Za-z\\d]?))\\/" +
10
+ "secrets\\/" +
11
+ "(?<secret>[A-Za-z\\d\\-_]+)");
12
+ const SECRET_VERSION_NAME_REGEX = new RegExp(SECRET_NAME_REGEX.source + "\\/versions\\/" + "(?<version>latest|[0-9]+)");
7
13
  const secretManagerConsoleUri = (projectId) => `https://console.cloud.google.com/security/secret-manager?project=${projectId}`;
8
14
  exports.secretManagerConsoleUri = secretManagerConsoleUri;
9
- const apiClient = new apiv2_1.Client({ urlPrefix: api_1.secretManagerOrigin, apiVersion: "v1beta1" });
10
- async function listSecrets(projectId) {
11
- const listRes = await apiClient.get(`/projects/${projectId}/secrets`);
12
- return listRes.body.secrets.map((s) => parseSecretResourceName(s.name));
13
- }
14
- exports.listSecrets = listSecrets;
15
+ const API_VERSION = "v1";
16
+ const client = new apiv2_1.Client({ urlPrefix: api_1.secretManagerOrigin, apiVersion: API_VERSION });
15
17
  async function getSecret(projectId, name) {
16
18
  var _a;
17
- const getRes = await apiClient.get(`/projects/${projectId}/secrets/${name}`);
19
+ const getRes = await client.get(`projects/${projectId}/secrets/${name}`);
18
20
  const secret = parseSecretResourceName(getRes.body.name);
19
21
  secret.labels = (_a = getRes.body.labels) !== null && _a !== void 0 ? _a : {};
20
22
  return secret;
21
23
  }
22
24
  exports.getSecret = getSecret;
25
+ async function listSecrets(projectId, filter) {
26
+ var _a;
27
+ const secrets = [];
28
+ const path = `projects/${projectId}/secrets`;
29
+ const baseOpts = filter ? { queryParams: { filter } } : {};
30
+ let pageToken = "";
31
+ while (true) {
32
+ const opts = pageToken === ""
33
+ ? baseOpts
34
+ : Object.assign(Object.assign({}, baseOpts), { queryParams: Object.assign(Object.assign({}, baseOpts === null || baseOpts === void 0 ? void 0 : baseOpts.queryParams), { pageToken }) });
35
+ const res = await client.get(path, opts);
36
+ for (const s of res.body.secrets) {
37
+ secrets.push(Object.assign(Object.assign({}, parseSecretResourceName(s.name)), { labels: (_a = s.labels) !== null && _a !== void 0 ? _a : {} }));
38
+ }
39
+ if (!res.body.nextPageToken) {
40
+ break;
41
+ }
42
+ pageToken = res.body.nextPageToken;
43
+ }
44
+ return secrets;
45
+ }
46
+ exports.listSecrets = listSecrets;
47
+ async function listSecretVersions(projectId, name, filter) {
48
+ const secrets = [];
49
+ const path = `projects/${projectId}/secrets/${name}/versions`;
50
+ const baseOpts = filter ? { queryParams: { filter } } : {};
51
+ let pageToken = "";
52
+ while (true) {
53
+ const opts = pageToken === ""
54
+ ? baseOpts
55
+ : Object.assign(Object.assign({}, baseOpts), { queryParams: Object.assign(Object.assign({}, baseOpts === null || baseOpts === void 0 ? void 0 : baseOpts.queryParams), { pageToken }) });
56
+ const res = await client.get(path, opts);
57
+ for (const s of res.body.versions || []) {
58
+ secrets.push(Object.assign(Object.assign({}, parseSecretVersionResourceName(s.name)), { state: s.state }));
59
+ }
60
+ if (!res.body.nextPageToken) {
61
+ break;
62
+ }
63
+ pageToken = res.body.nextPageToken;
64
+ }
65
+ return secrets;
66
+ }
67
+ exports.listSecretVersions = listSecretVersions;
23
68
  async function getSecretVersion(projectId, name, version) {
24
- const getRes = await apiClient.get(`/projects/${projectId}/secrets/${name}/versions/${version}`);
25
- return parseSecretVersionResourceName(getRes.body.name);
69
+ const getRes = await client.get(`projects/${projectId}/secrets/${name}/versions/${version}`);
70
+ return Object.assign(Object.assign({}, parseSecretVersionResourceName(getRes.body.name)), { state: getRes.body.state });
26
71
  }
27
72
  exports.getSecretVersion = getSecretVersion;
73
+ async function accessSecretVersion(projectId, name, version) {
74
+ const res = await client.get(`projects/${projectId}/secrets/${name}/versions/${version}:access`);
75
+ return Buffer.from(res.body.payload.data, "base64").toString();
76
+ }
77
+ exports.accessSecretVersion = accessSecretVersion;
78
+ async function destroySecretVersion(projectId, name, version) {
79
+ if (version === "latest") {
80
+ const sv = await getSecretVersion(projectId, name, "latest");
81
+ version = sv.versionId;
82
+ }
83
+ await client.post(`projects/${projectId}/secrets/${name}/versions/${version}:destroy`);
84
+ }
85
+ exports.destroySecretVersion = destroySecretVersion;
28
86
  async function secretExists(projectId, name) {
29
87
  try {
30
88
  await getSecret(projectId, name);
@@ -39,21 +97,27 @@ async function secretExists(projectId, name) {
39
97
  }
40
98
  exports.secretExists = secretExists;
41
99
  function parseSecretResourceName(resourceName) {
42
- const nameTokens = resourceName.split("/");
100
+ const match = SECRET_NAME_REGEX.exec(resourceName);
101
+ if (!(match === null || match === void 0 ? void 0 : match.groups)) {
102
+ throw new error_1.FirebaseError(`Invalid secret resource name [${resourceName}].`);
103
+ }
43
104
  return {
44
- projectId: nameTokens[1],
45
- name: nameTokens[3],
105
+ projectId: match.groups.project,
106
+ name: match.groups.secret,
46
107
  };
47
108
  }
48
109
  exports.parseSecretResourceName = parseSecretResourceName;
49
110
  function parseSecretVersionResourceName(resourceName) {
50
- const nameTokens = resourceName.split("/");
111
+ const match = resourceName.match(SECRET_VERSION_NAME_REGEX);
112
+ if (!(match === null || match === void 0 ? void 0 : match.groups)) {
113
+ throw new error_1.FirebaseError(`Invalid secret version resource name [${resourceName}].`);
114
+ }
51
115
  return {
52
116
  secret: {
53
- projectId: nameTokens[1],
54
- name: nameTokens[3],
117
+ projectId: match.groups.project,
118
+ name: match.groups.secret,
55
119
  },
56
- versionId: nameTokens[5],
120
+ versionId: match.groups.version,
57
121
  };
58
122
  }
59
123
  exports.parseSecretVersionResourceName = parseSecretVersionResourceName;
@@ -62,7 +126,8 @@ function toSecretVersionResourceName(secretVersion) {
62
126
  }
63
127
  exports.toSecretVersionResourceName = toSecretVersionResourceName;
64
128
  async function createSecret(projectId, name, labels) {
65
- const createRes = await apiClient.post(`/projects/${projectId}/secrets`, {
129
+ const createRes = await client.post(`projects/${projectId}/secrets`, {
130
+ name,
66
131
  replication: {
67
132
  automatic: {},
68
133
  },
@@ -71,41 +136,58 @@ async function createSecret(projectId, name, labels) {
71
136
  return parseSecretResourceName(createRes.body.name);
72
137
  }
73
138
  exports.createSecret = createSecret;
74
- async function addVersion(secret, payloadData) {
75
- const res = await apiClient.post(`/projects/${secret.projectId}/secrets/${secret.name}:addVersion`, {
139
+ async function patchSecret(projectId, name, labels) {
140
+ const fullName = `projects/${projectId}/secrets/${name}`;
141
+ const res = await client.patch(fullName, { name: fullName, labels }, { queryParams: { updateMask: "labels" } });
142
+ return parseSecretResourceName(res.body.name);
143
+ }
144
+ exports.patchSecret = patchSecret;
145
+ async function deleteSecret(projectId, name) {
146
+ const path = `projects/${projectId}/secrets/${name}`;
147
+ await client.delete(path);
148
+ }
149
+ exports.deleteSecret = deleteSecret;
150
+ async function addVersion(projectId, name, payloadData) {
151
+ const res = await client.post(`projects/${projectId}/secrets/${name}:addVersion`, {
76
152
  payload: {
77
153
  data: Buffer.from(payloadData).toString("base64"),
78
154
  },
79
155
  });
80
- const nameTokens = res.body.name.split("/");
81
- return {
82
- secret: {
83
- projectId: nameTokens[1],
84
- name: nameTokens[3],
85
- },
86
- versionId: nameTokens[5],
87
- };
156
+ return Object.assign(Object.assign({}, parseSecretVersionResourceName(res.body.name)), { state: res.body.state });
88
157
  }
89
158
  exports.addVersion = addVersion;
90
- async function grantServiceAgentRole(secret, serviceAccountEmail, role) {
91
- const getPolicyRes = await apiClient.get(`/projects/${secret.projectId}/secrets/${secret.name}:getIamPolicy`);
92
- const bindings = getPolicyRes.body.bindings || [];
93
- if (bindings.find((b) => b.role == role &&
94
- b.members.find((m) => m == `serviceAccount:${serviceAccountEmail}`))) {
95
- return;
96
- }
97
- bindings.push({
98
- role: role,
99
- members: [`serviceAccount:${serviceAccountEmail}`],
100
- });
101
- await apiClient.post(`/projects/${secret.projectId}/secrets/${secret.name}:setIamPolicy`, {
159
+ async function getIamPolicy(secret) {
160
+ const res = await client.get(`projects/${secret.projectId}/secrets/${secret.name}:getIamPolicy`);
161
+ return res.body;
162
+ }
163
+ exports.getIamPolicy = getIamPolicy;
164
+ async function setIamPolicy(secret, bindings) {
165
+ await client.post(`projects/${secret.projectId}/secrets/${secret.name}:setIamPolicy`, {
102
166
  policy: {
103
167
  bindings,
104
168
  },
105
- updateMask: {
106
- paths: "bindings",
107
- },
169
+ updateMask: "bindings",
108
170
  });
109
- (0, utils_1.logLabeledSuccess)("SecretManager", `Granted ${role} on projects/${secret.projectId}/secrets/${secret.name} to ${serviceAccountEmail}`);
110
171
  }
111
- exports.grantServiceAgentRole = grantServiceAgentRole;
172
+ exports.setIamPolicy = setIamPolicy;
173
+ async function ensureServiceAgentRole(secret, serviceAccountEmails, role) {
174
+ const policy = await module.exports.getIamPolicy(secret);
175
+ const bindings = policy.bindings || [];
176
+ let binding = bindings.find((b) => b.role === role);
177
+ if (!binding) {
178
+ binding = { role, members: [] };
179
+ bindings.push(binding);
180
+ }
181
+ let shouldShortCircuit = true;
182
+ for (const serviceAccount of serviceAccountEmails) {
183
+ if (!binding.members.find((m) => m === `serviceAccount:${serviceAccount}`)) {
184
+ binding.members.push(`serviceAccount:${serviceAccount}`);
185
+ shouldShortCircuit = false;
186
+ }
187
+ }
188
+ if (shouldShortCircuit)
189
+ return;
190
+ await module.exports.setIamPolicy(secret, bindings);
191
+ (0, utils_1.logLabeledSuccess)("secretmanager", `Granted ${role} on projects/${secret.projectId}/secrets/${secret.name} to ${serviceAccountEmails.join(", ")}`);
192
+ }
193
+ exports.ensureServiceAgentRole = ensureServiceAgentRole;
package/lib/previews.js CHANGED
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.previews = void 0;
4
4
  const lodash_1 = require("lodash");
5
5
  const configstore_1 = require("./configstore");
6
- exports.previews = Object.assign({ rtdbrules: false, ext: false, extdev: false, rtdbmanagement: false, functionsv2: false, golang: false, deletegcfartifacts: false, dotenv: false, artifactregistry: false }, configstore_1.configstore.get("previews"));
6
+ exports.previews = Object.assign({ rtdbrules: false, ext: false, extdev: false, rtdbmanagement: false, functionsv2: false, golang: false, deletegcfartifacts: false, artifactregistry: false }, configstore_1.configstore.get("previews"));
7
7
  if (process.env.FIREBASE_CLI_PREVIEWS) {
8
8
  process.env.FIREBASE_CLI_PREVIEWS.split(",").forEach((feature) => {
9
9
  if ((0, lodash_1.has)(exports.previews, feature)) {
@@ -27,10 +27,10 @@ class FunctionsServer {
27
27
  const nodeMajorVersion = (0, functionsEmulatorUtils_1.parseRuntimeVersion)(options.config.get("functions.runtime"));
28
28
  this.backend = {
29
29
  functionsDir,
30
- env: {},
31
30
  nodeMajorVersion,
31
+ env: {},
32
32
  };
33
- const args = Object.assign({ projectId, emulatableBackends: [this.backend], account }, partialArgs);
33
+ const args = Object.assign({ projectId, projectDir: options.config.projectDir, emulatableBackends: [this.backend], account }, partialArgs);
34
34
  if (options.host) {
35
35
  utils.assertIsStringOrUndefined(options.host);
36
36
  args.host = options.host;
package/lib/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.assertIsStringOrUndefined = exports.assertIsNumber = exports.assertIsString = exports.assertDefined = exports.thirtyDaysFromNow = exports.isRunningInWSL = exports.isCloudEnvironment = exports.datetimeString = exports.createDestroyer = exports.promiseWithSpinner = exports.setupLoggers = exports.tryParse = exports.tryStringify = exports.promiseProps = exports.promiseWhile = exports.promiseAllSettled = exports.getFunctionsEventProvider = exports.endpoint = exports.makeActiveProject = exports.streamToString = exports.stringToStream = exports.explainStdin = exports.allSettled = exports.reject = exports.logLabeledWarning = exports.logWarning = exports.logLabeledBullet = exports.logBullet = exports.logLabeledSuccess = exports.logSuccess = exports.addSubdomain = exports.addDatabaseNamespace = exports.getDatabaseViewDataUrl = exports.getDatabaseUrl = exports.envOverride = exports.getInheritedOption = exports.consoleUrl = exports.envOverrides = void 0;
3
+ exports.assertIsStringOrUndefined = exports.assertIsNumber = exports.assertIsString = exports.assertDefined = exports.thirtyDaysFromNow = exports.isRunningInWSL = exports.isCloudEnvironment = exports.datetimeString = exports.createDestroyer = exports.promiseWithSpinner = exports.setupLoggers = exports.tryParse = exports.tryStringify = exports.promiseProps = exports.promiseWhile = exports.promiseAllSettled = exports.getFunctionsEventProvider = exports.endpoint = exports.makeActiveProject = exports.streamToString = exports.stringToStream = exports.explainStdin = exports.allSettled = exports.reject = exports.logLabeledError = exports.logLabeledWarning = exports.logWarning = exports.logLabeledBullet = exports.logBullet = exports.logLabeledSuccess = exports.logSuccess = exports.addSubdomain = exports.addDatabaseNamespace = exports.getDatabaseViewDataUrl = exports.getDatabaseUrl = exports.envOverride = exports.getInheritedOption = exports.consoleUrl = exports.envOverrides = void 0;
4
4
  const _ = require("lodash");
5
5
  const url = require("url");
6
6
  const clc = require("cli-color");
@@ -17,6 +17,7 @@ const logger_1 = require("./logger");
17
17
  const IS_WINDOWS = process.platform === "win32";
18
18
  const SUCCESS_CHAR = IS_WINDOWS ? "+" : "✔";
19
19
  const WARNING_CHAR = IS_WINDOWS ? "!" : "⚠";
20
+ const ERROR_CHAR = IS_WINDOWS ? "!!" : "⬢";
20
21
  const THIRTY_DAYS_IN_MILLISECONDS = 30 * 24 * 60 * 60 * 1000;
21
22
  exports.envOverrides = [];
22
23
  function consoleUrl(project, path) {
@@ -104,6 +105,10 @@ function logLabeledWarning(label, message, type = "warn", data = undefined) {
104
105
  logger_1.logger[type](clc.yellow.bold(`${WARNING_CHAR} ${label}:`), message, data);
105
106
  }
106
107
  exports.logLabeledWarning = logLabeledWarning;
108
+ function logLabeledError(label, message, type = "error", data = undefined) {
109
+ logger_1.logger[type](clc.red.bold(`${ERROR_CHAR} ${label}:`), message, data);
110
+ }
111
+ exports.logLabeledError = logLabeledError;
107
112
  function reject(message, options) {
108
113
  return Promise.reject(new error_1.FirebaseError(message, options));
109
114
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "10.1.5",
3
+ "version": "10.2.0",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "firebase-tools",
9
- "version": "10.1.5",
9
+ "version": "10.2.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@google-cloud/pubsub": "^2.18.4",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "10.1.5",
3
+ "version": "10.2.0",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {