firebase-tools 15.22.4 → 15.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 (42) hide show
  1. package/lib/apiv2.js +91 -32
  2. package/lib/apphosting/backend.js +11 -4
  3. package/lib/commands/functions-delete.js +5 -3
  4. package/lib/commands/functions-lifecycle-list.js +94 -0
  5. package/lib/commands/functions-lifecycle-run.js +29 -0
  6. package/lib/commands/index.js +3 -0
  7. package/lib/database/import.js +3 -3
  8. package/lib/dataconnect/webhook.js +1 -2
  9. package/lib/deploy/functions/backend.js +9 -0
  10. package/lib/deploy/functions/build.js +3 -0
  11. package/lib/deploy/functions/prepare.js +90 -1
  12. package/lib/deploy/functions/prompts.js +62 -0
  13. package/lib/deploy/functions/release/fabricator.js +79 -4
  14. package/lib/deploy/functions/release/index.js +40 -25
  15. package/lib/deploy/functions/release/lifecycle.js +44 -51
  16. package/lib/deploy/functions/release/planner.js +41 -5
  17. package/lib/deploy/functions/runtimes/discovery/index.js +4 -3
  18. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +18 -1
  19. package/lib/deploy/functions/runtimes/node/index.js +1 -2
  20. package/lib/deploy/functions/runtimes/python/index.js +1 -2
  21. package/lib/deploy/hosting/uploader.js +2 -3
  22. package/lib/downloadUtils.js +3 -1
  23. package/lib/emulator/auth/operations.js +18 -8
  24. package/lib/emulator/downloadableEmulatorInfo.json +24 -24
  25. package/lib/emulator/storage/apis/gcloud.js +61 -0
  26. package/lib/emulator/storage/multipart.js +82 -2
  27. package/lib/emulator/taskQueue.js +3 -11
  28. package/lib/extensions/extensionsHelper.js +6 -4
  29. package/lib/gcp/iam.js +68 -2
  30. package/lib/gcp/knownRoles.json +99 -0
  31. package/lib/gcp/resourceManager.js +42 -1
  32. package/lib/gemini/fdcExperience.js +2 -2
  33. package/lib/hosting/initMiddleware.js +1 -1
  34. package/lib/hosting/proxy.js +42 -20
  35. package/lib/management/apps.js +4 -2
  36. package/lib/profiler.js +1 -2
  37. package/lib/streamUtils.js +24 -0
  38. package/lib/track.js +1 -2
  39. package/lib/tsconfig.compile.tsbuildinfo +1 -1
  40. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  41. package/lib/utils.js +4 -21
  42. package/package.json +2 -4
package/lib/apiv2.js CHANGED
@@ -7,10 +7,8 @@ exports.getAccessToken = getAccessToken;
7
7
  exports.noKeepAliveAgent = noKeepAliveAgent;
8
8
  const url_1 = require("url");
9
9
  const stream_1 = require("stream");
10
- const proxy_agent_1 = require("proxy-agent");
10
+ const undici_1 = require("undici");
11
11
  const retry = require("retry");
12
- const abort_controller_1 = require("abort-controller");
13
- const node_fetch_1 = require("node-fetch");
14
12
  const http = require("http");
15
13
  const https = require("https");
16
14
  const util_1 = require("util");
@@ -19,6 +17,7 @@ const error_1 = require("./error");
19
17
  const env_1 = require("./env");
20
18
  const logger_1 = require("./logger");
21
19
  const responseToError_1 = require("./responseToError");
20
+ const streamUtils_1 = require("./streamUtils");
22
21
  const FormData = require("form-data");
23
22
  const pkg = require("../package.json");
24
23
  const CLI_VERSION = pkg.version;
@@ -36,7 +35,6 @@ const standardHeaders = () => {
36
35
  exports.standardHeaders = standardHeaders;
37
36
  const GOOG_QUOTA_USER_HEADER = "x-goog-quota-user";
38
37
  exports.GOOG_USER_PROJECT_HEADER = "x-goog-user-project";
39
- const GOOGLE_CLOUD_QUOTA_PROJECT = process.env.GOOGLE_CLOUD_QUOTA_PROJECT;
40
38
  exports.CLI_OAUTH_PROJECT_NUMBER = "563584335869";
41
39
  let accessToken = "";
42
40
  let refreshToken = "";
@@ -56,11 +54,15 @@ async function getAccessToken() {
56
54
  return data.access_token;
57
55
  }
58
56
  function proxyURIFromEnv() {
59
- return (process.env.HTTPS_PROXY ||
57
+ const uri = process.env.HTTPS_PROXY ||
60
58
  process.env.https_proxy ||
61
59
  process.env.HTTP_PROXY ||
62
60
  process.env.http_proxy ||
63
- undefined);
61
+ undefined;
62
+ if (uri === "undefined" || uri === "null") {
63
+ return undefined;
64
+ }
65
+ return uri;
64
66
  }
65
67
  const httpAgentNoKeepAlive = new http.Agent({ keepAlive: false });
66
68
  const httpsAgentNoKeepAlive = new https.Agent({ keepAlive: false });
@@ -147,7 +149,7 @@ class Client {
147
149
  throw new error_1.FirebaseError("apiv2 will not handle HTTP errors while streaming and you must set `resolveOnHTTPError` and check for res.status >= 400 on your own", { exit: 2 });
148
150
  }
149
151
  let internalReqOptions = Object.assign(reqOptions, {
150
- headers: new node_fetch_1.Headers(reqOptions.headers),
152
+ headers: new Headers(reqOptions.headers),
151
153
  });
152
154
  internalReqOptions = this.addRequestHeaders(internalReqOptions);
153
155
  if (this.opts.auth) {
@@ -176,7 +178,7 @@ class Client {
176
178
  }
177
179
  addRequestHeaders(reqOptions) {
178
180
  if (!reqOptions.headers) {
179
- reqOptions.headers = new node_fetch_1.Headers();
181
+ reqOptions.headers = new Headers();
180
182
  }
181
183
  for (const [h, v] of Object.entries((0, exports.standardHeaders)())) {
182
184
  if (!reqOptions.headers.has(h)) {
@@ -189,15 +191,15 @@ class Client {
189
191
  }
190
192
  }
191
193
  if (!reqOptions.ignoreQuotaProject &&
192
- GOOGLE_CLOUD_QUOTA_PROJECT &&
193
- GOOGLE_CLOUD_QUOTA_PROJECT !== "") {
194
- reqOptions.headers.set(exports.GOOG_USER_PROJECT_HEADER, GOOGLE_CLOUD_QUOTA_PROJECT);
194
+ process.env.GOOGLE_CLOUD_QUOTA_PROJECT &&
195
+ process.env.GOOGLE_CLOUD_QUOTA_PROJECT !== "") {
196
+ reqOptions.headers.set(exports.GOOG_USER_PROJECT_HEADER, process.env.GOOGLE_CLOUD_QUOTA_PROJECT);
195
197
  }
196
198
  return reqOptions;
197
199
  }
198
200
  async addAuthHeader(reqOptions) {
199
201
  if (!reqOptions.headers) {
200
- reqOptions.headers = new node_fetch_1.Headers();
202
+ reqOptions.headers = new Headers();
201
203
  }
202
204
  let token;
203
205
  if (isLocalInsecureRequest(this.opts.urlPrefix)) {
@@ -236,31 +238,21 @@ class Client {
236
238
  headers: options.headers,
237
239
  method: options.method,
238
240
  redirect: options.redirect,
239
- compress: options.compress,
240
241
  };
241
- if (proxyURIFromEnv()) {
242
- fetchOptions.agent = new proxy_agent_1.ProxyAgent();
242
+ const proxyURI = proxyURIFromEnv();
243
+ if (proxyURI) {
244
+ fetchOptions.dispatcher = new undici_1.ProxyAgent({ uri: proxyURI });
243
245
  }
244
246
  if (options.signal) {
245
- const signal = options.signal;
246
- signal.reason = "";
247
- signal.throwIfAborted = () => {
248
- throw new error_1.FirebaseError("Aborted");
249
- };
250
- fetchOptions.signal = signal;
247
+ fetchOptions.signal = options.signal;
251
248
  }
252
249
  let reqTimeout;
253
250
  if (options.timeout) {
254
- const controller = new abort_controller_1.default();
251
+ const controller = new AbortController();
255
252
  reqTimeout = setTimeout(() => {
256
253
  controller.abort();
257
254
  }, options.timeout);
258
- const signal = controller.signal;
259
- signal.reason = "";
260
- signal.throwIfAborted = () => {
261
- throw new error_1.FirebaseError("Aborted");
262
- };
263
- fetchOptions.signal = signal;
255
+ fetchOptions.signal = controller.signal;
264
256
  }
265
257
  let bodyReplayable = true;
266
258
  if (typeof options.body === "string" || Buffer.isBuffer(options.body)) {
@@ -271,6 +263,7 @@ class Client {
271
263
  }
272
264
  else if (isStream(options.body)) {
273
265
  fetchOptions.body = options.body;
266
+ fetchOptions.duplex = "half";
274
267
  bodyReplayable = false;
275
268
  }
276
269
  else if (options.body !== undefined) {
@@ -302,10 +295,39 @@ class Client {
302
295
  }
303
296
  this.logRequest(options);
304
297
  try {
305
- res = await (0, node_fetch_1.default)(fetchURL, fetchOptions);
298
+ if (options.compress === false) {
299
+ const undiciOptions = {
300
+ method: fetchOptions.method,
301
+ headers: {},
302
+ decompress: false,
303
+ };
304
+ if (fetchOptions.dispatcher) {
305
+ undiciOptions.dispatcher = fetchOptions.dispatcher;
306
+ }
307
+ if (fetchOptions.signal) {
308
+ undiciOptions.signal = fetchOptions.signal;
309
+ }
310
+ if (fetchOptions.body) {
311
+ undiciOptions.body = fetchOptions.body;
312
+ }
313
+ if (fetchOptions.headers) {
314
+ for (const [key, value] of fetchOptions.headers.entries()) {
315
+ undiciOptions.headers[key] = value;
316
+ }
317
+ }
318
+ const undiciRes = await (0, undici_1.request)(fetchURL, undiciOptions);
319
+ res = new UndiciResponseCompat(undiciRes);
320
+ }
321
+ else {
322
+ res = await fetch(fetchURL, fetchOptions);
323
+ }
306
324
  }
307
325
  catch (thrown) {
308
- const err = thrown instanceof Error ? thrown : new Error(thrown);
326
+ const err = thrown && typeof thrown === "object" && thrown.cause instanceof Error
327
+ ? thrown.cause
328
+ : thrown instanceof Error
329
+ ? thrown
330
+ : new Error(thrown);
309
331
  logger_1.logger.debug(`*** [apiv2] error from fetch(${fetchURL}, ${JSON.stringify(fetchOptions)}): ${err}`);
310
332
  const isAbortError = err.name.includes("AbortError");
311
333
  if (isAbortError) {
@@ -339,7 +361,19 @@ class Client {
339
361
  body = (await res.text());
340
362
  }
341
363
  else if (options.responseType === "stream") {
342
- body = res.body;
364
+ if (res.body) {
365
+ if (typeof res.body.getReader === "function") {
366
+ body = stream_1.Readable.fromWeb(res.body);
367
+ }
368
+ else {
369
+ body = res.body;
370
+ }
371
+ }
372
+ else {
373
+ const emptyStream = new stream_1.Readable();
374
+ emptyStream.push(null);
375
+ body = emptyStream;
376
+ }
343
377
  }
344
378
  else {
345
379
  throw new error_1.FirebaseError(`Unable to interpret response. Please set responseType.`, {
@@ -354,7 +388,7 @@ class Client {
354
388
  isPrematureCloseError(err)) {
355
389
  disabledKeepAlive = true;
356
390
  fetchOptions.agent = noKeepAliveAgent;
357
- const closeHeaders = new node_fetch_1.Headers(fetchOptions.headers);
391
+ const closeHeaders = new Headers(fetchOptions.headers);
358
392
  closeHeaders.set("Connection", "close");
359
393
  fetchOptions.headers = closeHeaders;
360
394
  logger_1.logger.debug(`*** [apiv2] retrying ${fetchURL} without keep-alive after a premature close error`);
@@ -435,6 +469,31 @@ function isLocalInsecureRequest(urlPrefix) {
435
469
  const u = new url_1.URL(urlPrefix);
436
470
  return u.protocol === "http:";
437
471
  }
472
+ class UndiciResponseCompat {
473
+ constructor(undiciRes) {
474
+ this.undiciRes = undiciRes;
475
+ this.status = undiciRes.statusCode;
476
+ this.ok = undiciRes.statusCode >= 200 && undiciRes.statusCode < 300;
477
+ this.headers = new Headers();
478
+ for (const [key, value] of Object.entries(undiciRes.headers)) {
479
+ if (Array.isArray(value)) {
480
+ for (const v of value) {
481
+ this.headers.append(key, v);
482
+ }
483
+ }
484
+ else if (value !== undefined) {
485
+ this.headers.set(key, String(value));
486
+ }
487
+ }
488
+ this.body = undiciRes.body;
489
+ }
490
+ async text() {
491
+ if (!this.body) {
492
+ return "";
493
+ }
494
+ return (0, streamUtils_1.streamToString)(this.body);
495
+ }
496
+ }
438
497
  function bodyToString(body) {
439
498
  if (isStream(body)) {
440
499
  return "[stream]";
@@ -31,7 +31,6 @@ const ensureApiEnabled_1 = require("../ensureApiEnabled");
31
31
  const deploymentTool = require("../deploymentTool");
32
32
  const app_1 = require("./app");
33
33
  const ora = require("ora");
34
- const node_fetch_1 = require("node-fetch");
35
34
  const rollout_1 = require("./rollout");
36
35
  const fuzzy = require("fuzzy");
37
36
  const experiments_1 = require("../experiments");
@@ -45,7 +44,7 @@ const apphostingPollerOptions = {
45
44
  };
46
45
  async function tlsReady(url) {
47
46
  try {
48
- await (0, node_fetch_1.default)(url);
47
+ await fetch(url);
49
48
  return true;
50
49
  }
51
50
  catch (err) {
@@ -213,8 +212,16 @@ async function promptNewBackendId(projectId, location) {
213
212
  while (true) {
214
213
  const backendId = await (0, prompt_1.input)({
215
214
  default: "my-web-app",
216
- message: "Provide a name for your backend [1-30 characters]",
217
- validate: (s) => s.length >= 1 && s.length <= 30,
215
+ message: "Provide a name for your backend [3-30 characters]",
216
+ validate: (s) => {
217
+ if (!/^[a-z](?:[a-z0-9-]*[a-z0-9])?$/.test(s)) {
218
+ return "Must begin with a letter, can contain only lowercase, digits, hyphens, and cannot end with hyphen";
219
+ }
220
+ else if (s.length < 3 || s.length > 30) {
221
+ return "Must be between 3 and 30 characters";
222
+ }
223
+ return true;
224
+ },
218
225
  });
219
226
  try {
220
227
  await apphosting.getBackend(projectId, location, backendId);
@@ -40,14 +40,15 @@ exports.command = new command_1.Command("functions:delete [filters...]")
40
40
  if (options.region) {
41
41
  existingBackend.endpoints = { [options.region]: existingBackend.endpoints[options.region] };
42
42
  }
43
- const plan = planner.createDeploymentPlan({
43
+ const plan = await planner.createDeploymentPlan({
44
44
  wantBackend: backend.empty(),
45
45
  haveBackend: existingBackend,
46
46
  codebase: "",
47
+ projectId: context.projectId,
47
48
  filters: context.filters,
48
49
  deleteAll: true,
49
50
  });
50
- const allEpToDelete = Object.values(plan)
51
+ const allEpToDelete = Object.values(plan.regionalChangesets)
51
52
  .map((changes) => changes.endpointsToDelete)
52
53
  .reduce(functional_1.reduceFlat, [])
53
54
  .sort(backend.compareFunctions);
@@ -80,8 +81,9 @@ exports.command = new command_1.Command("functions:delete [filters...]")
80
81
  executor: new executor.QueueExecutor({}),
81
82
  sources: {},
82
83
  projectNumber: options.projectNumber || (await (0, getProjectNumber_1.getProjectNumber)({ projectId: context.projectId })),
84
+ projectId: context.projectId,
83
85
  });
84
- const summary = await fab.applyPlan(plan);
86
+ const summary = await fab.applyPlan({ default: plan });
85
87
  await reporter.logAndTrackDeployStats(summary);
86
88
  reporter.printErrors(summary);
87
89
  }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ exports.loadCodebaseBuild = loadCodebaseBuild;
5
+ const command_1 = require("../command");
6
+ const logger_1 = require("../logger");
7
+ const prepare_1 = require("../deploy/functions/prepare");
8
+ const projectConfig_1 = require("../functions/projectConfig");
9
+ const adminSdkConfig_1 = require("../emulator/adminSdkConfig");
10
+ const projectUtils_1 = require("../projectUtils");
11
+ const error_1 = require("../error");
12
+ const ensureApiEnabled = require("../ensureApiEnabled");
13
+ const api_1 = require("../api");
14
+ const prepareFunctionsUpload_1 = require("../deploy/functions/prepareFunctionsUpload");
15
+ const self = require("./functions-lifecycle-list");
16
+ async function loadCodebaseBuild(codebase, options) {
17
+ const projectId = (0, projectUtils_1.needProjectId)(options);
18
+ if (!options.config) {
19
+ throw new error_1.FirebaseError("Not in a Firebase project directory (firebase.json not found).");
20
+ }
21
+ const fnConfig = (0, projectConfig_1.normalizeAndValidate)(options.config.src.functions);
22
+ const hasCodebase = fnConfig.some((c) => c.codebase === codebase);
23
+ if (!hasCodebase) {
24
+ throw new error_1.FirebaseError(`Codebase "${codebase}" is not defined in firebase.json.`);
25
+ }
26
+ const firebaseConfig = await (0, adminSdkConfig_1.getProjectAdminSdkConfigOrCached)(projectId);
27
+ if (!firebaseConfig) {
28
+ throw new error_1.FirebaseError("Admin SDK config unexpectedly undefined - have you run firebase init?");
29
+ }
30
+ let runtimeConfig = { firebase: firebaseConfig };
31
+ if (fnConfig.some(projectConfig_1.shouldUseRuntimeConfig)) {
32
+ try {
33
+ const runtimeConfigApiEnabled = await ensureApiEnabled.check(projectId, (0, api_1.runtimeconfigOrigin)(), "runtimeconfig", true);
34
+ if (runtimeConfigApiEnabled) {
35
+ runtimeConfig = { ...runtimeConfig, ...(await (0, prepareFunctionsUpload_1.getFunctionsConfig)(projectId)) };
36
+ }
37
+ }
38
+ catch (err) {
39
+ logger_1.logger.debug("Could not check Runtime Config API status, assuming disabled:", err);
40
+ }
41
+ }
42
+ const wantBuilds = await (0, prepare_1.loadCodebases)(fnConfig, options, firebaseConfig, runtimeConfig, undefined);
43
+ const codebaseBuild = wantBuilds[codebase];
44
+ if (!codebaseBuild) {
45
+ throw new error_1.FirebaseError(`Failed to load build for codebase "${codebase}".`);
46
+ }
47
+ return codebaseBuild;
48
+ }
49
+ exports.command = new command_1.Command("functions:lifecycle:list <codebase>")
50
+ .description("list all the lifecycle hooks defined in a codebase")
51
+ .action(async (codebase, options) => {
52
+ const codebaseBuild = await self.loadCodebaseBuild(codebase, options);
53
+ const hooks = codebaseBuild.lifecycleHooks || {};
54
+ if (Object.keys(hooks).length === 0) {
55
+ logger_1.logger.info(`No lifecycle hooks configured for codebase "${codebase}".`);
56
+ return hooks;
57
+ }
58
+ for (const [event, hook] of Object.entries(hooks)) {
59
+ logger_1.logger.info(`\nEvent: ${event}`);
60
+ if ("task" in hook) {
61
+ logger_1.logger.info(` Action: Task`);
62
+ logger_1.logger.info(` Target Function: ${hook.task.function}`);
63
+ if (hook.task.body) {
64
+ logger_1.logger.info(` Body: ${JSON.stringify(hook.task.body, null, 2).replace(/\n/g, "\n ")}`);
65
+ }
66
+ }
67
+ else if ("call" in hook) {
68
+ logger_1.logger.info(` Action: Call`);
69
+ logger_1.logger.info(` Target Function: ${hook.call.function}`);
70
+ if (hook.call.params) {
71
+ logger_1.logger.info(` Params: ${JSON.stringify(hook.call.params, null, 2).replace(/\n/g, "\n ")}`);
72
+ }
73
+ }
74
+ else if ("http" in hook) {
75
+ logger_1.logger.info(` Action: HTTP`);
76
+ if (hook.http.function) {
77
+ logger_1.logger.info(` Target Function: ${hook.http.function}`);
78
+ }
79
+ if (hook.http.url) {
80
+ logger_1.logger.info(` URL: ${hook.http.url}`);
81
+ }
82
+ if (hook.http.method) {
83
+ logger_1.logger.info(` Method: ${hook.http.method}`);
84
+ }
85
+ if (hook.http.headers) {
86
+ logger_1.logger.info(` Headers: ${JSON.stringify(hook.http.headers, null, 2).replace(/\n/g, "\n ")}`);
87
+ }
88
+ if (hook.http.body) {
89
+ logger_1.logger.info(` Body: ${JSON.stringify(hook.http.body, null, 2).replace(/\n/g, "\n ")}`);
90
+ }
91
+ }
92
+ }
93
+ return hooks;
94
+ });
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const command_1 = require("../command");
5
+ const projectUtils_1 = require("../projectUtils");
6
+ const requirePermissions_1 = require("../requirePermissions");
7
+ const error_1 = require("../error");
8
+ const functions_lifecycle_list_1 = require("./functions-lifecycle-list");
9
+ const backend = require("../deploy/functions/backend");
10
+ const lifecycle_1 = require("../deploy/functions/release/lifecycle");
11
+ exports.command = new command_1.Command("functions:lifecycle:run <hookName> <codebase>")
12
+ .description("run a specific lifecycle hook in isolation")
13
+ .before(requirePermissions_1.requirePermissions, ["cloudfunctions.functions.list", "run.services.list"])
14
+ .action(async (hookName, codebase, options) => {
15
+ if (hookName !== "afterFirstDeploy" && hookName !== "afterRedeploy") {
16
+ throw new error_1.FirebaseError(`Invalid hook name "${hookName}". Supported hooks are "afterFirstDeploy" and "afterRedeploy".`);
17
+ }
18
+ const projectId = (0, projectUtils_1.needProjectId)(options);
19
+ const codebaseBuild = await (0, functions_lifecycle_list_1.loadCodebaseBuild)(codebase, options);
20
+ const hook = codebaseBuild.lifecycleHooks?.[hookName];
21
+ if (!hook) {
22
+ throw new error_1.FirebaseError(`No lifecycle hook "${hookName}" configured for codebase "${codebase}".`);
23
+ }
24
+ const context = {
25
+ projectId,
26
+ };
27
+ const existingBackend = await backend.existingBackend(context);
28
+ await (0, lifecycle_1.executeHook)(hookName, hook, existingBackend);
29
+ });
@@ -151,6 +151,9 @@ function load(client) {
151
151
  client.functions.log = loadCommand("functions-log");
152
152
  client.functions.shell = loadCommand("functions-shell");
153
153
  client.functions.list = loadCommand("functions-list");
154
+ client.functions.lifecycle = {};
155
+ client.functions.lifecycle.list = loadCommand("functions-lifecycle-list");
156
+ client.functions.lifecycle.run = loadCommand("functions-lifecycle-run");
154
157
  if (experiments.isEnabled("deletegcfartifacts")) {
155
158
  client.functions.deletegcfartifacts = loadCommand("functions-deletegcfartifacts");
156
159
  }
@@ -6,7 +6,6 @@ const Filter = require("stream-json/filters/Filter");
6
6
  const stream = require("stream");
7
7
  const StreamObject = require("stream-json/streamers/StreamObject");
8
8
  const apiv2_1 = require("../apiv2");
9
- const node_fetch_1 = require("node-fetch");
10
9
  const error_1 = require("../error");
11
10
  const utils_1 = require("../utils");
12
11
  class BatchChunks extends stream.Transform {
@@ -169,8 +168,9 @@ class DatabaseImporter {
169
168
  }
170
169
  catch (err) {
171
170
  const isTimeoutErr = err instanceof error_1.FirebaseError &&
172
- err.original instanceof node_fetch_1.FetchError &&
173
- err.original.code === "ETIMEDOUT";
171
+ (err.original?.name === "AbortError" ||
172
+ err.original?.code === "ETIMEDOUT" ||
173
+ err.original?.cause?.code === "ETIMEDOUT");
174
174
  if (isTimeoutErr) {
175
175
  await new Promise((res) => setTimeout(res, this.nonFatalRetryTimeout));
176
176
  return await doRequest();
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.port = exports.DEFAULT_PORT = exports.VSCODE_MESSAGE = void 0;
4
4
  exports.sendVSCodeMessage = sendVSCodeMessage;
5
- const node_fetch_1 = require("node-fetch");
6
5
  const logger_1 = require("../logger");
7
6
  var VSCODE_MESSAGE;
8
7
  (function (VSCODE_MESSAGE) {
@@ -15,7 +14,7 @@ exports.port = process.env.VSCODE_WEBHOOK_PORT || exports.DEFAULT_PORT;
15
14
  async function sendVSCodeMessage(body) {
16
15
  const jsonBody = JSON.stringify(body);
17
16
  try {
18
- return await (0, node_fetch_1.default)(`http://localhost:${exports.port}/vscode/notify`, {
17
+ return await fetch(`http://localhost:${exports.port}/vscode/notify`, {
19
18
  method: "POST",
20
19
  headers: {
21
20
  Accept: "application/json",
@@ -165,6 +165,7 @@ function of(...endpoints) {
165
165
  function merge(...backends) {
166
166
  const merged = of(...(0, functional_1.flattenArray)(backends.map((b) => allEndpoints(b))));
167
167
  const apiToReasons = {};
168
+ const requiredRoles = new Set();
168
169
  for (const b of backends) {
169
170
  for (const { api, reason } of b.requiredAPIs) {
170
171
  const reasons = apiToReasons[api] || new Set();
@@ -174,6 +175,11 @@ function merge(...backends) {
174
175
  apiToReasons[api] = reasons;
175
176
  }
176
177
  merged.environmentVariables = { ...merged.environmentVariables, ...b.environmentVariables };
178
+ if (b.requiredRoles) {
179
+ for (const role of b.requiredRoles) {
180
+ requiredRoles.add(role);
181
+ }
182
+ }
177
183
  if (b.lifecycleHooks) {
178
184
  merged.lifecycleHooks = { ...(merged.lifecycleHooks || {}), ...b.lifecycleHooks };
179
185
  }
@@ -181,6 +187,9 @@ function merge(...backends) {
181
187
  for (const [api, reasons] of Object.entries(apiToReasons)) {
182
188
  merged.requiredAPIs.push({ api, reason: Array.from(reasons).join(" ") });
183
189
  }
190
+ if (requiredRoles.size > 0) {
191
+ merged.requiredRoles = Array.from(requiredRoles);
192
+ }
184
193
  return merged;
185
194
  }
186
195
  function isEmptyBackend(backend) {
@@ -263,6 +263,9 @@ function toBackend(build, paramValues) {
263
263
  }
264
264
  const bkend = backend.of(...bkEndpoints);
265
265
  bkend.requiredAPIs = build.requiredAPIs;
266
+ if (build.requiredRoles) {
267
+ bkend.requiredRoles = build.requiredRoles;
268
+ }
266
269
  if (build.lifecycleHooks) {
267
270
  bkend.lifecycleHooks = build.lifecycleHooks;
268
271
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.EVENTARC_SOURCE_ENV = void 0;
4
+ exports.discoverSecurityDetails = discoverSecurityDetails;
4
5
  exports.prepare = prepare;
5
6
  exports.resolveDefaultRegionsForBuild = resolveDefaultRegionsForBuild;
6
7
  exports.inferDetailsFromExisting = inferDetailsFromExisting;
@@ -43,7 +44,94 @@ const backend_1 = require("./backend");
43
44
  const functional_1 = require("../../functional");
44
45
  const prepare_1 = require("../extensions/prepare");
45
46
  const prompt = require("../../prompt");
47
+ const iam = require("../../gcp/iam");
48
+ const resourcemanager = require("../../gcp/resourceManager");
46
49
  exports.EVENTARC_SOURCE_ENV = "EVENTARC_CLOUD_EVENT_SOURCE";
50
+ async function discoverSecurityDetails(codebase, want, have, projectId) {
51
+ const requiredRoles = want.requiredRoles;
52
+ const firstHave = backend.allEndpoints(have)[0];
53
+ let existingManagedSA;
54
+ let haveRolesEtag;
55
+ if (firstHave) {
56
+ haveRolesEtag = firstHave.labels?.["firebase-declarative-security-etag"];
57
+ existingManagedSA = firstHave.serviceAccount?.startsWith("firebase-fn-")
58
+ ? firstHave.serviceAccount
59
+ : undefined;
60
+ }
61
+ if (!requiredRoles && (!existingManagedSA || !haveRolesEtag)) {
62
+ return {};
63
+ }
64
+ if (requiredRoles &&
65
+ backend.someEndpoint(want, (e) => typeof e.serviceAccount === "string" &&
66
+ e.serviceAccount !== "default" &&
67
+ !e.serviceAccount.startsWith("firebase-fn-"))) {
68
+ throw new error_1.FirebaseError(`Cannot use explicit custom service accounts on functions while using declarative security in codebase ${codebase}.`);
69
+ }
70
+ if (!requiredRoles && existingManagedSA && haveRolesEtag) {
71
+ for (const endpoint of backend.allEndpoints(want)) {
72
+ if (!endpoint.serviceAccount || endpoint.serviceAccount === existingManagedSA) {
73
+ endpoint.serviceAccount = "default";
74
+ }
75
+ if (endpoint.labels) {
76
+ delete endpoint.labels["firebase-declarative-security-etag"];
77
+ }
78
+ }
79
+ return {
80
+ haveRolesEtag,
81
+ existingManagedSA,
82
+ };
83
+ }
84
+ let managedSA = existingManagedSA;
85
+ if (!managedSA) {
86
+ const saToCreate = await iam.generateManagedServiceAccountName(projectId, "firebase-fn");
87
+ managedSA = `${saToCreate}@${projectId}.iam.gserviceaccount.com`;
88
+ }
89
+ const existingSalt = haveRolesEtag ? haveRolesEtag.split("-")[0] : undefined;
90
+ const newEtag = iam.computeRolesEtag(requiredRoles, existingSalt);
91
+ for (const endpoint of backend.allEndpoints(want)) {
92
+ endpoint.serviceAccount = managedSA;
93
+ endpoint.labels = endpoint.labels || {};
94
+ endpoint.labels["firebase-declarative-security-etag"] = newEtag;
95
+ }
96
+ if (haveRolesEtag && haveRolesEtag === newEtag) {
97
+ return {
98
+ haveRoles: requiredRoles,
99
+ haveRolesEtag,
100
+ existingManagedSA,
101
+ managedSA,
102
+ newEtag,
103
+ };
104
+ }
105
+ const permissionsToTest = ["resourcemanager.projects.setIamPolicy"];
106
+ if (!existingManagedSA) {
107
+ permissionsToTest.push("iam.serviceAccounts.create");
108
+ }
109
+ const iamResult = await iam.testIamPermissions(projectId, permissionsToTest);
110
+ if (!iamResult.passed) {
111
+ if (!existingManagedSA) {
112
+ throw new error_1.FirebaseError(`Cannot enable declarative security because you do not have permissions necessary to create the service account. Please ask an IAM administrator to perform the next deploy.`);
113
+ }
114
+ else {
115
+ throw new error_1.FirebaseError(`You do not have access to make the policy changes required in codebase ${codebase} deploy. Please ask an IAM administrator to perform the next deploy.`);
116
+ }
117
+ }
118
+ let haveRoles = [];
119
+ if (existingManagedSA) {
120
+ try {
121
+ haveRoles = await resourcemanager.getServiceAccountRoles(projectId, managedSA);
122
+ }
123
+ catch (err) {
124
+ throw new error_1.FirebaseError(`The declarative security roles for codebase ${codebase} have changed, but you do not have access to see what has changed. Please ask an IAM administrator to perform the next deploy.`, { original: err });
125
+ }
126
+ }
127
+ return {
128
+ haveRoles,
129
+ haveRolesEtag,
130
+ existingManagedSA,
131
+ managedSA,
132
+ newEtag,
133
+ };
134
+ }
47
135
  async function prepare(context, options, payload) {
48
136
  const projectId = (0, projectUtils_1.needProjectId)(options);
49
137
  const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
@@ -196,7 +284,8 @@ async function prepare(context, options, payload) {
196
284
  const haveBackends = (0, functionsDeployHelper_1.groupEndpointsByCodebase)(wantBackends, backend.allEndpoints(existingBackend));
197
285
  for (const [codebase, wantBackend] of Object.entries(wantBackends)) {
198
286
  const haveBackend = haveBackends[codebase] || backend.empty();
199
- payload.functions[codebase] = { wantBackend, haveBackend };
287
+ const security = await discoverSecurityDetails(codebase, wantBackend, haveBackend, projectId);
288
+ payload.functions[codebase] = { wantBackend, haveBackend, ...security };
200
289
  }
201
290
  for (const [codebase, { wantBackend, haveBackend }] of Object.entries(payload.functions)) {
202
291
  inferDetailsFromExisting(wantBackend, haveBackend, codebaseUsesEnvs.includes(codebase));