firebase-tools 15.15.0 → 15.16.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 (54) hide show
  1. package/lib/agentSkills.js +3 -2
  2. package/lib/apphosting/backend.js +7 -19
  3. package/lib/apphosting/localbuilds.js +32 -4
  4. package/lib/apphosting/prompts.js +32 -11
  5. package/lib/apphosting/secrets/index.js +43 -0
  6. package/lib/apphosting/yaml.js +2 -4
  7. package/lib/bin/firebase.js +10 -0
  8. package/lib/bin/mcp.js +12 -1
  9. package/lib/commands/apphosting-backends-create.js +4 -10
  10. package/lib/commands/dataconnect-sdk-generate.js +2 -2
  11. package/lib/commands/deploy.js +6 -1
  12. package/lib/config.js +1 -0
  13. package/lib/dataconnect/build.js +6 -0
  14. package/lib/dataconnect/names.js +1 -1
  15. package/lib/deploy/apphosting/prepare.js +5 -2
  16. package/lib/deploy/firestore/prepare.js +1 -1
  17. package/lib/deploy/functions/backend.js +22 -14
  18. package/lib/deploy/functions/prepare.js +8 -2
  19. package/lib/deploy/functions/release/fabricator.js +26 -14
  20. package/lib/deploy/functions/runtimes/python/index.js +3 -0
  21. package/lib/deploy/functions/runtimes/supported/types.js +6 -0
  22. package/lib/deploy/hosting/prepare.js +1 -0
  23. package/lib/emulator/adminSdkConfig.js +2 -1
  24. package/lib/emulator/apphosting/serve.js +2 -39
  25. package/lib/emulator/commandUtils.js +4 -1
  26. package/lib/emulator/controller.js +4 -3
  27. package/lib/emulator/dataconnectEmulator.js +7 -4
  28. package/lib/emulator/downloadableEmulatorInfo.json +30 -30
  29. package/lib/emulator/functionsEmulator.js +12 -6
  30. package/lib/emulator/functionsEmulatorRuntime.js +12 -5
  31. package/lib/emulator/functionsRuntimeWorker.js +6 -3
  32. package/lib/experiments.js +6 -1
  33. package/lib/frameworks/angular/index.js +6 -1
  34. package/lib/gcp/rules.js +8 -4
  35. package/lib/init/features/agentSkills.js +2 -2
  36. package/lib/init/features/apphosting.js +2 -8
  37. package/lib/init/features/dataconnect/index.js +26 -15
  38. package/lib/init/features/dataconnect/sdk.js +8 -3
  39. package/lib/init/features/firestore/rules.js +2 -1
  40. package/lib/init/features/functions/dart.js +2 -0
  41. package/lib/init/features/storage/rules.js +2 -1
  42. package/lib/mcp/apps/update_environment/mcp-app.js +138 -0
  43. package/lib/mcp/index.js +57 -2
  44. package/lib/mcp/resources/index.js +2 -0
  45. package/lib/mcp/resources/update_environment_ui.js +32 -0
  46. package/lib/mcp/tools/core/get_security_rules.js +2 -1
  47. package/lib/mcp/util.js +19 -0
  48. package/lib/rulesDeploy.js +35 -16
  49. package/lib/tsconfig.compile.tsbuildinfo +1 -1
  50. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  51. package/package.json +1 -1
  52. package/schema/firebase-config.json +4 -2
  53. package/templates/init/functions/dart/analysis_options.yaml +30 -0
  54. package/templates/init/functions/dart/pubspec.yaml +1 -0
@@ -6,10 +6,11 @@ const child_process_1 = require("child_process");
6
6
  const utils = require("./utils");
7
7
  const prompt = require("./prompt");
8
8
  const error_1 = require("./error");
9
- async function promptForAgentSkills() {
9
+ async function promptForAgentSkills(options) {
10
10
  return prompt.confirm({
11
11
  message: "Would you like to install agent skills for Firebase?",
12
- default: true,
12
+ default: !options?.nonInteractive,
13
+ nonInteractive: options?.nonInteractive,
13
14
  });
14
15
  }
15
16
  async function installAgentSkills(options) {
@@ -66,7 +66,7 @@ async function awaitTlsReady(url) {
66
66
  }
67
67
  } while (!ready);
68
68
  }
69
- async function doSetup(projectId, nonInteractive, webAppName, backendId, serviceAccount, primaryRegion, rootDir, runtime, automaticBaseImageUpdatesDisabled) {
69
+ async function doSetup(projectId, nonInteractive, webAppName, backendId, serviceAccount, primaryRegion, rootDir, runtime) {
70
70
  await ensureRequiredApisEnabled(projectId);
71
71
  await ensureAppHostingComputeServiceAccount(projectId, serviceAccount ? serviceAccount : null);
72
72
  let location = primaryRegion;
@@ -99,25 +99,13 @@ async function doSetup(projectId, nonInteractive, webAppName, backendId, service
99
99
  if (!location || !backendId) {
100
100
  throw new error_1.FirebaseError("Internal error: location or backendId is not defined.");
101
101
  }
102
- if (runtime === undefined && (0, experiments_1.isEnabled)("abiu")) {
103
- if (nonInteractive) {
104
- runtime = prompts_1.DEFAULT_RUNTIME;
105
- }
106
- else {
107
- runtime = await (0, prompts_1.promptRuntime)(projectId, location);
108
- }
109
- }
110
- if (automaticBaseImageUpdatesDisabled === undefined && (0, experiments_1.isEnabled)("abiu")) {
111
- if (!nonInteractive) {
112
- automaticBaseImageUpdatesDisabled = !(await (0, prompts_1.promptAutomaticBaseImageUpdates)());
113
- }
114
- }
102
+ runtime = await (0, prompts_1.resolveRuntime)(projectId, location, nonInteractive, runtime);
115
103
  const webApp = await app_1.webApps.getOrCreateWebApp(projectId, webAppName ? webAppName : null, backendId);
116
104
  if (!webApp) {
117
105
  (0, utils_1.logWarning)(`Firebase web app not set`);
118
106
  }
119
107
  const createBackendSpinner = ora("Creating your new backend...").start();
120
- const backend = await createBackend(projectId, location, backendId, serviceAccount ? serviceAccount : null, gitRepositoryLink, webApp?.id, rootDir, runtime, automaticBaseImageUpdatesDisabled);
108
+ const backend = await createBackend(projectId, location, backendId, serviceAccount ? serviceAccount : null, gitRepositoryLink, webApp?.id, rootDir, runtime);
121
109
  createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`);
122
110
  if (nonInteractive) {
123
111
  return;
@@ -158,7 +146,7 @@ async function doSetup(projectId, nonInteractive, webAppName, backendId, service
158
146
  }
159
147
  (0, utils_1.logSuccess)(`Your backend is now deployed at:\n\thttps://${backend.uri}`);
160
148
  }
161
- async function doSetupSourceDeploy(projectId, backendId) {
149
+ async function doSetupSourceDeploy(projectId, backendId, nonInteractive, rootDir = "/") {
162
150
  const location = await promptLocation(projectId, "Select a primary region to host your backend:\n");
163
151
  const webAppSpinner = ora("Creating a new web app...\n").start();
164
152
  const webApp = await app_1.webApps.getOrCreateWebApp(projectId, null, backendId);
@@ -166,8 +154,9 @@ async function doSetupSourceDeploy(projectId, backendId) {
166
154
  (0, utils_1.logWarning)(`Firebase web app not set`);
167
155
  }
168
156
  webAppSpinner.stop();
157
+ const runtime = await (0, prompts_1.resolveRuntime)(projectId, location, nonInteractive);
169
158
  const createBackendSpinner = ora("Creating your new backend...").start();
170
- const backend = await createBackend(projectId, location, backendId, null, undefined, webApp?.id);
159
+ const backend = await createBackend(projectId, location, backendId, null, undefined, webApp?.id, rootDir, runtime);
171
160
  createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`);
172
161
  return {
173
162
  backend,
@@ -242,7 +231,7 @@ async function promptNewBackendId(projectId, location) {
242
231
  function defaultComputeServiceAccountEmail(projectId) {
243
232
  return `${DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME}@${projectId}.iam.gserviceaccount.com`;
244
233
  }
245
- async function createBackend(projectId, location, backendId, serviceAccount, repository, webAppId, rootDir = "/", runtime, automaticBaseImageUpdatesDisabled) {
234
+ async function createBackend(projectId, location, backendId, serviceAccount, repository, webAppId, rootDir = "/", runtime) {
246
235
  const defaultServiceAccount = defaultComputeServiceAccountEmail(projectId);
247
236
  const backendReqBody = {
248
237
  servingLocality: "GLOBAL_ACCESS",
@@ -258,7 +247,6 @@ async function createBackend(projectId, location, backendId, serviceAccount, rep
258
247
  };
259
248
  if ((0, experiments_1.isEnabled)("abiu")) {
260
249
  backendReqBody.runtime = { value: runtime ?? "" };
261
- backendReqBody.automaticBaseImageUpdatesDisabled = automaticBaseImageUpdatesDisabled;
262
250
  }
263
251
  async function createBackendAndPoll() {
264
252
  const op = await apphosting.createBackend(projectId, location, backendReqBody, backendId);
@@ -2,9 +2,24 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.localBuild = localBuild;
4
4
  const build_1 = require("@apphosting/build");
5
- async function localBuild(projectRoot, framework, env = {}) {
5
+ const secrets_1 = require("./secrets");
6
+ const prompt_1 = require("../prompt");
7
+ const error_1 = require("../error");
8
+ async function localBuild(projectId, projectRoot, framework, env = {}, options) {
9
+ const hasBuildAvailableSecrets = Object.values(env).some((v) => v.secret && (!v.availability || v.availability.includes("BUILD")));
10
+ if (hasBuildAvailableSecrets && !options?.allowLocalBuildSecrets) {
11
+ if (options?.nonInteractive) {
12
+ throw new error_1.FirebaseError("Using build-available secrets during a local build in non-interactive mode requires the --allow-local-build-secrets flag.");
13
+ }
14
+ if (!(await (0, prompt_1.confirm)({
15
+ message: "Your build includes secrets that are available to the build environment. Using secrets in local builds may leave sensitive values in local artifacts/temporary files. Do you want to continue?",
16
+ default: false,
17
+ }))) {
18
+ throw new error_1.FirebaseError("Cancelled local build due to BUILD-available secrets.");
19
+ }
20
+ }
6
21
  const originalEnv = { ...process.env };
7
- const addedEnv = toProcessEnv(env);
22
+ const addedEnv = await toProcessEnv(projectId, env);
8
23
  for (const [key, value] of Object.entries(addedEnv)) {
9
24
  process.env[key] = value;
10
25
  }
@@ -37,6 +52,19 @@ async function localBuild(projectRoot, framework, env = {}) {
37
52
  },
38
53
  };
39
54
  }
40
- function toProcessEnv(env) {
41
- return Object.fromEntries(Object.entries(env).map(([key, value]) => [key, value.value || ""]));
55
+ async function toProcessEnv(projectId, env) {
56
+ const entries = await Promise.all(Object.entries(env).map(async ([key, value]) => {
57
+ if (value.availability && !value.availability.includes("BUILD")) {
58
+ return null;
59
+ }
60
+ if (value.secret) {
61
+ const resolvedValue = await (0, secrets_1.loadSecret)(projectId, value.secret);
62
+ return [key, resolvedValue];
63
+ }
64
+ else {
65
+ return [key, value.value || ""];
66
+ }
67
+ }));
68
+ const filteredEntries = entries.filter((entry) => entry !== null);
69
+ return Object.fromEntries(filteredEntries);
42
70
  }
@@ -2,34 +2,55 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DEFAULT_RUNTIME = void 0;
4
4
  exports.promptRuntime = promptRuntime;
5
- exports.promptAutomaticBaseImageUpdates = promptAutomaticBaseImageUpdates;
5
+ exports.resolveRuntime = resolveRuntime;
6
6
  const prompt_1 = require("../prompt");
7
7
  const utils_1 = require("../utils");
8
8
  const apphosting = require("../gcp/apphosting");
9
+ const experiments_1 = require("../experiments");
9
10
  exports.DEFAULT_RUNTIME = "nodejs";
10
11
  async function promptRuntime(projectId, location) {
11
- const choices = [{ name: "Node.js (default)", value: exports.DEFAULT_RUNTIME }];
12
+ const choices = [];
13
+ let nodejsChoice = { name: "Node.js (default)", value: exports.DEFAULT_RUNTIME };
12
14
  try {
13
15
  const supportedRuntimes = await apphosting.listSupportedRuntimes(projectId, location);
14
16
  for (const r of supportedRuntimes) {
15
- if (r.runtimeId !== exports.DEFAULT_RUNTIME) {
16
- choices.push({ name: r.runtimeId, value: r.runtimeId });
17
+ const abiuText = r.automaticBaseImageUpdatesSupported
18
+ ? "Enables Automatic Base Image Updates"
19
+ : "No Automatic Base Image Updates";
20
+ const choiceName = `${r.runtimeId} - ${abiuText}`;
21
+ if (r.runtimeId === exports.DEFAULT_RUNTIME) {
22
+ nodejsChoice = { name: choiceName, value: r.runtimeId };
23
+ }
24
+ else {
25
+ choices.push({ name: choiceName, value: r.runtimeId });
17
26
  }
18
27
  }
28
+ choices.unshift(nodejsChoice);
19
29
  }
20
30
  catch (err) {
21
31
  (0, utils_1.logWarning)("Failed to list supported runtimes. Falling back to hardcoded list.");
22
- choices.push({ name: "nodejs22", value: "nodejs22" });
32
+ choices.push({ name: "nodejs - No Automatic Base Image Updates", value: "nodejs" });
33
+ choices.push({ name: "nodejs22 - Enables Automatic Base Image Updates", value: "nodejs22" });
23
34
  }
24
- return await (0, prompt_1.select)({
35
+ const selectedRuntime = await (0, prompt_1.select)({
25
36
  message: "Which runtime do you want to use?",
26
37
  choices: choices,
27
38
  default: exports.DEFAULT_RUNTIME,
28
39
  });
40
+ if (selectedRuntime === exports.DEFAULT_RUNTIME) {
41
+ (0, utils_1.logBullet)("ABIU will not be enabled for the unversioned 'nodejs' runtime.");
42
+ }
43
+ return selectedRuntime;
29
44
  }
30
- async function promptAutomaticBaseImageUpdates() {
31
- return await (0, prompt_1.confirm)({
32
- message: "Would you like to enable Automatic Base Image Updates (ABIU)?",
33
- default: true,
34
- });
45
+ async function resolveRuntime(projectId, location, nonInteractive, runtimeOption) {
46
+ if (runtimeOption !== undefined) {
47
+ return runtimeOption;
48
+ }
49
+ if (!(0, experiments_1.isEnabled)("abiu")) {
50
+ return undefined;
51
+ }
52
+ if (nonInteractive) {
53
+ return exports.DEFAULT_RUNTIME;
54
+ }
55
+ return promptRuntime(projectId, location);
35
56
  }
@@ -5,6 +5,7 @@ exports.serviceAccountsForBackend = serviceAccountsForBackend;
5
5
  exports.grantSecretAccess = grantSecretAccess;
6
6
  exports.grantEmailsSecretAccess = grantEmailsSecretAccess;
7
7
  exports.upsertSecret = upsertSecret;
8
+ exports.loadSecret = loadSecret;
8
9
  exports.fetchSecrets = fetchSecrets;
9
10
  exports.getSecretNameParts = getSecretNameParts;
10
11
  exports.apphostingSecretsSetAction = apphostingSecretsSetAction;
@@ -149,6 +150,48 @@ async function upsertSecret(project, secret, location) {
149
150
  }
150
151
  return false;
151
152
  }
153
+ const secretResourceRegex = /^projects\/([^/]+)\/secrets\/([^/]+)(?:\/versions\/((?:latest)|\d+))?$/;
154
+ const secretShorthandRegex = /^([^/@]+)(?:@((?:latest)|\d+))?$/;
155
+ async function loadSecret(project, name) {
156
+ let projectId;
157
+ let secretId;
158
+ let version;
159
+ const match = secretResourceRegex.exec(name);
160
+ if (match) {
161
+ projectId = match[1];
162
+ secretId = match[2];
163
+ version = match[3] || "latest";
164
+ }
165
+ else {
166
+ const match = secretShorthandRegex.exec(name);
167
+ if (!match) {
168
+ throw new error_1.FirebaseError(`Invalid secret name: ${name}`);
169
+ }
170
+ if (!project) {
171
+ throw new error_1.FirebaseError(`Cannot load secret ${match[1]} without a project. ` +
172
+ `Please use ${clc.bold("firebase use")} or pass the --project flag.`);
173
+ }
174
+ projectId = project;
175
+ secretId = match[1];
176
+ version = match[2] || "latest";
177
+ }
178
+ try {
179
+ return await gcsm.accessSecretVersion(projectId, secretId, version);
180
+ }
181
+ catch (err) {
182
+ if (err instanceof error_1.FirebaseError) {
183
+ const original = err.original;
184
+ if (typeof original === "object" && original !== null) {
185
+ if (original.code === 403 ||
186
+ original.context?.response?.statusCode === 403) {
187
+ utils.logLabeledError("apphosting", `Permission denied to access secret ${secretId}. Use ` +
188
+ `${clc.bold("firebase apphosting:secrets:grantaccess")} to get permissions.`);
189
+ }
190
+ }
191
+ }
192
+ throw err;
193
+ }
194
+ }
152
195
  async function fetchSecrets(projectId, secrets) {
153
196
  let secretsKeyValuePairs;
154
197
  try {
@@ -58,10 +58,8 @@ class AppHostingYamlConfig {
58
58
  exports.AppHostingYamlConfig = AppHostingYamlConfig;
59
59
  function toEnvMap(envs) {
60
60
  return Object.fromEntries(envs.map((env) => {
61
- const variable = env.variable;
62
- const tmp = { ...env };
63
- delete env.variable;
64
- return [variable, tmp];
61
+ const { variable, ...rest } = env;
62
+ return [variable, rest];
65
63
  }));
66
64
  }
67
65
  function toEnvList(envs) {
@@ -3,6 +3,16 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const semver = require("semver");
5
5
  const pkg = require("../../package.json");
6
+ const IGNORED_WARNINGS = [
7
+ "DEP0040",
8
+ ];
9
+ process.on("warning", (warning) => {
10
+ const nodeWarning = warning;
11
+ if (nodeWarning.code && IGNORED_WARNINGS.includes(nodeWarning.code)) {
12
+ return;
13
+ }
14
+ console.warn(nodeWarning.stack || nodeWarning.message);
15
+ });
6
16
  const nodeVersion = process.version;
7
17
  if (!semver.satisfies(nodeVersion, pkg.engines.node)) {
8
18
  console.error(`Firebase CLI v${pkg.version} is incompatible with Node.js ${nodeVersion} Please upgrade Node.js to version ${pkg.engines.node}`);
package/lib/bin/mcp.js CHANGED
@@ -49,6 +49,8 @@ Options:
49
49
  If specified, auto-detection is disabled for other features.
50
50
  --tools <tools> Comma-separated list of specific tools to enable. Disables
51
51
  auto-detection entirely.
52
+ --mode <mode> Server mode: stdio, sse (defaults to stdio).
53
+ --port <port> The port to listen on when running in SSE mode (defaults to 3000).
52
54
  -h, --help Show this help message.
53
55
  `;
54
56
  async function mcp() {
@@ -57,6 +59,8 @@ async function mcp() {
57
59
  only: { type: "string", default: "" },
58
60
  tools: { type: "string", default: "" },
59
61
  dir: { type: "string" },
62
+ mode: { type: "string", default: "stdio" },
63
+ port: { type: "string", default: "3000" },
60
64
  "generate-tool-list": { type: "boolean", default: false },
61
65
  "generate-prompt-list": { type: "boolean", default: false },
62
66
  "generate-resource-list": { type: "boolean", default: false },
@@ -83,6 +87,10 @@ async function mcp() {
83
87
  }
84
88
  if (earlyExit)
85
89
  return;
90
+ if (values.mode !== "stdio" && values.mode !== "sse") {
91
+ console.error("Error: --mode must be either 'stdio' or 'sse'");
92
+ process.exit(1);
93
+ }
86
94
  (0, env_1.setFirebaseMcp)(true);
87
95
  const mcpLogDir = (0, path_1.join)((0, os_1.homedir)(), ".cache", "firebase");
88
96
  await (0, promises_1.mkdir)(mcpLogDir, { recursive: true });
@@ -100,7 +108,10 @@ async function mcp() {
100
108
  enabledTools,
101
109
  projectRoot: values.dir ? (0, path_1.resolve)(values.dir) : undefined,
102
110
  });
103
- await server.start();
111
+ await server.start({
112
+ useSSE: values.mode === "sse",
113
+ port: values.port ? parseInt(values.port, 10) : undefined,
114
+ });
104
115
  if (process.stdin.isTTY)
105
116
  process.stderr.write(STARTUP_MESSAGE);
106
117
  }
@@ -19,10 +19,7 @@ exports.command = new command_1.Command("apphosting:backends:create")
19
19
  .option("--root-dir <rootDir>", "specify the root directory for the backend.");
20
20
  const abiuEnabled = experiments.isEnabled("abiu");
21
21
  if (abiuEnabled) {
22
- exports.command
23
- .option("--runtime [runtime]", "specify the runtime for the backend (e.g., nodejs, nodejs22)")
24
- .option("--automatic-base-image-updates", "enable automatic base image updates")
25
- .option("--no-automatic-base-image-updates", "disable automatic base image updates");
22
+ exports.command.option("--runtime [runtime]", "specify the runtime for the backend (e.g., nodejs, nodejs22)");
26
23
  }
27
24
  exports.command
28
25
  .before(requireAuth_1.requireAuth)
@@ -34,12 +31,9 @@ exports.command
34
31
  throw new error_1.FirebaseError(`--non-interactive option requires --backend and --primary-region`);
35
32
  }
36
33
  const abiuAllowed = experiments.isEnabled("abiu");
37
- if (!abiuAllowed && (options.runtime || options.automaticBaseImageUpdates !== undefined)) {
38
- throw new error_1.FirebaseError("The --runtime and --automatic-base-image-updates flags are only available when the 'abiu' experiment is enabled. To enable it, run 'firebase experiments:enable abiu'.");
34
+ if (!abiuAllowed && options.runtime) {
35
+ throw new error_1.FirebaseError("The --runtime flag is only available when the 'abiu' experiment is enabled. To enable it, run 'firebase experiments:enable abiu'.");
39
36
  }
40
37
  const runtime = abiuAllowed && typeof options.runtime === "string" ? options.runtime : undefined;
41
- const automaticBaseImageUpdatesDisabled = abiuAllowed
42
- ? options.automaticBaseImageUpdates === false
43
- : undefined;
44
- return (0, backend_1.doSetup)(projectId, options.nonInteractive, options.app, options.backend, options.serviceAccount, options.primaryRegion, options.rootDir, runtime, automaticBaseImageUpdatesDisabled);
38
+ return (0, backend_1.doSetup)(projectId, options.nonInteractive, options.app, options.backend, options.serviceAccount, options.primaryRegion, options.rootDir, runtime);
45
39
  });
@@ -42,7 +42,7 @@ exports.command = new command_1.Command("dataconnect:sdk:generate")
42
42
  },
43
43
  instructions: [],
44
44
  };
45
- await dataconnectInit.askQuestions(setup);
45
+ await dataconnectInit.askQuestions(setup, config, options);
46
46
  await dataconnectInit.actuate(setup, config, options);
47
47
  await (0, init_1.postInitSaves)(setup, config);
48
48
  justRanInit = true;
@@ -64,7 +64,7 @@ exports.command = new command_1.Command("dataconnect:sdk:generate")
64
64
  },
65
65
  instructions: [],
66
66
  };
67
- await dataconnectSdkInit.askQuestions(setup);
67
+ await dataconnectSdkInit.askQuestions(setup, config, options);
68
68
  await dataconnectSdkInit.actuate(setup, config);
69
69
  justRanInit = true;
70
70
  serviceInfosWithSDKs = await loadAllWithSDKs(projectId, config, options);
@@ -16,6 +16,7 @@ const colorette_1 = require("colorette");
16
16
  const interactive_1 = require("../hosting/interactive");
17
17
  const utils_1 = require("../utils");
18
18
  const api_1 = require("../hosting/api");
19
+ const experiments = require("../experiments");
19
20
  exports.VALID_DEPLOY_TARGETS = [
20
21
  "database",
21
22
  "storage",
@@ -92,7 +93,11 @@ exports.command = new command_1.Command("deploy")
92
93
  '(e.g. "--only dataconnect:serviceId,dataconnect:serviceId:connectorId,dataconnect:serviceId:schema")')
93
94
  .option("--except <targets>", 'deploy to all targets except specified (e.g. "database")')
94
95
  .option("--dry-run", "perform a dry run of your deployment. Validates your changes and builds your code without deploying any changes to your project. " +
95
- "In order to provide better validation, this may still enable APIs on the target project")
96
+ "In order to provide better validation, this may still enable APIs on the target project");
97
+ if (experiments.isEnabled("apphostinglocalbuilds")) {
98
+ exports.command.option("--allow-local-build-secrets", "allow the use of build-available secrets in local builds for App Hosting without interactive confirmation");
99
+ }
100
+ exports.command
96
101
  .before(requireConfig_1.requireConfig)
97
102
  .before((options) => {
98
103
  options.filteredTargets = (0, filterTargets_1.filterTargets)(options, exports.VALID_DEPLOY_TARGETS);
package/lib/config.js CHANGED
@@ -197,6 +197,7 @@ class Config {
197
197
  const shouldWrite = await (0, prompt_1.confirm)({
198
198
  message: "File " + clc.underline(path) + " already exists. Overwrite?",
199
199
  default: !!confirmByDefault,
200
+ nonInteractive: this.options.nonInteractive,
200
201
  });
201
202
  if (!shouldWrite) {
202
203
  utils.logBullet("Skipping write of " + clc.bold(path));
@@ -34,6 +34,12 @@ async function build(options, configDir, deployStats) {
34
34
  return buildResult?.metadata ?? {};
35
35
  }
36
36
  async function handleBuildErrors(errors, nonInteractive, force, dryRun) {
37
+ const fatalDeploys = errors.filter((w) => w.extensions?.warningLevel === "ALWAYS_REQUIRED");
38
+ if (fatalDeploys.length) {
39
+ utils.logLabeledError("dataconnect", `There are deployment requirements that are always required and cannot be bypassed:\n` +
40
+ (0, graphqlError_1.prettifyTable)(fatalDeploys));
41
+ throw new error_1.FirebaseError("Deployment failed due to unbypassable requirements.");
42
+ }
37
43
  if (errors.filter((w) => !w.extensions?.warningLevel).length) {
38
44
  throw new error_1.FirebaseError(`There are errors in your schema and connector files:\n${errors.map(graphqlError_1.prettify).join("\n")}`);
39
45
  }
@@ -55,7 +55,7 @@ function parseCloudSQLInstanceName(cloudSQLInstanceName) {
55
55
  throw new error_1.FirebaseError(`${cloudSQLInstanceName} is not a valid cloudSQL instance name`);
56
56
  }
57
57
  const toString = () => {
58
- return `projects/${projectId}/locations/${location}/services/${instanceId}`;
58
+ return `projects/${projectId}/locations/${location}/instances/${instanceId}`;
59
59
  };
60
60
  return {
61
61
  projectId,
@@ -110,7 +110,7 @@ async function default_1(context, options) {
110
110
  const selectedBackends = selected.map((id) => notFoundBackends.find((backend) => backend.backendId === id));
111
111
  for (const cfg of selectedBackends) {
112
112
  (0, utils_1.logLabeledBullet)("apphosting", `Creating a new backend ${cfg.backendId}...`);
113
- const { location } = await (0, backend_1.doSetupSourceDeploy)(projectId, cfg.backendId);
113
+ const { location } = await (0, backend_1.doSetupSourceDeploy)(projectId, cfg.backendId, options.nonInteractive, cfg.rootDir);
114
114
  context.backendConfigs[cfg.backendId] = cfg;
115
115
  context.backendLocations[cfg.backendId] = location;
116
116
  }
@@ -133,7 +133,10 @@ async function default_1(context, options) {
133
133
  await injectEnvVarsFromApphostingConfig(configs.filter((c) => c.backendId === cfg.backendId), options, buildEnv, runtimeEnv);
134
134
  await injectAutoInitEnvVars(cfg, backends, buildEnv, runtimeEnv);
135
135
  try {
136
- const { outputFiles, annotations, buildConfig } = await (0, localbuilds_1.localBuild)(options.projectRoot || "./", "nextjs", buildEnv[cfg.backendId] || {});
136
+ const { outputFiles, annotations, buildConfig } = await (0, localbuilds_1.localBuild)(projectId, options.projectRoot || "./", "nextjs", buildEnv[cfg.backendId] || {}, {
137
+ nonInteractive: options.nonInteractive,
138
+ allowLocalBuildSecrets: !!options.allowLocalBuildSecrets,
139
+ });
137
140
  if (outputFiles.length !== 1) {
138
141
  throw new error_1.FirebaseError(`Local build for backend ${cfg.backendId} failed: No output files found.`);
139
142
  }
@@ -13,7 +13,7 @@ const error_1 = require("../../error");
13
13
  const types = require("../../firestore/api-types");
14
14
  const api_2 = require("../../firestore/api");
15
15
  function prepareRules(context, rulesDeploy, databaseId, rulesFile) {
16
- rulesDeploy.addFile(rulesFile);
16
+ rulesDeploy.addFile(rulesFile, databaseId);
17
17
  context.firestore.rules.push({
18
18
  databaseId,
19
19
  rulesFile,
@@ -210,20 +210,8 @@ async function loadExistingBackend(ctx) {
210
210
  existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
211
211
  }
212
212
  unreachableRegions.gcfV1 = gcfV1Results.unreachable;
213
- if (experiments.isEnabled("functionsrunapionly") || experiments.isEnabled("dartfunctions")) {
214
- try {
215
- const runServices = await run.listServices(ctx.projectId);
216
- for (const service of runServices) {
217
- const endpoint = run.endpointFromService(service);
218
- existingBackend.endpoints[endpoint.region] =
219
- existingBackend.endpoints[endpoint.region] || {};
220
- existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
221
- }
222
- }
223
- catch (err) {
224
- logger_1.logger.debug(err.message);
225
- unreachableRegions.run = ["unknown"];
226
- }
213
+ if (experiments.isEnabled("functionsrunapionly")) {
214
+ await loadCloudRunServices(ctx, existingBackend, unreachableRegions, false);
227
215
  }
228
216
  else {
229
217
  const gcfV2Results = await gcfV2.listAllFunctions(ctx.projectId);
@@ -233,11 +221,31 @@ async function loadExistingBackend(ctx) {
233
221
  existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
234
222
  }
235
223
  unreachableRegions.gcfV2 = gcfV2Results.unreachable;
224
+ if (experiments.isEnabled("dartfunctions")) {
225
+ await loadCloudRunServices(ctx, existingBackend, unreachableRegions, true);
226
+ }
236
227
  }
237
228
  ctx.existingBackend = existingBackend;
238
229
  ctx.unreachableRegions = unreachableRegions;
239
230
  return ctx.existingBackend;
240
231
  }
232
+ async function loadCloudRunServices(ctx, existingBackend, unreachableRegions, onlyMissing) {
233
+ try {
234
+ const runServices = await run.listServices(ctx.projectId);
235
+ for (const service of runServices) {
236
+ const endpoint = run.endpointFromService(service);
237
+ if (!onlyMissing || !existingBackend.endpoints[endpoint.region]?.[endpoint.id]) {
238
+ existingBackend.endpoints[endpoint.region] =
239
+ existingBackend.endpoints[endpoint.region] || {};
240
+ existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
241
+ }
242
+ }
243
+ }
244
+ catch (err) {
245
+ logger_1.logger.debug(`Error loading Cloud Run services: ${err.message}`);
246
+ unreachableRegions.run = ["unknown"];
247
+ }
248
+ }
241
249
  async function checkAvailability(context, want) {
242
250
  await existingBackend(context);
243
251
  const gcfV1Regions = new Set();
@@ -187,13 +187,19 @@ async function prepare(context, options, payload) {
187
187
  context.sources[codebase] = source;
188
188
  }
189
189
  payload.functions = {};
190
- const haveBackends = (0, functionsDeployHelper_1.groupEndpointsByCodebase)(wantBackends, backend.allEndpoints(await backend.existingBackend(context)));
190
+ const existingBackend = await backend.existingBackend(context);
191
+ for (const [codebase, wantBackend] of Object.entries(wantBackends)) {
192
+ const relevantEndpoints = backend
193
+ .allEndpoints(existingBackend)
194
+ .filter((e) => e.codebase === codebase || e.codebase === undefined);
195
+ await resolveDefaultRegions(wantBackend, backend.of(...relevantEndpoints));
196
+ }
197
+ const haveBackends = (0, functionsDeployHelper_1.groupEndpointsByCodebase)(wantBackends, backend.allEndpoints(existingBackend));
191
198
  for (const [codebase, wantBackend] of Object.entries(wantBackends)) {
192
199
  const haveBackend = haveBackends[codebase] || backend.empty();
193
200
  payload.functions[codebase] = { wantBackend, haveBackend };
194
201
  }
195
202
  for (const [codebase, { wantBackend, haveBackend }] of Object.entries(payload.functions)) {
196
- await resolveDefaultRegions(wantBackend, haveBackend);
197
203
  inferDetailsFromExisting(wantBackend, haveBackend, codebaseUsesEnvs.includes(codebase));
198
204
  await (0, triggerRegionHelper_1.ensureTriggerRegions)(wantBackend);
199
205
  resolveCpuAndConcurrency(wantBackend);
@@ -69,9 +69,9 @@ class Fabricator {
69
69
  results: [],
70
70
  };
71
71
  const changesets = Object.values(plan);
72
- const scraperV1 = new sourceTokenScraper_1.SourceTokenScraper();
73
- const scraperV2 = new sourceTokenScraper_1.SourceTokenScraper();
74
72
  const createAndUpdatePromises = changesets.map((changes) => {
73
+ const scraperV1 = new sourceTokenScraper_1.SourceTokenScraper();
74
+ const scraperV2 = new sourceTokenScraper_1.SourceTokenScraper();
75
75
  return this.applyUpserts(changes, scraperV1, scraperV2);
76
76
  });
77
77
  const createAndUpdateResultsArray = await Promise.allSettled(createAndUpdatePromises);
@@ -542,7 +542,20 @@ class Fabricator {
542
542
  endpoint.runServiceId = endpoint.id;
543
543
  })
544
544
  .catch(rethrowAs(endpoint, "create"));
545
- await this.setInvoker(endpoint);
545
+ const serviceName = `projects/${endpoint.project}/locations/${endpoint.region}/services/${endpoint.runServiceId}`;
546
+ if (backend.isHttpsTriggered(endpoint)) {
547
+ const invoker = endpoint.httpsTrigger.invoker || ["public"];
548
+ if (!invoker.includes("private")) {
549
+ await this.executor
550
+ .run(() => run.setInvokerCreate(endpoint.project, serviceName, invoker))
551
+ .catch(rethrowAs(endpoint, "set invoker"));
552
+ }
553
+ }
554
+ else if (backend.isCallableTriggered(endpoint)) {
555
+ await this.executor
556
+ .run(() => run.setInvokerCreate(endpoint.project, serviceName, ["public"]))
557
+ .catch(rethrowAs(endpoint, "set invoker"));
558
+ }
546
559
  }
547
560
  async updateRunFunction(update) {
548
561
  const endpoint = update.endpoint;
@@ -567,7 +580,16 @@ class Fabricator {
567
580
  endpoint.runServiceId = endpoint.id;
568
581
  })
569
582
  .catch(rethrowAs(endpoint, "update"));
570
- await this.setInvoker(endpoint);
583
+ const serviceName = `projects/${endpoint.project}/locations/${endpoint.region}/services/${endpoint.runServiceId}`;
584
+ let invoker;
585
+ if (backend.isHttpsTriggered(endpoint)) {
586
+ invoker = endpoint.httpsTrigger.invoker === null ? ["public"] : endpoint.httpsTrigger.invoker;
587
+ }
588
+ if (invoker) {
589
+ await this.executor
590
+ .run(() => run.setInvokerUpdate(endpoint.project, serviceName, invoker))
591
+ .catch(rethrowAs(endpoint, "set invoker"));
592
+ }
571
593
  }
572
594
  async deleteRunFunction(endpoint) {
573
595
  await this.runFunctionExecutor
@@ -584,16 +606,6 @@ class Fabricator {
584
606
  })
585
607
  .catch(rethrowAs(endpoint, "delete"));
586
608
  }
587
- async setInvoker(endpoint) {
588
- if (backend.isHttpsTriggered(endpoint)) {
589
- const invoker = endpoint.httpsTrigger.invoker || ["public"];
590
- if (!invoker.includes("private")) {
591
- await this.executor
592
- .run(() => run.setInvokerUpdate(endpoint.project, `projects/${endpoint.project}/locations/${endpoint.region}/services/${endpoint.runServiceId}`, invoker))
593
- .catch(rethrowAs(endpoint, "set invoker"));
594
- }
595
- }
596
- }
597
609
  async setRunTraits(serviceName, endpoint) {
598
610
  await this.functionExecutor
599
611
  .run(async () => {
@@ -45,6 +45,9 @@ function getPythonBinary(runtime) {
45
45
  else if (runtime === "python313") {
46
46
  return "python3.13";
47
47
  }
48
+ else if (runtime === "python314") {
49
+ return "python3.14";
50
+ }
48
51
  (0, functional_1.assertExhaustive)(runtime, `Unhandled python runtime ${runtime}`);
49
52
  }
50
53
  class Delegate {
@@ -89,6 +89,12 @@ exports.RUNTIMES = runtimes({
89
89
  deprecationDate: "2029-10-10",
90
90
  decommissionDate: "2030-04-10",
91
91
  },
92
+ python314: {
93
+ friendly: "Python 3.14",
94
+ status: "GA",
95
+ deprecationDate: "2030-10-10",
96
+ decommissionDate: "2031-04-10",
97
+ },
92
98
  dart3: {
93
99
  friendly: "Dart 3",
94
100
  status: "experimental",