firebase-tools 15.22.2 → 15.22.4

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.
@@ -24,6 +24,7 @@ const EXPORTED_JSON_KEYS = [
24
24
  "phoneNumber",
25
25
  "disabled",
26
26
  "customAttributes",
27
+ "mfaInfo",
27
28
  ];
28
29
  const EXPORTED_JSON_KEYS_RENAMING = {
29
30
  lastLoginAt: "lastSignedInAt",
@@ -27,6 +27,7 @@ const ALLOWED_JSON_KEYS = [
27
27
  "phoneNumber",
28
28
  "disabled",
29
29
  "customAttributes",
30
+ "mfaInfo",
30
31
  ];
31
32
  const ALLOWED_JSON_KEYS_RENAMING = {
32
33
  lastSignedInAt: "lastLoginAt",
package/lib/apiv2.js CHANGED
@@ -4,6 +4,7 @@ exports.Client = exports.CLI_OAUTH_PROJECT_NUMBER = exports.GOOG_USER_PROJECT_HE
4
4
  exports.setRefreshToken = setRefreshToken;
5
5
  exports.setAccessToken = setAccessToken;
6
6
  exports.getAccessToken = getAccessToken;
7
+ exports.noKeepAliveAgent = noKeepAliveAgent;
7
8
  const url_1 = require("url");
8
9
  const stream_1 = require("stream");
9
10
  const proxy_agent_1 = require("proxy-agent");
@@ -2,9 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runUniversalMaker = runUniversalMaker;
4
4
  exports.localBuild = localBuild;
5
+ exports.validateLocalBuildNodeVersion = validateLocalBuildNodeVersion;
5
6
  const childProcess = require("child_process");
6
7
  const fs = require("fs-extra");
7
8
  const path = require("path");
9
+ const semver = require("semver");
8
10
  const index_1 = require("./secrets/index");
9
11
  const prompt_1 = require("../prompt");
10
12
  const error_1 = require("../error");
@@ -138,3 +140,51 @@ async function toProcessEnv(projectId, env) {
138
140
  }));
139
141
  return Object.fromEntries(resolvedEntries);
140
142
  }
143
+ function validateLocalBuildNodeVersion(backend, projectRoot) {
144
+ const runtimeValue = backend.runtime?.value ?? "";
145
+ const isLegacyRuntime = runtimeValue === "" || runtimeValue === "nodejs";
146
+ const abiuEnabled = !isLegacyRuntime && !backend.automaticBaseImageUpdatesDisabled;
147
+ if (!abiuEnabled) {
148
+ throw new error_1.FirebaseError(`Local builds are only supported for backends with ABIU (Automatic Base Image Updates) enabled. ` +
149
+ `Your backend is currently configured with a non-ABIU runtime ("${runtimeValue || "unspecified"}"). ` +
150
+ `Please update your backend to a versioned runtime (e.g., nodejs22) to enable local builds.`, { exit: 1 });
151
+ }
152
+ const targetMajorMatch = runtimeValue.match(/^nodejs(\d+)$/);
153
+ if (!targetMajorMatch) {
154
+ (0, utils_1.logLabeledWarning)("apphosting", `Unable to extract Node.js major version from the backend runtime ("${runtimeValue}"). ` +
155
+ `Skipping local Node.js version compatibility checks.`);
156
+ return;
157
+ }
158
+ const targetMajor = parseInt(targetMajorMatch[1], 10);
159
+ let localNodeVersion;
160
+ try {
161
+ localNodeVersion = childProcess.execSync("node -v", { encoding: "utf8" }).trim();
162
+ }
163
+ catch {
164
+ (0, utils_1.logLabeledWarning)("apphosting", `Unable to detect your local Node.js version (is 'node' installed and in your PATH?). ` +
165
+ `Skipping local Node.js version compatibility checks.`);
166
+ return;
167
+ }
168
+ const packageJsonPath = path.join(projectRoot, "package.json");
169
+ const packageJson = fs.readJsonSync(packageJsonPath, { throws: false });
170
+ const enginesNode = packageJson?.engines?.node;
171
+ if (enginesNode) {
172
+ (0, utils_1.logLabeledWarning)("apphosting", `Your package.json specifies Node.js engine "${enginesNode}". ` +
173
+ `Please note that local builds do NOT use the "engines" field to resolve or download Node.js. ` +
174
+ `Instead, your local build uses your host machine's active Node.js version (${localNodeVersion}) to compile the app, ` +
175
+ `and your deployed app will run on the backend's configured ABIU runtime (${runtimeValue}).`);
176
+ const targetRange = `^${targetMajor}.0.0`;
177
+ if (semver.validRange(enginesNode) && !semver.intersects(targetRange, enginesNode)) {
178
+ (0, utils_1.logLabeledWarning)("apphosting", `The Node.js version range specified in your package.json engines ("${enginesNode}") ` +
179
+ `does not satisfy your backend's target ABIU runtime version (Node.js ${targetMajor}). ` +
180
+ `Please update your package.json engines to align with your backend configuration.`);
181
+ }
182
+ }
183
+ const localMajorMatch = localNodeVersion.match(/^v?(\d+)/);
184
+ const localMajor = localMajorMatch ? parseInt(localMajorMatch[1], 10) : null;
185
+ if (localMajor !== null && localMajor !== targetMajor) {
186
+ (0, utils_1.logLabeledWarning)("apphosting", `Local Node.js version (${localNodeVersion}) does not match your backend's target Node.js version (Node.js ${targetMajor}). ` +
187
+ `This mismatch may cause runtime issues. ` +
188
+ `Please switch your local environment to Node.js ${targetMajor} to ensure build-to-run parity.`);
189
+ }
190
+ }
@@ -4,6 +4,7 @@ exports.default = default_1;
4
4
  exports.injectEnvVarsFromApphostingConfig = injectEnvVarsFromApphostingConfig;
5
5
  exports.injectAutoInitEnvVars = injectAutoInitEnvVars;
6
6
  exports.getBackendConfigs = getBackendConfigs;
7
+ exports.injectAngularEnvVars = injectAngularEnvVars;
7
8
  const crypto = require("crypto");
8
9
  const fs = require("fs");
9
10
  const os = require("os");
@@ -134,10 +135,29 @@ async function default_1(context, options) {
134
135
  continue;
135
136
  }
136
137
  experiments.assertEnabled("apphostinglocalbuilds", "locally build App Hosting backends");
138
+ let backend = backends.find((b) => (0, apphosting_1.parseBackendName)(b.name).id === cfg.backendId);
139
+ if (!backend) {
140
+ const location = context.backendLocations[cfg.backendId];
141
+ if (location) {
142
+ const apphosting = await Promise.resolve().then(() => require("../../gcp/apphosting"));
143
+ try {
144
+ backend = await apphosting.getBackend(projectId, location, cfg.backendId);
145
+ }
146
+ catch {
147
+ }
148
+ }
149
+ }
150
+ if (!backend) {
151
+ throw new error_1.FirebaseError(`Backend ${cfg.backendId} not found.`);
152
+ }
153
+ const rootDir = path.resolve(options.projectRoot || process.cwd());
154
+ const appDir = path.join(rootDir, cfg.rootDir || "");
155
+ (0, localbuilds_1.validateLocalBuildNodeVersion)(backend, appDir);
137
156
  (0, utils_1.logLabeledBullet)("apphosting", `Starting local build for backend ${cfg.backendId}`);
138
157
  await injectEnvVarsFromApphostingConfig(configs.filter((c) => c.backendId === cfg.backendId), options, buildEnv, runtimeEnv);
139
158
  await injectAutoInitEnvVars(cfg, backends, buildEnv, runtimeEnv);
140
- const rootDir = path.resolve(options.projectRoot || process.cwd());
159
+ const location = context.backendLocations[cfg.backendId];
160
+ await injectAngularEnvVars(cfg, rootDir, projectId, location, buildEnv, runtimeEnv);
141
161
  const pathHash = crypto.createHash("md5").update(rootDir).digest("hex").substring(0, 8);
142
162
  const localBuildScratchDir = path.join(os.tmpdir(), `apphosting-local-build-${cfg.backendId}-${pathHash}`);
143
163
  try {
@@ -266,3 +286,82 @@ async function prepareLocalBuildScratchDirectory(rootDir, localBuildScratchDir,
266
286
  fs.copyFileSync(file.name, destPath);
267
287
  }
268
288
  }
289
+ function isAngularApplication(appDir) {
290
+ const packageJsonPath = path.join(appDir, "package.json");
291
+ if (fs.existsSync(packageJsonPath)) {
292
+ try {
293
+ const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
294
+ if (parsed && typeof parsed === "object") {
295
+ const pkg = parsed;
296
+ const angularDeps = ["@angular/core", "@angular/ssr", "@angular/platform-server"];
297
+ if (angularDeps.some((d) => pkg.dependencies?.[d] || pkg.devDependencies?.[d])) {
298
+ return true;
299
+ }
300
+ }
301
+ }
302
+ catch (e) {
303
+ (0, utils_1.logLabeledError)("apphosting", `error when checking if application is angular: ${e instanceof Error ? e.message : String(e)}`);
304
+ }
305
+ }
306
+ const angularJsonPath = path.join(appDir, "angular.json");
307
+ if (fs.existsSync(angularJsonPath)) {
308
+ return true;
309
+ }
310
+ return false;
311
+ }
312
+ async function injectAngularEnvVars(cfg, projectRoot, projectId, location, buildEnv, runtimeEnv) {
313
+ const appDir = path.join(projectRoot, cfg.rootDir || "");
314
+ if (!isAngularApplication(appDir)) {
315
+ return;
316
+ }
317
+ if (!location) {
318
+ throw new error_1.FirebaseError(`Failed to find location for backend ${cfg.backendId}.`);
319
+ }
320
+ const backendId = cfg.backendId;
321
+ runtimeEnv[backendId] ?? (runtimeEnv[backendId] = {});
322
+ buildEnv[backendId] ?? (buildEnv[backendId] = {});
323
+ const allowedProxyHeaders = [
324
+ "x-forwarded-host",
325
+ "x-forwarded-port",
326
+ "x-forwarded-proto",
327
+ "x-forwarded-for",
328
+ ];
329
+ const allowedProxyHeadersValue = allowedProxyHeaders.join(",");
330
+ let shouldInjectProxyHeaders = true;
331
+ if (runtimeEnv[backendId]["NG_TRUST_PROXY_HEADERS"]) {
332
+ const userProxyHeadersStr = runtimeEnv[backendId]["NG_TRUST_PROXY_HEADERS"].value;
333
+ const userProxyHeaders = userProxyHeadersStr
334
+ ? userProxyHeadersStr
335
+ .split(",")
336
+ .map((h) => h.trim().toLowerCase())
337
+ .filter(Boolean)
338
+ : [];
339
+ const isSubset = userProxyHeaders.length > 0 && userProxyHeaders.every((h) => allowedProxyHeaders.includes(h));
340
+ if (isSubset) {
341
+ shouldInjectProxyHeaders = false;
342
+ }
343
+ else {
344
+ throw new error_1.FirebaseError(`User-defined RUNTIME environment variable NG_TRUST_PROXY_HEADERS contains invalid headers. Allowed values: ${allowedProxyHeadersValue}`);
345
+ }
346
+ }
347
+ if (shouldInjectProxyHeaders) {
348
+ runtimeEnv[backendId]["NG_TRUST_PROXY_HEADERS"] = {
349
+ value: allowedProxyHeadersValue,
350
+ availability: ["RUNTIME"],
351
+ };
352
+ }
353
+ if (!runtimeEnv[backendId]["NG_ALLOWED_HOSTS"]) {
354
+ const projectNumber = await (0, getProjectNumber_1.getProjectNumber)({ project: projectId });
355
+ const sharedDomain = "hosted.app";
356
+ const defaultHosts = [
357
+ `${backendId}-${projectNumber}.${location}.run.app`,
358
+ `${backendId}--${projectId}.${location}.${sharedDomain}`,
359
+ `${backendId}--${projectId}.web.app`,
360
+ `${backendId}--${projectId}.firebaseapp.com`,
361
+ ];
362
+ runtimeEnv[backendId]["NG_ALLOWED_HOSTS"] = {
363
+ value: defaultHosts.join(","),
364
+ availability: ["RUNTIME"],
365
+ };
366
+ }
367
+ }
@@ -174,6 +174,9 @@ function merge(...backends) {
174
174
  apiToReasons[api] = reasons;
175
175
  }
176
176
  merged.environmentVariables = { ...merged.environmentVariables, ...b.environmentVariables };
177
+ if (b.lifecycleHooks) {
178
+ merged.lifecycleHooks = { ...(merged.lifecycleHooks || {}), ...b.lifecycleHooks };
179
+ }
177
180
  }
178
181
  for (const [api, reasons] of Object.entries(apiToReasons)) {
179
182
  merged.requiredAPIs.push({ api, reason: Array.from(reasons).join(" ") });
@@ -181,7 +184,9 @@ function merge(...backends) {
181
184
  return merged;
182
185
  }
183
186
  function isEmptyBackend(backend) {
184
- return (Object.keys(backend.requiredAPIs).length === 0 && Object.keys(backend.endpoints).length === 0);
187
+ return (Object.keys(backend.requiredAPIs).length === 0 &&
188
+ Object.keys(backend.endpoints).length === 0 &&
189
+ Object.keys(backend.lifecycleHooks || {}).length === 0);
185
190
  }
186
191
  function functionName(cloudFunction) {
187
192
  return `projects/${cloudFunction.project}/locations/${cloudFunction.region}/functions/${cloudFunction.id}`;
@@ -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.lifecycleHooks) {
267
+ bkend.lifecycleHooks = build.lifecycleHooks;
268
+ }
266
269
  return bkend;
267
270
  }
268
271
  function discoverTrigger(endpoint, region, r) {
@@ -375,4 +378,26 @@ function applyPrefix(build, prefix) {
375
378
  }
376
379
  }
377
380
  build.endpoints = newEndpoints;
381
+ if (build.lifecycleHooks) {
382
+ for (const hook of Object.values(build.lifecycleHooks)) {
383
+ if ("task" in hook) {
384
+ if (hook.task?.function) {
385
+ hook.task.function = `${prefix}-${hook.task.function}`;
386
+ }
387
+ }
388
+ else if ("call" in hook) {
389
+ if (hook.call?.function) {
390
+ hook.call.function = `${prefix}-${hook.call.function}`;
391
+ }
392
+ }
393
+ else if ("http" in hook) {
394
+ if (hook.http?.function) {
395
+ hook.http.function = `${prefix}-${hook.http.function}`;
396
+ }
397
+ }
398
+ else {
399
+ (0, functional_1.assertExhaustive)(hook);
400
+ }
401
+ }
402
+ }
378
403
  }
@@ -203,7 +203,7 @@ async function prepare(context, options, payload) {
203
203
  await (0, triggerRegionHelper_1.ensureTriggerRegions)(wantBackend);
204
204
  resolveCpuAndConcurrency(wantBackend);
205
205
  resolveDefaultTimeout(wantBackend);
206
- validate.endpointsAreValid(wantBackend);
206
+ validate.endpointsAreValid(wantBackend, existingBackend);
207
207
  inferBlockingDetails(wantBackend);
208
208
  }
209
209
  const wantBackend = backend.merge(...Object.values(wantBackends));
@@ -18,6 +18,7 @@ const error_1 = require("../../../error");
18
18
  const getProjectNumber_1 = require("../../../getProjectNumber");
19
19
  const extensions_1 = require("../../extensions");
20
20
  const artifacts = require("../../../functions/artifacts");
21
+ const lifecycle_1 = require("./lifecycle");
21
22
  async function release(context, options, payload) {
22
23
  if (context.extensions && payload.extensions) {
23
24
  await (0, extensions_1.release)(context.extensions, options, payload.extensions);
@@ -88,6 +89,9 @@ async function release(context, options, payload) {
88
89
  reporter.printErrors(summary);
89
90
  const wantBackend = backend.merge(...Object.values(payload.functions).map((p) => p.wantBackend));
90
91
  printTriggerUrls(wantBackend, projectNumber);
92
+ for (const [codebase, { wantBackend: w, haveBackend: h }] of Object.entries(payload.functions)) {
93
+ await (0, lifecycle_1.executeLifecycleHooks)(w, h, plan, codebase);
94
+ }
91
95
  await setupArtifactCleanupPolicies(options, options.projectId, Object.keys(wantBackend.endpoints));
92
96
  const allErrors = summary.results.filter((r) => r.error).map((r) => r.error);
93
97
  if (allErrors.length) {
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.determineDeploymentDelta = determineDeploymentDelta;
4
+ exports.executeLifecycleHooks = executeLifecycleHooks;
5
+ const backend = require("../backend");
6
+ const error_1 = require("../../../error");
7
+ const logger_1 = require("../../../logger");
8
+ const utils_1 = require("../../../utils");
9
+ const cloudtasks = require("../../../gcp/cloudtasks");
10
+ const computeEngine = require("../../../gcp/computeEngine");
11
+ const projects_1 = require("../../../management/projects");
12
+ const functional_1 = require("../../../functional");
13
+ function determineDeploymentDelta(haveBackend) {
14
+ const hasExistingEndpoints = backend.someEndpoint(haveBackend, () => true);
15
+ if (!hasExistingEndpoints) {
16
+ return "afterInstall";
17
+ }
18
+ return "afterUpdate";
19
+ }
20
+ async function executeLifecycleHooks(wantBackend, haveBackend, plan, codebase) {
21
+ const delta = determineDeploymentDelta(haveBackend);
22
+ const hooks = wantBackend.lifecycleHooks || {};
23
+ const hook = hooks[delta];
24
+ if (!hook) {
25
+ logger_1.logger.debug(`No lifecycle hook configured for event: ${delta}`);
26
+ return false;
27
+ }
28
+ if (delta === "afterUpdate" && plan) {
29
+ const relevantChangesets = Object.entries(plan)
30
+ .filter(([key]) => {
31
+ if (!codebase)
32
+ return true;
33
+ if (key.startsWith(`${codebase}-`))
34
+ return true;
35
+ if (codebase === "default" && key.startsWith("-"))
36
+ return true;
37
+ return false;
38
+ })
39
+ .map(([, c]) => c);
40
+ const hasResourceModifications = relevantChangesets.some((changeset) => changeset.endpointsToCreate.length > 0 ||
41
+ changeset.endpointsToUpdate.length > 0 ||
42
+ changeset.endpointsToDelete.length > 0);
43
+ if (!hasResourceModifications) {
44
+ (0, utils_1.logLabeledBullet)("functions", "No resources modified in codebase. Skipping afterUpdate lifecycle hook.");
45
+ return false;
46
+ }
47
+ }
48
+ if ("task" in hook) {
49
+ (0, utils_1.logLabeledBullet)("functions", `Executing ${delta} lifecycle hook targeting: ${hook.task.function}...`);
50
+ await executeTaskQueueHook(hook.task, wantBackend);
51
+ return true;
52
+ }
53
+ else if ("call" in hook) {
54
+ throw new error_1.FirebaseError(`Lifecycle hook action type "call" is not supported.`);
55
+ }
56
+ else if ("http" in hook) {
57
+ throw new error_1.FirebaseError(`Lifecycle hook action type "http" is not supported.`);
58
+ }
59
+ else {
60
+ (0, functional_1.assertExhaustive)(hook);
61
+ }
62
+ }
63
+ async function executeTaskQueueHook(taskHook, wantBackend) {
64
+ const targetEndpoint = findTargetEndpoint(wantBackend, taskHook.function);
65
+ if (!targetEndpoint) {
66
+ throw new error_1.FirebaseError(`Target endpoint "${taskHook.function}" not found in backend for lifecycle hook.`);
67
+ }
68
+ if (!backend.isTaskQueueTriggered(targetEndpoint)) {
69
+ throw new error_1.FirebaseError(`Target endpoint "${taskHook.function}" is not a task queue function.`);
70
+ }
71
+ const queueName = cloudtasks.queueNameForEndpoint(targetEndpoint);
72
+ const bodyStr = taskHook.body ? JSON.stringify(taskHook.body) : "";
73
+ const body = bodyStr ? Buffer.from(bodyStr).toString("base64") : undefined;
74
+ const url = targetEndpoint.uri;
75
+ if (!url) {
76
+ throw new error_1.FirebaseError(`Target endpoint "${taskHook.function}" does not have a trigger URI.`);
77
+ }
78
+ const projectMetadata = await (0, projects_1.getProject)(targetEndpoint.project);
79
+ const projectNumber = projectMetadata.projectNumber;
80
+ const sa = targetEndpoint.serviceAccount || (await computeEngine.getDefaultServiceAccount(projectNumber));
81
+ const task = {
82
+ httpRequest: {
83
+ url,
84
+ httpMethod: "POST",
85
+ headers: {
86
+ "Content-Type": "application/json",
87
+ },
88
+ oidcToken: {
89
+ serviceAccountEmail: sa,
90
+ audience: url,
91
+ },
92
+ },
93
+ };
94
+ if (body) {
95
+ task.httpRequest.body = body;
96
+ }
97
+ try {
98
+ await cloudtasks.enqueueTask(queueName, task);
99
+ (0, utils_1.logLabeledSuccess)("functions", `Successfully queued task for lifecycle hook ${taskHook.function} in queue ${queueName}.`);
100
+ (0, utils_1.logLabeledBullet)("functions", `View logs for ${taskHook.function} at: ${getCloudConsoleLogUrl(targetEndpoint)}`);
101
+ }
102
+ catch (err) {
103
+ const errorMsg = err instanceof Error ? err.message : String(err);
104
+ (0, utils_1.logLabeledWarning)("functions", `Failed to enqueue task for lifecycle hook ${taskHook.function}: ${errorMsg}`);
105
+ }
106
+ }
107
+ function findTargetEndpoint(backendSpec, targetId) {
108
+ for (const endpoint of backend.allEndpoints(backendSpec)) {
109
+ if (endpoint.id === targetId) {
110
+ return endpoint;
111
+ }
112
+ }
113
+ return undefined;
114
+ }
115
+ function getCloudConsoleLogUrl(endpoint) {
116
+ const { project, region, id } = endpoint;
117
+ const serviceName = endpoint.runServiceId || id;
118
+ const query = `resource.type="cloud_run_revision"\nresource.labels.service_name="${serviceName}"\nresource.labels.location="${region}"`;
119
+ return `https://console.cloud.google.com/logs/query;query=${encodeURIComponent(query)};project=${project}`;
120
+ }
@@ -22,6 +22,7 @@ function buildFromV1Alpha1(yaml, project, region, runtime) {
22
22
  requiredAPIs: "array",
23
23
  endpoints: "object",
24
24
  extensions: "object",
25
+ lifecycleHooks: "object",
25
26
  });
26
27
  const bd = build.empty();
27
28
  bd.params = manifest.params || [];
@@ -41,6 +42,38 @@ function buildFromV1Alpha1(yaml, project, region, runtime) {
41
42
  bd.extensions[id] = be;
42
43
  }
43
44
  }
45
+ if (manifest.lifecycleHooks) {
46
+ bd.lifecycleHooks = {};
47
+ for (const id of Object.keys(manifest.lifecycleHooks)) {
48
+ if (id !== "afterInstall" && id !== "afterUpdate") {
49
+ throw new error_1.FirebaseError(`Invalid eventType "${id}" for lifecycle hook.`);
50
+ }
51
+ const hook = manifest.lifecycleHooks[id];
52
+ if (!hook || typeof hook !== "object") {
53
+ throw new error_1.FirebaseError(`Invalid lifecycle hook configuration for "${id}".`);
54
+ }
55
+ if (hook.task) {
56
+ if (typeof hook.task.function !== "string" || !hook.task.function) {
57
+ throw new error_1.FirebaseError(`Invalid target "${hook.task.function || ""}" for lifecycle hook "${id}"`);
58
+ }
59
+ }
60
+ else if (hook.call) {
61
+ if (typeof hook.call.function !== "string" || !hook.call.function) {
62
+ throw new error_1.FirebaseError(`Invalid target "${hook.call.function || ""}" for lifecycle hook "${id}"`);
63
+ }
64
+ }
65
+ else if (hook.http) {
66
+ const target = hook.http.url || hook.http.function;
67
+ if (typeof target !== "string" || !target) {
68
+ throw new error_1.FirebaseError(`Invalid target "${target || ""}" for lifecycle hook "${id}"`);
69
+ }
70
+ }
71
+ else {
72
+ throw new error_1.FirebaseError(`No action (task, call, or http) specified for lifecycle hook "${id}"`);
73
+ }
74
+ bd.lifecycleHooks[id] = hook;
75
+ }
76
+ }
44
77
  return bd;
45
78
  }
46
79
  function parseRequiredAPIs(manifest) {
@@ -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
+ }
@@ -44,13 +44,13 @@
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": {
@@ -646,7 +646,9 @@ class FirestoreApi {
646
646
  pageSize: limit,
647
647
  },
648
648
  });
649
- return res.body;
649
+ return {
650
+ operations: res.body?.operations || [],
651
+ };
650
652
  }
651
653
  async describeOperation(project, databaseId, operationName) {
652
654
  const url = `/projects/${project}/databases/${databaseId}/operations/${operationName}`;
@@ -7,6 +7,7 @@ exports.updateQueue = updateQueue;
7
7
  exports.upsertQueue = upsertQueue;
8
8
  exports.purgeQueue = purgeQueue;
9
9
  exports.deleteQueue = deleteQueue;
10
+ exports.enqueueTask = enqueueTask;
10
11
  exports.setIamPolicy = setIamPolicy;
11
12
  exports.getIamPolicy = getIamPolicy;
12
13
  exports.setEnqueuer = setEnqueuer;
@@ -77,6 +78,9 @@ async function purgeQueue(name) {
77
78
  async function deleteQueue(name) {
78
79
  await client.delete(name);
79
80
  }
81
+ async function enqueueTask(queueName, task) {
82
+ await client.post(`${queueName}/tasks`, { task });
83
+ }
80
84
  async function setIamPolicy(name, policy) {
81
85
  const res = await client.post(`${name}:setIamPolicy`, {
82
86
  policy,
@@ -20,7 +20,17 @@ function getAuthClient(config) {
20
20
  if (authClient) {
21
21
  return authClient;
22
22
  }
23
- authClient = new google_auth_library_1.GoogleAuth(config);
23
+ const authConfig = {
24
+ ...config,
25
+ clientOptions: {
26
+ ...config.clientOptions,
27
+ transporterOptions: {
28
+ ...config.clientOptions?.transporterOptions,
29
+ agent: apiv2.noKeepAliveAgent,
30
+ },
31
+ },
32
+ };
33
+ authClient = new google_auth_library_1.GoogleAuth(authConfig);
24
34
  return authClient;
25
35
  }
26
36
  async function autoAuth(options, authScopes) {