firebase-tools 13.5.2 → 13.6.1

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 (116) hide show
  1. package/lib/accountExporter.js +1 -1
  2. package/lib/accountImporter.js +1 -1
  3. package/lib/api.js +112 -58
  4. package/lib/apiv2.js +3 -1
  5. package/lib/appdistribution/client.js +4 -4
  6. package/lib/apphosting/config.js +31 -0
  7. package/lib/apphosting/githubConnections.js +261 -0
  8. package/lib/{init/features/apphosting → apphosting}/index.js +26 -22
  9. package/lib/{init/features/apphosting → apphosting}/repo.js +25 -13
  10. package/lib/apphosting/secrets/dialogs.js +169 -0
  11. package/lib/apphosting/secrets/index.js +98 -0
  12. package/lib/auth.js +17 -17
  13. package/lib/commands/apphosting-backends-create.js +4 -2
  14. package/lib/commands/apphosting-backends-delete.js +1 -1
  15. package/lib/commands/apphosting-secrets-describe.js +29 -0
  16. package/lib/commands/apphosting-secrets-grantaccess.js +45 -0
  17. package/lib/commands/apphosting-secrets-set.js +102 -0
  18. package/lib/commands/functions-secrets-access.js +2 -2
  19. package/lib/commands/functions-secrets-describe.js +14 -0
  20. package/lib/commands/functions-secrets-destroy.js +2 -2
  21. package/lib/commands/functions-secrets-get.js +3 -17
  22. package/lib/commands/functions-secrets-prune.js +2 -1
  23. package/lib/commands/functions-secrets-set.js +2 -2
  24. package/lib/commands/hosting-disable.js +1 -1
  25. package/lib/commands/index.js +5 -0
  26. package/lib/commands/open.js +1 -1
  27. package/lib/database/metadata.js +3 -3
  28. package/lib/defaultCredentials.js +2 -2
  29. package/lib/deploy/extensions/v2FunctionHelper.js +1 -1
  30. package/lib/deploy/functions/build.js +1 -1
  31. package/lib/deploy/functions/checkIam.js +3 -6
  32. package/lib/deploy/functions/containerCleaner.js +5 -15
  33. package/lib/deploy/functions/deploy.js +8 -2
  34. package/lib/deploy/functions/ensure.js +1 -1
  35. package/lib/deploy/functions/params.js +2 -2
  36. package/lib/deploy/functions/prepare.js +16 -7
  37. package/lib/deploy/functions/release/fabricator.js +8 -8
  38. package/lib/deploy/functions/runtimes/discovery/index.js +2 -2
  39. package/lib/deploy/functions/runtimes/index.js +6 -43
  40. package/lib/deploy/functions/runtimes/node/index.js +3 -2
  41. package/lib/deploy/functions/runtimes/node/parseRuntimeAndValidateSDK.js +15 -34
  42. package/lib/deploy/functions/runtimes/node/parseTriggers.js +2 -2
  43. package/lib/deploy/functions/runtimes/python/index.js +11 -7
  44. package/lib/deploy/functions/runtimes/supported.js +135 -0
  45. package/lib/deploy/hosting/uploader.js +1 -1
  46. package/lib/deploy/index.js +1 -1
  47. package/lib/deploy/remoteconfig/functions.js +1 -1
  48. package/lib/emulator/adminSdkConfig.js +1 -1
  49. package/lib/emulator/controller.js +8 -1
  50. package/lib/emulator/downloadableEmulators.js +6 -6
  51. package/lib/emulator/functionsEmulator.js +2 -2
  52. package/lib/emulator/hub.js +5 -0
  53. package/lib/ensureApiEnabled.js +1 -1
  54. package/lib/extensions/emulator/specHelper.js +4 -3
  55. package/lib/extensions/extensionsApi.js +5 -5
  56. package/lib/extensions/extensionsHelper.js +4 -4
  57. package/lib/extensions/provisioningHelper.js +2 -2
  58. package/lib/extensions/publishHelpers.js +1 -1
  59. package/lib/extensions/publisherApi.js +3 -3
  60. package/lib/extensions/resolveSource.js +1 -1
  61. package/lib/extensions/secretsUtils.js +1 -1
  62. package/lib/extensions/tos.js +1 -1
  63. package/lib/fetchMOTD.js +1 -1
  64. package/lib/fetchWebSetup.js +2 -2
  65. package/lib/firestore/api-sort.js +17 -0
  66. package/lib/firestore/api.js +9 -2
  67. package/lib/firestore/checkDatabaseType.js +1 -1
  68. package/lib/firestore/delete.js +1 -1
  69. package/lib/firestore/pretty-print.js +11 -2
  70. package/lib/functional.js +2 -2
  71. package/lib/functions/secrets.js +42 -24
  72. package/lib/functionsConfig.js +1 -1
  73. package/lib/gcp/apphosting.js +17 -4
  74. package/lib/gcp/artifactregistry.js +1 -1
  75. package/lib/gcp/auth.js +1 -1
  76. package/lib/gcp/cloudbilling.js +1 -1
  77. package/lib/gcp/cloudbuild.js +8 -4
  78. package/lib/gcp/cloudfunctions.js +6 -6
  79. package/lib/gcp/cloudfunctionsv2.js +4 -4
  80. package/lib/gcp/cloudlogging.js +1 -1
  81. package/lib/gcp/cloudmonitoring.js +1 -1
  82. package/lib/gcp/cloudscheduler.js +3 -3
  83. package/lib/gcp/cloudtasks.js +1 -1
  84. package/lib/gcp/computeEngine.js +7 -0
  85. package/lib/gcp/devConnect.js +45 -22
  86. package/lib/gcp/eventarc.js +1 -1
  87. package/lib/gcp/firestore.js +2 -2
  88. package/lib/gcp/iam.js +11 -3
  89. package/lib/gcp/identityPlatform.js +1 -1
  90. package/lib/gcp/pubsub.js +1 -1
  91. package/lib/gcp/resourceManager.js +2 -1
  92. package/lib/gcp/rules.js +1 -1
  93. package/lib/gcp/run.js +1 -1
  94. package/lib/gcp/runtimeconfig.js +1 -1
  95. package/lib/gcp/secretManager.js +54 -14
  96. package/lib/gcp/serviceusage.js +21 -5
  97. package/lib/gcp/storage.js +12 -8
  98. package/lib/hosting/api.js +2 -2
  99. package/lib/hosting/cloudRunProxy.js +1 -1
  100. package/lib/init/features/database.js +1 -1
  101. package/lib/init/features/extensions/index.js +1 -1
  102. package/lib/init/features/functions/index.js +2 -2
  103. package/lib/init/features/functions/python.js +4 -3
  104. package/lib/init/features/hosting/github.js +4 -3
  105. package/lib/init/features/index.js +1 -1
  106. package/lib/management/apps.js +4 -4
  107. package/lib/management/database.js +1 -1
  108. package/lib/management/projects.js +4 -4
  109. package/lib/remoteconfig/get.js +1 -1
  110. package/lib/remoteconfig/rollback.js +1 -1
  111. package/lib/remoteconfig/versionslist.js +1 -1
  112. package/lib/shortenUrl.js +2 -2
  113. package/lib/utils.js +1 -1
  114. package/package.json +1 -1
  115. package/schema/firebase-config.json +12 -2
  116. /package/lib/{init/features/apphosting → apphosting}/constants.js +0 -0
@@ -3,31 +3,33 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.orchestrateRollout = exports.setDefaultTrafficPolicy = exports.createBackend = exports.doSetup = void 0;
4
4
  const clc = require("colorette");
5
5
  const repo = require("./repo");
6
- const poller = require("../../../operation-poller");
7
- const apphosting = require("../../../gcp/apphosting");
8
- const utils_1 = require("../../../utils");
9
- const api_1 = require("../../../api");
10
- const apphosting_1 = require("../../../gcp/apphosting");
11
- const resourceManager_1 = require("../../../gcp/resourceManager");
12
- const iam_1 = require("../../../gcp/iam");
13
- const error_1 = require("../../../error");
14
- const prompt_1 = require("../../../prompt");
6
+ const poller = require("../operation-poller");
7
+ const apphosting = require("../gcp/apphosting");
8
+ const githubConnections = require("./githubConnections");
9
+ const utils_1 = require("../utils");
10
+ const api_1 = require("../api");
11
+ const apphosting_1 = require("../gcp/apphosting");
12
+ const resourceManager_1 = require("../gcp/resourceManager");
13
+ const iam = require("../gcp/iam");
14
+ const error_1 = require("../error");
15
+ const prompt_1 = require("../prompt");
15
16
  const constants_1 = require("./constants");
16
- const ensureApiEnabled_1 = require("../../../ensureApiEnabled");
17
- const deploymentTool = require("../../../deploymentTool");
17
+ const ensureApiEnabled_1 = require("../ensureApiEnabled");
18
+ const deploymentTool = require("../deploymentTool");
18
19
  const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute";
19
20
  const apphostingPollerOptions = {
20
- apiOrigin: api_1.apphostingOrigin,
21
+ apiOrigin: (0, api_1.apphostingOrigin)(),
21
22
  apiVersion: apphosting_1.API_VERSION,
22
23
  masterTimeout: 25 * 60 * 1000,
23
24
  maxBackoff: 10000,
24
25
  };
25
- async function doSetup(projectId, location, serviceAccount) {
26
+ async function doSetup(projectId, location, serviceAccount, withDevConnect) {
26
27
  await Promise.all([
27
- (0, ensureApiEnabled_1.ensure)(projectId, api_1.cloudbuildOrigin, "apphosting", true),
28
- (0, ensureApiEnabled_1.ensure)(projectId, api_1.secretManagerOrigin, "apphosting", true),
29
- (0, ensureApiEnabled_1.ensure)(projectId, api_1.cloudRunApiOrigin, "apphosting", true),
30
- (0, ensureApiEnabled_1.ensure)(projectId, api_1.artifactRegistryDomain, "apphosting", true),
28
+ ...(withDevConnect ? [(0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.developerConnectOrigin)(), "apphosting", true)] : []),
29
+ (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.cloudbuildOrigin)(), "apphosting", true),
30
+ (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.secretManagerOrigin)(), "apphosting", true),
31
+ (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.cloudRunApiOrigin)(), "apphosting", true),
32
+ (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.artifactRegistryDomain)(), "apphosting", true),
31
33
  ]);
32
34
  const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId);
33
35
  if (location) {
@@ -53,8 +55,10 @@ async function doSetup(projectId, location, serviceAccount) {
53
55
  default: "my-web-app",
54
56
  message: "Create a name for your backend [1-30 characters]",
55
57
  });
56
- const cloudBuildConnRepo = await repo.linkGitHubRepository(projectId, location);
57
- const backend = await createBackend(projectId, location, backendId, cloudBuildConnRepo, serviceAccount);
58
+ const gitRepositoryConnection = withDevConnect
59
+ ? await githubConnections.linkGitHubRepository(projectId, location)
60
+ : await repo.linkGitHubRepository(projectId, location);
61
+ const backend = await createBackend(projectId, location, backendId, gitRepositoryConnection, serviceAccount);
58
62
  const branch = await (0, prompt_1.promptOnce)({
59
63
  name: "branch",
60
64
  type: "input",
@@ -111,9 +115,9 @@ async function createBackend(projectId, location, backendId, repository, service
111
115
  rootDirectory: "/",
112
116
  },
113
117
  labels: deploymentTool.labels(),
114
- computeServiceAccount: serviceAccount || defaultServiceAccount,
118
+ serviceAccount: serviceAccount || defaultServiceAccount,
115
119
  };
116
- delete backendReqBody.computeServiceAccount;
120
+ delete backendReqBody.serviceAccount;
117
121
  async function createBackendAndPoll() {
118
122
  const op = await apphosting.createBackend(projectId, location, backendReqBody, backendId);
119
123
  return await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
@@ -137,7 +141,7 @@ async function createBackend(projectId, location, backendId, repository, service
137
141
  exports.createBackend = createBackend;
138
142
  async function provisionDefaultComputeServiceAccount(projectId) {
139
143
  try {
140
- await (0, iam_1.createServiceAccount)(projectId, DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME, "Firebase App Hosting compute service account", "Default service account used to run builds and deploys for Firebase App Hosting");
144
+ await iam.createServiceAccount(projectId, DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME, "Firebase App Hosting compute service account", "Default service account used to run builds and deploys for Firebase App Hosting");
141
145
  }
142
146
  catch (err) {
143
147
  if (err.status !== 409) {
@@ -1,15 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fetchAllRepositories = exports.listAppHostingConnections = exports.getOrCreateRepository = exports.getOrCreateConnection = exports.createConnection = exports.linkGitHubRepository = exports.parseConnectionName = void 0;
3
+ exports.fetchAllRepositories = exports.listAppHostingConnections = exports.getOrCreateRepository = exports.getOrCreateConnection = exports.createConnection = exports.getOrCreateOauthConnection = exports.linkGitHubRepository = exports.parseConnectionName = void 0;
4
4
  const clc = require("colorette");
5
- const gcb = require("../../../gcp/cloudbuild");
6
- const rm = require("../../../gcp/resourceManager");
7
- const poller = require("../../../operation-poller");
8
- const utils = require("../../../utils");
9
- const api_1 = require("../../../api");
10
- const error_1 = require("../../../error");
11
- const prompt_1 = require("../../../prompt");
12
- const getProjectNumber_1 = require("../../../getProjectNumber");
5
+ const gcb = require("../gcp/cloudbuild");
6
+ const rm = require("../gcp/resourceManager");
7
+ const poller = require("../operation-poller");
8
+ const utils = require("../utils");
9
+ const api_1 = require("../api");
10
+ const error_1 = require("../error");
11
+ const prompt_1 = require("../prompt");
12
+ const getProjectNumber_1 = require("../getProjectNumber");
13
13
  const fuzzy = require("fuzzy");
14
14
  const inquirer = require("inquirer");
15
15
  const APPHOSTING_CONN_PATTERN = /.+\/apphosting-github-conn-.+$/;
@@ -29,7 +29,7 @@ function parseConnectionName(name) {
29
29
  }
30
30
  exports.parseConnectionName = parseConnectionName;
31
31
  const gcbPollerOptions = {
32
- apiOrigin: api_1.cloudbuildOrigin,
32
+ apiOrigin: (0, api_1.cloudbuildOrigin)(),
33
33
  apiVersion: "v2",
34
34
  masterTimeout: 25 * 60 * 1000,
35
35
  maxBackoff: 10000,
@@ -56,7 +56,6 @@ async function linkGitHubRepository(projectId, location) {
56
56
  const oauthConn = await getOrCreateOauthConnection(projectId, location);
57
57
  const existingConns = await listAppHostingConnections(projectId);
58
58
  if (existingConns.length === 0) {
59
- await ensureSecretManagerAdminGrant(projectId);
60
59
  existingConns.push(await createFullyInstalledConnection(projectId, location, generateConnectionId(), oauthConn));
61
60
  }
62
61
  let repoRemoteUri;
@@ -99,7 +98,19 @@ async function createFullyInstalledConnection(projectId, location, connectionId,
99
98
  return conn;
100
99
  }
101
100
  async function getOrCreateOauthConnection(projectId, location) {
102
- let conn = await getOrCreateConnection(projectId, location, APPHOSTING_OAUTH_CONN_NAME);
101
+ let conn;
102
+ try {
103
+ conn = await gcb.getConnection(projectId, location, APPHOSTING_OAUTH_CONN_NAME);
104
+ }
105
+ catch (err) {
106
+ if (err.status === 404) {
107
+ await ensureSecretManagerAdminGrant(projectId);
108
+ conn = await createConnection(projectId, location, APPHOSTING_OAUTH_CONN_NAME);
109
+ }
110
+ else {
111
+ throw err;
112
+ }
113
+ }
103
114
  while (conn.installationState.stage === "PENDING_USER_OAUTH") {
104
115
  utils.logBullet("You must authorize the Cloud Build GitHub app.");
105
116
  utils.logBullet("Sign in to GitHub and authorize Cloud Build GitHub app:");
@@ -115,6 +126,7 @@ async function getOrCreateOauthConnection(projectId, location) {
115
126
  }
116
127
  return conn;
117
128
  }
129
+ exports.getOrCreateOauthConnection = getOrCreateOauthConnection;
118
130
  async function promptRepositoryUri(projectId, connections) {
119
131
  const { repos, remoteUriToConnection } = await fetchAllRepositories(projectId, connections);
120
132
  const remoteUri = await (0, prompt_1.promptOnce)({
@@ -146,7 +158,7 @@ async function promptRepositoryUri(projectId, connections) {
146
158
  }
147
159
  async function ensureSecretManagerAdminGrant(projectId) {
148
160
  const projectNumber = await (0, getProjectNumber_1.getProjectNumber)({ projectId });
149
- const cbsaEmail = gcb.serviceAgentEmail(projectNumber);
161
+ const cbsaEmail = gcb.getDefaultServiceAgent(projectNumber);
150
162
  const alreadyGranted = await rm.serviceAccountHasRoles(projectId, cbsaEmail, ["roles/secretmanager.admin"], true);
151
163
  if (alreadyGranted) {
152
164
  return;
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.envVarForSecret = exports.selectBackendServiceAccounts = exports.GRANT_ACCESS_IN_FUTURE = exports.WARN_NO_BACKENDS = exports.selectFromMetadata = exports.tableForBackends = exports.serviceAccountDisplay = exports.toMetadata = void 0;
4
+ const clc = require("colorette");
5
+ const Table = require("cli-table");
6
+ const _1 = require(".");
7
+ const apphosting = require("../../gcp/apphosting");
8
+ const prompt = require("../../prompt");
9
+ const utils = require("../../utils");
10
+ const logger_1 = require("../../logger");
11
+ const env = require("../../functions/env");
12
+ function toMetadata(projectNumber, backends) {
13
+ const metadata = [];
14
+ for (const backend of backends) {
15
+ const [, , , location, , id] = backend.name.split("/");
16
+ metadata.push(Object.assign({ location, id }, (0, _1.serviceAccountsForBackend)(projectNumber, backend)));
17
+ }
18
+ return metadata.sort((left, right) => {
19
+ const cmplocation = left.location.localeCompare(right.location);
20
+ if (cmplocation) {
21
+ return cmplocation;
22
+ }
23
+ return left.id.localeCompare(right.id);
24
+ });
25
+ }
26
+ exports.toMetadata = toMetadata;
27
+ function serviceAccountDisplay(metadata) {
28
+ if (sameServiceAccount(metadata)) {
29
+ return metadata.runServiceAccount;
30
+ }
31
+ return `${metadata.buildServiceAccount}, ${metadata.runServiceAccount}`;
32
+ }
33
+ exports.serviceAccountDisplay = serviceAccountDisplay;
34
+ function sameServiceAccount(metadata) {
35
+ return metadata.buildServiceAccount === metadata.runServiceAccount;
36
+ }
37
+ const matchesServiceAccounts = (target) => (test) => {
38
+ return (target.buildServiceAccount === test.buildServiceAccount &&
39
+ target.runServiceAccount === test.runServiceAccount);
40
+ };
41
+ function tableForBackends(metadata) {
42
+ const headers = [
43
+ "location",
44
+ "backend",
45
+ metadata.every(sameServiceAccount) ? "service account" : "service accounts",
46
+ ];
47
+ const rows = metadata.map((m) => [m.location, m.id, serviceAccountDisplay(m)]);
48
+ return [headers, rows];
49
+ }
50
+ exports.tableForBackends = tableForBackends;
51
+ function selectFromMetadata(input, selected) {
52
+ const buildAccounts = new Set();
53
+ const runAccounts = new Set();
54
+ for (const sa of selected) {
55
+ if (input.find((m) => m.buildServiceAccount === sa)) {
56
+ buildAccounts.add(sa);
57
+ }
58
+ else {
59
+ runAccounts.add(sa);
60
+ }
61
+ }
62
+ return {
63
+ buildServiceAccounts: [...buildAccounts],
64
+ runServiceAccounts: [...runAccounts],
65
+ };
66
+ }
67
+ exports.selectFromMetadata = selectFromMetadata;
68
+ exports.WARN_NO_BACKENDS = "To use this secret, your backend's service account must have secret accessor permission. " +
69
+ "It does not look like you have a backend yet. After creating a backend, grant access with " +
70
+ clc.bold("firebase apphosting:secrets:grantAccess");
71
+ exports.GRANT_ACCESS_IN_FUTURE = `To grant access in the future, run ${clc.bold("firebase apphosting:secrets:grantaccess")}`;
72
+ async function selectBackendServiceAccounts(projectNumber, projectId, options) {
73
+ const listBackends = await apphosting.listBackends(projectId, "-");
74
+ if (listBackends.unreachable.length) {
75
+ utils.logLabeledWarning("apphosting", `Could not reach location(s) ${listBackends.unreachable.join(", ")}. You may need to run ` +
76
+ `${clc.bold("firebase apphosting:secrets:grantAccess")} at a later time if you have backends in these locations`);
77
+ }
78
+ if (!listBackends.backends.length) {
79
+ utils.logLabeledWarning("apphosting", exports.WARN_NO_BACKENDS);
80
+ return { buildServiceAccounts: [], runServiceAccounts: [] };
81
+ }
82
+ if (listBackends.backends.length === 1) {
83
+ const grant = await prompt.confirm({
84
+ nonInteractive: options.nonInteractive,
85
+ default: true,
86
+ message: "To use this secret, your backend's service account must have secret accessor permission. Would you like to grant it now?",
87
+ });
88
+ if (grant) {
89
+ return (0, _1.toMulti)((0, _1.serviceAccountsForBackend)(projectNumber, listBackends.backends[0]));
90
+ }
91
+ utils.logLabeledBullet("apphosting", exports.GRANT_ACCESS_IN_FUTURE);
92
+ return { buildServiceAccounts: [], runServiceAccounts: [] };
93
+ }
94
+ const metadata = toMetadata(projectNumber, listBackends.backends);
95
+ if (metadata.every(matchesServiceAccounts(metadata[0]))) {
96
+ utils.logLabeledBullet("apphosting", "To use this secret, your backend's service account must have secret accessor permission. All of your backends use " +
97
+ (sameServiceAccount(metadata[0]) ? "service account " : "service accounts ") +
98
+ serviceAccountDisplay(metadata[0]) +
99
+ ". Granting access to one backend will grant access to all backends.");
100
+ const grant = await prompt.confirm({
101
+ nonInteractive: options.nonInteractive,
102
+ default: true,
103
+ message: "Would you like to grant it now?",
104
+ });
105
+ if (grant) {
106
+ return selectFromMetadata(metadata, [
107
+ metadata[0].buildServiceAccount,
108
+ metadata[0].runServiceAccount,
109
+ ]);
110
+ }
111
+ utils.logLabeledBullet("apphosting", exports.GRANT_ACCESS_IN_FUTURE);
112
+ return { buildServiceAccounts: [], runServiceAccounts: [] };
113
+ }
114
+ utils.logLabeledBullet("apphosting", "To use this secret, your backend's service account must have secret accessor permission. Your backends use the following service accounts:");
115
+ const tableData = tableForBackends(metadata);
116
+ const table = new Table({
117
+ head: tableData[0],
118
+ style: { head: ["green"] },
119
+ rows: tableData[1],
120
+ });
121
+ logger_1.logger.info(table.toString());
122
+ const allAccounts = metadata.reduce((accum, row) => {
123
+ accum.add(row.buildServiceAccount);
124
+ accum.add(row.runServiceAccount);
125
+ return accum;
126
+ }, new Set());
127
+ const chosen = await prompt.promptOnce({
128
+ type: "checkbox",
129
+ message: "Which service accounts would you like to grant access? " +
130
+ "Press Space to select accounts, then Enter to confirm your choices.",
131
+ choices: [...allAccounts.values()].sort(),
132
+ });
133
+ if (!chosen.length) {
134
+ utils.logLabeledBullet("apphosting", exports.GRANT_ACCESS_IN_FUTURE);
135
+ }
136
+ return selectFromMetadata(metadata, chosen);
137
+ }
138
+ exports.selectBackendServiceAccounts = selectBackendServiceAccounts;
139
+ function toUpperSnakeCase(key) {
140
+ return key
141
+ .replace(/[.-]/g, "_")
142
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
143
+ .toUpperCase();
144
+ }
145
+ async function envVarForSecret(secret) {
146
+ const upper = toUpperSnakeCase(secret);
147
+ if (upper === secret) {
148
+ try {
149
+ env.validateKey(secret);
150
+ return secret;
151
+ }
152
+ catch (_a) {
153
+ }
154
+ }
155
+ do {
156
+ const test = await prompt.promptOnce({
157
+ message: "What environment variable name would you like to use?",
158
+ default: upper,
159
+ });
160
+ try {
161
+ env.validateKey(test);
162
+ return test;
163
+ }
164
+ catch (err) {
165
+ utils.logLabeledError("apphosting", err.message);
166
+ }
167
+ } while (true);
168
+ }
169
+ exports.envVarForSecret = envVarForSecret;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.upsertSecret = exports.grantSecretAccess = exports.serviceAccountsForBackend = exports.toMulti = void 0;
4
+ const error_1 = require("../../error");
5
+ const gcsm = require("../../gcp/secretManager");
6
+ const gcb = require("../../gcp/cloudbuild");
7
+ const gce = require("../../gcp/computeEngine");
8
+ const secretManager_1 = require("../../gcp/secretManager");
9
+ const secretManager_2 = require("../../gcp/secretManager");
10
+ const utils = require("../../utils");
11
+ const prompt = require("../../prompt");
12
+ function toMulti(accounts) {
13
+ const m = {
14
+ buildServiceAccounts: [accounts.buildServiceAccount],
15
+ runServiceAccounts: [],
16
+ };
17
+ if (accounts.buildServiceAccount !== accounts.runServiceAccount) {
18
+ m.runServiceAccounts.push(accounts.runServiceAccount);
19
+ }
20
+ return m;
21
+ }
22
+ exports.toMulti = toMulti;
23
+ function serviceAccountsForBackend(projectNumber, backend) {
24
+ if (backend.serviceAccount) {
25
+ return {
26
+ buildServiceAccount: backend.serviceAccount,
27
+ runServiceAccount: backend.serviceAccount,
28
+ };
29
+ }
30
+ return {
31
+ buildServiceAccount: gcb.getDefaultServiceAccount(projectNumber),
32
+ runServiceAccount: gce.getDefaultServiceAccount(projectNumber),
33
+ };
34
+ }
35
+ exports.serviceAccountsForBackend = serviceAccountsForBackend;
36
+ async function grantSecretAccess(projectId, secretName, accounts) {
37
+ const newBindings = [
38
+ {
39
+ role: "roles/secretmanager.secretAccessor",
40
+ members: [...accounts.buildServiceAccounts, ...accounts.runServiceAccounts].map((sa) => `serviceAccount:${sa}`),
41
+ },
42
+ {
43
+ role: "roles/secretmanager.viewer",
44
+ members: accounts.buildServiceAccounts.map((sa) => `serviceAccount:${sa}`),
45
+ },
46
+ ];
47
+ let existingBindings;
48
+ try {
49
+ existingBindings = (await gcsm.getIamPolicy({ projectId, name: secretName })).bindings || [];
50
+ }
51
+ catch (err) {
52
+ 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: err });
53
+ }
54
+ try {
55
+ const updatedBindings = existingBindings.concat(newBindings);
56
+ await gcsm.setIamPolicy({ projectId, name: secretName }, updatedBindings);
57
+ }
58
+ catch (err) {
59
+ throw new error_1.FirebaseError(`Failed to set IAM bindings ${JSON.stringify(newBindings)} on secret: ${secretName}. Ensure you have the permissions to do so and try again.`, { original: err });
60
+ }
61
+ utils.logSuccess(`Successfully set IAM bindings on secret ${secretName}.\n`);
62
+ }
63
+ exports.grantSecretAccess = grantSecretAccess;
64
+ async function upsertSecret(project, secret, location) {
65
+ var _a, _b, _c, _d;
66
+ let existing;
67
+ try {
68
+ existing = await gcsm.getSecret(project, secret);
69
+ }
70
+ catch (err) {
71
+ if (err.status !== 404) {
72
+ throw new error_1.FirebaseError("Unexpected error loading secret", { original: err });
73
+ }
74
+ await gcsm.createSecret(project, secret, gcsm.labels("apphosting"), location);
75
+ return true;
76
+ }
77
+ const replication = (_a = existing.replication) === null || _a === void 0 ? void 0 : _a.userManaged;
78
+ if (location &&
79
+ (((_b = replication === null || replication === void 0 ? void 0 : replication.replicas) === null || _b === void 0 ? void 0 : _b.length) !== 1 || ((_d = (_c = replication === null || replication === void 0 ? void 0 : replication.replicas) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.location) !== location)) {
80
+ utils.logLabeledError("apphosting", "Secret replication policies cannot be changed after creation");
81
+ return null;
82
+ }
83
+ if ((0, secretManager_2.isFunctionsManaged)(existing)) {
84
+ utils.logLabeledWarning("apphosting", `Cloud Functions for Firebase currently manages versions of ${secret}. Continuing will disable ` +
85
+ "automatic deletion of old versions.");
86
+ const stopTracking = await prompt.confirm({
87
+ message: "Do you wish to continue?",
88
+ default: false,
89
+ });
90
+ if (!stopTracking) {
91
+ return null;
92
+ }
93
+ delete existing.labels[secretManager_1.FIREBASE_MANAGED];
94
+ await gcsm.patchSecret(project, secret, existing.labels);
95
+ }
96
+ return false;
97
+ }
98
+ exports.upsertSecret = upsertSecret;
package/lib/auth.js CHANGED
@@ -172,10 +172,10 @@ function queryParamString(args) {
172
172
  return tokens.join("&");
173
173
  }
174
174
  function getLoginUrl(callbackUrl, userHint) {
175
- return (api_1.authOrigin +
175
+ return ((0, api_1.authOrigin)() +
176
176
  "/o/oauth2/auth?" +
177
177
  queryParamString({
178
- client_id: api_1.clientId,
178
+ client_id: (0, api_1.clientId)(),
179
179
  scope: SCOPES.join(" "),
180
180
  response_type: "code",
181
181
  state: _nonce,
@@ -186,8 +186,8 @@ function getLoginUrl(callbackUrl, userHint) {
186
186
  async function getTokensFromAuthorizationCode(code, callbackUrl, verifier) {
187
187
  const params = {
188
188
  code: code,
189
- client_id: api_1.clientId,
190
- client_secret: api_1.clientSecret,
189
+ client_id: (0, api_1.clientId)(),
190
+ client_secret: (0, api_1.clientSecret)(),
191
191
  redirect_uri: callbackUrl,
192
192
  grant_type: "authorization_code",
193
193
  };
@@ -196,7 +196,7 @@ async function getTokensFromAuthorizationCode(code, callbackUrl, verifier) {
196
196
  }
197
197
  let res;
198
198
  try {
199
- const client = new apiv2.Client({ urlPrefix: api_1.authOrigin, auth: false });
199
+ const client = new apiv2.Client({ urlPrefix: (0, api_1.authOrigin)(), auth: false });
200
200
  const form = new FormData();
201
201
  for (const [k, v] of Object.entries(params)) {
202
202
  form.append(k, v);
@@ -229,20 +229,20 @@ async function getTokensFromAuthorizationCode(code, callbackUrl, verifier) {
229
229
  }
230
230
  const GITHUB_SCOPES = ["read:user", "repo", "public_repo"];
231
231
  function getGithubLoginUrl(callbackUrl) {
232
- return (api_1.githubOrigin +
232
+ return ((0, api_1.githubOrigin)() +
233
233
  "/login/oauth/authorize?" +
234
234
  queryParamString({
235
- client_id: api_1.githubClientId,
235
+ client_id: (0, api_1.githubClientId)(),
236
236
  state: _nonce,
237
237
  redirect_uri: callbackUrl,
238
238
  scope: GITHUB_SCOPES.join(" "),
239
239
  }));
240
240
  }
241
241
  async function getGithubTokensFromAuthorizationCode(code, callbackUrl) {
242
- const client = new apiv2.Client({ urlPrefix: api_1.githubOrigin, auth: false });
242
+ const client = new apiv2.Client({ urlPrefix: (0, api_1.githubOrigin)(), auth: false });
243
243
  const data = {
244
- client_id: api_1.githubClientId,
245
- client_secret: api_1.githubClientSecret,
244
+ client_id: (0, api_1.githubClientId)(),
245
+ client_secret: (0, api_1.githubClientSecret)(),
246
246
  code,
247
247
  redirect_uri: callbackUrl,
248
248
  state: _nonce,
@@ -276,7 +276,7 @@ function urlsafeBase64(base64string) {
276
276
  async function loginRemotely() {
277
277
  var _a;
278
278
  const authProxyClient = new apiv2.Client({
279
- urlPrefix: api_1.authProxyOrigin,
279
+ urlPrefix: (0, api_1.authProxyOrigin)(),
280
280
  auth: false,
281
281
  });
282
282
  const sessionId = (0, uuid_1.v4)();
@@ -285,7 +285,7 @@ async function loginRemotely() {
285
285
  const attestToken = (_a = (await authProxyClient.post("/attest", {
286
286
  session_id: sessionId,
287
287
  })).body) === null || _a === void 0 ? void 0 : _a.token;
288
- const loginUrl = `${api_1.authProxyOrigin}/login?code_challenge=${codeChallenge}&session=${sessionId}&attest=${attestToken}`;
288
+ const loginUrl = `${(0, api_1.authProxyOrigin)()}/login?code_challenge=${codeChallenge}&session=${sessionId}&attest=${attestToken}`;
289
289
  logger_1.logger.info();
290
290
  logger_1.logger.info("To sign in to the Firebase CLI:");
291
291
  logger_1.logger.info();
@@ -304,7 +304,7 @@ async function loginRemotely() {
304
304
  message: "Enter authorization code:",
305
305
  });
306
306
  try {
307
- const tokens = await getTokensFromAuthorizationCode(code, `${api_1.authProxyOrigin}/complete`, codeVerifier);
307
+ const tokens = await getTokensFromAuthorizationCode(code, `${(0, api_1.authProxyOrigin)()}/complete`, codeVerifier);
308
308
  void (0, track_1.trackGA4)("login", { method: "google_remote" });
309
309
  return {
310
310
  user: jwt.decode(tokens.id_token, { json: true }),
@@ -457,11 +457,11 @@ async function refreshTokens(refreshToken, authScopes) {
457
457
  var _a, _b;
458
458
  logger_1.logger.debug("> refreshing access token with scopes:", JSON.stringify(authScopes));
459
459
  try {
460
- const client = new apiv2.Client({ urlPrefix: api_1.googleOrigin, auth: false });
460
+ const client = new apiv2.Client({ urlPrefix: (0, api_1.googleOrigin)(), auth: false });
461
461
  const data = {
462
462
  refresh_token: refreshToken,
463
- client_id: api_1.clientId,
464
- client_secret: api_1.clientSecret,
463
+ client_id: (0, api_1.clientId)(),
464
+ client_secret: (0, api_1.clientSecret)(),
465
465
  grant_type: "refresh_token",
466
466
  scope: (authScopes || []).join(" "),
467
467
  };
@@ -528,7 +528,7 @@ async function logout(refreshToken) {
528
528
  }
529
529
  logoutCurrentSession(refreshToken);
530
530
  try {
531
- const client = new apiv2.Client({ urlPrefix: api_1.authOrigin, auth: false });
531
+ const client = new apiv2.Client({ urlPrefix: (0, api_1.authOrigin)(), auth: false });
532
532
  await client.get("/o/oauth2/revoke", { queryParams: { token: refreshToken } });
533
533
  }
534
534
  catch (thrown) {
@@ -4,17 +4,19 @@ exports.command = void 0;
4
4
  const command_1 = require("../command");
5
5
  const projectUtils_1 = require("../projectUtils");
6
6
  const requireInteractive_1 = require("../requireInteractive");
7
- const apphosting_1 = require("../init/features/apphosting");
7
+ const apphosting_1 = require("../apphosting");
8
8
  const apphosting_2 = require("../gcp/apphosting");
9
9
  exports.command = new command_1.Command("apphosting:backends:create")
10
10
  .description("create a backend in a Firebase project")
11
11
  .option("-l, --location <location>", "specify the region of the backend", "")
12
12
  .option("-s, --service-account <serviceAccount>", "specify the service account used to run the server", "")
13
+ .option("-w, --with-dev-connect", "use the Developer Connect flow insetad of Cloud Build Repositories (testing)", false)
13
14
  .before(apphosting_2.ensureApiEnabled)
14
15
  .before(requireInteractive_1.default)
15
16
  .action(async (options) => {
16
17
  const projectId = (0, projectUtils_1.needProjectId)(options);
17
18
  const location = options.location;
18
19
  const serviceAccount = options.serviceAccount;
19
- await (0, apphosting_1.doSetup)(projectId, location, serviceAccount);
20
+ const withDevConnect = options.withDevConnect;
21
+ await (0, apphosting_1.doSetup)(projectId, location, serviceAccount, withDevConnect);
20
22
  });
@@ -5,7 +5,7 @@ const command_1 = require("../command");
5
5
  const projectUtils_1 = require("../projectUtils");
6
6
  const error_1 = require("../error");
7
7
  const prompt_1 = require("../prompt");
8
- const constants_1 = require("../init/features/apphosting/constants");
8
+ const constants_1 = require("../apphosting/constants");
9
9
  const utils = require("../utils");
10
10
  const apphosting = require("../gcp/apphosting");
11
11
  const apphosting_backends_list_1 = require("./apphosting-backends-list");
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const command_1 = require("../command");
5
+ const projectUtils_1 = require("../projectUtils");
6
+ const logger_1 = require("../logger");
7
+ const requireAuth_1 = require("../requireAuth");
8
+ const secretManager_1 = require("../gcp/secretManager");
9
+ const secretManager = require("../gcp/secretManager");
10
+ const requirePermissions_1 = require("../requirePermissions");
11
+ const Table = require("cli-table");
12
+ exports.command = new command_1.Command("apphosting:secrets:describe <secretName>")
13
+ .description("Get metadata for secret and its versions.")
14
+ .before(requireAuth_1.requireAuth)
15
+ .before(secretManager.ensureApi)
16
+ .before(requirePermissions_1.requirePermissions, ["secretmanager.secrets.get"])
17
+ .action(async (secretName, options) => {
18
+ const projectId = (0, projectUtils_1.needProjectId)(options);
19
+ const versions = await (0, secretManager_1.listSecretVersions)(projectId, secretName);
20
+ const table = new Table({
21
+ head: ["Name", "Version", "Status", "Create Time"],
22
+ style: { head: ["yellow"] },
23
+ });
24
+ for (const version of versions) {
25
+ table.push([secretName, version.versionId, version.state, version.createTime]);
26
+ }
27
+ logger_1.logger.info(table.toString());
28
+ return { secrets: versions };
29
+ });
@@ -0,0 +1,45 @@
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
+ exports.command = new command_1.Command("apphosting:secrets:grantaccess <secretName>")
13
+ .description("grant service accounts permissions to the provided secret")
14
+ .option("-l, --location <location>", "app backend location")
15
+ .option("-b, --backend <backend>", "app backend name")
16
+ .before(requireAuth_1.requireAuth)
17
+ .before(secretManager.ensureApi)
18
+ .before(apphosting.ensureApiEnabled)
19
+ .before(requirePermissions_1.requirePermissions, [
20
+ "secretmanager.secrets.create",
21
+ "secretmanager.secrets.get",
22
+ "secretmanager.secrets.update",
23
+ "secretmanager.versions.add",
24
+ "secretmanager.secrets.getIamPolicy",
25
+ "secretmanager.secrets.setIamPolicy",
26
+ ])
27
+ .action(async (secretName, options) => {
28
+ const projectId = (0, projectUtils_1.needProjectId)(options);
29
+ const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
30
+ if (!options.location) {
31
+ throw new error_1.FirebaseError("Missing required flag --location. See firebase apphosting:secrets:grantaccess --help for more info");
32
+ }
33
+ const location = options.location;
34
+ if (!options.backend) {
35
+ throw new error_1.FirebaseError("Missing required flag --backend. See firebase apphosting:secrets:grantaccess --help for more info");
36
+ }
37
+ const exists = await secretManager.secretExists(projectId, secretName);
38
+ if (!exists) {
39
+ throw new error_1.FirebaseError(`Cannot find secret ${secretName}`);
40
+ }
41
+ const backendId = options.backend;
42
+ const backend = await apphosting.getBackend(projectId, location, backendId);
43
+ const accounts = secrets.toMulti(secrets.serviceAccountsForBackend(projectNumber, backend));
44
+ await secrets.grantSecretAccess(projectId, secretName, accounts);
45
+ });