firebase-tools 10.5.0 → 10.6.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 (37) hide show
  1. package/lib/commands/functions-secrets-destroy.js +23 -3
  2. package/lib/commands/functions-secrets-prune.js +15 -12
  3. package/lib/commands/functions-secrets-set.js +51 -4
  4. package/lib/deploy/functions/backend.js +1 -5
  5. package/lib/deploy/functions/prepare.js +13 -3
  6. package/lib/deploy/functions/release/fabricator.js +1 -3
  7. package/lib/deploy/functions/release/index.js +21 -0
  8. package/lib/deploy/functions/release/planner.js +1 -2
  9. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +11 -10
  10. package/lib/deploy/functions/runtimes/node/index.js +1 -1
  11. package/lib/deploy/functions/runtimes/node/parseTriggers.js +5 -19
  12. package/lib/deploy/functions/services/storage.js +1 -6
  13. package/lib/emulator/extensions/postinstall.js +41 -0
  14. package/lib/emulator/functionsEmulatorShared.js +16 -20
  15. package/lib/emulator/storage/apis/firebase.js +6 -6
  16. package/lib/emulator/storage/apis/gcloud.js +9 -6
  17. package/lib/emulator/storage/files.js +6 -3
  18. package/lib/emulator/storage/index.js +9 -1
  19. package/lib/emulator/storage/metadata.js +18 -8
  20. package/lib/emulator/storage/rules/manager.js +7 -17
  21. package/lib/emulator/storage/server.js +38 -12
  22. package/lib/extensions/askUserForParam.js +14 -11
  23. package/lib/extensions/emulator/optionsHelper.js +5 -7
  24. package/lib/extensions/emulator/triggerHelper.js +11 -14
  25. package/lib/extensions/extensionsApi.js +2 -1
  26. package/lib/extensions/manifest.js +1 -1
  27. package/lib/extensions/paramHelper.js +16 -10
  28. package/lib/functions/env.js +10 -2
  29. package/lib/functions/runtimeConfigExport.js +10 -6
  30. package/lib/functions/secrets.js +99 -6
  31. package/lib/gcp/cloudfunctions.js +6 -13
  32. package/lib/gcp/cloudfunctionsv2.js +14 -23
  33. package/lib/gcp/cloudtasks.js +5 -3
  34. package/lib/gcp/secretManager.js +1 -1
  35. package/lib/utils.js +30 -1
  36. package/npm-shrinkwrap.json +2 -2
  37. package/package.json +1 -1
@@ -1,11 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.pruneSecrets = exports.of = exports.ensureSecret = exports.ensureValidKey = exports.labels = exports.isFirebaseManaged = void 0;
3
+ exports.updateEndpointSecret = exports.pruneAndDestroySecrets = exports.pruneSecrets = exports.inUse = exports.of = exports.ensureSecret = exports.ensureValidKey = exports.labels = exports.isFirebaseManaged = void 0;
4
+ const utils = require("../utils");
5
+ const poller = require("../operation-poller");
6
+ const gcf = require("../gcp/cloudfunctions");
4
7
  const secretManager_1 = require("../gcp/secretManager");
5
8
  const error_1 = require("../error");
6
9
  const utils_1 = require("../utils");
7
10
  const prompt_1 = require("../prompt");
8
11
  const env_1 = require("./env");
12
+ const logger_1 = require("../logger");
13
+ const api_1 = require("../api");
14
+ const functional_1 = require("../functional");
9
15
  const FIREBASE_MANGED = "firebase-managed";
10
16
  function isFirebaseManaged(secret) {
11
17
  return Object.keys(secret.labels || []).includes(FIREBASE_MANGED);
@@ -17,8 +23,7 @@ function labels() {
17
23
  exports.labels = labels;
18
24
  function toUpperSnakeCase(key) {
19
25
  return key
20
- .replace("-", "_")
21
- .replace(".", "_")
26
+ .replace(/[.-]/g, "_")
22
27
  .replace(/([a-z])([A-Z])/g, "$1_$2")
23
28
  .toUpperCase();
24
29
  }
@@ -80,19 +85,40 @@ function of(endpoints) {
80
85
  return endpoints.reduce((envs, endpoint) => [...envs, ...(endpoint.secretEnvironmentVariables || [])], []);
81
86
  }
82
87
  exports.of = of;
88
+ function inUse(projectInfo, secret, endpoint) {
89
+ const { projectId, projectNumber } = projectInfo;
90
+ for (const sev of of([endpoint])) {
91
+ if ((sev.projectId === projectId || sev.projectId === projectNumber) &&
92
+ sev.secret === secret.name) {
93
+ return true;
94
+ }
95
+ }
96
+ return false;
97
+ }
98
+ exports.inUse = inUse;
83
99
  async function pruneSecrets(projectInfo, endpoints) {
84
100
  const { projectId, projectNumber } = projectInfo;
85
101
  const pruneKey = (name, version) => `${name}@${version}`;
86
102
  const prunedSecrets = new Set();
87
103
  const haveSecrets = await (0, secretManager_1.listSecrets)(projectId, `labels.${FIREBASE_MANGED}=true`);
88
104
  for (const secret of haveSecrets) {
89
- const versions = await (0, secretManager_1.listSecretVersions)(projectId, secret.name, `state: ENABLED`);
105
+ const versions = await (0, secretManager_1.listSecretVersions)(projectId, secret.name, `NOT state: DESTROYED`);
90
106
  for (const version of versions) {
91
107
  prunedSecrets.add(pruneKey(secret.name, version.versionId));
92
108
  }
93
109
  }
94
- const sevs = of(endpoints).filter((sev) => sev.projectId === projectId || sev.projectId === projectNumber);
95
- for (const sev of sevs) {
110
+ const secrets = [];
111
+ for (const secret of of(endpoints)) {
112
+ if (!secret.version) {
113
+ throw new error_1.FirebaseError(`Secret ${secret.secret} version is unexpectedly empty.`);
114
+ }
115
+ if (secret.projectId === projectId || secret.projectId === projectNumber) {
116
+ if (secret.version) {
117
+ secrets.push(Object.assign(Object.assign({}, secret), { version: secret.version }));
118
+ }
119
+ }
120
+ }
121
+ for (const sev of secrets) {
96
122
  let name = sev.secret;
97
123
  if (name.includes("/")) {
98
124
  const secret = (0, secretManager_1.parseSecretResourceName)(name);
@@ -110,3 +136,70 @@ async function pruneSecrets(projectInfo, endpoints) {
110
136
  .map(([secret, version]) => ({ projectId, version, secret, key: secret }));
111
137
  }
112
138
  exports.pruneSecrets = pruneSecrets;
139
+ async function pruneAndDestroySecrets(projectInfo, endpoints) {
140
+ const { projectId, projectNumber } = projectInfo;
141
+ logger_1.logger.debug("Pruning secrets to find unused secret versions...");
142
+ const unusedSecrets = await module.exports.pruneSecrets({ projectId, projectNumber }, endpoints);
143
+ if (unusedSecrets.length === 0) {
144
+ return { destroyed: [], erred: [] };
145
+ }
146
+ const destroyed = [];
147
+ const erred = [];
148
+ const msg = unusedSecrets.map((s) => `${s.secret}@${s.version}`);
149
+ logger_1.logger.debug(`Found unused secret versions: ${msg}. Destroying them...`);
150
+ const destroyResults = await utils.allSettled(unusedSecrets.map(async (sev) => {
151
+ await (0, secretManager_1.destroySecretVersion)(sev.projectId, sev.secret, sev.version);
152
+ return sev;
153
+ }));
154
+ for (const result of destroyResults) {
155
+ if (result.status === "fulfilled") {
156
+ destroyed.push(result.value);
157
+ }
158
+ else {
159
+ erred.push(result.reason);
160
+ }
161
+ }
162
+ return { destroyed, erred };
163
+ }
164
+ exports.pruneAndDestroySecrets = pruneAndDestroySecrets;
165
+ async function updateEndpointSecret(projectInfo, secretVersion, endpoint) {
166
+ const { projectId, projectNumber } = projectInfo;
167
+ if (!inUse(projectInfo, secretVersion.secret, endpoint)) {
168
+ return endpoint;
169
+ }
170
+ const updatedSevs = [];
171
+ for (const sev of of([endpoint])) {
172
+ const updatedSev = Object.assign({}, sev);
173
+ if ((updatedSev.projectId === projectId || updatedSev.projectId === projectNumber) &&
174
+ updatedSev.secret === secretVersion.secret.name) {
175
+ updatedSev.version = secretVersion.versionId;
176
+ }
177
+ updatedSevs.push(updatedSev);
178
+ }
179
+ if (endpoint.platform === "gcfv1") {
180
+ const fn = gcf.functionFromEndpoint(endpoint, "");
181
+ const op = await gcf.updateFunction({
182
+ name: fn.name,
183
+ runtime: fn.runtime,
184
+ entryPoint: fn.entryPoint,
185
+ secretEnvironmentVariables: updatedSevs,
186
+ });
187
+ const gcfV1PollerOptions = {
188
+ apiOrigin: api_1.functionsOrigin,
189
+ apiVersion: gcf.API_VERSION,
190
+ masterTimeout: 25 * 60 * 1000,
191
+ maxBackoff: 10000,
192
+ pollerName: `update-${endpoint.region}-${endpoint.id}`,
193
+ operationResourceName: op.name,
194
+ };
195
+ const cfn = await poller.pollOperation(gcfV1PollerOptions);
196
+ return gcf.endpointFromFunction(cfn);
197
+ }
198
+ else if (endpoint.platform === "gcfv2") {
199
+ throw new error_1.FirebaseError(`Unsupported platform ${endpoint.platform}`);
200
+ }
201
+ else {
202
+ (0, functional_1.assertExhaustive)(endpoint.platform);
203
+ }
204
+ }
205
+ exports.updateEndpointSecret = updateEndpointSecret;
@@ -232,12 +232,7 @@ function endpointFromFunction(gcfFunction) {
232
232
  trigger = {
233
233
  eventTrigger: {
234
234
  eventType: gcfFunction.eventTrigger.eventType,
235
- eventFilters: [
236
- {
237
- attribute: "resource",
238
- value: gcfFunction.eventTrigger.resource,
239
- },
240
- ],
235
+ eventFilters: { resource: gcfFunction.eventTrigger.resource },
241
236
  retry: !!((_e = gcfFunction.eventTrigger.failurePolicy) === null || _e === void 0 ? void 0 : _e.retry),
242
237
  },
243
238
  };
@@ -254,7 +249,8 @@ function endpointFromFunction(gcfFunction) {
254
249
  if (securityLevel) {
255
250
  endpoint.securityLevel = securityLevel;
256
251
  }
257
- proto.copyIfPresent(endpoint, gcfFunction, "serviceAccountEmail", "availableMemoryMb", "timeout", "minInstances", "maxInstances", "ingressSettings", "labels", "environmentVariables", "secretEnvironmentVariables", "sourceUploadUrl");
252
+ proto.copyIfPresent(endpoint, gcfFunction, "serviceAccountEmail", "availableMemoryMb", "minInstances", "maxInstances", "ingressSettings", "labels", "environmentVariables", "secretEnvironmentVariables", "sourceUploadUrl");
253
+ proto.renameIfPresent(endpoint, gcfFunction, "timeoutSeconds", "timeout", proto.secondsFromDuration);
258
254
  if (gcfFunction.vpcConnector) {
259
255
  endpoint.vpc = { connector: gcfFunction.vpcConnector };
260
256
  proto.renameIfPresent(endpoint.vpc, gcfFunction, "egressSettings", "vpcConnectorEgressSettings");
@@ -278,13 +274,9 @@ function functionFromEndpoint(endpoint, sourceUploadUrl) {
278
274
  };
279
275
  proto.copyIfPresent(gcfFunction, endpoint, "labels");
280
276
  if (backend.isEventTriggered(endpoint)) {
281
- const resourceFilter = backend.findEventFilter(endpoint, "resource");
282
- if (!resourceFilter) {
283
- throw new error_1.FirebaseError("Invalid event trigger definition. Expected event filter with 'resource' attribute.");
284
- }
285
277
  gcfFunction.eventTrigger = {
286
278
  eventType: endpoint.eventTrigger.eventType,
287
- resource: resourceFilter.value,
279
+ resource: endpoint.eventTrigger.eventFilters.resource,
288
280
  };
289
281
  gcfFunction.eventTrigger.failurePolicy = endpoint.eventTrigger.retry
290
282
  ? { retry: {} }
@@ -311,7 +303,8 @@ function functionFromEndpoint(endpoint, sourceUploadUrl) {
311
303
  gcfFunction.httpsTrigger.securityLevel = endpoint.securityLevel;
312
304
  }
313
305
  }
314
- proto.copyIfPresent(gcfFunction, endpoint, "serviceAccountEmail", "timeout", "availableMemoryMb", "minInstances", "maxInstances", "ingressSettings", "environmentVariables", "secretEnvironmentVariables");
306
+ proto.copyIfPresent(gcfFunction, endpoint, "serviceAccountEmail", "availableMemoryMb", "minInstances", "maxInstances", "ingressSettings", "environmentVariables", "secretEnvironmentVariables");
307
+ proto.renameIfPresent(gcfFunction, endpoint, "timeout", "timeoutSeconds", proto.durationFromSeconds);
315
308
  if (endpoint.vpc) {
316
309
  proto.renameIfPresent(gcfFunction, endpoint.vpc, "vpcConnector", "connector");
317
310
  proto.renameIfPresent(gcfFunction, endpoint.vpc, "vpcConnectorEgressSettings", "egressSettings");
@@ -166,9 +166,8 @@ function functionFromEndpoint(endpoint, source) {
166
166
  serviceConfig: {},
167
167
  };
168
168
  proto.copyIfPresent(gcfFunction, endpoint, "labels");
169
- proto.copyIfPresent(gcfFunction.serviceConfig, endpoint, "environmentVariables", "serviceAccountEmail", "ingressSettings");
169
+ proto.copyIfPresent(gcfFunction.serviceConfig, endpoint, "environmentVariables", "serviceAccountEmail", "ingressSettings", "timeoutSeconds");
170
170
  proto.renameIfPresent(gcfFunction.serviceConfig, endpoint, "availableMemory", "availableMemoryMb", (mb) => `${mb}M`);
171
- proto.renameIfPresent(gcfFunction.serviceConfig, endpoint, "timeoutSeconds", "timeout", proto.secondsFromDuration);
172
171
  proto.renameIfPresent(gcfFunction.serviceConfig, endpoint, "minInstanceCount", "minInstances");
173
172
  proto.renameIfPresent(gcfFunction.serviceConfig, endpoint, "maxInstanceCount", "maxInstances");
174
173
  if (endpoint.vpc) {
@@ -180,23 +179,19 @@ function functionFromEndpoint(endpoint, source) {
180
179
  eventType: endpoint.eventTrigger.eventType,
181
180
  };
182
181
  if (gcfFunction.eventTrigger.eventType === v2_1.PUBSUB_PUBLISH_EVENT) {
183
- const pubsubFilter = backend.findEventFilter(endpoint, "topic");
184
- if (!pubsubFilter) {
185
- throw new error_1.FirebaseError("Invalid pubsub endpoint. Expected eventFilter with 'topic' attribute but found none.");
186
- }
187
- gcfFunction.eventTrigger.pubsubTopic = pubsubFilter.value;
188
- for (const filter of endpoint.eventTrigger.eventFilters) {
189
- if (filter.attribute === "topic") {
182
+ gcfFunction.eventTrigger.pubsubTopic = endpoint.eventTrigger.eventFilters.topic;
183
+ gcfFunction.eventTrigger.eventFilters = [];
184
+ for (const [attribute, value] of Object.entries(endpoint.eventTrigger.eventFilters)) {
185
+ if (attribute === "topic")
190
186
  continue;
191
- }
192
- if (!gcfFunction.eventTrigger.eventFilters) {
193
- gcfFunction.eventTrigger.eventFilters = [];
194
- }
195
- gcfFunction.eventTrigger.eventFilters.push(filter);
187
+ gcfFunction.eventTrigger.eventFilters.push({ attribute, value });
196
188
  }
197
189
  }
198
190
  else {
199
- gcfFunction.eventTrigger.eventFilters = endpoint.eventTrigger.eventFilters;
191
+ gcfFunction.eventTrigger.eventFilters = [];
192
+ for (const [attribute, value] of Object.entries(endpoint.eventTrigger.eventFilters)) {
193
+ gcfFunction.eventTrigger.eventFilters.push({ attribute, value });
194
+ }
200
195
  }
201
196
  proto.renameIfPresent(gcfFunction.eventTrigger, endpoint.eventTrigger, "triggerRegion", "region");
202
197
  if (endpoint.eventTrigger.retry) {
@@ -239,19 +234,16 @@ function endpointFromFunction(gcfFunction) {
239
234
  trigger = {
240
235
  eventTrigger: {
241
236
  eventType: gcfFunction.eventTrigger.eventType,
242
- eventFilters: [],
237
+ eventFilters: {},
243
238
  retry: false,
244
239
  },
245
240
  };
246
241
  if (gcfFunction.eventTrigger.pubsubTopic) {
247
- trigger.eventTrigger.eventFilters.push({
248
- attribute: "topic",
249
- value: gcfFunction.eventTrigger.pubsubTopic,
250
- });
242
+ trigger.eventTrigger.eventFilters.topic = gcfFunction.eventTrigger.pubsubTopic;
251
243
  }
252
244
  else {
253
245
  for (const { attribute, value } of gcfFunction.eventTrigger.eventFilters || []) {
254
- trigger.eventTrigger.eventFilters.push({ attribute, value });
246
+ trigger.eventTrigger.eventFilters[attribute] = value;
255
247
  }
256
248
  }
257
249
  proto.renameIfPresent(trigger.eventTrigger, gcfFunction.eventTrigger, "region", "triggerRegion");
@@ -265,9 +257,8 @@ function endpointFromFunction(gcfFunction) {
265
257
  const endpoint = Object.assign(Object.assign({ platform: "gcfv2", id,
266
258
  project,
267
259
  region }, trigger), { entryPoint: gcfFunction.buildConfig.entryPoint, runtime: gcfFunction.buildConfig.runtime, uri: gcfFunction.serviceConfig.uri });
268
- proto.copyIfPresent(endpoint, gcfFunction.serviceConfig, "serviceAccountEmail", "ingressSettings", "environmentVariables");
260
+ proto.copyIfPresent(endpoint, gcfFunction.serviceConfig, "serviceAccountEmail", "ingressSettings", "environmentVariables", "timeoutSeconds");
269
261
  proto.renameIfPresent(endpoint, gcfFunction.serviceConfig, "availableMemoryMb", "availableMemory", megabytes);
270
- proto.renameIfPresent(endpoint, gcfFunction.serviceConfig, "timeout", "timeoutSeconds", proto.durationFromSeconds);
271
262
  proto.renameIfPresent(endpoint, gcfFunction.serviceConfig, "minInstances", "minInstanceCount");
272
263
  proto.renameIfPresent(endpoint, gcfFunction.serviceConfig, "maxInstances", "maxInstanceCount");
273
264
  proto.copyIfPresent(endpoint, gcfFunction, "labels");
@@ -13,7 +13,6 @@ const client = new apiv2_1.Client({
13
13
  exports.DEFAULT_SETTINGS = {
14
14
  rateLimits: {
15
15
  maxConcurrentDispatches: 1000,
16
- maxBurstSize: 100,
17
16
  maxDispatchesPerSecond: 500,
18
17
  },
19
18
  state: "RUNNING",
@@ -133,10 +132,13 @@ exports.queueNameForEndpoint = queueNameForEndpoint;
133
132
  function queueFromEndpoint(endpoint) {
134
133
  const queue = Object.assign(Object.assign({}, JSON.parse(JSON.stringify(exports.DEFAULT_SETTINGS))), { name: queueNameForEndpoint(endpoint) });
135
134
  if (endpoint.taskQueueTrigger.rateLimits) {
136
- proto.copyIfPresent(queue.rateLimits, endpoint.taskQueueTrigger.rateLimits, "maxBurstSize", "maxConcurrentDispatches", "maxDispatchesPerSecond");
135
+ proto.copyIfPresent(queue.rateLimits, endpoint.taskQueueTrigger.rateLimits, "maxConcurrentDispatches", "maxDispatchesPerSecond");
137
136
  }
138
137
  if (endpoint.taskQueueTrigger.retryConfig) {
139
- proto.copyIfPresent(queue.retryConfig, endpoint.taskQueueTrigger.retryConfig, "maxAttempts", "maxBackoff", "maxDoublings", "maxRetryDuration", "minBackoff");
138
+ proto.copyIfPresent(queue.retryConfig, endpoint.taskQueueTrigger.retryConfig, "maxAttempts", "maxDoublings");
139
+ proto.renameIfPresent(queue.retryConfig, endpoint.taskQueueTrigger.retryConfig, "maxRetryDuration", "maxRetrySeconds", proto.durationFromSeconds);
140
+ proto.renameIfPresent(queue.retryConfig, endpoint.taskQueueTrigger.retryConfig, "maxBackoff", "maxBackoffSeconds", proto.durationFromSeconds);
141
+ proto.renameIfPresent(queue.retryConfig, endpoint.taskQueueTrigger.retryConfig, "minBackoff", "minBackoffSeconds", proto.durationFromSeconds);
140
142
  }
141
143
  return queue;
142
144
  }
@@ -133,7 +133,7 @@ async function createSecret(projectId, name, labels) {
133
133
  },
134
134
  labels,
135
135
  }, { queryParams: { secretId: name } });
136
- return parseSecretResourceName(createRes.body.name);
136
+ return Object.assign(Object.assign({}, parseSecretResourceName(createRes.body.name)), { labels });
137
137
  }
138
138
  exports.createSecret = createSecret;
139
139
  async function patchSecret(projectId, name, labels) {
package/lib/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.groupBy = 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;
3
+ exports.cloneDeep = exports.groupBy = 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");
@@ -406,3 +406,32 @@ function groupBy(arr, f) {
406
406
  }, {});
407
407
  }
408
408
  exports.groupBy = groupBy;
409
+ function cloneArray(arr) {
410
+ return arr.map((e) => cloneDeep(e));
411
+ }
412
+ function cloneObject(obj) {
413
+ const clone = {};
414
+ for (const [k, v] of Object.entries(obj)) {
415
+ clone[k] = cloneDeep(v);
416
+ }
417
+ return clone;
418
+ }
419
+ function cloneDeep(obj) {
420
+ if (typeof obj !== "object" || !obj) {
421
+ return obj;
422
+ }
423
+ if (obj instanceof RegExp) {
424
+ return RegExp(obj, obj.flags);
425
+ }
426
+ if (obj instanceof Date) {
427
+ return new Date(obj);
428
+ }
429
+ if (Array.isArray(obj)) {
430
+ return cloneArray(obj);
431
+ }
432
+ if (obj instanceof Map) {
433
+ return new Map(obj.entries());
434
+ }
435
+ return cloneObject(obj);
436
+ }
437
+ exports.cloneDeep = cloneDeep;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "10.5.0",
3
+ "version": "10.6.0",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "firebase-tools",
9
- "version": "10.5.0",
9
+ "version": "10.6.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.5.0",
3
+ "version": "10.6.0",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {