firebase-tools 9.22.0 → 9.23.3

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 (40) hide show
  1. package/CHANGELOG.md +1 -3
  2. package/lib/commands/functions-delete.js +7 -1
  3. package/lib/commands/remoteconfig-get.js +6 -5
  4. package/lib/deploy/extensions/params.js +39 -0
  5. package/lib/deploy/extensions/planner.js +11 -12
  6. package/lib/deploy/extensions/prepare.js +9 -1
  7. package/lib/deploy/functions/checkIam.js +65 -4
  8. package/lib/deploy/functions/containerCleaner.js +68 -77
  9. package/lib/deploy/functions/eventTypes.js +10 -0
  10. package/lib/deploy/functions/prepare.js +12 -1
  11. package/lib/deploy/functions/release/fabricator.js +6 -1
  12. package/lib/deploy/functions/release/index.js +5 -1
  13. package/lib/deploy/functions/release/reporter.js +4 -0
  14. package/lib/deploy/functions/runtimes/node/parseTriggers.js +3 -9
  15. package/lib/deploy/functions/services/index.js +38 -0
  16. package/lib/deploy/functions/services/storage.js +43 -0
  17. package/lib/deploy/functions/triggerRegionHelper.js +6 -30
  18. package/lib/deploy/index.js +9 -1
  19. package/lib/emulator/auth/handlers.js +1 -1
  20. package/lib/emulator/auth/operations.js +27 -9
  21. package/lib/emulator/auth/widget_ui.js +17 -3
  22. package/lib/emulator/downloadableEmulators.js +5 -5
  23. package/lib/emulator/functionsEmulator.js +15 -1
  24. package/lib/emulator/functionsEmulatorShared.js +1 -0
  25. package/lib/emulator/pubsubEmulator.js +58 -45
  26. package/lib/emulator/storage/cloudFunctions.js +13 -6
  27. package/lib/ensureApiEnabled.js +11 -14
  28. package/lib/extensions/askUserForParam.js +32 -8
  29. package/lib/extensions/emulator/triggerHelper.js +1 -0
  30. package/lib/functions/env.js +2 -2
  31. package/lib/gcp/cloudfunctions.js +8 -6
  32. package/lib/gcp/cloudfunctionsv2.js +35 -3
  33. package/lib/gcp/docker.js +29 -1
  34. package/lib/gcp/location.js +44 -0
  35. package/lib/gcp/storage.js +48 -32
  36. package/lib/init/features/functions/index.js +3 -3
  37. package/lib/init/features/hosting/github.js +3 -0
  38. package/lib/init/features/project.js +2 -1
  39. package/lib/projectUtils.js +10 -1
  40. package/package.json +4 -4
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.serviceForEndpoint = exports.EVENT_SERVICE_MAPPING = exports.StorageService = exports.PubSubService = exports.NoOpService = void 0;
4
+ const backend = require("../backend");
5
+ const storage_1 = require("./storage");
6
+ const noop = () => Promise.resolve();
7
+ exports.NoOpService = {
8
+ name: "noop",
9
+ api: "",
10
+ requiredProjectBindings: undefined,
11
+ ensureTriggerRegion: noop,
12
+ };
13
+ exports.PubSubService = {
14
+ name: "pubsub",
15
+ api: "pubsub.googleapis.com",
16
+ requiredProjectBindings: undefined,
17
+ ensureTriggerRegion: noop,
18
+ };
19
+ exports.StorageService = {
20
+ name: "storage",
21
+ api: "storage.googleapis.com",
22
+ requiredProjectBindings: storage_1.obtainStorageBindings,
23
+ ensureTriggerRegion: storage_1.ensureStorageTriggerRegion,
24
+ };
25
+ exports.EVENT_SERVICE_MAPPING = {
26
+ "google.cloud.pubsub.topic.v1.messagePublished": exports.PubSubService,
27
+ "google.cloud.storage.object.v1.finalized": exports.StorageService,
28
+ "google.cloud.storage.object.v1.archived": exports.StorageService,
29
+ "google.cloud.storage.object.v1.deleted": exports.StorageService,
30
+ "google.cloud.storage.object.v1.metadataUpdated": exports.StorageService,
31
+ };
32
+ function serviceForEndpoint(endpoint) {
33
+ if (!backend.isEventTriggered(endpoint)) {
34
+ return exports.NoOpService;
35
+ }
36
+ return exports.EVENT_SERVICE_MAPPING[endpoint.eventTrigger.eventType] || exports.NoOpService;
37
+ }
38
+ exports.serviceForEndpoint = serviceForEndpoint;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ensureStorageTriggerRegion = exports.obtainStorageBindings = void 0;
4
+ const storage = require("../../../gcp/storage");
5
+ const logger_1 = require("../../../logger");
6
+ const error_1 = require("../../../error");
7
+ const location_1 = require("../../../gcp/location");
8
+ const PUBSUB_PUBLISHER_ROLE = "roles/pubsub.publisher";
9
+ async function obtainStorageBindings(projectId, existingPolicy) {
10
+ const storageResponse = await storage.getServiceAccount(projectId);
11
+ const storageServiceAgent = `serviceAccount:${storageResponse.email_address}`;
12
+ let pubsubBinding = existingPolicy.bindings.find((b) => b.role === PUBSUB_PUBLISHER_ROLE);
13
+ if (!pubsubBinding) {
14
+ pubsubBinding = {
15
+ role: PUBSUB_PUBLISHER_ROLE,
16
+ members: [],
17
+ };
18
+ }
19
+ if (!pubsubBinding.members.find((m) => m === storageServiceAgent)) {
20
+ pubsubBinding.members.push(storageServiceAgent);
21
+ }
22
+ return [pubsubBinding];
23
+ }
24
+ exports.obtainStorageBindings = obtainStorageBindings;
25
+ async function ensureStorageTriggerRegion(endpoint, eventTrigger) {
26
+ if (!eventTrigger.region) {
27
+ logger_1.logger.debug("Looking up bucket region for the storage event trigger");
28
+ try {
29
+ const bucket = await storage.getBucket(eventTrigger.eventFilters.bucket);
30
+ eventTrigger.region = bucket.location.toLowerCase();
31
+ logger_1.logger.debug("Setting the event trigger region to", eventTrigger.region, ".");
32
+ }
33
+ catch (err) {
34
+ throw new error_1.FirebaseError("Can't find the storage bucket region", { original: err });
35
+ }
36
+ }
37
+ if (endpoint.region !== eventTrigger.region &&
38
+ eventTrigger.region !== "us-central1" &&
39
+ !location_1.regionInLocation(endpoint.region, eventTrigger.region)) {
40
+ throw new error_1.FirebaseError(`A function in region ${endpoint.region} cannot listen to a bucket in region ${eventTrigger.region}`);
41
+ }
42
+ }
43
+ exports.ensureStorageTriggerRegion = ensureStorageTriggerRegion;
@@ -1,40 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.lookupMissingTriggerRegions = void 0;
3
+ exports.ensureTriggerRegions = void 0;
4
4
  const backend = require("./backend");
5
- const storage = require("../../gcp/storage");
6
- const error_1 = require("../../error");
7
- const logger_1 = require("../../logger");
8
- const noop = () => Promise.resolve();
9
- const LOOKUP_BY_EVENT_TYPE = {
10
- "google.cloud.pubsub.topic.v1.messagePublished": noop,
11
- "google.cloud.storage.object.v1.finalized": lookupBucketRegion,
12
- "google.cloud.storage.object.v1.archived": lookupBucketRegion,
13
- "google.cloud.storage.object.v1.deleted": lookupBucketRegion,
14
- "google.cloud.storage.object.v1.metadataUpdated": lookupBucketRegion,
15
- };
16
- async function lookupMissingTriggerRegions(want) {
5
+ const services_1 = require("./services");
6
+ async function ensureTriggerRegions(want) {
17
7
  const regionLookups = [];
18
8
  for (const ep of backend.allEndpoints(want)) {
19
- if (ep.platform === "gcfv1" || !backend.isEventTriggered(ep) || ep.eventTrigger.region) {
9
+ if (ep.platform === "gcfv1" || !backend.isEventTriggered(ep)) {
20
10
  continue;
21
11
  }
22
- const lookup = LOOKUP_BY_EVENT_TYPE[ep.eventTrigger.eventType];
23
- if (!lookup) {
24
- logger_1.logger.debug("Don't know how to look up trigger region for event type", ep.eventTrigger.eventType, ". Deploy will fail unless this event type is global");
25
- continue;
26
- }
27
- regionLookups.push(lookup(ep));
12
+ regionLookups.push(services_1.serviceForEndpoint(ep).ensureTriggerRegion(ep, ep.eventTrigger));
28
13
  }
29
14
  await Promise.all(regionLookups);
30
15
  }
31
- exports.lookupMissingTriggerRegions = lookupMissingTriggerRegions;
32
- async function lookupBucketRegion(endpoint) {
33
- try {
34
- const bucket = await storage.getBucket(endpoint.eventTrigger.eventFilters.bucket);
35
- endpoint.eventTrigger.region = bucket.location.toLowerCase();
36
- }
37
- catch (err) {
38
- throw new error_1.FirebaseError("Can't find the storage bucket region", { original: err });
39
- }
40
- }
16
+ exports.ensureTriggerRegions = ensureTriggerRegions;
@@ -38,6 +38,7 @@ var deploy = function (targetNames, options, customContext = {}) {
38
38
  var deploys = [];
39
39
  var releases = [];
40
40
  var postdeploys = [];
41
+ var startTime = Date.now();
41
42
  for (var i = 0; i < targetNames.length; i++) {
42
43
  var targetName = targetNames[i];
43
44
  var target = TARGETS[targetName];
@@ -75,8 +76,15 @@ var deploy = function (targetNames, options, customContext = {}) {
75
76
  })
76
77
  .then(function () {
77
78
  if (_.has(options, "config.notes.databaseRules")) {
78
- track("Rules Deploy", options.config.notes.databaseRules);
79
+ return track("Rules Deploy", options.config.notes.databaseRules);
79
80
  }
81
+ return;
82
+ })
83
+ .then(function () {
84
+ const duration = Date.now() - startTime;
85
+ return track("Product Deploy", [...targetNames].sort().join(","), duration);
86
+ })
87
+ .then(function () {
80
88
  logger.info();
81
89
  utils.logSuccess(clc.underline.bold("Deploy complete!"));
82
90
  logger.info();
@@ -178,7 +178,7 @@ function registerHandlers(app, getProjectStateByApiKey) {
178
178
  : `
179
179
  <span class="mdc-list-item__graphic material-icons" aria-hidden=true>person</span>`}
180
180
  <span class="mdc-list-item__text"><span class="mdc-list-item__primary-text">${info.displayName || "(No display name)"}</span>
181
- <span class="mdc-list-item__secondary-text fallback-secondary-text">${info.email || ""}</span>
181
+ <span class="mdc-list-item__secondary-text fallback-secondary-text" id="reuse-email">${info.email || ""}</span>
182
182
  </li>`)
183
183
  .join("\n");
184
184
  res.end(widget_ui_1.WIDGET_UI.replace(widget_ui_1.PROVIDERS_LIST_PLACEHOLDER, options));
@@ -1010,7 +1010,7 @@ function signInWithEmailLink(state, reqBody) {
1010
1010
  }
1011
1011
  }
1012
1012
  function signInWithIdp(state, reqBody) {
1013
- var _a, _b;
1013
+ var _a, _b, _c;
1014
1014
  errors_1.assert(!state.disableAuth, "PROJECT_DISABLED");
1015
1015
  errors_1.assert(state.usageMode !== state_1.UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION");
1016
1016
  if (reqBody.returnRefreshToken) {
@@ -1041,7 +1041,16 @@ function signInWithIdp(state, reqBody) {
1041
1041
  throw new errors_1.NotImplementedError("The Auth Emulator only supports sign-in with credentials (id_token required).");
1042
1042
  }
1043
1043
  }
1044
- let { response, rawId } = fakeFetchUserInfoFromIdp(providerId, claims);
1044
+ let samlResponse;
1045
+ let signInAttributes = undefined;
1046
+ if (normalizedUri.searchParams.get("SAMLResponse")) {
1047
+ samlResponse = JSON.parse(normalizedUri.searchParams.get("SAMLResponse"));
1048
+ signInAttributes = (_b = samlResponse.assertion) === null || _b === void 0 ? void 0 : _b.attributeStatements;
1049
+ errors_1.assert(samlResponse.assertion, "INVALID_IDP_RESPONSE ((Missing assertion in SAMLResponse.))");
1050
+ errors_1.assert(samlResponse.assertion.subject, "INVALID_IDP_RESPONSE ((Missing assertion.subject in SAMLResponse.))");
1051
+ errors_1.assert(samlResponse.assertion.subject.nameId, "INVALID_IDP_RESPONSE ((Missing assertion.subject.nameId in SAMLResponse.))");
1052
+ }
1053
+ let { response, rawId } = fakeFetchUserInfoFromIdp(providerId, claims, samlResponse);
1045
1054
  response.oauthAccessToken =
1046
1055
  oauthAccessToken || `FirebaseAuthEmulatorFakeAccessToken_${providerId}`;
1047
1056
  response.oauthIdToken = oauthIdToken;
@@ -1101,12 +1110,12 @@ function signInWithIdp(state, reqBody) {
1101
1110
  if (state instanceof state_1.TenantProjectState) {
1102
1111
  response.tenantId = state.tenantId;
1103
1112
  }
1104
- if ((state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") && ((_b = user.mfaInfo) === null || _b === void 0 ? void 0 : _b.length)) {
1113
+ if ((state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") && ((_c = user.mfaInfo) === null || _c === void 0 ? void 0 : _c.length)) {
1105
1114
  return Object.assign(Object.assign({}, response), mfaPending(state, user, providerId));
1106
1115
  }
1107
1116
  else {
1108
1117
  user = state.updateUserByLocalId(user.localId, { lastLoginAt: Date.now().toString() });
1109
- return Object.assign(Object.assign({}, response), issueTokens(state, user, providerId));
1118
+ return Object.assign(Object.assign({}, response), issueTokens(state, user, providerId, { signInAttributes }));
1110
1119
  }
1111
1120
  }
1112
1121
  function signInWithPassword(state, reqBody) {
@@ -1391,7 +1400,7 @@ function redactPasswordHash(user) {
1391
1400
  function hashPassword(password, salt) {
1392
1401
  return `fakeHash:salt=${salt}:password=${password}`;
1393
1402
  }
1394
- function issueTokens(state, user, signInProvider, { extraClaims, secondFactor, } = {}) {
1403
+ function issueTokens(state, user, signInProvider, { extraClaims, secondFactor, signInAttributes, } = {}) {
1395
1404
  user = state.updateUserByLocalId(user.localId, { lastRefreshAt: new Date().toISOString() });
1396
1405
  const usageMode = state.usageMode === state_1.UsageMode.PASSTHROUGH ? "passthrough" : undefined;
1397
1406
  const tenantId = state instanceof state_1.TenantProjectState ? state.tenantId : undefined;
@@ -1404,6 +1413,7 @@ function issueTokens(state, user, signInProvider, { extraClaims, secondFactor, }
1404
1413
  secondFactor,
1405
1414
  usageMode,
1406
1415
  tenantId,
1416
+ signInAttributes,
1407
1417
  });
1408
1418
  const refreshToken = state.usageMode === state_1.UsageMode.DEFAULT
1409
1419
  ? state.createRefreshTokenFor(user, signInProvider, {
@@ -1435,7 +1445,7 @@ function parseIdToken(state, idToken) {
1435
1445
  const signInProvider = decoded.payload.firebase.sign_in_provider;
1436
1446
  return { user, signInProvider, payload: decoded.payload };
1437
1447
  }
1438
- function generateJwt(user, { projectId, signInProvider, expiresInSeconds, extraClaims = {}, secondFactor, usageMode, tenantId, }) {
1448
+ function generateJwt(user, { projectId, signInProvider, expiresInSeconds, extraClaims = {}, secondFactor, usageMode, tenantId, signInAttributes, }) {
1439
1449
  const identities = {};
1440
1450
  if (user.email) {
1441
1451
  identities["email"] = [user.email];
@@ -1459,6 +1469,7 @@ function generateJwt(user, { projectId, signInProvider, expiresInSeconds, extraC
1459
1469
  sign_in_second_factor: secondFactor === null || secondFactor === void 0 ? void 0 : secondFactor.provider,
1460
1470
  usage_mode: usageMode,
1461
1471
  tenant: tenantId,
1472
+ sign_in_attributes: signInAttributes,
1462
1473
  } });
1463
1474
  const jwtStr = jsonwebtoken_1.sign(customPayloadFields, "", {
1464
1475
  algorithm: "none",
@@ -1603,7 +1614,8 @@ function parseClaims(idTokenOrJsonClaims) {
1603
1614
  errors_1.assert(typeof claims.sub === "string", 'INVALID_IDP_RESPONSE : ((The "sub" field must be a string.))');
1604
1615
  return claims;
1605
1616
  }
1606
- function fakeFetchUserInfoFromIdp(providerId, claims) {
1617
+ function fakeFetchUserInfoFromIdp(providerId, claims, samlResponse) {
1618
+ var _a, _b, _c, _d, _e;
1607
1619
  const rawId = claims.sub;
1608
1620
  const email = claims.email ? utils_1.canonicalizeEmailAddress(claims.email) : undefined;
1609
1621
  const emailVerified = !!claims.email_verified;
@@ -1620,7 +1632,7 @@ function fakeFetchUserInfoFromIdp(providerId, claims) {
1620
1632
  emailVerified,
1621
1633
  photoUrl,
1622
1634
  };
1623
- let federatedId;
1635
+ let federatedId = rawId;
1624
1636
  switch (providerId) {
1625
1637
  case "google.com": {
1626
1638
  federatedId = `https://accounts.google.com/${rawId}`;
@@ -1643,8 +1655,14 @@ function fakeFetchUserInfoFromIdp(providerId, claims) {
1643
1655
  });
1644
1656
  break;
1645
1657
  }
1658
+ case (_a = providerId.match(/^saml\./)) === null || _a === void 0 ? void 0 : _a.input:
1659
+ const nameId = (_c = (_b = samlResponse === null || samlResponse === void 0 ? void 0 : samlResponse.assertion) === null || _b === void 0 ? void 0 : _b.subject) === null || _c === void 0 ? void 0 : _c.nameId;
1660
+ response.email = nameId && utils_1.isValidEmailAddress(nameId) ? nameId : response.email;
1661
+ response.emailVerified = true;
1662
+ response.rawUserInfo = JSON.stringify((_d = samlResponse === null || samlResponse === void 0 ? void 0 : samlResponse.assertion) === null || _d === void 0 ? void 0 : _d.attributeStatements);
1663
+ break;
1664
+ case (_e = providerId.match(/^oidc\./)) === null || _e === void 0 ? void 0 : _e.input:
1646
1665
  default:
1647
- federatedId = rawId;
1648
1666
  response.rawUserInfo = JSON.stringify(claims);
1649
1667
  break;
1650
1668
  }
@@ -33,6 +33,7 @@ var firebaseAppId = query.get('appId');
33
33
  var apn = query.get('apn');
34
34
  var ibi = query.get('ibi');
35
35
  var appIdentifier = apn || ibi;
36
+ var isSamlProvider = !!providerId.match(/^saml\./);
36
37
  assert(
37
38
  appName || clientId || firebaseAppId || appIdentifier,
38
39
  'Missing one of appName / clientId / appId / apn / ibi query params.'
@@ -157,21 +158,34 @@ var reuseAccountEls = document.querySelectorAll('.js-reuse-account');
157
158
  if (reuseAccountEls.length) {
158
159
  [].forEach.call(reuseAccountEls, function (el) {
159
160
  var urlEncodedIdToken = el.dataset.idToken;
161
+ const decoded = JSON.parse(decodeURIComponent(urlEncodedIdToken));
160
162
  el.addEventListener('click', function (e) {
161
163
  e.preventDefault();
162
- finishWithUser(urlEncodedIdToken);
164
+ finishWithUser(urlEncodedIdToken, decoded.email);
163
165
  });
164
166
  });
165
167
  } else {
166
168
  document.querySelector('.js-accounts-help-text').textContent = "No " + formattedProviderId + " accounts exist in the Auth Emulator.";
167
169
  }
168
170
 
169
- function finishWithUser(urlEncodedIdToken) {
171
+ function finishWithUser(urlEncodedIdToken, email) {
170
172
  // Use widget URL, but replace all query parameters (no apiKey etc.).
171
173
  var url = window.location.href.split('?')[0];
172
174
  // Avoid URLSearchParams for browser compatibility.
173
175
  url += '?providerId=' + encodeURIComponent(providerId);
174
176
  url += '&id_token=' + urlEncodedIdToken;
177
+
178
+ // Save reasonable defaults for SAML providers
179
+ if (isSamlProvider) {
180
+ url += '&SAMLResponse=' + encodeURIComponent(JSON.stringify({
181
+ assertion: {
182
+ subject: {
183
+ nameId: email,
184
+ },
185
+ },
186
+ }));
187
+ }
188
+
175
189
  saveAuthEvent({
176
190
  type: authType,
177
191
  eventId: eventId,
@@ -220,7 +234,7 @@ document.getElementById('main-form').addEventListener('submit', function(e) {
220
234
  if (screenName) claims.screenName = screenName;
221
235
  if (photoUrl) claims.photoUrl = photoUrl;
222
236
 
223
- finishWithUser(createFakeClaims(claims));
237
+ finishWithUser(createFakeClaims(claims), claims.email);
224
238
  }
225
239
  });
226
240
 
@@ -17,13 +17,13 @@ const EMULATOR_INSTANCE_KILL_TIMEOUT = 4000;
17
17
  const CACHE_DIR = process.env.FIREBASE_EMULATORS_PATH || path.join(os.homedir(), ".cache", "firebase", "emulators");
18
18
  exports.DownloadDetails = {
19
19
  database: {
20
- downloadPath: path.join(CACHE_DIR, "firebase-database-emulator-v4.7.2.jar"),
21
- version: "4.7.2",
20
+ downloadPath: path.join(CACHE_DIR, "firebase-database-emulator-v4.7.3.jar"),
21
+ version: "4.7.3",
22
22
  opts: {
23
23
  cacheDir: CACHE_DIR,
24
- remoteUrl: "https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v4.7.2.jar",
25
- expectedSize: 28910604,
26
- expectedChecksum: "264e5df0c0661c064ef7dc9ce8179aba",
24
+ remoteUrl: "https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v4.7.3.jar",
25
+ expectedSize: 28862098,
26
+ expectedChecksum: "8f696f24ee89c937a789498a0c0e4899",
27
27
  namePrefix: "firebase-database-emulator",
28
28
  },
29
29
  },
@@ -29,6 +29,7 @@ const defaultCredentials_1 = require("../defaultCredentials");
29
29
  const adminSdkConfig_1 = require("./adminSdkConfig");
30
30
  const functionsEnv = require("../functions/env");
31
31
  const types_2 = require("./events/types");
32
+ const validate_1 = require("../deploy/functions/validate");
32
33
  const EVENT_INVOKE = "functions:invoke";
33
34
  const DATABASE_PATH_PATTERN = new RegExp("^projects/[^/]+/instances/([^/]+)/refs(/.*)$");
34
35
  class FunctionsEmulator {
@@ -138,6 +139,9 @@ class FunctionsEmulator {
138
139
  else {
139
140
  triggerKey = `${this.args.projectId}:${proto.eventType}`;
140
141
  }
142
+ if (proto.data.bucket) {
143
+ triggerKey += `:${proto.data.bucket}`;
144
+ }
141
145
  const triggers = this.multicastTriggers[triggerKey] || [];
142
146
  triggers.forEach((triggerId) => {
143
147
  this.workQueue.submit(() => {
@@ -247,6 +251,13 @@ class FunctionsEmulator {
247
251
  return !anyEnabledMatch;
248
252
  });
249
253
  for (const definition of toSetup) {
254
+ try {
255
+ validate_1.functionIdsAreValid([definition]);
256
+ }
257
+ catch (e) {
258
+ this.logger.logLabeled("WARN", `functions[${definition.id}]`, `Invalid function id: ${e.message}`);
259
+ continue;
260
+ }
250
261
  let added = false;
251
262
  let url = undefined;
252
263
  if (definition.httpsTrigger) {
@@ -396,7 +407,10 @@ class FunctionsEmulator {
396
407
  }
397
408
  addStorageTrigger(projectId, key, eventTrigger) {
398
409
  logger_1.logger.debug(`addStorageTrigger`, JSON.stringify({ eventTrigger }));
399
- const eventTriggerId = `${projectId}:${eventTrigger.eventType}`;
410
+ const bucket = eventTrigger.resource.startsWith("projects/_/buckets/")
411
+ ? eventTrigger.resource.split("/")[3]
412
+ : eventTrigger.resource;
413
+ const eventTriggerId = `${projectId}:${eventTrigger.eventType}:${bucket}`;
400
414
  const triggers = this.multicastTriggers[eventTriggerId] || [];
401
415
  triggers.push(key);
402
416
  this.multicastTriggers[eventTriggerId] = triggers;
@@ -55,6 +55,7 @@ function emulatedFunctionsByRegion(definitions) {
55
55
  defDeepCopy.regions = [region];
56
56
  defDeepCopy.region = region;
57
57
  defDeepCopy.id = `${region}-${defDeepCopy.name}`;
58
+ defDeepCopy.platform = defDeepCopy.platform || "gcfv1";
58
59
  regionDefinitions.push(defDeepCopy);
59
60
  }
60
61
  }
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PubsubEmulator = void 0;
4
4
  const uuid = require("uuid");
5
5
  const pubsub_1 = require("@google-cloud/pubsub");
6
- const api = require("../api");
7
6
  const downloadableEmulators = require("./downloadableEmulators");
7
+ const apiv2_1 = require("../apiv2");
8
8
  const emulatorLogger_1 = require("./emulatorLogger");
9
9
  const types_1 = require("../emulator/types");
10
10
  const constants_1 = require("./constants");
@@ -87,47 +87,54 @@ class PubsubEmulator {
87
87
  this.triggersForTopic.set(topicName, triggers);
88
88
  this.subscriptionForTopic.set(topicName, sub);
89
89
  }
90
- getRequestOptions(topic, message, signatureType) {
91
- const baseOpts = {
92
- origin: `http://${registry_1.EmulatorRegistry.getInfoHostString(registry_1.EmulatorRegistry.get(types_1.Emulators.FUNCTIONS).getInfo())}`,
93
- };
94
- if (signatureType === "event") {
95
- return Object.assign(Object.assign({}, baseOpts), { data: {
96
- context: {
97
- eventId: uuid.v4(),
98
- resource: {
99
- service: "pubsub.googleapis.com",
100
- name: `projects/${this.args.projectId}/topics/${topic}`,
101
- },
102
- eventType: "google.pubsub.topic.publish",
103
- timestamp: message.publishTime.toISOString(),
104
- },
105
- data: {
106
- data: message.data,
107
- attributes: message.attributes,
108
- },
109
- } });
90
+ ensureFunctionsClient() {
91
+ if (this.client != undefined)
92
+ return;
93
+ const funcEmulator = registry_1.EmulatorRegistry.get(types_1.Emulators.FUNCTIONS);
94
+ if (!funcEmulator) {
95
+ throw new error_1.FirebaseError(`Attempted to execute pubsub trigger but could not find the Functions emulator`);
110
96
  }
111
- else if (signatureType === "cloudevent") {
112
- const data = {
113
- message: {
114
- messageId: message.id,
115
- publishTime: message.publishTime,
116
- attributes: message.attributes,
117
- orderingKey: message.orderingKey,
118
- data: message.data.toString("base64"),
97
+ this.client = new apiv2_1.Client({
98
+ urlPrefix: `http://${registry_1.EmulatorRegistry.getInfoHostString(funcEmulator.getInfo())}`,
99
+ auth: false,
100
+ });
101
+ }
102
+ createLegacyEventRequestBody(topic, message) {
103
+ return {
104
+ context: {
105
+ eventId: uuid.v4(),
106
+ resource: {
107
+ service: "pubsub.googleapis.com",
108
+ name: `projects/${this.args.projectId}/topics/${topic}`,
119
109
  },
120
- subscription: this.subscriptionForTopic.get(topic).name,
121
- };
122
- const ce = {
123
- specVersion: 1,
124
- type: "google.cloud.pubsub.topic.v1.messagePublished",
125
- source: `//pubsub.googleapis.com/projects/${this.args.projectId}/topics/${topic}`,
126
- data,
127
- };
128
- return Object.assign(Object.assign({}, baseOpts), { headers: { "Content-Type": "application/cloudevents+json; charset=UTF-8" }, data: ce });
129
- }
130
- throw new error_1.FirebaseError(`Unsupported trigger signature: ${signatureType}`);
110
+ eventType: "google.pubsub.topic.publish",
111
+ timestamp: message.publishTime.toISOString(),
112
+ },
113
+ data: {
114
+ data: message.data,
115
+ attributes: message.attributes,
116
+ },
117
+ };
118
+ }
119
+ createCloudEventRequestBody(topic, message) {
120
+ const data = {
121
+ message: {
122
+ messageId: message.id,
123
+ publishTime: message.publishTime,
124
+ attributes: message.attributes,
125
+ orderingKey: message.orderingKey,
126
+ data: message.data.toString("base64"),
127
+ },
128
+ subscription: this.subscriptionForTopic.get(topic).name,
129
+ };
130
+ return {
131
+ specversion: "1",
132
+ id: uuid.v4(),
133
+ time: message.publishTime.toISOString(),
134
+ type: "google.cloud.pubsub.topic.v1.messagePublished",
135
+ source: `//pubsub.googleapis.com/projects/${this.args.projectId}/topics/${topic}`,
136
+ data,
137
+ };
131
138
  }
132
139
  async onMessage(topicName, message) {
133
140
  this.logger.logLabeled("DEBUG", "pubsub", `onMessage(${topicName}, ${message.id})`);
@@ -135,14 +142,20 @@ class PubsubEmulator {
135
142
  if (!triggers || triggers.length === 0) {
136
143
  throw new error_1.FirebaseError(`No trigger for topic: ${topicName}`);
137
144
  }
138
- if (!registry_1.EmulatorRegistry.get(types_1.Emulators.FUNCTIONS)) {
139
- throw new error_1.FirebaseError(`Attempted to execute pubsub trigger for topic ${topicName} but could not find Functions emulator`);
140
- }
141
145
  this.logger.logLabeled("DEBUG", "pubsub", `Executing ${triggers.length} matching triggers (${JSON.stringify(triggers.map((t) => t.triggerKey))})`);
146
+ this.ensureFunctionsClient();
142
147
  for (const { triggerKey, signatureType } of triggers) {
143
- const reqOpts = this.getRequestOptions(topicName, message, signatureType);
144
148
  try {
145
- await api.request("POST", `/functions/projects/${this.args.projectId}/triggers/${triggerKey}`, reqOpts);
149
+ const path = `/functions/projects/${this.args.projectId}/triggers/${triggerKey}`;
150
+ if (signatureType === "event") {
151
+ await this.client.post(path, this.createLegacyEventRequestBody(topicName, message));
152
+ }
153
+ else if (signatureType === "cloudevent") {
154
+ await this.client.post(path, this.createCloudEventRequestBody(topicName, message), { headers: { "Content-Type": "application/cloudevents+json; charset=UTF-8" } });
155
+ }
156
+ else {
157
+ throw new error_1.FirebaseError(`Unsupported trigger signature: ${signatureType}`);
158
+ }
146
159
  }
147
160
  catch (e) {
148
161
  this.logger.logLabeled("DEBUG", "pubsub", e);
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.StorageCloudFunctions = void 0;
4
+ const uuid = require("uuid");
4
5
  const registry_1 = require("../registry");
5
6
  const types_1 = require("../types");
6
7
  const emulatorLogger_1 = require("../emulatorLogger");
@@ -57,7 +58,7 @@ class StorageCloudFunctions {
57
58
  }
58
59
  createLegacyEventRequestBody(action, objectMetadataPayload) {
59
60
  const timestamp = new Date();
60
- return JSON.stringify({
61
+ return {
61
62
  eventId: `${timestamp.getTime()}`,
62
63
  timestamp: metadata_1.toSerializedDate(timestamp),
63
64
  eventType: `google.storage.object.${action}`,
@@ -67,20 +68,26 @@ class StorageCloudFunctions {
67
68
  type: "storage#object",
68
69
  },
69
70
  data: objectMetadataPayload,
70
- });
71
+ };
71
72
  }
72
73
  createCloudEventRequestBody(action, objectMetadataPayload) {
73
74
  const ceAction = STORAGE_V2_ACTION_MAP[action];
74
75
  if (!ceAction) {
75
- throw new Error("Action is not definied as a CloudEvents action");
76
+ throw new Error("Action is not defined as a CloudEvents action");
76
77
  }
77
78
  const data = objectMetadataPayload;
78
- return JSON.stringify({
79
- specVersion: 1,
79
+ let time = new Date().toISOString();
80
+ if (data.updated) {
81
+ time = typeof data.updated === "string" ? data.updated : data.updated.toISOString();
82
+ }
83
+ return {
84
+ specversion: "1",
85
+ id: uuid.v4(),
80
86
  type: `google.cloud.storage.object.v1.${ceAction}`,
81
87
  source: `//storage.googleapis.com/projects/_/buckets/${objectMetadataPayload.bucket}/objects/${objectMetadataPayload.name}`,
88
+ time,
82
89
  data,
83
- });
90
+ };
84
91
  }
85
92
  }
86
93
  exports.StorageCloudFunctions = StorageCloudFunctions;
@@ -1,22 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ensure = exports.enable = exports.check = exports.POLL_SETTINGS = void 0;
4
- const _ = require("lodash");
3
+ exports.ensure = exports.check = exports.POLL_SETTINGS = void 0;
5
4
  const cli_color_1 = require("cli-color");
6
5
  const track = require("./track");
7
- const api = require("./api");
6
+ const api_1 = require("./api");
7
+ const apiv2_1 = require("./apiv2");
8
8
  const utils = require("./utils");
9
9
  const error_1 = require("./error");
10
10
  exports.POLL_SETTINGS = {
11
11
  pollInterval: 10000,
12
12
  pollsBeforeRetry: 12,
13
13
  };
14
+ const apiClient = new apiv2_1.Client({
15
+ urlPrefix: api_1.serviceUsageOrigin,
16
+ apiVersion: "v1",
17
+ });
14
18
  async function check(projectId, apiName, prefix, silent = false) {
15
- const response = await api.request("GET", `/v1/projects/${projectId}/services/${apiName}`, {
16
- auth: true,
17
- origin: api.serviceUsageOrigin,
18
- });
19
- const isEnabled = _.get(response.body, "state") === "ENABLED";
19
+ const res = await apiClient.get(`/projects/${projectId}/services/${apiName}`);
20
+ const isEnabled = res.body.state === "ENABLED";
20
21
  if (isEnabled && !silent) {
21
22
  utils.logLabeledSuccess(prefix, `required API ${cli_color_1.bold(apiName)} is enabled`);
22
23
  }
@@ -25,10 +26,7 @@ async function check(projectId, apiName, prefix, silent = false) {
25
26
  exports.check = check;
26
27
  async function enable(projectId, apiName) {
27
28
  try {
28
- await api.request("POST", `/v1/projects/${projectId}/services/${apiName}:enable`, {
29
- auth: true,
30
- origin: api.serviceUsageOrigin,
31
- });
29
+ await apiClient.post(`/projects/${projectId}/services/${apiName}:enable`);
32
30
  }
33
31
  catch (err) {
34
32
  if (error_1.isBillingError(err)) {
@@ -39,7 +37,6 @@ https://console.firebase.google.com/project/${projectId}/usage/details`);
39
37
  throw err;
40
38
  }
41
39
  }
42
- exports.enable = enable;
43
40
  async function pollCheckEnabled(projectId, apiName, prefix, silent, enablementRetries, pollRetries = 0) {
44
41
  if (pollRetries > exports.POLL_SETTINGS.pollsBeforeRetry) {
45
42
  return enableApiWithRetries(projectId, apiName, prefix, silent, enablementRetries + 1);
@@ -49,7 +46,7 @@ async function pollCheckEnabled(projectId, apiName, prefix, silent, enablementRe
49
46
  });
50
47
  const isEnabled = await check(projectId, apiName, prefix, silent);
51
48
  if (isEnabled) {
52
- track("api_enabled", apiName);
49
+ void track("api_enabled", apiName);
53
50
  return;
54
51
  }
55
52
  if (!silent) {