firebase-tools 11.3.0 → 11.4.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.
package/lib/apiv2.js CHANGED
@@ -15,6 +15,7 @@ const responseToError_1 = require("./responseToError");
15
15
  const FormData = require("form-data");
16
16
  const pkg = require("../package.json");
17
17
  const CLI_VERSION = pkg.version;
18
+ const GOOG_QUOTA_USER = "x-goog-quota-user";
18
19
  let accessToken = "";
19
20
  let refreshToken = "";
20
21
  function setRefreshToken(token = "") {
@@ -309,6 +310,10 @@ class Client {
309
310
  }
310
311
  const logURL = this.requestURL(options);
311
312
  logger_1.logger.debug(`>>> [apiv2][query] ${options.method} ${logURL} ${queryParamsLog}`);
313
+ const headers = options.headers;
314
+ if (headers && headers.has(GOOG_QUOTA_USER)) {
315
+ logger_1.logger.debug(`>>> [apiv2][(partial)header] ${options.method} ${logURL} x-goog-quota-user=${headers.get(GOOG_QUOTA_USER) || ""}`);
316
+ }
312
317
  if (options.body !== undefined) {
313
318
  let logBody = "[omitted]";
314
319
  if (!((_b = options.skipLog) === null || _b === void 0 ? void 0 : _b.body)) {
@@ -19,6 +19,7 @@ function targetsHaveFilters(...targets) {
19
19
  function targetsHaveNoFilters(...targets) {
20
20
  return targets.some((t) => !t.includes(":"));
21
21
  }
22
+ const FILTERABLE_TARGETS = new Set(["hosting", "functions", "firestore", "storage", "database"]);
22
23
  async function checkValidTargetFilters(options) {
23
24
  const only = !options.only ? [] : options.only.split(",");
24
25
  return new Promise((resolve, reject) => {
@@ -28,10 +29,10 @@ async function checkValidTargetFilters(options) {
28
29
  if (options.except) {
29
30
  return reject(new error_1.FirebaseError("Cannot specify both --only and --except"));
30
31
  }
31
- const nonFilteredTypes = deploy_1.VALID_DEPLOY_TARGETS.filter((t) => !["hosting", "functions", "firestore"].includes(t));
32
+ const nonFilteredTypes = deploy_1.VALID_DEPLOY_TARGETS.filter((t) => !FILTERABLE_TARGETS.has(t));
32
33
  const targetsForNonFilteredTypes = targetsForTypes(only, ...nonFilteredTypes);
33
34
  if (targetsForNonFilteredTypes.length && targetsHaveFilters(...targetsForNonFilteredTypes)) {
34
- return reject(new error_1.FirebaseError("Filters specified with colons (e.g. --only functions:func1,functions:func2) are only supported for functions, hosting, and firestore"));
35
+ return reject(new error_1.FirebaseError("Filters specified with colons (e.g. --only functions:func1,functions:func2) are only supported for functions, hosting, storage, and firestore"));
35
36
  }
36
37
  const targetsForFunctions = targetsForTypes(only, "functions");
37
38
  if (targetsForFunctions.length &&
@@ -19,6 +19,23 @@ function getRulesConfig(projectId, options) {
19
19
  if (dbConfig === undefined) {
20
20
  return [];
21
21
  }
22
+ const rc = options.rc;
23
+ let allDatabases = !options.only;
24
+ const onlyDatabases = new Set();
25
+ if (options.only) {
26
+ const split = options.only.split(",");
27
+ if (split.includes("database")) {
28
+ allDatabases = true;
29
+ }
30
+ else {
31
+ for (const value of split) {
32
+ if (value.startsWith("database:")) {
33
+ const target = value.split(":")[1];
34
+ onlyDatabases.add(target);
35
+ }
36
+ }
37
+ }
38
+ }
22
39
  if (!Array.isArray(dbConfig)) {
23
40
  if (dbConfig && dbConfig.rules) {
24
41
  utils.assertIsStringOrUndefined(options.instance);
@@ -31,22 +48,32 @@ function getRulesConfig(projectId, options) {
31
48
  }
32
49
  }
33
50
  const results = [];
34
- const rc = options.rc;
35
51
  for (const c of dbConfig) {
36
- if (c.target) {
37
- rc.requireTarget(projectId, "database", c.target);
38
- const instances = rc.target(projectId, "database", c.target);
39
- for (const i of instances) {
40
- results.push({ instance: i, rules: c.rules });
52
+ const { instance, target } = c;
53
+ if (target) {
54
+ if (allDatabases || onlyDatabases.has(target)) {
55
+ rc.requireTarget(projectId, "database", target);
56
+ const instances = rc.target(projectId, "database", target);
57
+ for (const i of instances) {
58
+ results.push({ instance: i, rules: c.rules });
59
+ }
60
+ onlyDatabases.delete(target);
41
61
  }
42
62
  }
43
- else if (c.instance) {
44
- results.push(c);
63
+ else if (instance) {
64
+ if (allDatabases) {
65
+ results.push(c);
66
+ }
45
67
  }
46
68
  else {
47
69
  throw new error_1.FirebaseError('Must supply either "target" or "instance" in database config');
48
70
  }
49
71
  }
72
+ if (!allDatabases && onlyDatabases.size !== 0) {
73
+ throw new error_1.FirebaseError(`Could not find configurations in firebase.json for the following database targets: ${[
74
+ ...onlyDatabases,
75
+ ].join(", ")}`);
76
+ }
50
77
  return results;
51
78
  }
52
79
  exports.getRulesConfig = getRulesConfig;
@@ -67,6 +67,10 @@ async function release(context, options, payload) {
67
67
  const allErrors = summary.results.filter((r) => r.error).map((r) => r.error);
68
68
  if (allErrors.length) {
69
69
  const opts = allErrors.length === 1 ? { original: allErrors[0] } : { children: allErrors };
70
+ logger_1.logger.debug("Functions deploy failed.");
71
+ for (const error of allErrors) {
72
+ logger_1.logger.debug(JSON.stringify(error, null, 2));
73
+ }
70
74
  throw new error_1.FirebaseError("There was an error deploying functions", Object.assign(Object.assign({}, opts), { exit: 2 }));
71
75
  }
72
76
  }
@@ -28,10 +28,12 @@ function buildFromV1Alpha1(yaml, project, region, runtime) {
28
28
  (0, parsing_1.requireKeys)("", manifest, "endpoints");
29
29
  (0, parsing_1.assertKeyTypes)("", manifest, {
30
30
  specVersion: "string",
31
+ params: "array",
31
32
  requiredAPIs: "array",
32
33
  endpoints: "object",
33
34
  });
34
35
  const bd = build.empty();
36
+ bd.params = manifest.params || [];
35
37
  bd.requiredAPIs = parseRequiredAPIs(manifest);
36
38
  for (const id of Object.keys(manifest.endpoints)) {
37
39
  const me = manifest.endpoints[id];
@@ -49,6 +51,7 @@ function backendFromV1Alpha1(yaml, project, region, runtime) {
49
51
  (0, parsing_1.requireKeys)("", manifest, "endpoints");
50
52
  (0, parsing_1.assertKeyTypes)("", manifest, {
51
53
  specVersion: "string",
54
+ params: "array",
52
55
  requiredAPIs: "array",
53
56
  endpoints: "object",
54
57
  });
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.convertConfig = void 0;
4
4
  const error_1 = require("../../error");
5
5
  const backend_1 = require("../functions/backend");
6
+ const backend = require("../functions/backend");
6
7
  function has(obj, k) {
7
8
  return obj[k] !== undefined;
8
9
  }
@@ -39,24 +40,66 @@ async function convertConfig(context, payload, config, finalize) {
39
40
  if (!config) {
40
41
  return out;
41
42
  }
42
- const endpointBeingDeployed = (serviceId, region = "us-central1") => {
43
- var _a;
43
+ const endpointFromBackend = (targetBackend, functionsEndpointInfo) => {
44
+ const backendsForId = backend.allEndpoints(targetBackend).filter((endpoint) => {
45
+ return endpoint.id === functionsEndpointInfo.serviceId;
46
+ });
47
+ const matchingBackends = backendsForId.filter((endpoint) => {
48
+ return ((!functionsEndpointInfo.region || endpoint.region === functionsEndpointInfo.region) &&
49
+ (!functionsEndpointInfo.platform || endpoint.platform === functionsEndpointInfo.platform));
50
+ });
51
+ if (matchingBackends.length > 1) {
52
+ throw new error_1.FirebaseError(`More than one backend found for function name: ${functionsEndpointInfo.serviceId}. If the function is deployed in multiple regions, you must specify a region.`);
53
+ }
54
+ if (matchingBackends.length === 1) {
55
+ const endpoint = matchingBackends[0];
56
+ if (endpoint && (0, backend_1.isHttpsTriggered)(endpoint)) {
57
+ return endpoint;
58
+ }
59
+ }
60
+ return;
61
+ };
62
+ const endpointBeingDeployed = (functionsEndpointInfo) => {
44
63
  for (const { wantBackend } of Object.values(payload.functions || {})) {
45
- const endpoint = (_a = wantBackend === null || wantBackend === void 0 ? void 0 : wantBackend.endpoints[region]) === null || _a === void 0 ? void 0 : _a[serviceId];
46
- if (endpoint && (0, backend_1.isHttpsTriggered)(endpoint) && endpoint.platform === "gcfv2")
64
+ if (!wantBackend) {
65
+ continue;
66
+ }
67
+ const endpoint = endpointFromBackend(wantBackend, functionsEndpointInfo);
68
+ if (endpoint) {
47
69
  return endpoint;
70
+ }
48
71
  }
49
- return undefined;
72
+ return;
50
73
  };
51
- const matchingEndpoint = async (serviceId, region = "us-central1") => {
52
- const pendingEndpoint = endpointBeingDeployed(serviceId, region);
74
+ const matchingEndpoint = async (functionsEndpointInfo) => {
75
+ const pendingEndpoint = endpointBeingDeployed(functionsEndpointInfo);
53
76
  if (pendingEndpoint)
54
77
  return pendingEndpoint;
55
78
  const backend = await (0, backend_1.existingBackend)(context);
56
79
  return (0, backend_1.allEndpoints)(backend).find((it) => (0, backend_1.isHttpsTriggered)(it) &&
57
- it.platform === "gcfv2" &&
58
- it.id === serviceId &&
59
- it.region === region);
80
+ it.id === functionsEndpointInfo.serviceId &&
81
+ (!functionsEndpointInfo.platform || it.platform === functionsEndpointInfo.platform) &&
82
+ (!functionsEndpointInfo.region || it.region === functionsEndpointInfo.region));
83
+ };
84
+ const findEndpointWithValidRegion = async (rewrite, context) => {
85
+ if ("function" in rewrite) {
86
+ const foundEndpointToBeDeployed = endpointBeingDeployed({
87
+ serviceId: rewrite.function,
88
+ region: rewrite.region,
89
+ });
90
+ if (foundEndpointToBeDeployed) {
91
+ return foundEndpointToBeDeployed;
92
+ }
93
+ const existingBackend = await backend.existingBackend(context);
94
+ const endpointAlreadyDeployed = endpointFromBackend(existingBackend, {
95
+ serviceId: rewrite.function,
96
+ region: rewrite.region,
97
+ });
98
+ if (endpointAlreadyDeployed) {
99
+ return endpointAlreadyDeployed;
100
+ }
101
+ }
102
+ return;
60
103
  };
61
104
  if (Array.isArray(config.rewrites)) {
62
105
  out.rewrites = [];
@@ -66,19 +109,30 @@ async function convertConfig(context, payload, config, finalize) {
66
109
  vRewrite.path = rewrite.destination;
67
110
  }
68
111
  else if ("function" in rewrite) {
69
- if (!finalize && endpointBeingDeployed(rewrite.function, rewrite.region))
112
+ if (!finalize &&
113
+ endpointBeingDeployed({
114
+ serviceId: rewrite.function,
115
+ platform: "gcfv2",
116
+ region: rewrite.region,
117
+ })) {
70
118
  continue;
71
- const endpoint = await matchingEndpoint(rewrite.function, rewrite.region);
119
+ }
120
+ const endpoint = await matchingEndpoint({
121
+ serviceId: rewrite.function,
122
+ platform: "gcfv2",
123
+ region: rewrite.region,
124
+ });
72
125
  if (endpoint) {
73
126
  vRewrite.run = { serviceId: endpoint.id, region: endpoint.region };
74
127
  }
75
128
  else {
76
129
  vRewrite.function = rewrite.function;
77
- if (rewrite.region) {
78
- vRewrite.functionRegion = rewrite.region;
130
+ const foundEndpoint = await findEndpointWithValidRegion(rewrite, context);
131
+ if (foundEndpoint) {
132
+ vRewrite.functionRegion = foundEndpoint.region;
79
133
  }
80
134
  else {
81
- vRewrite.functionRegion = "us-central1";
135
+ throw new error_1.FirebaseError(`Unable to find a valid endpoint for function ${vRewrite.function}`);
82
136
  }
83
137
  }
84
138
  }
@@ -86,8 +140,14 @@ async function convertConfig(context, payload, config, finalize) {
86
140
  vRewrite.dynamicLinks = rewrite.dynamicLinks;
87
141
  }
88
142
  else if ("run" in rewrite) {
89
- if (!finalize && endpointBeingDeployed(rewrite.run.serviceId, rewrite.run.region))
143
+ if (!finalize &&
144
+ endpointBeingDeployed({
145
+ serviceId: rewrite.run.serviceId,
146
+ platform: "gcfv2",
147
+ region: rewrite.run.region,
148
+ })) {
90
149
  continue;
150
+ }
91
151
  vRewrite.run = Object.assign({ region: "us-central1" }, rewrite.run);
92
152
  }
93
153
  out.rewrites.push(vRewrite);
@@ -52,7 +52,7 @@ const deploy = async function (targetNames, options, customContext = {}) {
52
52
  for (const targetName of targetNames) {
53
53
  const target = TARGETS[targetName];
54
54
  if (!target) {
55
- return Promise.reject(new error_1.FirebaseError((0, cli_color_1.bold)(targetName) + " is not a valid deploy target", { exit: 1 }));
55
+ return Promise.reject(new error_1.FirebaseError(`${(0, cli_color_1.bold)(targetName)} is not a valid deploy target`));
56
56
  }
57
57
  predeploys.push((0, lifecycleHooks_1.lifecycleHooks)(targetName, "predeploy"));
58
58
  prepares.push(target.prepare);
@@ -3,25 +3,48 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const _ = require("lodash");
4
4
  const gcp = require("../../gcp");
5
5
  const rulesDeploy_1 = require("../../rulesDeploy");
6
+ const error_1 = require("../../error");
6
7
  async function default_1(context, options) {
7
8
  let rulesConfig = options.config.get("storage");
8
9
  if (!rulesConfig) {
9
10
  return;
10
11
  }
12
+ const onlyTargets = new Set();
13
+ let allStorage = !options.only;
14
+ if (options.only) {
15
+ const split = options.only.split(",");
16
+ if (split.includes("storage")) {
17
+ allStorage = true;
18
+ }
19
+ else {
20
+ for (const value of split) {
21
+ if (value.startsWith("storage:")) {
22
+ onlyTargets.add(value.split(":")[1]);
23
+ }
24
+ }
25
+ }
26
+ }
11
27
  _.set(context, "storage.rules", rulesConfig);
12
28
  const rulesDeploy = new rulesDeploy_1.RulesDeploy(options, rulesDeploy_1.RulesetServiceType.FIREBASE_STORAGE);
13
29
  _.set(context, "storage.rulesDeploy", rulesDeploy);
14
- if (typeof rulesConfig === "object" && rulesConfig !== null) {
30
+ if (!Array.isArray(rulesConfig)) {
15
31
  const defaultBucket = await gcp.storage.getDefaultBucket(options.project);
16
32
  rulesConfig = [Object.assign(rulesConfig, { bucket: defaultBucket })];
17
33
  _.set(context, "storage.rules", rulesConfig);
18
34
  }
19
- rulesConfig.forEach((ruleConfig) => {
20
- if (ruleConfig.target) {
21
- options.rc.requireTarget(context.projectId, "storage", ruleConfig.target);
35
+ for (const ruleConfig of rulesConfig) {
36
+ const target = ruleConfig.target;
37
+ if (target) {
38
+ options.rc.requireTarget(context.projectId, "storage", target);
22
39
  }
23
- rulesDeploy.addFile(ruleConfig.rules);
24
- });
40
+ if (allStorage || onlyTargets.has(target)) {
41
+ rulesDeploy.addFile(ruleConfig.rules);
42
+ onlyTargets.delete(target);
43
+ }
44
+ }
45
+ if (!allStorage && onlyTargets.size !== 0) {
46
+ throw new error_1.FirebaseError(`Could not find rules for the following storage targets: ${[...onlyTargets].join(", ")}`);
47
+ }
25
48
  await rulesDeploy.compile();
26
49
  }
27
50
  exports.default = default_1;
@@ -301,7 +301,6 @@ async function startAll(options, showUI = true) {
301
301
  const extensionsBackends = await extensionEmulator.getExtensionBackends();
302
302
  const filteredExtensionsBackends = extensionEmulator.filterUnemulatedTriggers(options, extensionsBackends);
303
303
  emulatableBackends.push(...filteredExtensionsBackends);
304
- void (0, track_1.track)("Emulator Run", types_1.Emulators.EXTENSIONS);
305
304
  await startEmulator(extensionEmulator);
306
305
  }
307
306
  if (emulatableBackends.length) {
@@ -646,6 +646,9 @@ class FunctionsEmulator {
646
646
  envs.GCLOUD_PROJECT = this.args.projectId;
647
647
  envs.K_REVISION = "1";
648
648
  envs.PORT = "80";
649
+ if (trigger === null || trigger === void 0 ? void 0 : trigger.timeoutSeconds) {
650
+ envs.FUNCTIONS_EMULATOR_TIMEOUT_SECONDS = trigger.timeoutSeconds.toString();
651
+ }
649
652
  if (trigger) {
650
653
  const target = trigger.entryPoint;
651
654
  envs.FUNCTION_TARGET = target;
@@ -537,8 +537,8 @@ async function processHTTPS(trigger) {
537
537
  rejectEphemeralServer(err);
538
538
  }
539
539
  });
540
- logDebug(`Attempting to listen to port: ${process.env.PORT}`);
541
540
  server = ephemeralServer.listen(process.env.PORT);
541
+ logDebug(`Listening to port: ${process.env.PORT}`);
542
542
  server.on("error", rejectEphemeralServer);
543
543
  });
544
544
  }
@@ -51,6 +51,17 @@ const ESCAPE_SEQUENCES_TO_CHARACTERS = {
51
51
  "\\'": "'",
52
52
  '\\"': '"',
53
53
  };
54
+ const ALL_ESCAPE_SEQUENCES_RE = /\\[nrtv\\'"]/g;
55
+ const CHARACTERS_TO_ESCAPE_SEQUENCES = {
56
+ "\n": "\\n",
57
+ "\r": "\\r",
58
+ "\t": "\\t",
59
+ "\v": "\\v",
60
+ "\\": "\\\\",
61
+ "'": "\\'",
62
+ '"': '\\"',
63
+ };
64
+ const ALL_ESCAPABLE_CHARACTERS_RE = /[\n\r\t\v\\'"]/g;
54
65
  function parse(data) {
55
66
  const envs = {};
56
67
  const errors = [];
@@ -63,7 +74,7 @@ function parse(data) {
63
74
  if ((quotesMatch = /^(["'])(.*)\1$/ms.exec(v)) != null) {
64
75
  v = quotesMatch[2];
65
76
  if (quotesMatch[1] === '"') {
66
- v = v.replace(/\\[nrtv\\'"]/g, (match) => ESCAPE_SEQUENCES_TO_CHARACTERS[match]);
77
+ v = v.replace(ALL_ESCAPE_SEQUENCES_RE, (match) => ESCAPE_SEQUENCES_TO_CHARACTERS[match]);
67
78
  }
68
79
  }
69
80
  envs[k] = v;
@@ -146,9 +157,43 @@ function hasUserEnvs({ functionsSource, projectId, projectAlias, isEmulator, })
146
157
  }
147
158
  exports.hasUserEnvs = hasUserEnvs;
148
159
  function writeUserEnvs(toWrite, envOpts) {
149
- throw new error_1.FirebaseError("Persisting user-defined parameters to .env files is not yet implemented.");
160
+ if (Object.keys(toWrite).length === 0) {
161
+ return;
162
+ }
163
+ const { functionsSource, projectId, projectAlias, isEmulator } = envOpts;
164
+ let envFiles = findEnvfiles(functionsSource, projectId, projectAlias, isEmulator);
165
+ if (envFiles.length === 0) {
166
+ envFiles = [createEnvFile(envOpts)];
167
+ }
168
+ const currentEnvs = loadUserEnvs(envOpts);
169
+ for (const k of Object.keys(toWrite)) {
170
+ validateKey(k);
171
+ if (currentEnvs.hasOwnProperty(k)) {
172
+ throw new error_1.FirebaseError(`Attempted to write param-defined key ${k} to .env files, but it was already defined.`);
173
+ }
174
+ }
175
+ const mostSpecificEnv = path.join(functionsSource, envFiles[envFiles.length - 1]);
176
+ (0, utils_1.logBullet)(clc.cyan.bold("functions: ") + `Writing new parameter values to disk: ${mostSpecificEnv}`);
177
+ for (const k of Object.keys(toWrite)) {
178
+ fs.appendFileSync(mostSpecificEnv, formatUserEnvForWrite(k, toWrite[k]));
179
+ }
150
180
  }
151
181
  exports.writeUserEnvs = writeUserEnvs;
182
+ function createEnvFile(envOpts) {
183
+ const fileToWrite = envOpts.isEmulator
184
+ ? FUNCTIONS_EMULATOR_DOTENV
185
+ : `.env.${envOpts.projectAlias || envOpts.projectId}`;
186
+ logger_1.logger.debug(`Creating ${fileToWrite}...`);
187
+ fs.writeFileSync(path.join(envOpts.functionsSource, fileToWrite), "", { flag: "wx" });
188
+ return fileToWrite;
189
+ }
190
+ function formatUserEnvForWrite(key, value) {
191
+ const escapedValue = value.replace(ALL_ESCAPABLE_CHARACTERS_RE, (match) => CHARACTERS_TO_ESCAPE_SEQUENCES[match]);
192
+ if (escapedValue !== value) {
193
+ return `${key}="${escapedValue}"\n`;
194
+ }
195
+ return `${key}=${escapedValue}\n`;
196
+ }
152
197
  function loadUserEnvs({ functionsSource, projectId, projectAlias, isEmulator, }) {
153
198
  var _a;
154
199
  const envFiles = findEnvfiles(functionsSource, projectId, projectAlias, isEmulator);
package/lib/gcp/iam.js CHANGED
@@ -2,11 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.testIamPermissions = exports.testResourceIamPermissions = exports.getRole = exports.deleteServiceAccount = exports.createServiceAccountKey = exports.getServiceAccount = exports.createServiceAccount = void 0;
4
4
  const api_1 = require("../api");
5
- const lodash_1 = require("lodash");
6
5
  const logger_1 = require("../logger");
7
6
  const apiv2_1 = require("../apiv2");
8
- const API_VERSION = "v1";
9
- const apiClient = new apiv2_1.Client({ urlPrefix: api_1.iamOrigin, apiVersion: API_VERSION });
7
+ const apiClient = new apiv2_1.Client({ urlPrefix: api_1.iamOrigin, apiVersion: "v1" });
10
8
  async function createServiceAccount(projectId, accountId, description, displayName) {
11
9
  const response = await apiClient.post(`/projects/${projectId}/serviceAccounts`, {
12
10
  accountId,
@@ -31,8 +29,8 @@ async function createServiceAccountKey(projectId, serviceAccountName) {
31
29
  return response.body;
32
30
  }
33
31
  exports.createServiceAccountKey = createServiceAccountKey;
34
- function deleteServiceAccount(projectId, accountEmail) {
35
- return apiClient.delete(`/projects/${projectId}/serviceAccounts/${accountEmail}`, {
32
+ async function deleteServiceAccount(projectId, accountEmail) {
33
+ await apiClient.delete(`/projects/${projectId}/serviceAccounts/${accountEmail}`, {
36
34
  resolveOnHTTPError: true,
37
35
  });
38
36
  }
@@ -44,25 +42,30 @@ async function getRole(role) {
44
42
  return response.body;
45
43
  }
46
44
  exports.getRole = getRole;
47
- async function testResourceIamPermissions(origin, apiVersion, resourceName, permissions) {
45
+ async function testResourceIamPermissions(origin, apiVersion, resourceName, permissions, quotaUser = "") {
48
46
  const localClient = new apiv2_1.Client({ urlPrefix: origin, apiVersion });
49
47
  if (process.env.FIREBASE_SKIP_INFORMATIONAL_IAM) {
50
- logger_1.logger.debug("[iam] skipping informational check of permissions", JSON.stringify(permissions), "on resource", resourceName);
51
- return { allowed: permissions, missing: [], passed: true };
48
+ logger_1.logger.debug(`[iam] skipping informational check of permissions ${JSON.stringify(permissions)} on resource ${resourceName}`);
49
+ return { allowed: Array.from(permissions).sort(), missing: [], passed: true };
50
+ }
51
+ const headers = {};
52
+ if (quotaUser) {
53
+ headers["x-goog-quota-user"] = quotaUser;
54
+ }
55
+ const response = await localClient.post(`/${resourceName}:testIamPermissions`, { permissions }, { headers });
56
+ const allowed = new Set(response.body.permissions || []);
57
+ const missing = new Set(permissions);
58
+ for (const p of allowed) {
59
+ missing.delete(p);
52
60
  }
53
- const response = await localClient.post(`/${resourceName}:testIamPermissions`, {
54
- permissions,
55
- });
56
- const allowed = (response.body.permissions || []).sort();
57
- const missing = (0, lodash_1.difference)(permissions, allowed);
58
61
  return {
59
- allowed,
60
- missing,
61
- passed: missing.length === 0,
62
+ allowed: Array.from(allowed).sort(),
63
+ missing: Array.from(missing).sort(),
64
+ passed: missing.size === 0,
62
65
  };
63
66
  }
64
67
  exports.testResourceIamPermissions = testResourceIamPermissions;
65
68
  async function testIamPermissions(projectId, permissions) {
66
- return testResourceIamPermissions(api_1.resourceManagerOrigin, "v1", `projects/${projectId}`, permissions);
69
+ return testResourceIamPermissions(api_1.resourceManagerOrigin, "v1", `projects/${projectId}`, permissions, `projects/${projectId}`);
67
70
  }
68
71
  exports.testIamPermissions = testIamPermissions;
package/lib/rc.js CHANGED
@@ -136,15 +136,9 @@ class RC {
136
136
  requireTarget(project, type, name) {
137
137
  const target = this.target(project, type, name);
138
138
  if (!target.length) {
139
- throw new error_1.FirebaseError("Deploy target " +
140
- clc.bold(name) +
141
- " not configured for project " +
142
- clc.bold(project) +
143
- ". Configure with:\n\n firebase target:apply " +
144
- type +
145
- " " +
146
- name +
147
- " <resources...>");
139
+ throw new error_1.FirebaseError(`Deploy target ${clc.bold(name)} not configured for project ${clc.bold(project)}. Configure with:
140
+
141
+ firebase target:apply ${type} ${name} <resources...>`);
148
142
  }
149
143
  return target;
150
144
  }
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RulesDeploy = exports.RulesetServiceType = void 0;
4
4
  const _ = require("lodash");
5
5
  const clc = require("cli-color");
6
- const fs = require("fs");
6
+ const fs = require("fs-extra");
7
7
  const gcp = require("./gcp");
8
8
  const logger_1 = require("./logger");
9
9
  const error_1 = require("./error");
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "11.3.0",
3
+ "version": "11.4.0",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "firebase-tools",
9
- "version": "11.3.0",
9
+ "version": "11.4.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@google-cloud/pubsub": "^3.0.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "11.3.0",
3
+ "version": "11.4.0",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {
@@ -30,6 +30,7 @@
30
30
  "test:extensions-deploy": "bash ./scripts/extensions-deploy-tests/run.sh",
31
31
  "test:extensions-emulator": "bash ./scripts/extensions-emulator-tests/run.sh",
32
32
  "test:hosting": "bash ./scripts/hosting-tests/run.sh",
33
+ "test:hosting-rewrites": "bash ./scripts/hosting-tests/rewrites-tests/run.sh",
33
34
  "test:triggers-end-to-end": "bash ./scripts/triggers-end-to-end-tests/run.sh",
34
35
  "test:storage-deploy": "bash ./scripts/storage-deploy-tests/run.sh",
35
36
  "test:storage-emulator-integration": "bash ./scripts/storage-emulator-integration/run.sh"