firebase-tools 15.20.0 → 15.22.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 (48) hide show
  1. package/lib/appdistribution/yaml_helper.js +2 -2
  2. package/lib/apphosting/constants.js +2 -1
  3. package/lib/apphosting/localbuilds.js +4 -2
  4. package/lib/apphosting/secrets/index.js +77 -0
  5. package/lib/archiveDirectory.js +2 -2
  6. package/lib/auth.js +2 -3
  7. package/lib/bin/cli.js +26 -18
  8. package/lib/commands/apphosting-secrets-revokeaccess.js +56 -0
  9. package/lib/commands/apptesting.js +11 -2
  10. package/lib/commands/crashlytics-symbols-upload.js +2 -2
  11. package/lib/commands/index.js +1 -0
  12. package/lib/crashlytics/sourcemap.js +2 -3
  13. package/lib/database/import.js +2 -2
  14. package/lib/dataconnect/build.js +6 -6
  15. package/lib/deploy/apphosting/util.js +9 -1
  16. package/lib/deploy/functions/prepare.js +39 -3
  17. package/lib/deploy/functions/prepareFunctionsUpload.js +1 -2
  18. package/lib/deploy/functions/release/index.js +0 -5
  19. package/lib/emulator/auth/apiSpec.js +307 -33
  20. package/lib/emulator/auth/cloudFunctions.js +2 -2
  21. package/lib/emulator/auth/operations.js +99 -9
  22. package/lib/emulator/auth/state.js +27 -0
  23. package/lib/emulator/downloadableEmulatorInfo.json +31 -31
  24. package/lib/emulator/functionsEmulatorShell.js +4 -4
  25. package/lib/emulator/functionsRuntimeWorker.js +2 -2
  26. package/lib/emulator/pubsubEmulator.js +3 -3
  27. package/lib/emulator/storage/apis/firebase.js +2 -2
  28. package/lib/emulator/storage/cloudFunctions.js +2 -2
  29. package/lib/emulator/storage/metadata.js +1 -2
  30. package/lib/emulator/storage/persistence.js +2 -2
  31. package/lib/emulator/storage/upload.js +3 -3
  32. package/lib/env.js +20 -4
  33. package/lib/experiments.js +1 -2
  34. package/lib/frameworks/angular/index.js +3 -2
  35. package/lib/frameworks/angular/utils.js +100 -2
  36. package/lib/frameworks/astro/index.js +1 -1
  37. package/lib/frameworks/astro/utils.js +1 -1
  38. package/lib/functions/python.js +4 -4
  39. package/lib/hosting/cloudRunProxy.js +8 -0
  40. package/lib/index.js +2 -2
  41. package/lib/init/features/apphosting.js +8 -1
  42. package/lib/init/features/functions/python.js +32 -20
  43. package/lib/localFunction.js +4 -2
  44. package/lib/track.js +2 -2
  45. package/lib/tsconfig.compile.tsbuildinfo +1 -1
  46. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  47. package/lib/utils.js +70 -0
  48. package/package.json +3 -7
@@ -32,7 +32,7 @@ function toYamlTestCases(testCases) {
32
32
  }));
33
33
  }
34
34
  function toYaml(testCases) {
35
- return jsYaml.safeDump({ tests: toYamlTestCases(testCases) });
35
+ return jsYaml.dump({ tests: toYamlTestCases(testCases) });
36
36
  }
37
37
  function castExists(it, thing) {
38
38
  if (it == null) {
@@ -77,7 +77,7 @@ function fromYamlTestCases(appName, yamlTestCases) {
77
77
  function fromYaml(appName, yaml) {
78
78
  let parsedYaml;
79
79
  try {
80
- parsedYaml = jsYaml.safeLoad(yaml);
80
+ parsedYaml = jsYaml.load(yaml);
81
81
  }
82
82
  catch (err) {
83
83
  throw new error_1.FirebaseError(`Failed to parse YAML: ${(0, error_1.getErrMsg)(err)}`);
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LOCAL_BUILD_DIR_NAME = exports.ALLOWED_DEPLOY_METHODS = exports.DEFAULT_DEPLOY_METHOD = exports.DEFAULT_LOCATION = void 0;
3
+ exports.CLOUD_RUN_SIZE_LIMIT_BYTES = exports.LOCAL_BUILD_DIR_NAME = exports.ALLOWED_DEPLOY_METHODS = exports.DEFAULT_DEPLOY_METHOD = exports.DEFAULT_LOCATION = void 0;
4
4
  exports.DEFAULT_LOCATION = "us-east4";
5
5
  exports.DEFAULT_DEPLOY_METHOD = "github";
6
6
  exports.ALLOWED_DEPLOY_METHODS = [{ name: "Deploy using github", value: "github" }];
7
7
  exports.LOCAL_BUILD_DIR_NAME = ".local_build";
8
+ exports.CLOUD_RUN_SIZE_LIMIT_BYTES = 250 * 1024 * 1024;
@@ -31,11 +31,13 @@ function executeUniversalMakerBinary(universalMakerBinary, projectRoot, addedEnv
31
31
  },
32
32
  stdio: "pipe",
33
33
  });
34
+ const failed = !!res.error || res.status !== 0;
35
+ const log = (msg) => (failed ? logger_1.logger.info(msg) : logger_1.logger.debug(msg));
34
36
  if (res.stdout) {
35
- logger_1.logger.debug("[Universal Maker stdout]:\n" + res.stdout.toString());
37
+ log("[Universal Maker stdout]:\n" + res.stdout.toString());
36
38
  }
37
39
  if (res.stderr) {
38
- logger_1.logger.debug("[Universal Maker stderr]:\n" + res.stderr.toString());
40
+ log("[Universal Maker stderr]:\n" + res.stderr.toString());
39
41
  }
40
42
  if (res.error) {
41
43
  throw res.error;
@@ -4,6 +4,8 @@ exports.toMulti = toMulti;
4
4
  exports.serviceAccountsForBackend = serviceAccountsForBackend;
5
5
  exports.grantSecretAccess = grantSecretAccess;
6
6
  exports.grantEmailsSecretAccess = grantEmailsSecretAccess;
7
+ exports.revokeSecretAccess = revokeSecretAccess;
8
+ exports.revokeEmailsSecretAccess = revokeEmailsSecretAccess;
7
9
  exports.upsertSecret = upsertSecret;
8
10
  exports.loadSecret = loadSecret;
9
11
  exports.fetchSecrets = fetchSecrets;
@@ -117,6 +119,81 @@ async function grantEmailsSecretAccess(projectId, secretNames, emails) {
117
119
  utils.logSuccess(`Successfully set IAM bindings on secret ${secretName}.\n`);
118
120
  }
119
121
  }
122
+ async function revokeSecretAccess(projectId, secretName, accounts) {
123
+ const bindingsToRevoke = [
124
+ {
125
+ role: "roles/secretmanager.secretAccessor",
126
+ members: [...accounts.buildServiceAccounts, ...accounts.runServiceAccounts].map((sa) => `serviceAccount:${sa}`),
127
+ },
128
+ {
129
+ role: "roles/secretmanager.viewer",
130
+ members: accounts.buildServiceAccounts.map((sa) => `serviceAccount:${sa}`),
131
+ },
132
+ ];
133
+ await revokeSecretBindings(projectId, secretName, bindingsToRevoke);
134
+ }
135
+ async function revokeEmailsSecretAccess(projectId, secretNames, emails) {
136
+ const bindingsToRevoke = [
137
+ {
138
+ role: "roles/secretmanager.secretAccessor",
139
+ members: emails.flatMap((email) => [`user:${email}`, `group:${email}`]),
140
+ },
141
+ ];
142
+ for (const secretName of secretNames) {
143
+ await revokeSecretBindings(projectId, secretName, bindingsToRevoke);
144
+ }
145
+ }
146
+ async function revokeSecretBindings(projectId, secretName, bindingsToRevoke) {
147
+ let existingBindings;
148
+ try {
149
+ existingBindings = (await gcsm.getIamPolicy({ projectId, name: secretName })).bindings || [];
150
+ }
151
+ catch (err) {
152
+ throw new error_1.FirebaseError(`Failed to get IAM bindings on secret: ${secretName}. Ensure you have the permissions to do so and try again.`, { original: (0, error_1.getError)(err) });
153
+ }
154
+ const removalsByRole = new Map();
155
+ for (const binding of bindingsToRevoke) {
156
+ let members = removalsByRole.get(binding.role);
157
+ if (!members) {
158
+ members = new Set();
159
+ removalsByRole.set(binding.role, members);
160
+ }
161
+ for (const member of binding.members) {
162
+ members.add(member);
163
+ }
164
+ }
165
+ let updated = false;
166
+ const bindings = [];
167
+ for (const binding of existingBindings) {
168
+ const removals = removalsByRole.get(binding.role);
169
+ if (!removals || binding.condition) {
170
+ bindings.push(binding);
171
+ continue;
172
+ }
173
+ const members = binding.members.filter((member) => !removals.has(member));
174
+ if (members.length !== binding.members.length) {
175
+ updated = true;
176
+ }
177
+ if (members.length) {
178
+ bindings.push({ ...binding, members });
179
+ }
180
+ else {
181
+ updated = true;
182
+ }
183
+ }
184
+ if (!updated) {
185
+ utils.logSuccess(`No matching IAM bindings found on secret ${secretName}.\n`);
186
+ return;
187
+ }
188
+ try {
189
+ await gcsm.setIamPolicy({ projectId, name: secretName }, bindings);
190
+ }
191
+ catch (err) {
192
+ throw new error_1.FirebaseError(`Failed to revoke IAM bindings ${JSON.stringify(bindingsToRevoke)} on secret: ${secretName}. Ensure you have the permissions to do so and try again. ` +
193
+ "For more information visit https://cloud.google.com/secret-manager/docs/manage-access-to-secrets#required-roles", { original: (0, error_1.getError)(err) });
194
+ }
195
+ utils.logSuccess(`Successfully revoked IAM bindings on secret ${secretName}.\n`);
196
+ }
120
197
  async function upsertSecret(project, secret, location) {
121
198
  let existing;
122
199
  try {
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.archiveDirectory = archiveDirectory;
4
4
  const archiver = require("archiver");
5
- const filesize = require("filesize");
5
+ const utils_1 = require("./utils");
6
6
  const fs = require("fs");
7
7
  const path = require("path");
8
8
  const tmp = require("tmp");
@@ -21,7 +21,7 @@ async function archiveDirectory(sourceDirectory, options = {}) {
21
21
  const makeArchive = zipDirectory(sourceDirectory, tempFile, options);
22
22
  try {
23
23
  const archive = await makeArchive;
24
- logger_1.logger.debug(`Archived ${filesize(archive.size)} in ${sourceDirectory}.`);
24
+ logger_1.logger.debug(`Archived ${(0, utils_1.formatFilesize)(archive.size)} in ${sourceDirectory}.`);
25
25
  return archive;
26
26
  }
27
27
  catch (err) {
package/lib/auth.js CHANGED
@@ -37,7 +37,6 @@ const logger_1 = require("./logger");
37
37
  const prompt_1 = require("./prompt");
38
38
  const scopes = require("./scopes");
39
39
  const defaultCredentials_1 = require("./defaultCredentials");
40
- const uuid_1 = require("uuid");
41
40
  const crypto_1 = require("crypto");
42
41
  const track_1 = require("./track");
43
42
  const api_1 = require("./api");
@@ -308,7 +307,7 @@ async function loginPrototyper() {
308
307
  urlPrefix: (0, api_1.authProxyOrigin)(),
309
308
  auth: false,
310
309
  });
311
- const sessionId = (0, uuid_1.v4)();
310
+ const sessionId = (0, crypto_1.randomUUID)();
312
311
  const codeVerifier = (0, crypto_1.randomBytes)(32).toString("hex");
313
312
  const codeChallenge = urlsafeBase64((0, crypto_1.createHash)("sha256").update(codeVerifier).digest("base64"));
314
313
  const attestToken = (await authProxyClient.post("/attest", {
@@ -341,7 +340,7 @@ async function loginRemotely() {
341
340
  urlPrefix: (0, api_1.authProxyOrigin)(),
342
341
  auth: false,
343
342
  });
344
- const sessionId = (0, uuid_1.v4)();
343
+ const sessionId = (0, crypto_1.randomUUID)();
345
344
  const codeVerifier = (0, crypto_1.randomBytes)(32).toString("hex");
346
345
  const codeChallenge = urlsafeBase64((0, crypto_1.createHash)("sha256").update(codeVerifier).digest("base64"));
347
346
  const attestToken = (await authProxyClient.post("/attest", {
package/lib/bin/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadAllCommands = loadAllCommands;
3
4
  exports.cli = cli;
4
5
  const updateNotifierPkg = require("update-notifier-cjs");
5
6
  const clc = require("colorette");
@@ -17,6 +18,29 @@ const fsutils = require("../fsutils");
17
18
  const utils = require("../utils");
18
19
  const fetchMOTD_1 = require("../fetchMOTD");
19
20
  const command_1 = require("../command");
21
+ const env_1 = require("../env");
22
+ function loadAllCommands(client) {
23
+ const seen = new Set();
24
+ const loadAll = (obj) => {
25
+ if (seen.has(obj))
26
+ return;
27
+ seen.add(obj);
28
+ for (const [key, value] of Object.entries(obj)) {
29
+ if (key === "cli") {
30
+ continue;
31
+ }
32
+ if ((0, command_1.isCommandModule)(value)) {
33
+ value.load();
34
+ }
35
+ if ((typeof value === "object" || typeof value === "function") &&
36
+ value !== null &&
37
+ !Array.isArray(value)) {
38
+ loadAll(value);
39
+ }
40
+ }
41
+ };
42
+ loadAll(client);
43
+ }
20
44
  function cli(pkg) {
21
45
  const updateNotifier = updateNotifierPkg({ pkg });
22
46
  const args = process.argv.slice(2);
@@ -31,6 +55,7 @@ function cli(pkg) {
31
55
  logger_1.logger.debug("CLI Version: ", pkg.version);
32
56
  logger_1.logger.debug("Platform: ", process.platform);
33
57
  logger_1.logger.debug("Node Version: ", process.version);
58
+ logger_1.logger.debug("Detected Agent:", (0, env_1.detectAIAgent)());
34
59
  logger_1.logger.debug("Time: ", new Date().toString());
35
60
  if (utils.envOverrides.length) {
36
61
  logger_1.logger.debug("Env Overrides:", utils.envOverrides.join(", "));
@@ -97,24 +122,7 @@ function cli(pkg) {
97
122
  client.getCommand(commandName);
98
123
  }
99
124
  if (isHelp) {
100
- const seen = new Set();
101
- const loadAll = (obj) => {
102
- if (seen.has(obj))
103
- return;
104
- seen.add(obj);
105
- for (const [key, value] of Object.entries(obj)) {
106
- if ((0, command_1.isCommandModule)(value)) {
107
- value.load();
108
- }
109
- else if (typeof value === "object" &&
110
- value !== null &&
111
- !Array.isArray(value) &&
112
- key !== "cli") {
113
- loadAll(value);
114
- }
115
- }
116
- };
117
- loadAll(client);
125
+ loadAllCommands(client);
118
126
  }
119
127
  if (!args.length) {
120
128
  client.cli.help();
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const command_1 = require("../command");
5
+ const projectUtils_1 = require("../projectUtils");
6
+ const error_1 = require("../error");
7
+ const requireAuth_1 = require("../requireAuth");
8
+ const secretManager = require("../gcp/secretManager");
9
+ const requirePermissions_1 = require("../requirePermissions");
10
+ const apphosting = require("../gcp/apphosting");
11
+ const secrets = require("../apphosting/secrets");
12
+ const backend_1 = require("../apphosting/backend");
13
+ exports.command = new command_1.Command("apphosting:secrets:revokeaccess <secretNames>")
14
+ .description("Revoke service accounts, users, or groups permissions from the provided secret(s). Can pass one or more secrets, separated by a comma")
15
+ .option("-l, --location <location>", "the location of the backend to revoke secret access from. Cannot be combined with --emails", "-")
16
+ .option("-b, --backend <backend>", "the name of the backend to revoke secret access from. Cannot be combined with --emails")
17
+ .option("-e, --emails <emails>", "comma delimited list of user or group emails to revoke secret access from. Cannot be combined with --backend")
18
+ .before(requireAuth_1.requireAuth)
19
+ .before(secretManager.ensureApi)
20
+ .before(apphosting.ensureApiEnabled)
21
+ .before(requirePermissions_1.requirePermissions, [
22
+ "secretmanager.secrets.get",
23
+ "secretmanager.secrets.getIamPolicy",
24
+ "secretmanager.secrets.setIamPolicy",
25
+ ])
26
+ .action(async (secretNames, options) => {
27
+ const projectId = (0, projectUtils_1.needProjectId)(options);
28
+ if (!options.backend && !options.emails) {
29
+ throw new error_1.FirebaseError("Missing required flag --backend or --emails. See firebase apphosting:secrets:revokeaccess --help for more info");
30
+ }
31
+ if (options.backend && options.emails) {
32
+ throw new error_1.FirebaseError("Cannot specify both --backend and --emails. See firebase apphosting:secrets:revokeaccess --help for more info");
33
+ }
34
+ const secretList = secretNames.split(",");
35
+ for (const secretName of secretList) {
36
+ const exists = await secretManager.secretExists(projectId, secretName);
37
+ if (!exists) {
38
+ throw new error_1.FirebaseError(`Cannot find secret ${secretName}`);
39
+ }
40
+ }
41
+ if (options.emails) {
42
+ return await secrets.revokeEmailsSecretAccess(projectId, secretList, String(options.emails).split(","));
43
+ }
44
+ const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
45
+ const backendId = options.backend;
46
+ const location = options.location;
47
+ let backend;
48
+ if (location === "" || location === "-") {
49
+ backend = await (0, backend_1.getBackendForAmbiguousLocation)(projectId, backendId, "Please select the location of your backend:");
50
+ }
51
+ else {
52
+ backend = await apphosting.getBackend(projectId, location, backendId);
53
+ }
54
+ const accounts = secrets.toMulti(await secrets.serviceAccountsForBackend(projectNumber, backend));
55
+ await Promise.all(secretList.map((secretName) => secrets.revokeSecretAccess(projectId, secretName, accounts)));
56
+ });
@@ -30,6 +30,9 @@ exports.command = new command_1.Command("apptesting:execute [release-binary-file
30
30
  .option("--test-devices-file <string>", "Path to file containing a list of semicolon- or newline-separated devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.")
31
31
  .option("--test-non-blocking", "Run automated tests without waiting for them to complete. Visit the Firebase console for the test results.")
32
32
  .option("--results-bucket <bucket>", "The name of a Google Cloud Storage bucket where raw test results will be stored. If this flag is not set, Firebase creates a default bucket for you. Note that the bucket must be owned by a billing-enabled project, and that using a non-default bucket will result in billing charges for the storage used.")
33
+ .option("--test-username <string>", "username for automatic login")
34
+ .option("--test-password <string>", "password for automatic login. If using a real password, use --test-password-file instead to avoid putting sensitive info in history and logs.")
35
+ .option("--test-password-file <string>", "path to file containing password for automatic login")
33
36
  .before(requireAuth_1.requireAuth)
34
37
  .action(async (target, options) => {
35
38
  const appName = (0, options_parser_util_1.getAppName)(options);
@@ -40,6 +43,11 @@ exports.command = new command_1.Command("apptesting:execute [release-binary-file
40
43
  }
41
44
  const tests = await (0, parseTestFiles_1.parseTestFiles)(testDir, undefined, options.testFilePattern, options.testNamePattern);
42
45
  const testDevices = (0, options_parser_util_1.parseTestDevices)(options.testDevices, options.testDevicesFile);
46
+ const loginCredential = (0, options_parser_util_1.getLoginCredential)({
47
+ username: options.testUsername,
48
+ password: options.testPassword,
49
+ passwordFile: options.testPasswordFile,
50
+ });
43
51
  if (!tests.length) {
44
52
  throw new error_1.FirebaseError(`No tests found under test directory ${testDir}`);
45
53
  }
@@ -62,7 +70,7 @@ exports.command = new command_1.Command("apptesting:execute [release-binary-file
62
70
  utils.logBullet(`Using release ${release.displayVersion} created at ${release.createTime}`);
63
71
  }
64
72
  invokeSpinner.start();
65
- releaseTests = await invokeTests(client, release.name, tests, !testDevices.length ? defaultDevices : testDevices, resultsBucket);
73
+ releaseTests = await invokeTests(client, release.name, tests, !testDevices.length ? defaultDevices : testDevices, resultsBucket, loginCredential);
66
74
  invokeSpinner.text = `${(0, parseTestFiles_1.pluralizeTests)(releaseTests.length)} started successfully!`;
67
75
  invokeSpinner.succeed();
68
76
  }
@@ -78,7 +86,7 @@ exports.command = new command_1.Command("apptesting:execute [release-binary-file
78
86
  utils.logBullet(`View detailed results in the Firebase Console:\n${release.firebaseConsoleUri}`);
79
87
  }
80
88
  });
81
- async function invokeTests(client, releaseName, testDefs, devices, resultsBucket) {
89
+ async function invokeTests(client, releaseName, testDefs, devices, resultsBucket, loginCredential) {
82
90
  try {
83
91
  const releaseTests = [];
84
92
  for (const testDef of testDefs) {
@@ -89,6 +97,7 @@ async function invokeTests(client, releaseName, testDefs, devices, resultsBucket
89
97
  aiInstructions,
90
98
  displayName: testDef.testCase.displayName,
91
99
  resultsBucket,
100
+ loginCredential,
92
101
  }));
93
102
  }
94
103
  return releaseTests;
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.command = void 0;
4
4
  const os = require("os");
5
5
  const path = require("path");
6
- const uuid = require("uuid");
6
+ const crypto_1 = require("crypto");
7
7
  const command_1 = require("../command");
8
8
  const error_1 = require("../error");
9
9
  const utils = require("../utils");
@@ -28,7 +28,7 @@ exports.command = new command_1.Command("crashlytics:symbols:upload <symbolFiles
28
28
  const jarOptions = {
29
29
  app,
30
30
  generator,
31
- cachePath: path.join(SYMBOL_CACHE_ROOT_DIR, `crashlytics-${uuid.v4()}`, "nativeSymbols", app.replace(/:/g, "-"), generator),
31
+ cachePath: path.join(SYMBOL_CACHE_ROOT_DIR, `crashlytics-${(0, crypto_1.randomUUID)()}`, "nativeSymbols", app.replace(/:/g, "-"), generator),
32
32
  symbolFile: "",
33
33
  generate: true,
34
34
  };
@@ -195,6 +195,7 @@ function load(client) {
195
195
  client.apphosting.backends.delete = loadCommand("apphosting-backends-delete");
196
196
  client.apphosting.secrets = {};
197
197
  client.apphosting.secrets.set = loadCommand("apphosting-secrets-set");
198
+ client.apphosting.secrets.revokeaccess = loadCommand("apphosting-secrets-revokeaccess");
198
199
  client.apphosting.secrets.grantaccess = loadCommand("apphosting-secrets-grantaccess");
199
200
  client.apphosting.secrets.describe = loadCommand("apphosting-secrets-describe");
200
201
  client.apphosting.secrets.access = loadCommand("apphosting-secrets-access");
@@ -15,7 +15,6 @@ exports.registerSourceMap = registerSourceMap;
15
15
  const fs = require("fs");
16
16
  const path = require("path");
17
17
  const node_child_process_1 = require("node:child_process");
18
- const pLimit = require("p-limit");
19
18
  const apiv2_1 = require("../apiv2");
20
19
  const error_1 = require("../error");
21
20
  const logger_1 = require("../logger");
@@ -104,7 +103,7 @@ async function findSourceMapMappings(files, rootDir) {
104
103
  const mappings = [];
105
104
  const mapFilePathsSet = new Set(mapFiles.map((f) => f.name));
106
105
  const mapFilesLinkedInJsComment = new Set();
107
- const limit = pLimit(exports.CONCURRENCY);
106
+ const limit = (0, utils_1.pLimit)(exports.CONCURRENCY);
108
107
  const results = await Promise.all(jsFiles.map((jsFile) => limit(async () => {
109
108
  const mapFilePath = await getLinkedSourceMapPath(jsFile.name);
110
109
  return { jsFile, mapFilePath };
@@ -164,7 +163,7 @@ async function getLinkedSourceMapPath(jsFilePath) {
164
163
  }
165
164
  async function uploadSourceMaps(mappings, request) {
166
165
  const { projectId, bucketName, appVersion, options } = request;
167
- const limit = pLimit(exports.CONCURRENCY);
166
+ const limit = (0, utils_1.pLimit)(exports.CONCURRENCY);
168
167
  const results = await Promise.all(mappings.map((mapping) => limit(async () => {
169
168
  const uploadRequest = {
170
169
  projectId,
@@ -8,7 +8,7 @@ const StreamObject = require("stream-json/streamers/StreamObject");
8
8
  const apiv2_1 = require("../apiv2");
9
9
  const node_fetch_1 = require("node-fetch");
10
10
  const error_1 = require("../error");
11
- const pLimit = require("p-limit");
11
+ const utils_1 = require("../utils");
12
12
  class BatchChunks extends stream.Transform {
13
13
  constructor(maxSize, opts) {
14
14
  super({ ...opts, objectMode: true });
@@ -92,7 +92,7 @@ class DatabaseImporter {
92
92
  this.payloadSize = payloadSize;
93
93
  this.nonFatalRetryTimeout = 1000;
94
94
  this.client = new apiv2_1.Client({ urlPrefix: dbUrl.origin, auth: true });
95
- this.limit = pLimit(concurrency);
95
+ this.limit = (0, utils_1.pLimit)(concurrency);
96
96
  }
97
97
  async execute() {
98
98
  await this.checkLocationIsEmpty();
@@ -36,9 +36,9 @@ async function build(options, configDir, deployStats) {
36
36
  async function handleBuildErrors(errors, nonInteractive, force, dryRun) {
37
37
  const fatalDeploys = errors.filter((w) => w.extensions?.warningLevel === "ALWAYS_REQUIRED");
38
38
  if (fatalDeploys.length) {
39
- utils.logLabeledError("dataconnect", `There are deployment requirements that are always required and cannot be bypassed:\n` +
39
+ utils.logLabeledError("dataconnect", `There are requirements that are always required and cannot be bypassed:\n` +
40
40
  (0, graphqlError_1.prettifyTable)(fatalDeploys));
41
- throw new error_1.FirebaseError("Deployment failed due to unbypassable requirements.");
41
+ throw new error_1.FirebaseError("Failed due to unbypassable requirements.");
42
42
  }
43
43
  if (errors.filter((w) => !w.extensions?.warningLevel).length) {
44
44
  throw new error_1.FirebaseError(`There are errors in your schema and connector files:\n${errors.map(graphqlError_1.prettify).join("\n")}`);
@@ -47,7 +47,7 @@ async function handleBuildErrors(errors, nonInteractive, force, dryRun) {
47
47
  if (requiredForces.length && !force) {
48
48
  utils.logLabeledError("dataconnect", `There are changes in your schema or connectors that will result in broken behavior:\n` +
49
49
  (0, graphqlError_1.prettifyTable)(requiredForces));
50
- throw new error_1.FirebaseError("Rerun this command with --force to deploy these changes.");
50
+ throw new error_1.FirebaseError("Rerun this command with --force to proceed with these changes.");
51
51
  }
52
52
  const interactiveAcks = errors.filter((w) => w.extensions?.warningLevel === "INTERACTIVE_ACK");
53
53
  const requiredAcks = errors.filter((w) => w.extensions?.warningLevel === "REQUIRE_ACK");
@@ -59,7 +59,7 @@ async function handleBuildErrors(errors, nonInteractive, force, dryRun) {
59
59
  utils.logLabeledWarning("dataconnect", `There are changes in your schema or connectors that may break your existing applications or introduce operations that are insecure. These changes require explicit acknowledgement to proceed. You may either reject the changes and update your sources with the suggested workaround(s), if any, or acknowledge these changes and proceed with the deployment:\n` +
60
60
  (0, graphqlError_1.prettifyTable)(requiredAcks));
61
61
  if (nonInteractive && !force) {
62
- throw new error_1.FirebaseError("Explicit acknowledgement required for breaking schema or connector changes and new insecure operations. Rerun this command with --force to deploy these changes.");
62
+ throw new error_1.FirebaseError("Explicit acknowledgement required for breaking schema or connector changes and new insecure operations. Rerun this command with --force to proceed with these changes.");
63
63
  }
64
64
  else if (!nonInteractive && !force && !dryRun) {
65
65
  const result = await (0, prompt_1.select)({
@@ -68,7 +68,7 @@ async function handleBuildErrors(errors, nonInteractive, force, dryRun) {
68
68
  default: "abort",
69
69
  });
70
70
  if (result === "abort") {
71
- throw new error_1.FirebaseError(`Deployment aborted.`);
71
+ throw new error_1.FirebaseError("Aborted.");
72
72
  }
73
73
  }
74
74
  }
@@ -82,7 +82,7 @@ async function handleBuildErrors(errors, nonInteractive, force, dryRun) {
82
82
  default: "proceed",
83
83
  });
84
84
  if (result === "abort") {
85
- throw new error_1.FirebaseError(`Deployment aborted.`);
85
+ throw new error_1.FirebaseError("Aborted.");
86
86
  }
87
87
  }
88
88
  }
@@ -9,9 +9,11 @@ const path = require("path");
9
9
  const tar = require("tar");
10
10
  const tmp = require("tmp");
11
11
  const error_1 = require("../../error");
12
+ const logger_1 = require("../../logger");
12
13
  const fsAsync = require("../../fsAsync");
13
14
  const utils_1 = require("../../utils");
14
- async function createLocalBuildTarArchive(config, rootDir, outputFiles) {
15
+ const constants_1 = require("../../apphosting/constants");
16
+ async function createLocalBuildTarArchive(config, rootDir, outputFiles, sizeLimitBytes = constants_1.CLOUD_RUN_SIZE_LIMIT_BYTES) {
15
17
  const tmpFile = tmp.fileSync({ prefix: `${config.backendId}-`, postfix: ".tar.gz" }).name;
16
18
  const filesToPackage = outputFiles.length > 0 ? outputFiles : ["."];
17
19
  const allFiles = [];
@@ -52,6 +54,12 @@ async function createLocalBuildTarArchive(config, rootDir, outputFiles) {
52
54
  cwd: rootDir,
53
55
  portable: true,
54
56
  }, allFiles);
57
+ const stats = fs.statSync(tmpFile);
58
+ if (config.localBuild && stats.size > sizeLimitBytes) {
59
+ const sizeInMB = stats.size / (1024 * 1024);
60
+ const limitInMB = sizeLimitBytes / (1024 * 1024);
61
+ logger_1.logger.warn(`The final build artifact is larger than ${limitInMB.toFixed(0)}MB (current size: ${sizeInMB.toFixed(2)}MB). Please reduce the size of your build artifacts.`);
62
+ }
55
63
  return tmpFile;
56
64
  }
57
65
  async function createSourceDeployArchive(config, rootDir, targetSubDir) {
@@ -208,7 +208,7 @@ async function prepare(context, options, payload) {
208
208
  }
209
209
  const wantBackend = backend.merge(...Object.values(wantBackends));
210
210
  const haveBackend = backend.merge(...Object.values(haveBackends));
211
- await ensureAllRequiredAPIsEnabled(projectNumber, wantBackend);
211
+ await ensureAllRequiredAPIsEnabled(projectNumber, wantBackend, options);
212
212
  await warnIfNewGenkitFunctionIsMissingSecrets(wantBackend, haveBackend, options);
213
213
  warnIfDartBackendHasUnsupportedTriggers(wantBackend);
214
214
  const matchingBackend = backend.matchingBackend(wantBackend, (endpoint) => {
@@ -435,8 +435,44 @@ async function warnIfNewGenkitFunctionIsMissingSecrets(have, want, options) {
435
435
  }
436
436
  }
437
437
  }
438
- async function ensureAllRequiredAPIsEnabled(projectNumber, wantBackend) {
439
- await Promise.all(Object.values(wantBackend.requiredAPIs).map(({ api }) => {
438
+ const STANDARD_APIS = [
439
+ "cloudfunctions.googleapis.com",
440
+ "runtimeconfig.googleapis.com",
441
+ "cloudbuild.googleapis.com",
442
+ "artifactregistry.googleapis.com",
443
+ "run.googleapis.com",
444
+ "eventarc.googleapis.com",
445
+ "pubsub.googleapis.com",
446
+ "storage.googleapis.com",
447
+ "secretmanager.googleapis.com",
448
+ "cloudscheduler.googleapis.com",
449
+ "cloudtasks.googleapis.com",
450
+ ];
451
+ async function ensureAllRequiredAPIsEnabled(projectNumber, wantBackend = backend.empty(), options = {}) {
452
+ const requiredApis = Object.values(wantBackend?.requiredAPIs ?? {});
453
+ const [standardApis, additionalApis] = (0, functional_1.partition)(requiredApis, ({ api }) => STANDARD_APIS.includes(api));
454
+ if (additionalApis.length > 0) {
455
+ const checks = await Promise.all(additionalApis.map(({ api }) => ensureApiEnabled.check(projectNumber, api, "functions", true)));
456
+ const missingApis = additionalApis.filter((_, i) => !checks[i]);
457
+ if (missingApis.length > 0) {
458
+ const apiList = missingApis
459
+ .map(({ api, reason }) => ` - ${api}${reason ? `: ${reason}` : ""}`)
460
+ .join("\n");
461
+ const confirm = await prompt.confirm({
462
+ message: `This codebase depends on the following additional API(s) which are currently disabled:\n${apiList}\nWould you like to enable them?`,
463
+ default: false,
464
+ force: options.force,
465
+ nonInteractive: options.nonInteractive,
466
+ });
467
+ if (!confirm) {
468
+ throw new error_1.FirebaseError("Must enable required APIs to deploy.");
469
+ }
470
+ await Promise.all(missingApis.map(({ api }) => {
471
+ return ensureApiEnabled.ensure(projectNumber, api, "functions", false);
472
+ }));
473
+ }
474
+ }
475
+ await Promise.all(standardApis.map(({ api }) => {
440
476
  return ensureApiEnabled.ensure(projectNumber, api, "functions", false);
441
477
  }));
442
478
  if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2")) {
@@ -6,7 +6,6 @@ exports.prepareFunctionsUpload = prepareFunctionsUpload;
6
6
  exports.convertToSortedKeyValueArray = convertToSortedKeyValueArray;
7
7
  const archiver = require("archiver");
8
8
  const clc = require("colorette");
9
- const filesize = require("filesize");
10
9
  const fs = require("fs");
11
10
  const path = require("path");
12
11
  const tmp = require("tmp");
@@ -120,7 +119,7 @@ async function packageSource(projectDir, sourceDir, config, additionalSources, r
120
119
  " packaged " +
121
120
  clc.bold(sourceDir) +
122
121
  " (" +
123
- filesize(archive.pointer()) +
122
+ utils.formatFilesize(archive.pointer()) +
124
123
  ") for uploading");
125
124
  const sourceHash = crypto.createHash("sha1").update(hashes.sort().join("")).digest("hex");
126
125
  const hash = configHash ? `${sourceHash}.${configHash}` : sourceHash;
@@ -18,7 +18,6 @@ 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 supported_1 = require("../runtimes/supported");
22
21
  async function release(context, options, payload) {
23
22
  if (context.extensions && payload.extensions) {
24
23
  await (0, extensions_1.release)(context.extensions, options, payload.extensions);
@@ -89,10 +88,6 @@ async function release(context, options, payload) {
89
88
  reporter.printErrors(summary);
90
89
  const wantBackend = backend.merge(...Object.values(payload.functions).map((p) => p.wantBackend));
91
90
  printTriggerUrls(wantBackend, projectNumber);
92
- if (backend.someEndpoint(wantBackend, (endpoint) => (0, supported_1.runtimeIsLanguage)(endpoint.runtime, "dart"))) {
93
- utils.logLabeledBullet("functions", "Dart functions may not yet be visible in the Firebase Console. " +
94
- `View them in the Cloud Console at https://console.cloud.google.com/run/services?project=${context.projectId}`);
95
- }
96
91
  await setupArtifactCleanupPolicies(options, options.projectId, Object.keys(wantBackend.endpoints));
97
92
  const allErrors = summary.results.filter((r) => r.error).map((r) => r.error);
98
93
  if (allErrors.length) {