firebase-tools 9.22.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +5 -3
  2. package/lib/api.js +0 -1
  3. package/lib/commands/database-remove.js +2 -2
  4. package/lib/commands/database-set.js +2 -2
  5. package/lib/commands/database-update.js +2 -2
  6. package/lib/commands/firestore-delete.js +2 -2
  7. package/lib/commands/functions-delete.js +7 -1
  8. package/lib/commands/hosting-disable.js +3 -3
  9. package/lib/commands/remoteconfig-get.js +6 -5
  10. package/lib/deploy/extensions/params.js +39 -0
  11. package/lib/deploy/extensions/planner.js +11 -12
  12. package/lib/deploy/extensions/prepare.js +9 -1
  13. package/lib/deploy/functions/checkIam.js +65 -4
  14. package/lib/deploy/functions/containerCleaner.js +68 -77
  15. package/lib/deploy/functions/deploy.js +6 -7
  16. package/lib/deploy/functions/eventTypes.js +10 -0
  17. package/lib/deploy/functions/prepare.js +12 -1
  18. package/lib/deploy/functions/release/fabricator.js +6 -1
  19. package/lib/deploy/functions/release/index.js +6 -2
  20. package/lib/deploy/functions/release/reporter.js +4 -0
  21. package/lib/deploy/functions/runtimes/node/parseRuntimeAndValidateSDK.js +1 -1
  22. package/lib/deploy/functions/runtimes/node/parseTriggers.js +3 -9
  23. package/lib/deploy/functions/services/index.js +38 -0
  24. package/lib/deploy/functions/services/storage.js +43 -0
  25. package/lib/deploy/functions/triggerRegionHelper.js +6 -30
  26. package/lib/deploy/index.js +9 -1
  27. package/lib/emulator/auth/handlers.js +1 -1
  28. package/lib/emulator/auth/operations.js +27 -9
  29. package/lib/emulator/auth/widget_ui.js +17 -3
  30. package/lib/emulator/functionsEmulator.js +15 -1
  31. package/lib/emulator/functionsEmulatorRuntime.js +1 -1
  32. package/lib/emulator/functionsEmulatorShared.js +1 -0
  33. package/lib/emulator/pubsubEmulator.js +58 -45
  34. package/lib/emulator/storage/cloudFunctions.js +13 -6
  35. package/lib/ensureApiEnabled.js +11 -14
  36. package/lib/extensions/askUserForParam.js +32 -8
  37. package/lib/extensions/emulator/triggerHelper.js +1 -0
  38. package/lib/functions/env.js +2 -2
  39. package/lib/gcp/cloudfunctions.js +8 -6
  40. package/lib/gcp/cloudfunctionsv2.js +35 -3
  41. package/lib/gcp/docker.js +29 -1
  42. package/lib/gcp/location.js +44 -0
  43. package/lib/gcp/storage.js +48 -32
  44. package/lib/init/features/functions/index.js +3 -3
  45. package/lib/init/features/hosting/github.js +3 -0
  46. package/lib/init/features/project.js +2 -1
  47. package/lib/projectUtils.js +10 -1
  48. package/package.json +5 -5
  49. package/templates/init/functions/javascript/package.lint.json +1 -1
  50. package/templates/init/functions/javascript/package.nolint.json +1 -1
  51. package/templates/init/functions/typescript/package.lint.json +1 -1
  52. package/templates/init/functions/typescript/package.nolint.json +1 -1
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PUBSUB_V2_EVENT = exports.STORAGE_V2_EVENTS = void 0;
4
+ exports.STORAGE_V2_EVENTS = [
5
+ "google.cloud.storage.object.v1.finalized",
6
+ "google.cloud.storage.object.v1.archived",
7
+ "google.cloud.storage.object.v1.deleted",
8
+ "google.cloud.storage.object.v1.metadataUpdated",
9
+ ];
10
+ exports.PUBSUB_V2_EVENT = "google.cloud.pubsub.topic.v1.messagePublished";
@@ -19,12 +19,20 @@ const validate = require("./validate");
19
19
  const utils = require("../../utils");
20
20
  const logger_1 = require("../../logger");
21
21
  const triggerRegionHelper_1 = require("./triggerRegionHelper");
22
+ const checkIam_1 = require("./checkIam");
22
23
  function hasUserConfig(config) {
23
24
  return Object.keys(config).length > 1;
24
25
  }
25
26
  function hasDotenv(opts) {
26
27
  return previews_1.previews.dotenv && functionsEnv.hasUserEnvs(opts);
27
28
  }
29
+ async function maybeEnableAR(projectId) {
30
+ if (previews_1.previews.artifactregistry) {
31
+ return ensureApiEnabled.check(projectId, "artifactregistry.googleapis.com", "functions", true);
32
+ }
33
+ await ensureApiEnabled.ensure(projectId, "artifactregistry.googleapis.com", "functions");
34
+ return true;
35
+ }
28
36
  async function prepare(context, options, payload) {
29
37
  if (!options.config.src.functions) {
30
38
  return;
@@ -39,8 +47,10 @@ async function prepare(context, options, payload) {
39
47
  ensureApiEnabled.ensure(projectId, "cloudfunctions.googleapis.com", "functions"),
40
48
  ensureApiEnabled.check(projectId, "runtimeconfig.googleapis.com", "runtimeconfig", true),
41
49
  ensureCloudBuildEnabled_1.ensureCloudBuildEnabled(projectId),
50
+ maybeEnableAR(projectId),
42
51
  ]);
43
52
  context.runtimeConfigEnabled = checkAPIsEnabled[1];
53
+ context.artifactRegistryEnabled = checkAPIsEnabled[3];
44
54
  const firebaseConfig = await functionsConfig.getFirebaseConfig(options);
45
55
  context.firebaseConfig = firebaseConfig;
46
56
  const runtimeConfig = await prepareFunctionsUpload_1.getFunctionsConfig(context);
@@ -103,8 +113,9 @@ async function prepare(context, options, payload) {
103
113
  return functionsDeployHelper_1.functionMatchesAnyGroup(endpoint, context.filters);
104
114
  });
105
115
  const haveBackend = await backend.existingBackend(context);
116
+ await checkIam_1.ensureServiceAgentRoles(projectId, wantBackend, haveBackend);
106
117
  inferDetailsFromExisting(wantBackend, haveBackend, usedDotenv);
107
- await triggerRegionHelper_1.lookupMissingTriggerRegions(wantBackend);
118
+ await triggerRegionHelper_1.ensureTriggerRegions(wantBackend);
108
119
  await prompts_1.promptForFailurePolicies(options, matchingBackend, haveBackend);
109
120
  await prompts_1.promptForMinInstances(options, matchingBackend, haveBackend);
110
121
  await backend.checkAvailability(context, wantBackend);
@@ -124,7 +124,9 @@ class Fabricator {
124
124
  await this.setTrigger(endpoint);
125
125
  }
126
126
  async updateEndpoint(update, scraper) {
127
- update.endpoint.labels = Object.assign(Object.assign({}, update.endpoint.labels), deploymentTool.labels());
127
+ if (update.deleteAndRecreate || update.endpoint.platform !== "gcfv2") {
128
+ update.endpoint.labels = Object.assign(Object.assign({}, update.endpoint.labels), deploymentTool.labels());
129
+ }
128
130
  if (update.deleteAndRecreate) {
129
131
  await this.deleteEndpoint(update.deleteAndRecreate);
130
132
  await this.createEndpoint(update.endpoint, scraper);
@@ -157,6 +159,9 @@ class Fabricator {
157
159
  throw new Error("Precondition failed");
158
160
  }
159
161
  const apiFunction = gcf.functionFromEndpoint(endpoint, this.sourceUrl);
162
+ if (apiFunction.httpsTrigger) {
163
+ apiFunction.httpsTrigger.securityLevel = "SECURE_ALWAYS";
164
+ }
160
165
  apiFunction.sourceToken = await scraper.tokenPromise();
161
166
  const resultFunction = await this.functionExecutor
162
167
  .run(async () => {
@@ -37,7 +37,7 @@ async function release(context, options, payload) {
37
37
  const fab = new fabricator.Fabricator({
38
38
  functionExecutor,
39
39
  executor: new executor.QueueExecutor({}),
40
- sourceUrl: context.uploadUrl,
40
+ sourceUrl: context.sourceUrl,
41
41
  storage: context.storage,
42
42
  appEngineLocation: functionsConfig_1.getAppEngineLocation(context.firebaseConfig),
43
43
  });
@@ -49,7 +49,11 @@ async function release(context, options, payload) {
49
49
  const deletedEndpoints = Object.values(plan)
50
50
  .map((r) => r.endpointsToDelete)
51
51
  .reduce(functional_1.reduceFlat, []);
52
- await containerCleaner.cleanupBuildImages(haveEndpoints, deletedEndpoints);
52
+ const opts = {};
53
+ if (!context.artifactRegistryEnabled) {
54
+ opts.ar = new containerCleaner.NoopArtifactRegistryCleaner();
55
+ }
56
+ await containerCleaner.cleanupBuildImages(haveEndpoints, deletedEndpoints, opts);
53
57
  const allErrors = summary.results.filter((r) => r.error).map((r) => r.error);
54
58
  if (allErrors.length) {
55
59
  const opts = allErrors.length == 1 ? { original: allErrors[0] } : { children: allErrors };
@@ -29,8 +29,10 @@ async function logAndTrackDeployStats(summary) {
29
29
  let totalSuccesses = 0;
30
30
  let totalAborts = 0;
31
31
  const reports = [];
32
+ const regions = new Set();
32
33
  for (const result of summary.results) {
33
34
  const tag = triggerTag(result.endpoint);
35
+ regions.add(result.endpoint.region);
34
36
  totalTime += result.durationMs;
35
37
  if (!result.error) {
36
38
  totalSuccesses++;
@@ -45,6 +47,8 @@ async function logAndTrackDeployStats(summary) {
45
47
  reports.push(track.track("function_deploy_failure", tag, result.durationMs));
46
48
  }
47
49
  }
50
+ const regionCountTag = regions.size < 5 ? regions.size.toString() : ">=5";
51
+ reports.push(track.track("functions_region_count", regionCountTag, 1));
48
52
  const gcfv1 = summary.results.find((r) => r.endpoint.platform === "gcfv1");
49
53
  const gcfv2 = summary.results.find((r) => r.endpoint.platform === "gcfv2");
50
54
  const tag = gcfv1 && gcfv2 ? "v1+v2" : gcfv1 ? "v1" : "v2";
@@ -18,7 +18,7 @@ const ENGINE_RUNTIMES = {
18
18
  const ENGINE_RUNTIMES_NAMES = Object.values(ENGINE_RUNTIMES);
19
19
  exports.RUNTIME_NOT_SET = "`runtime` field is required but was not found in firebase.json.\n" +
20
20
  "To fix this, add the following lines to the `functions` section of your firebase.json:\n" +
21
- '"runtime": "nodejs14"\n';
21
+ '"runtime": "nodejs16"\n';
22
22
  exports.UNSUPPORTED_NODE_VERSION_FIREBASE_JSON_MSG = clc.bold(`functions.runtime value is unsupported. ` +
23
23
  `Valid choices are: ${clc.bold("nodejs{10|12|14|16}")}.`);
24
24
  exports.UNSUPPORTED_NODE_VERSION_PACKAGE_JSON_MSG = clc.bold(`package.json in functions directory has an engines field which is unsupported. ` +
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.addResourcesToBackend = exports.discoverBackend = exports.useStrategy = exports.GCS_EVENTS = void 0;
3
+ exports.addResourcesToBackend = exports.discoverBackend = exports.useStrategy = void 0;
4
4
  const path = require("path");
5
5
  const _ = require("lodash");
6
6
  const child_process_1 = require("child_process");
@@ -9,13 +9,8 @@ const logger_1 = require("../../../../logger");
9
9
  const backend = require("../../backend");
10
10
  const api = require("../../../../api");
11
11
  const proto = require("../../../../gcp/proto");
12
+ const eventTypes_1 = require("../../eventTypes");
12
13
  const TRIGGER_PARSER = path.resolve(__dirname, "./triggerParser.js");
13
- exports.GCS_EVENTS = new Set([
14
- "google.cloud.storage.object.v1.finalized",
15
- "google.cloud.storage.object.v1.archived",
16
- "google.cloud.storage.object.v1.deleted",
17
- "google.cloud.storage.object.v1.metadataUpdated",
18
- ]);
19
14
  function removeInspectOptions(options) {
20
15
  return options.filter((opt) => !opt.startsWith("--inspect"));
21
16
  }
@@ -64,7 +59,6 @@ async function discoverBackend(projectId, sourceDir, runtime, configValues, envs
64
59
  }
65
60
  exports.discoverBackend = discoverBackend;
66
61
  function addResourcesToBackend(projectId, runtime, annotation, want) {
67
- var _a;
68
62
  Object.freeze(annotation);
69
63
  for (const region of annotation.regions || [api.functionsDefaultRegion]) {
70
64
  let triggered;
@@ -99,7 +93,7 @@ function addResourcesToBackend(projectId, runtime, annotation, want) {
99
93
  retry: !!annotation.failurePolicy,
100
94
  },
101
95
  };
102
- if (exports.GCS_EVENTS.has(((_a = annotation.eventTrigger) === null || _a === void 0 ? void 0 : _a.eventType) || "")) {
96
+ if (eventTypes_1.STORAGE_V2_EVENTS.find((event) => { var _a; return event === (((_a = annotation.eventTrigger) === null || _a === void 0 ? void 0 : _a.eventType) || ""); })) {
103
97
  triggered.eventTrigger.eventFilters = {
104
98
  bucket: annotation.eventTrigger.resource,
105
99
  };
@@ -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
 
@@ -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;
@@ -135,7 +135,7 @@ async function assertResolveDeveloperNodeModule(frb, name) {
135
135
  async function verifyDeveloperNodeModules(frb) {
136
136
  const modBundles = [
137
137
  { name: "firebase-admin", isDev: false, minVersion: "8.9.0" },
138
- { name: "firebase-functions", isDev: false, minVersion: "3.3.0" },
138
+ { name: "firebase-functions", isDev: false, minVersion: "3.13.1" },
139
139
  ];
140
140
  for (const modBundle of modBundles) {
141
141
  const resolution = await resolveDeveloperNodeModule(frb, modBundle.name);
@@ -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
  }