firebase-tools 15.22.3 → 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 (49) hide show
  1. package/lib/accountExporter.js +1 -0
  2. package/lib/accountImporter.js +1 -0
  3. package/lib/apiv2.js +91 -32
  4. package/lib/apphosting/backend.js +11 -4
  5. package/lib/apphosting/localbuilds.js +50 -0
  6. package/lib/commands/functions-delete.js +5 -3
  7. package/lib/commands/functions-lifecycle-list.js +94 -0
  8. package/lib/commands/functions-lifecycle-run.js +29 -0
  9. package/lib/commands/index.js +3 -0
  10. package/lib/database/import.js +3 -3
  11. package/lib/dataconnect/webhook.js +1 -2
  12. package/lib/deploy/apphosting/prepare.js +100 -1
  13. package/lib/deploy/functions/backend.js +15 -1
  14. package/lib/deploy/functions/build.js +28 -0
  15. package/lib/deploy/functions/prepare.js +91 -2
  16. package/lib/deploy/functions/prompts.js +62 -0
  17. package/lib/deploy/functions/release/fabricator.js +79 -4
  18. package/lib/deploy/functions/release/index.js +42 -23
  19. package/lib/deploy/functions/release/lifecycle.js +113 -0
  20. package/lib/deploy/functions/release/planner.js +41 -5
  21. package/lib/deploy/functions/runtimes/discovery/index.js +4 -3
  22. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +50 -0
  23. package/lib/deploy/functions/runtimes/node/index.js +1 -2
  24. package/lib/deploy/functions/runtimes/python/index.js +1 -2
  25. package/lib/deploy/functions/validate.js +44 -1
  26. package/lib/deploy/hosting/uploader.js +2 -3
  27. package/lib/downloadUtils.js +3 -1
  28. package/lib/emulator/auth/operations.js +18 -8
  29. package/lib/emulator/downloadableEmulatorInfo.json +31 -31
  30. package/lib/emulator/storage/apis/gcloud.js +61 -0
  31. package/lib/emulator/storage/multipart.js +82 -2
  32. package/lib/emulator/taskQueue.js +3 -11
  33. package/lib/extensions/extensionsHelper.js +6 -4
  34. package/lib/firestore/api.js +3 -1
  35. package/lib/gcp/cloudtasks.js +4 -0
  36. package/lib/gcp/iam.js +68 -2
  37. package/lib/gcp/knownRoles.json +99 -0
  38. package/lib/gcp/resourceManager.js +42 -1
  39. package/lib/gemini/fdcExperience.js +2 -2
  40. package/lib/hosting/initMiddleware.js +1 -1
  41. package/lib/hosting/proxy.js +42 -20
  42. package/lib/management/apps.js +4 -2
  43. package/lib/profiler.js +1 -2
  44. package/lib/streamUtils.js +24 -0
  45. package/lib/track.js +1 -2
  46. package/lib/tsconfig.compile.tsbuildinfo +1 -1
  47. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  48. package/lib/utils.js +4 -21
  49. package/package.json +2 -4
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.determineDeploymentEvent = determineDeploymentEvent;
4
+ exports.executeLifecycleHooks = executeLifecycleHooks;
5
+ exports.executeHook = executeHook;
6
+ const backend = require("../backend");
7
+ const error_1 = require("../../../error");
8
+ const logger_1 = require("../../../logger");
9
+ const utils_1 = require("../../../utils");
10
+ const cloudtasks = require("../../../gcp/cloudtasks");
11
+ const computeEngine = require("../../../gcp/computeEngine");
12
+ const projects_1 = require("../../../management/projects");
13
+ const functional_1 = require("../../../functional");
14
+ function determineDeploymentEvent(haveBackend) {
15
+ const hasExistingEndpoints = backend.someEndpoint(haveBackend, () => true);
16
+ if (!hasExistingEndpoints) {
17
+ return "afterFirstDeploy";
18
+ }
19
+ return "afterRedeploy";
20
+ }
21
+ async function executeLifecycleHooks(wantBackend, haveBackend, plan, codebase) {
22
+ const event = determineDeploymentEvent(haveBackend);
23
+ const hooks = wantBackend.lifecycleHooks || {};
24
+ const hook = hooks[event];
25
+ if (!hook) {
26
+ logger_1.logger.debug(`No lifecycle hook configured for event: ${event}`);
27
+ return false;
28
+ }
29
+ if (event === "afterRedeploy" && plan) {
30
+ const codebasePlans = codebase ? [plan[codebase]].filter(Boolean) : Object.values(plan);
31
+ const hasResourceModifications = codebasePlans.some((codebasePlan) => Object.values(codebasePlan.regionalChangesets).some((changeset) => changeset.endpointsToCreate.length > 0 ||
32
+ changeset.endpointsToUpdate.length > 0 ||
33
+ changeset.endpointsToDelete.length > 0));
34
+ if (!hasResourceModifications) {
35
+ (0, utils_1.logLabeledBullet)("functions", `No resources modified for codebase: ${codebase ?? "default"}. Skipping afterRedeploy lifecycle hook.`);
36
+ return false;
37
+ }
38
+ }
39
+ try {
40
+ await executeHook(event, hook, wantBackend);
41
+ return true;
42
+ }
43
+ catch (err) {
44
+ const errorMsg = err instanceof Error ? err.message : String(err);
45
+ (0, utils_1.logLabeledWarning)("functions", `Failed to execute ${event} lifecycle hook: ${errorMsg}`);
46
+ (0, utils_1.logLabeledBullet)("functions", `You can retry the lifecycle hook in isolation by running: firebase functions:lifecycle:run ${event} ${codebase ?? "default"}`);
47
+ return false;
48
+ }
49
+ }
50
+ async function executeTaskQueueHook(taskHook, wantBackend) {
51
+ const targetEndpoint = backend.findEndpoint(wantBackend, (ep) => ep.id === taskHook.function);
52
+ if (!targetEndpoint) {
53
+ throw new error_1.FirebaseError(`Target endpoint "${taskHook.function}" not found in backend for lifecycle hook.`);
54
+ }
55
+ if (!backend.isTaskQueueTriggered(targetEndpoint)) {
56
+ throw new error_1.FirebaseError(`Target endpoint "${taskHook.function}" is not a task queue function.`);
57
+ }
58
+ const queueName = cloudtasks.queueNameForEndpoint(targetEndpoint);
59
+ const bodyStr = taskHook.body ? JSON.stringify(taskHook.body) : "";
60
+ const body = bodyStr ? Buffer.from(bodyStr).toString("base64") : undefined;
61
+ const url = targetEndpoint.uri;
62
+ if (!url) {
63
+ throw new error_1.FirebaseError(`Target endpoint "${taskHook.function}" does not have a trigger URI.`);
64
+ }
65
+ const projectMetadata = await (0, projects_1.getProject)(targetEndpoint.project);
66
+ const projectNumber = projectMetadata.projectNumber;
67
+ const sa = targetEndpoint.serviceAccount || (await computeEngine.getDefaultServiceAccount(projectNumber));
68
+ const task = {
69
+ httpRequest: {
70
+ url,
71
+ httpMethod: "POST",
72
+ headers: {
73
+ "Content-Type": "application/json",
74
+ },
75
+ oidcToken: {
76
+ serviceAccountEmail: sa,
77
+ audience: url,
78
+ },
79
+ },
80
+ };
81
+ if (body) {
82
+ task.httpRequest.body = body;
83
+ }
84
+ await cloudtasks.enqueueTask(queueName, task);
85
+ (0, utils_1.logLabeledSuccess)("functions", `Successfully queued task for lifecycle hook ${taskHook.function} in queue ${queueName}.`);
86
+ return targetEndpoint;
87
+ }
88
+ function getCloudConsoleLogUrl(endpoint) {
89
+ const { project, region, id } = endpoint;
90
+ const serviceName = endpoint.runServiceId || id;
91
+ const query = `resource.type="cloud_run_revision"\nresource.labels.service_name="${serviceName}"\nresource.labels.location="${region}"`;
92
+ return `https://console.cloud.google.com/logs/query;query=${encodeURIComponent(query)};project=${project}`;
93
+ }
94
+ async function executeHook(event, hook, backendSpec) {
95
+ let executedEndpoint;
96
+ if ("task" in hook) {
97
+ (0, utils_1.logLabeledBullet)("functions", `Executing ${event} lifecycle hook targeting: ${hook.task.function}...`);
98
+ executedEndpoint = await executeTaskQueueHook(hook.task, backendSpec);
99
+ }
100
+ else if ("call" in hook) {
101
+ throw new error_1.FirebaseError(`Lifecycle hook action type "call" is not supported.`);
102
+ }
103
+ else if ("http" in hook) {
104
+ throw new error_1.FirebaseError(`Lifecycle hook action type "http" is not supported.`);
105
+ }
106
+ else {
107
+ (0, functional_1.assertExhaustive)(hook);
108
+ }
109
+ if (executedEndpoint) {
110
+ (0, utils_1.logLabeledBullet)("functions", `View logs for ${event} at: ${getCloudConsoleLogUrl(executedEndpoint)}`);
111
+ }
112
+ return executedEndpoint;
113
+ }
@@ -75,9 +75,25 @@ function calculateUpdate(want, have) {
75
75
  }
76
76
  return update;
77
77
  }
78
- function createDeploymentPlan(args) {
79
- let { wantBackend, haveBackend, codebase, filters, deleteAll } = args;
80
- let deployment = {};
78
+ async function createDeploymentPlan(args) {
79
+ let { wantBackend, haveBackend, codebase, filters, deleteAll, haveRoles, existingManagedSA, managedSA, } = args;
80
+ const requiredRoles = wantBackend.requiredRoles;
81
+ const roles = haveRoles || [];
82
+ let rolesToAdd;
83
+ let rolesToRemove;
84
+ let serviceAccountToCreate;
85
+ let serviceAccountToDelete;
86
+ const isFiltered = !!(filters && filters.length > 0 && !deleteAll);
87
+ if (requiredRoles) {
88
+ rolesToAdd = requiredRoles.filter((r) => !roles.includes(r));
89
+ rolesToRemove = roles.filter((r) => !requiredRoles.includes(r));
90
+ if (!existingManagedSA && managedSA) {
91
+ serviceAccountToCreate = managedSA;
92
+ }
93
+ }
94
+ else if (existingManagedSA && !isFiltered) {
95
+ serviceAccountToDelete = existingManagedSA;
96
+ }
81
97
  wantBackend = backend.matchingBackend(wantBackend, (endpoint) => {
82
98
  return (0, functionsDeployHelper_1.endpointMatchesAnyFilter)(endpoint, filters);
83
99
  });
@@ -85,13 +101,14 @@ function createDeploymentPlan(args) {
85
101
  haveBackend = backend.matchingBackend(haveBackend, (endpoint) => {
86
102
  return wantedEndpoint(endpoint) || (0, functionsDeployHelper_1.endpointMatchesAnyFilter)(endpoint, filters);
87
103
  });
104
+ const regionalChangesets = {};
88
105
  const regions = new Set([
89
106
  ...Object.keys(wantBackend.endpoints),
90
107
  ...Object.keys(haveBackend.endpoints),
91
108
  ]);
92
109
  for (const region of regions) {
93
110
  const changesets = calculateChangesets(wantBackend.endpoints[region] || {}, haveBackend.endpoints[region] || {}, (e) => `${codebase}-${e.region}-${e.availableMemoryMb || "default"}`, deleteAll);
94
- deployment = { ...deployment, ...changesets };
111
+ Object.assign(regionalChangesets, changesets);
95
112
  }
96
113
  if (upgradedToGCFv2WithoutSettingConcurrency(wantBackend, haveBackend)) {
97
114
  utils.logLabeledBullet("functions", "You are updating one or more functions to Google Cloud Functions v2, " +
@@ -99,7 +116,26 @@ function createDeploymentPlan(args) {
99
116
  "default to 80 concurrent executions, but existing functions keep the " +
100
117
  "old default of 1. You can change this with the 'concurrency' option.");
101
118
  }
102
- return deployment;
119
+ if (requiredRoles) {
120
+ if (!managedSA) {
121
+ throw new error_1.FirebaseError("managedServiceAccount is required when requiredRoles is defined.", {
122
+ exit: 1,
123
+ });
124
+ }
125
+ return {
126
+ regionalChangesets,
127
+ rolesToAdd,
128
+ rolesToRemove,
129
+ serviceAccountToCreate,
130
+ managedServiceAccount: managedSA,
131
+ };
132
+ }
133
+ else {
134
+ return {
135
+ regionalChangesets,
136
+ serviceAccountToDelete,
137
+ };
138
+ }
103
139
  }
104
140
  function upgradedToGCFv2WithoutSettingConcurrency(want, have) {
105
141
  return backend.someEndpoint(want, (endpoint) => {
@@ -5,7 +5,6 @@ exports.yamlToBuild = yamlToBuild;
5
5
  exports.detectFromYaml = detectFromYaml;
6
6
  exports.detectFromPort = detectFromPort;
7
7
  exports.detectFromOutputPath = detectFromOutputPath;
8
- const node_fetch_1 = require("node-fetch");
9
8
  const fs = require("fs");
10
9
  const path = require("path");
11
10
  const yaml = require("yaml");
@@ -65,12 +64,14 @@ async function detectFromPort(port, project, runtime, initialDelay = 0, timeout
65
64
  const url = `http://127.0.0.1:${port}/__/functions.yaml`;
66
65
  while (true) {
67
66
  try {
68
- res = await Promise.race([(0, node_fetch_1.default)(url), timedOut]);
67
+ res = await Promise.race([fetch(url), timedOut]);
69
68
  break;
70
69
  }
71
70
  catch (err) {
71
+ const realErr = err?.cause || err;
72
72
  if (err?.name === "FetchError" ||
73
- ["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT"].includes(err?.code)) {
73
+ realErr?.name === "FetchError" ||
74
+ ["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT"].includes(realErr?.code)) {
74
75
  continue;
75
76
  }
76
77
  throw err;
@@ -20,12 +20,17 @@ function buildFromV1Alpha1(yaml, project, region, runtime) {
20
20
  specVersion: "string",
21
21
  params: "array",
22
22
  requiredAPIs: "array",
23
+ requiredRoles: "array",
23
24
  endpoints: "object",
24
25
  extensions: "object",
26
+ lifecycleHooks: "object",
25
27
  });
26
28
  const bd = build.empty();
27
29
  bd.params = manifest.params || [];
28
30
  bd.requiredAPIs = parseRequiredAPIs(manifest);
31
+ if (manifest.requiredRoles) {
32
+ bd.requiredRoles = parseRequiredRoles(manifest);
33
+ }
29
34
  for (const id of Object.keys(manifest.endpoints)) {
30
35
  const me = manifest.endpoints[id];
31
36
  assertBuildEndpoint(me, id);
@@ -41,8 +46,53 @@ function buildFromV1Alpha1(yaml, project, region, runtime) {
41
46
  bd.extensions[id] = be;
42
47
  }
43
48
  }
49
+ if (manifest.lifecycleHooks) {
50
+ bd.lifecycleHooks = {};
51
+ for (const id of Object.keys(manifest.lifecycleHooks)) {
52
+ if (id !== "afterFirstDeploy" && id !== "afterRedeploy") {
53
+ throw new error_1.FirebaseError(`Invalid eventType "${id}" for lifecycle hook.`);
54
+ }
55
+ const hook = manifest.lifecycleHooks[id];
56
+ if (!hook || typeof hook !== "object") {
57
+ throw new error_1.FirebaseError(`Invalid lifecycle hook configuration for "${id}".`);
58
+ }
59
+ if (hook.task) {
60
+ if (typeof hook.task.function !== "string" || !hook.task.function) {
61
+ throw new error_1.FirebaseError(`Invalid target "${hook.task.function || ""}" for lifecycle hook "${id}"`);
62
+ }
63
+ }
64
+ else if (hook.call) {
65
+ if (typeof hook.call.function !== "string" || !hook.call.function) {
66
+ throw new error_1.FirebaseError(`Invalid target "${hook.call.function || ""}" for lifecycle hook "${id}"`);
67
+ }
68
+ }
69
+ else if (hook.http) {
70
+ const target = hook.http.url || hook.http.function;
71
+ if (typeof target !== "string" || !target) {
72
+ throw new error_1.FirebaseError(`Invalid target "${target || ""}" for lifecycle hook "${id}"`);
73
+ }
74
+ }
75
+ else {
76
+ throw new error_1.FirebaseError(`No action (task, call, or http) specified for lifecycle hook "${id}"`);
77
+ }
78
+ bd.lifecycleHooks[id] = hook;
79
+ }
80
+ }
44
81
  return bd;
45
82
  }
83
+ const ROLE_REGEX = /^(roles\/[a-zA-Z0-9_\.\-]+|projects\/[a-zA-Z0-9\-]+\/roles\/[a-zA-Z0-9_\.\-]+|organizations\/[0-9]+\/roles\/[a-zA-Z0-9_\.\-]+)$/;
84
+ function parseRequiredRoles(manifest) {
85
+ const roles = manifest.requiredRoles || [];
86
+ for (const role of roles) {
87
+ if (typeof role !== "string") {
88
+ throw new error_1.FirebaseError(`Invalid role "${JSON.stringify(role)}". Expected string`);
89
+ }
90
+ if (!ROLE_REGEX.test(role)) {
91
+ throw new error_1.FirebaseError(`Invalid IAM role format "${role}".`);
92
+ }
93
+ }
94
+ return Array.from(new Set(roles));
95
+ }
46
96
  function parseRequiredAPIs(manifest) {
47
97
  const requiredAPIs = manifest.requiredAPIs || [];
48
98
  for (const { api, reason } of requiredAPIs) {
@@ -8,7 +8,6 @@ const path = require("path");
8
8
  const portfinder = require("portfinder");
9
9
  const semver = require("semver");
10
10
  const spawn = require("cross-spawn");
11
- const node_fetch_1 = require("node-fetch");
12
11
  const error_1 = require("../../../../error");
13
12
  const parseRuntimeAndValidateSDK_1 = require("./parseRuntimeAndValidateSDK");
14
13
  const logger_1 = require("../../../../logger");
@@ -156,7 +155,7 @@ class Delegate {
156
155
  childProcess.once("error", reject);
157
156
  });
158
157
  try {
159
- await (0, node_fetch_1.default)(`http://localhost:${port}/__/quitquitquit`);
158
+ await fetch(`http://localhost:${port}/__/quitquitquit`);
160
159
  }
161
160
  catch (e) {
162
161
  logger_1.logger.debug("Failed to call quitquitquit. This often means the server failed to start", e);
@@ -5,7 +5,6 @@ exports.tryCreateDelegate = tryCreateDelegate;
5
5
  exports.getPythonBinary = getPythonBinary;
6
6
  const fs = require("fs");
7
7
  const path = require("path");
8
- const node_fetch_1 = require("node-fetch");
9
8
  const util_1 = require("util");
10
9
  const portfinder = require("portfinder");
11
10
  const discovery = require("../discovery");
@@ -130,7 +129,7 @@ class Delegate {
130
129
  });
131
130
  return Promise.resolve(async () => {
132
131
  try {
133
- await (0, node_fetch_1.default)(`http://127.0.0.1:${port}/__/quitquitquit`);
132
+ await fetch(`http://127.0.0.1:${port}/__/quitquitquit`);
134
133
  }
135
134
  catch (e) {
136
135
  logger_1.logger.debug("Failed to call quitquitquit. This often means the server failed to start", e);
@@ -9,6 +9,7 @@ exports.functionsDirectoryExists = functionsDirectoryExists;
9
9
  exports.functionIdsAreValid = functionIdsAreValid;
10
10
  exports.secretsAreValid = secretsAreValid;
11
11
  exports.checkFiltersIntegrity = checkFiltersIntegrity;
12
+ exports.validateLifecycleHooks = validateLifecycleHooks;
12
13
  const path = require("path");
13
14
  const clc = require("colorette");
14
15
  const error_1 = require("../../error");
@@ -20,6 +21,7 @@ const fsutils = require("../../fsutils");
20
21
  const backend = require("./backend");
21
22
  const utils = require("../../utils");
22
23
  const secrets = require("../../functions/secrets");
24
+ const functional_1 = require("../../functional");
23
25
  const MAX_V1_TIMEOUT_SECONDS = 540;
24
26
  const MAX_V2_EVENTS_TIMEOUT_SECONDS = 540;
25
27
  const MAX_V2_SCHEDULE_TIMEOUT_SECONDS = 1800;
@@ -49,7 +51,8 @@ function validateScheduledTimeout(ep) {
49
51
  `The attempt deadline will be capped at ${exports.MAX_V2_SCHEDULE_ATTEMPT_DEADLINE_SECONDS} seconds.`);
50
52
  }
51
53
  }
52
- function endpointsAreValid(wantBackend) {
54
+ function endpointsAreValid(wantBackend, existingBackend) {
55
+ validateLifecycleHooks(wantBackend, existingBackend);
53
56
  const endpoints = backend.allEndpoints(wantBackend);
54
57
  functionIdsAreValid(endpoints);
55
58
  validateTimeoutConfig(endpoints);
@@ -288,3 +291,43 @@ function checkFiltersIntegrity(wantBackends, filters) {
288
291
  }
289
292
  }
290
293
  }
294
+ function validateLifecycleHooks(wantBackend, existingBackend) {
295
+ if (!wantBackend.lifecycleHooks) {
296
+ return;
297
+ }
298
+ const endpoints = [
299
+ ...backend.allEndpoints(wantBackend),
300
+ ...(existingBackend ? backend.allEndpoints(existingBackend) : []),
301
+ ];
302
+ for (const [eventType, hook] of Object.entries(wantBackend.lifecycleHooks)) {
303
+ const keys = Object.keys(hook).filter((k) => ["task", "call", "http"].includes(k));
304
+ if (keys.length !== 1) {
305
+ throw new error_1.FirebaseError(`Lifecycle hook "${eventType}" must specify exactly one action (task, call, or http).`);
306
+ }
307
+ if ("task" in hook) {
308
+ const targetEndpoint = findAndValidateTargetEndpoint(endpoints, hook.task.function, eventType);
309
+ if (!backend.isTaskQueueTriggered(targetEndpoint)) {
310
+ throw new error_1.FirebaseError(`Lifecycle hook "${eventType}" expects a task queue function.`);
311
+ }
312
+ }
313
+ else if ("call" in hook) {
314
+ throw new error_1.FirebaseError(`Lifecycle hook action type "call" is not supported in the CLI yet.`);
315
+ }
316
+ else if ("http" in hook) {
317
+ throw new error_1.FirebaseError(`Lifecycle hook action type "http" is not supported in the CLI yet.`);
318
+ }
319
+ else {
320
+ (0, functional_1.assertExhaustive)(hook);
321
+ }
322
+ }
323
+ }
324
+ function findAndValidateTargetEndpoint(endpoints, functionName, eventType) {
325
+ const targetEndpoint = endpoints.find((e) => e.id === functionName);
326
+ if (!targetEndpoint) {
327
+ throw new error_1.FirebaseError(`Target endpoint "${functionName}" not found in backend for lifecycle hook "${eventType}".`);
328
+ }
329
+ if (targetEndpoint.platform === "gcfv1") {
330
+ throw new error_1.FirebaseError(`Target endpoint "${functionName}" is a GCF Gen 1 function. Lifecycle hooks are only supported for GCF Gen 2 functions.`);
331
+ }
332
+ return targetEndpoint;
333
+ }
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Uploader = void 0;
4
4
  const lodash_1 = require("lodash");
5
- const abort_controller_1 = require("abort-controller");
6
5
  const clc = require("colorette");
7
6
  const crypto = require("crypto");
8
7
  const fs = require("fs");
@@ -172,7 +171,7 @@ class Uploader {
172
171
  if (!this.uploadClient) {
173
172
  throw new error_1.FirebaseError("No upload client available.", { exit: 2 });
174
173
  }
175
- const controller = new abort_controller_1.default();
174
+ const controller = new AbortController();
176
175
  const timeout = setTimeout(() => {
177
176
  controller.abort();
178
177
  }, this.uploadTimeout(this.hashMap[toUpload]));
@@ -190,7 +189,7 @@ class Uploader {
190
189
  }
191
190
  if (res.status !== 200) {
192
191
  const errorMessage = await res.response.text();
193
- logger_1.logger.debug(`[hosting][upload] ${this.hashMap[toUpload]} (${toUpload}) HTTP ERROR ${res.status}: headers=${JSON.stringify(res.response.headers.raw())} ${errorMessage}`);
192
+ logger_1.logger.debug(`[hosting][upload] ${this.hashMap[toUpload]} (${toUpload}) HTTP ERROR ${res.status}: headers=${JSON.stringify(Object.fromEntries(res.response.headers.entries()))} ${errorMessage}`);
194
193
  throw new Error(`Unexpected error while uploading file: ${errorMessage}`);
195
194
  }
196
195
  }
@@ -10,6 +10,7 @@ const ProgressBar = require("progress");
10
10
  const tmp = require("tmp");
11
11
  const apiv2_1 = require("./apiv2");
12
12
  const error_1 = require("./error");
13
+ const streamUtils_1 = require("./streamUtils");
13
14
  async function downloadToTmp(remoteUrl, auth = false) {
14
15
  const u = new url_1.URL(remoteUrl);
15
16
  const c = new apiv2_1.Client({ urlPrefix: u.origin, auth });
@@ -24,7 +25,8 @@ async function downloadToTmp(remoteUrl, auth = false) {
24
25
  resolveOnHTTPError: true,
25
26
  });
26
27
  if (res.status !== 200) {
27
- throw new error_1.FirebaseError(`download failed, status ${res.status}: ${await res.response.text()}`, {
28
+ const errorText = await (0, streamUtils_1.streamToString)(res.body);
29
+ throw new error_1.FirebaseError(`download failed, status ${res.status}: ${errorText}`, {
28
30
  status: res.status,
29
31
  });
30
32
  }
@@ -6,8 +6,6 @@ exports.setAccountInfoImpl = setAccountInfoImpl;
6
6
  exports.parseBlockingFunctionJwt = parseBlockingFunctionJwt;
7
7
  const url_1 = require("url");
8
8
  const jsonwebtoken_1 = require("jsonwebtoken");
9
- const node_fetch_1 = require("node-fetch");
10
- const abort_controller_1 = require("abort-controller");
11
9
  const utils_1 = require("./utils");
12
10
  const errors_1 = require("./errors");
13
11
  const types_1 = require("../types");
@@ -2183,7 +2181,7 @@ async function fetchBlockingFunction(state, event, user, options = {}, oauthToke
2183
2181
  jwt,
2184
2182
  },
2185
2183
  };
2186
- const controller = new abort_controller_1.default();
2184
+ const controller = new AbortController();
2187
2185
  const timeout = setTimeout(() => {
2188
2186
  controller.abort();
2189
2187
  }, timeoutMs);
@@ -2193,11 +2191,23 @@ async function fetchBlockingFunction(state, event, user, options = {}, oauthToke
2193
2191
  let text;
2194
2192
  try {
2195
2193
  const signal = controller.signal;
2196
- signal.reason = "";
2197
- signal.throwIfAborted = () => {
2198
- throw new error_1.FirebaseError("Aborted");
2199
- };
2200
- const res = await (0, node_fetch_1.default)(url, {
2194
+ try {
2195
+ if (!("reason" in signal)) {
2196
+ signal.reason = "";
2197
+ }
2198
+ }
2199
+ catch (e) {
2200
+ }
2201
+ try {
2202
+ if (!("throwIfAborted" in signal)) {
2203
+ signal.throwIfAborted = () => {
2204
+ throw new error_1.FirebaseError("Aborted");
2205
+ };
2206
+ }
2207
+ }
2208
+ catch (e) {
2209
+ }
2210
+ const res = await fetch(url, {
2201
2211
  method: "POST",
2202
2212
  headers: { "Content-Type": "application/json" },
2203
2213
  body: JSON.stringify(reqBody),
@@ -44,46 +44,46 @@
44
44
  }
45
45
  },
46
46
  "pubsub": {
47
- "version": "0.8.33",
48
- "expectedSize": 52948835,
49
- "expectedChecksum": "7dc33e0c8bb37948228e49a18375d53b",
50
- "expectedChecksumSHA256": "93768f8763d85c37f7f6e0f64d10195abaa088f4ff559b7d9fb8a2fc5520848d",
51
- "remoteUrl": "https://storage.googleapis.com/firebase-preview-drop/emulator/pubsub-emulator-0.8.33.zip",
52
- "downloadPathRelativeToCacheDir": "pubsub-emulator-0.8.33.zip",
53
- "binaryPathRelativeToCacheDir": "pubsub-emulator-0.8.33/pubsub-emulator/bin/cloud-pubsub-emulator"
47
+ "version": "0.8.34",
48
+ "expectedSize": 53008019,
49
+ "expectedChecksum": "3eb75c292dfa72d48a36428a49bac314",
50
+ "expectedChecksumSHA256": "062a71d30cbc0cc29de6e55f22ae9fc7eacb02c60272a2c4692826fdb83f0c25",
51
+ "remoteUrl": "https://storage.googleapis.com/firebase-preview-drop/emulator/pubsub-emulator-0.8.34.zip",
52
+ "downloadPathRelativeToCacheDir": "pubsub-emulator-0.8.34.zip",
53
+ "binaryPathRelativeToCacheDir": "pubsub-emulator-0.8.34/pubsub-emulator/bin/cloud-pubsub-emulator"
54
54
  },
55
55
  "dataconnect": {
56
56
  "darwin": {
57
- "version": "3.4.14",
58
- "expectedSize": 32468976,
59
- "expectedChecksum": "d62fc079187a1adc2ceef7dc0e1a1e44",
60
- "expectedChecksumSHA256": "4bbf0ae04ca83dcc765804549d30f94d02c7a8b6baeef5f3c52104e771627e43",
61
- "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-amd64-v3.4.14",
62
- "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.14"
57
+ "version": "3.4.15",
58
+ "expectedSize": 32502016,
59
+ "expectedChecksum": "00f4b06ef1a6a70fbba7b2b8c1cd8eac",
60
+ "expectedChecksumSHA256": "8ea50a699023e5464b3306689e464716b8df7c51cadfe4a3db5d346981e1a916",
61
+ "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-amd64-v3.4.15",
62
+ "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.15"
63
63
  },
64
64
  "darwin_arm64": {
65
- "version": "3.4.14",
66
- "expectedSize": 30604754,
67
- "expectedChecksum": "306e88ebe314eec82ae44842aa87d20e",
68
- "expectedChecksumSHA256": "3eeaf0dbfd48d3d7cf761e256e8850bc6159dccc128dd59f87bbeeaaaa6a62c6",
69
- "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-arm64-v3.4.14",
70
- "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.14"
65
+ "version": "3.4.15",
66
+ "expectedSize": 30638050,
67
+ "expectedChecksum": "5b824b0d725e87f4f1820f8c3d50f2ce",
68
+ "expectedChecksumSHA256": "563ea6788e2291b82ea1cd0744f2594f0661b79277bc13e4da190b939a99293d",
69
+ "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-arm64-v3.4.15",
70
+ "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.15"
71
71
  },
72
72
  "win32": {
73
- "version": "3.4.14",
74
- "expectedSize": 32509952,
75
- "expectedChecksum": "c5cbd9482be394bf466d4328b975d88e",
76
- "expectedChecksumSHA256": "b9f14587c5d5f87bccd093e1dddc9f5bb2a77be007201028d9ee8339e79219f0",
77
- "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-amd64-v3.4.14",
78
- "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.14.exe"
73
+ "version": "3.4.15",
74
+ "expectedSize": 32543232,
75
+ "expectedChecksum": "dbdbdca5482d386a142cd602177e811d",
76
+ "expectedChecksumSHA256": "01b1408750b2f1612d3a99e89702a600803f9d8f77669e5ad2502651ed560796",
77
+ "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-amd64-v3.4.15",
78
+ "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.15.exe"
79
79
  },
80
80
  "linux": {
81
- "version": "3.4.14",
82
- "expectedSize": 31625400,
83
- "expectedChecksum": "506a09ca12b61df98f705728047219dd",
84
- "expectedChecksumSHA256": "257d7aa4f4dd1e4de9c9d3db9f7b7834323f0d53e658e6814a933b1ec73ca27d",
85
- "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-amd64-v3.4.14",
86
- "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.14"
81
+ "version": "3.4.15",
82
+ "expectedSize": 31654072,
83
+ "expectedChecksum": "b49d21a4704ddf9420fa58d0c233821d",
84
+ "expectedChecksumSHA256": "b152edccdf50f3e8e3d6904d6cd1437eb88cb7a447ede3e3dd5092d12262bdc1",
85
+ "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-amd64-v3.4.15",
86
+ "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.15"
87
87
  }
88
88
  }
89
89
  }
@@ -320,6 +320,67 @@ function createCloudEndpoints(emulator) {
320
320
  }
321
321
  return (0, shared_1.sendFileBytes)(getObjectResponse.metadata, getObjectResponse.data, req, res);
322
322
  });
323
+ gcloudStorageAPI.post("/:bucketId", (req, res, next) => {
324
+ adminStorageLayer.createBucket(req.params.bucketId);
325
+ next();
326
+ }, async (req, res) => {
327
+ const contentTypeHeader = req.header("content-type");
328
+ if (!contentTypeHeader?.includes("multipart/form-data")) {
329
+ return res.status(400).send("Content-Type must be multipart/form-data");
330
+ }
331
+ try {
332
+ const bodyBuffer = await (0, request_1.reqBodyToBuffer)(req);
333
+ const formData = (0, multipart_1.parseFormDataMultipartRequest)(contentTypeHeader, bodyBuffer);
334
+ const keyPart = formData.find((p) => p.name === "key");
335
+ const filePart = formData.find((p) => p.type === "file");
336
+ if (keyPart?.type !== "field" || filePart?.type !== "file") {
337
+ return res.status(400).send("Missing 'key' or file.");
338
+ }
339
+ const metadata = {
340
+ contentType: filePart.contentType,
341
+ metadata: {},
342
+ };
343
+ const HEADER_MAP = {
344
+ "content-type": "contentType",
345
+ "cache-control": "cacheControl",
346
+ "content-disposition": "contentDisposition",
347
+ "content-encoding": "contentEncoding",
348
+ "content-language": "contentLanguage",
349
+ };
350
+ for (const part of formData) {
351
+ if (part.type === "file" || part.name === "key")
352
+ continue;
353
+ const key = part.name.toLowerCase();
354
+ if (key.startsWith("x-goog-meta-")) {
355
+ metadata.metadata[key.substring(12)] = part.value;
356
+ }
357
+ else if (HEADER_MAP[key]) {
358
+ metadata[HEADER_MAP[key]] = part.value.trim();
359
+ }
360
+ }
361
+ const upload = uploadService.multipartUpload({
362
+ bucketId: req.params.bucketId,
363
+ objectId: keyPart.value,
364
+ dataRaw: filePart.data,
365
+ metadata: metadata,
366
+ authorization: req.header("authorization"),
367
+ });
368
+ await adminStorageLayer.uploadObject(upload);
369
+ return res.sendStatus(204);
370
+ }
371
+ catch (err) {
372
+ if (err instanceof errors_1.ForbiddenError) {
373
+ return res.sendStatus(403);
374
+ }
375
+ if (err instanceof errors_1.NotFoundError) {
376
+ return res.sendStatus(404);
377
+ }
378
+ if (err instanceof Error) {
379
+ return res.status(400).send(err.message);
380
+ }
381
+ throw err;
382
+ }
383
+ });
323
384
  gcloudStorageAPI.post("/b/:bucketId/o/:objectId/:method(rewriteTo|copyTo)/b/:destBucketId/o/:destObjectId", (req, res, next) => {
324
385
  if (req.params.method === "rewriteTo" && req.query.rewriteToken) {
325
386
  return next();