firebase-tools 9.20.0 → 9.23.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 (98) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/lib/api.js +2 -0
  3. package/lib/apiv2.js +7 -4
  4. package/lib/commands/crashlytics-symbols-upload.js +2 -2
  5. package/lib/commands/deploy.js +9 -1
  6. package/lib/commands/ext-configure.js +1 -1
  7. package/lib/commands/ext-dev-deprecate.js +63 -0
  8. package/lib/commands/ext-dev-undeprecate.js +56 -0
  9. package/lib/commands/ext-dev-unpublish.js +10 -3
  10. package/lib/commands/ext-export.js +44 -0
  11. package/lib/commands/ext-install.js +1 -1
  12. package/lib/commands/ext-update.js +1 -1
  13. package/lib/commands/functions-delete.js +55 -42
  14. package/lib/commands/functions-list.js +11 -11
  15. package/lib/commands/index.js +6 -5
  16. package/lib/commands/init.js +3 -0
  17. package/lib/config.js +3 -2
  18. package/lib/deploy/extensions/args.js +2 -0
  19. package/lib/deploy/extensions/deploy.js +49 -0
  20. package/lib/deploy/extensions/deploymentSummary.js +52 -0
  21. package/lib/deploy/extensions/errors.js +31 -0
  22. package/lib/deploy/extensions/index.js +8 -0
  23. package/lib/deploy/extensions/params.js +39 -0
  24. package/lib/deploy/extensions/planner.js +94 -0
  25. package/lib/deploy/extensions/prepare.js +111 -0
  26. package/lib/deploy/extensions/release.js +43 -0
  27. package/lib/deploy/extensions/secrets.js +150 -0
  28. package/lib/deploy/extensions/tasks.js +98 -0
  29. package/lib/deploy/extensions/validate.js +17 -0
  30. package/lib/deploy/functions/backend.js +84 -115
  31. package/lib/deploy/functions/checkIam.js +73 -12
  32. package/lib/deploy/functions/containerCleaner.js +97 -50
  33. package/lib/deploy/functions/deploy.js +4 -10
  34. package/lib/deploy/functions/eventTypes.js +10 -0
  35. package/lib/deploy/functions/functionsDeployHelper.js +3 -68
  36. package/lib/deploy/functions/prepare.js +72 -29
  37. package/lib/deploy/functions/pricing.js +17 -17
  38. package/lib/deploy/functions/prompts.js +22 -21
  39. package/lib/deploy/functions/release/executor.js +39 -0
  40. package/lib/deploy/functions/release/fabricator.js +425 -0
  41. package/lib/deploy/functions/release/index.js +73 -0
  42. package/lib/deploy/functions/release/planner.js +162 -0
  43. package/lib/deploy/functions/release/reporter.js +165 -0
  44. package/lib/deploy/functions/release/sourceTokenScraper.js +28 -0
  45. package/lib/deploy/functions/release/timer.js +14 -0
  46. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +129 -126
  47. package/lib/deploy/functions/runtimes/node/parseTriggers.js +32 -54
  48. package/lib/deploy/functions/services/index.js +38 -0
  49. package/lib/deploy/functions/services/storage.js +43 -0
  50. package/lib/deploy/functions/triggerRegionHelper.js +9 -25
  51. package/lib/deploy/functions/validate.js +1 -24
  52. package/lib/deploy/index.js +10 -1
  53. package/lib/emulator/auth/apiSpec.js +37 -6
  54. package/lib/emulator/auth/operations.js +45 -17
  55. package/lib/emulator/auth/server.js +16 -2
  56. package/lib/emulator/auth/state.js +34 -15
  57. package/lib/emulator/auth/widget_ui.js +14 -0
  58. package/lib/emulator/downloadableEmulators.js +7 -7
  59. package/lib/emulator/functionsEmulator.js +18 -4
  60. package/lib/emulator/storage/cloudFunctions.js +37 -7
  61. package/lib/ensureApiEnabled.js +10 -12
  62. package/lib/extensions/askUserForParam.js +14 -6
  63. package/lib/extensions/checkProjectBilling.js +7 -7
  64. package/lib/extensions/export.js +107 -0
  65. package/lib/extensions/extensionsApi.js +103 -21
  66. package/lib/extensions/extensionsHelper.js +5 -2
  67. package/lib/extensions/listExtensions.js +16 -11
  68. package/lib/extensions/paramHelper.js +6 -4
  69. package/lib/extensions/provisioningHelper.js +16 -3
  70. package/lib/extensions/refs.js +9 -1
  71. package/lib/extensions/secretsUtils.js +10 -9
  72. package/lib/extensions/updateHelper.js +12 -2
  73. package/lib/extensions/versionHelper.js +14 -0
  74. package/lib/extensions/warnings.js +33 -1
  75. package/lib/gcp/artifactregistry.js +16 -0
  76. package/lib/gcp/cloudfunctions.js +25 -72
  77. package/lib/gcp/cloudfunctionsv2.js +46 -98
  78. package/lib/gcp/cloudscheduler.js +22 -16
  79. package/lib/gcp/cloudtasks.js +143 -0
  80. package/lib/gcp/docker.js +36 -2
  81. package/lib/gcp/location.js +44 -0
  82. package/lib/gcp/proto.js +2 -2
  83. package/lib/gcp/pubsub.js +1 -9
  84. package/lib/gcp/secretManager.js +27 -6
  85. package/lib/gcp/storage.js +48 -32
  86. package/lib/init/features/project.js +2 -1
  87. package/lib/previews.js +1 -1
  88. package/lib/projectUtils.js +10 -1
  89. package/lib/utils.js +30 -1
  90. package/package.json +5 -4
  91. package/schema/firebase-config.json +9 -0
  92. package/lib/deploy/functions/deploymentPlanner.js +0 -113
  93. package/lib/deploy/functions/deploymentTimer.js +0 -23
  94. package/lib/deploy/functions/errorHandler.js +0 -75
  95. package/lib/deploy/functions/release.js +0 -116
  96. package/lib/deploy/functions/tasks.js +0 -324
  97. package/lib/functions/listFunctions.js +0 -10
  98. package/lib/functionsDelete.js +0 -60
@@ -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,32 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setTriggerRegion = 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
- async function setTriggerRegion(want, have) {
8
- var _a;
9
- for (const wantFn of want) {
10
- if (wantFn.platform === "gcfv1" || !backend.isEventTrigger(wantFn.trigger)) {
5
+ const services_1 = require("./services");
6
+ async function ensureTriggerRegions(want) {
7
+ const regionLookups = [];
8
+ for (const ep of backend.allEndpoints(want)) {
9
+ if (ep.platform === "gcfv1" || !backend.isEventTriggered(ep)) {
11
10
  continue;
12
11
  }
13
- const match = (_a = have.find(backend.sameFunctionName(wantFn))) === null || _a === void 0 ? void 0 : _a.trigger;
14
- if (match === null || match === void 0 ? void 0 : match.region) {
15
- wantFn.trigger.region = match.region;
16
- }
17
- else {
18
- await setTriggerRegionFromTriggerType(wantFn.trigger);
19
- }
20
- }
21
- }
22
- exports.setTriggerRegion = setTriggerRegion;
23
- async function setTriggerRegionFromTriggerType(trigger) {
24
- if (trigger.eventFilters.bucket) {
25
- try {
26
- trigger.region = (await storage.getBucket(trigger.eventFilters.bucket)).location.toLowerCase();
27
- }
28
- catch (err) {
29
- throw new error_1.FirebaseError("Can't find the storage bucket region", { original: err });
30
- }
12
+ regionLookups.push(services_1.serviceForEndpoint(ep).ensureTriggerRegion(ep, ep.eventTrigger));
31
13
  }
14
+ await Promise.all(regionLookups);
32
15
  }
16
+ exports.ensureTriggerRegions = ensureTriggerRegions;
@@ -1,10 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.checkForInvalidChangeOfTrigger = exports.functionIdsAreValid = exports.functionsDirectoryExists = void 0;
3
+ exports.functionIdsAreValid = exports.functionsDirectoryExists = void 0;
4
4
  const clc = require("cli-color");
5
5
  const error_1 = require("../../error");
6
- const functionsDeployHelper_1 = require("./functionsDeployHelper");
7
- const backend = require("./backend");
8
6
  const fsutils = require("../../fsutils");
9
7
  const projectPath = require("../../projectPath");
10
8
  function functionsDirectoryExists(options, sourceDirName) {
@@ -36,24 +34,3 @@ function functionIdsAreValid(functions) {
36
34
  }
37
35
  }
38
36
  exports.functionIdsAreValid = functionIdsAreValid;
39
- function checkForInvalidChangeOfTrigger(fn, exFn) {
40
- var _a, _b;
41
- const wantEventTrigger = backend.isEventTrigger(fn.trigger);
42
- const haveEventTrigger = backend.isEventTrigger(exFn.trigger);
43
- if (!wantEventTrigger && haveEventTrigger) {
44
- throw new error_1.FirebaseError(`[${functionsDeployHelper_1.getFunctionLabel(fn)}] Changing from a background triggered function to an HTTPS function is not allowed. Please delete your function and create a new one instead.`);
45
- }
46
- if (wantEventTrigger && !haveEventTrigger) {
47
- throw new error_1.FirebaseError(`[${functionsDeployHelper_1.getFunctionLabel(fn)}] Changing from an HTTPS function to an background triggered function is not allowed. Please delete your function and create a new one instead.`);
48
- }
49
- if (fn.platform == "gcfv2" && exFn.platform == "gcfv1") {
50
- throw new error_1.FirebaseError(`[${functionsDeployHelper_1.getFunctionLabel(fn)}] Upgrading from GCFv1 to GCFv2 is not yet supported. Please delete your old function or wait for this feature to be ready.`);
51
- }
52
- if (fn.platform == "gcfv1" && exFn.platform == "gcfv2") {
53
- throw new error_1.FirebaseError(`[${functionsDeployHelper_1.getFunctionLabel(fn)}] Functions cannot be downgraded from GCFv2 to GCFv1`);
54
- }
55
- if (((_a = exFn.labels) === null || _a === void 0 ? void 0 : _a["deployment-scheduled"]) && !((_b = fn.labels) === null || _b === void 0 ? void 0 : _b["deployment-scheduled"])) {
56
- throw new error_1.FirebaseError(`[${functionsDeployHelper_1.getFunctionLabel(fn)}] Scheduled functions cannot be changed to event handler or HTTP functions`);
57
- }
58
- }
59
- exports.checkForInvalidChangeOfTrigger = checkForInvalidChangeOfTrigger;
@@ -15,6 +15,7 @@ var TARGETS = {
15
15
  functions: require("./functions"),
16
16
  storage: require("./storage"),
17
17
  remoteconfig: require("./remoteconfig"),
18
+ extensions: require("./extensions"),
18
19
  };
19
20
  var _noop = function () {
20
21
  return Promise.resolve();
@@ -37,6 +38,7 @@ var deploy = function (targetNames, options, customContext = {}) {
37
38
  var deploys = [];
38
39
  var releases = [];
39
40
  var postdeploys = [];
41
+ var startTime = Date.now();
40
42
  for (var i = 0; i < targetNames.length; i++) {
41
43
  var targetName = targetNames[i];
42
44
  var target = TARGETS[targetName];
@@ -74,8 +76,15 @@ var deploy = function (targetNames, options, customContext = {}) {
74
76
  })
75
77
  .then(function () {
76
78
  if (_.has(options, "config.notes.databaseRules")) {
77
- track("Rules Deploy", options.config.notes.databaseRules);
79
+ return track("Rules Deploy", options.config.notes.databaseRules);
78
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 () {
79
88
  logger.info();
80
89
  utils.logSuccess(clc.underline.bold("Deploy complete!"));
81
90
  logger.info();
@@ -3580,6 +3580,37 @@ exports.default = {
3580
3580
  tags: ["emulator"],
3581
3581
  },
3582
3582
  },
3583
+ "/emulator/v1/projects/{targetProjectId}/tenants/{tenantId}/accounts": {
3584
+ parameters: [
3585
+ {
3586
+ name: "targetProjectId",
3587
+ in: "path",
3588
+ description: "The ID of the Google Cloud project that the accounts belong to.",
3589
+ required: true,
3590
+ schema: { type: "string" },
3591
+ },
3592
+ {
3593
+ name: "tenantId",
3594
+ in: "path",
3595
+ description: "The ID of the Identity Platform tenant the accounts belongs to. If not specified, accounts on the Identity Platform project are returned.",
3596
+ required: true,
3597
+ schema: { type: "string" },
3598
+ },
3599
+ ],
3600
+ servers: [{ url: "" }],
3601
+ delete: {
3602
+ description: "Remove all accounts in the project, regardless of state.",
3603
+ operationId: "emulator.projects.accounts.delete",
3604
+ responses: {
3605
+ 200: {
3606
+ description: "Successful response",
3607
+ content: { "application/json": { schema: { type: "object" } } },
3608
+ },
3609
+ },
3610
+ security: [],
3611
+ tags: ["emulator"],
3612
+ },
3613
+ },
3583
3614
  "/emulator/v1/projects/{targetProjectId}/config": {
3584
3615
  parameters: [
3585
3616
  {
@@ -4978,7 +5009,7 @@ exports.default = {
4978
5009
  type: "string",
4979
5010
  },
4980
5011
  postBody: {
4981
- description: "If the user is signing in with an authorization response obtained via a previous CreateAuthUri authorization request, this is the body of the HTTP POST callback from the IdP, if present. Otherwise, if the user is signing in with a manually provided IdP credential, this should be a URL-encoded form that contains the credential (e.g. an ID token or access token for OAuth 2.0 IdPs) and the provider ID of the IdP that issued the credential. For example, if the user is signing in to the Google provider using a Google ID token, this should be set to `id_token=[GOOGLE_ID_TOKEN]&providerId=google.com`, where `[GOOGLE_ID_TOKEN]` should be replaced with the Google ID token. If the user is signing in to the Facebook provider using a Facebook access token, this should be set to `access_token=[FACEBOOK_ACCESS_TOKEN]&providerId=facebook.com`, where `[FACEBOOK_ACCESS_TOKEN]` should be replaced with the Facebook access token. If the user is signing in to the Twitter provider using a Twitter OAuth 1.0 credential, this should be set to `access_token=[TWITTER_ACCESS_TOKEN]&oauth_token_secret=[TWITTER_TOKEN_SECRET]&providerId=twitter.com`, where `[TWITTER_ACCESS_TOKEN]` and `[TWITTER_TOKEN_SECRET]` should be replaced with the Twitter OAuth access token and Twitter OAuth token secret respectively.",
5012
+ description: "If the user is signing in with an authorization response obtained via a previous CreateAuthUri authorization request, this is the body of the HTTP POST callback from the IdP, if present. Otherwise, if the user is signing in with a manually provided IdP credential, this should be a URL-encoded form that contains the credential (e.g. an ID token or access token for OAuth 2.0 IdPs) and the provider ID of the IdP that issued the credential. For example, if the user is signing in to the Google provider using a Google ID token, this should be set to `id_token=[GOOGLE_ID_TOKEN]&providerId=google.com`, where `[GOOGLE_ID_TOKEN]` should be replaced with the Google ID token. If the user is signing in to the Facebook provider using a Facebook authentication token, this should be set to `id_token=[FACEBOOK_AUTHENTICATION_TOKEN]&providerId=facebook.com&nonce= [NONCE]`, where `[FACEBOOK_AUTHENTICATION_TOKEN]` should be replaced with the Facebook authentication token. Nonce is required for validating the token. The request will fail if no nonce is provided. If the user is signing in to the Facebook provider using a Facebook access token, this should be set to `access_token=[FACEBOOK_ACCESS_TOKEN]&providerId=facebook.com`, where `[FACEBOOK_ACCESS_TOKEN]` should be replaced with the Facebook access token. If the user is signing in to the Twitter provider using a Twitter OAuth 1.0 credential, this should be set to `access_token=[TWITTER_ACCESS_TOKEN]&oauth_token_secret=[TWITTER_TOKEN_SECRET]&providerId=twitter.com`, where `[TWITTER_ACCESS_TOKEN]` and `[TWITTER_TOKEN_SECRET]` should be replaced with the Twitter OAuth access token and Twitter OAuth token secret respectively.",
4982
5013
  type: "string",
4983
5014
  },
4984
5015
  requestUri: {
@@ -6665,16 +6696,16 @@ exports.default = {
6665
6696
  type: "object",
6666
6697
  },
6667
6698
  GoogleIamV1Binding: {
6668
- description: "Associates `members` with a `role`.",
6699
+ description: "Associates `members`, or principals, with a `role`.",
6669
6700
  properties: {
6670
6701
  condition: { $ref: "#/components/schemas/GoogleTypeExpr" },
6671
6702
  members: {
6672
- description: "Specifies the identities requesting access for a Cloud Platform resource. `members` can have the following values: * `allUsers`: A special identifier that represents anyone who is on the internet; with or without a Google account. * `allAuthenticatedUsers`: A special identifier that represents anyone who is authenticated with a Google account or a service account. * `user:{emailid}`: An email address that represents a specific Google account. For example, `alice@example.com` . * `serviceAccount:{emailid}`: An email address that represents a service account. For example, `my-other-app@appspot.gserviceaccount.com`. * `group:{emailid}`: An email address that represents a Google group. For example, `admins@example.com`. * `deleted:user:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a user that has been recently deleted. For example, `alice@example.com?uid=123456789012345678901`. If the user is recovered, this value reverts to `user:{emailid}` and the recovered user retains the role in the binding. * `deleted:serviceAccount:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a service account that has been recently deleted. For example, `my-other-app@appspot.gserviceaccount.com?uid=123456789012345678901`. If the service account is undeleted, this value reverts to `serviceAccount:{emailid}` and the undeleted service account retains the role in the binding. * `deleted:group:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a Google group that has been recently deleted. For example, `admins@example.com?uid=123456789012345678901`. If the group is recovered, this value reverts to `group:{emailid}` and the recovered group retains the role in the binding. * `domain:{domain}`: The G Suite domain (primary) that represents all the users of that domain. For example, `google.com` or `example.com`. ",
6703
+ description: "Specifies the principals requesting access for a Cloud Platform resource. `members` can have the following values: * `allUsers`: A special identifier that represents anyone who is on the internet; with or without a Google account. * `allAuthenticatedUsers`: A special identifier that represents anyone who is authenticated with a Google account or a service account. * `user:{emailid}`: An email address that represents a specific Google account. For example, `alice@example.com` . * `serviceAccount:{emailid}`: An email address that represents a service account. For example, `my-other-app@appspot.gserviceaccount.com`. * `group:{emailid}`: An email address that represents a Google group. For example, `admins@example.com`. * `deleted:user:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a user that has been recently deleted. For example, `alice@example.com?uid=123456789012345678901`. If the user is recovered, this value reverts to `user:{emailid}` and the recovered user retains the role in the binding. * `deleted:serviceAccount:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a service account that has been recently deleted. For example, `my-other-app@appspot.gserviceaccount.com?uid=123456789012345678901`. If the service account is undeleted, this value reverts to `serviceAccount:{emailid}` and the undeleted service account retains the role in the binding. * `deleted:group:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a Google group that has been recently deleted. For example, `admins@example.com?uid=123456789012345678901`. If the group is recovered, this value reverts to `group:{emailid}` and the recovered group retains the role in the binding. * `domain:{domain}`: The G Suite domain (primary) that represents all the users of that domain. For example, `google.com` or `example.com`. ",
6673
6704
  items: { type: "string" },
6674
6705
  type: "array",
6675
6706
  },
6676
6707
  role: {
6677
- description: "Role that is assigned to `members`. For example, `roles/viewer`, `roles/editor`, or `roles/owner`.",
6708
+ description: "Role that is assigned to the list of `members`, or principals. For example, `roles/viewer`, `roles/editor`, or `roles/owner`.",
6678
6709
  type: "string",
6679
6710
  },
6680
6711
  },
@@ -6697,7 +6728,7 @@ exports.default = {
6697
6728
  type: "object",
6698
6729
  },
6699
6730
  GoogleIamV1Policy: {
6700
- description: 'An Identity and Access Management (IAM) policy, which specifies access controls for Google Cloud resources. A `Policy` is a collection of `bindings`. A `binding` binds one or more `members` to a single `role`. Members can be user accounts, service accounts, Google groups, and domains (such as G Suite). A `role` is a named list of permissions; each `role` can be an IAM predefined role or a user-created custom role. For some types of Google Cloud resources, a `binding` can also specify a `condition`, which is a logical expression that allows access to a resource only if the expression evaluates to `true`. A condition can add constraints based on attributes of the request, the resource, or both. To learn which resources support conditions in their IAM policies, see the [IAM documentation](https://cloud.google.com/iam/help/conditions/resource-policies). **JSON example:** { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp(\'2020-10-01T00:00:00.000Z\')", } } ], "etag": "BwWWja0YfJA=", "version": 3 } **YAML example:** bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp(\'2020-10-01T00:00:00.000Z\') etag: BwWWja0YfJA= version: 3 For a description of IAM and its features, see the [IAM documentation](https://cloud.google.com/iam/docs/).',
6731
+ description: 'An Identity and Access Management (IAM) policy, which specifies access controls for Google Cloud resources. A `Policy` is a collection of `bindings`. A `binding` binds one or more `members`, or principals, to a single `role`. Principals can be user accounts, service accounts, Google groups, and domains (such as G Suite). A `role` is a named list of permissions; each `role` can be an IAM predefined role or a user-created custom role. For some types of Google Cloud resources, a `binding` can also specify a `condition`, which is a logical expression that allows access to a resource only if the expression evaluates to `true`. A condition can add constraints based on attributes of the request, the resource, or both. To learn which resources support conditions in their IAM policies, see the [IAM documentation](https://cloud.google.com/iam/help/conditions/resource-policies). **JSON example:** { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp(\'2020-10-01T00:00:00.000Z\')", } } ], "etag": "BwWWja0YfJA=", "version": 3 } **YAML example:** bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp(\'2020-10-01T00:00:00.000Z\') etag: BwWWja0YfJA= version: 3 For a description of IAM and its features, see the [IAM documentation](https://cloud.google.com/iam/docs/).',
6701
6732
  properties: {
6702
6733
  auditConfigs: {
6703
6734
  description: "Specifies cloud audit logging configuration for this policy.",
@@ -6705,7 +6736,7 @@ exports.default = {
6705
6736
  type: "array",
6706
6737
  },
6707
6738
  bindings: {
6708
- description: "Associates a list of `members` to a `role`. Optionally, may specify a `condition` that determines how and when the `bindings` are applied. Each of the `bindings` must contain at least one member. The `bindings` in a `Policy` can refer to up to 1,500 members; up to 250 of these members can be Google groups. Each occurrence of a member counts towards these limits. For example, if the `bindings` grant 50 different roles to `user:alice@example.com`, and not to any other member, then you can add another 1,450 members to the `bindings` in the `Policy`.",
6739
+ description: "Associates a list of `members`, or principals, with a `role`. Optionally, may specify a `condition` that determines how and when the `bindings` are applied. Each of the `bindings` must contain at least one principal. The `bindings` in a `Policy` can refer to up to 1,500 principals; up to 250 of these principals can be Google groups. Each occurrence of a principal counts towards these limits. For example, if the `bindings` grant 50 different roles to `user:alice@example.com`, and not to any other principal, then you can add another 1,450 principals to the `bindings` in the `Policy`.",
6709
6740
  items: { $ref: "#/components/schemas/GoogleIamV1Binding" },
6710
6741
  type: "array",
6711
6742
  },
@@ -160,8 +160,8 @@ function signUp(state, reqBody, ctx) {
160
160
  generateEnrollmentIds: true,
161
161
  });
162
162
  }
163
- if (reqBody.tenantId) {
164
- updates.tenantId = reqBody.tenantId;
163
+ if (state instanceof state_1.TenantProjectState) {
164
+ updates.tenantId = state.tenantId;
165
165
  }
166
166
  let user;
167
167
  if (reqBody.idToken) {
@@ -267,6 +267,8 @@ function batchCreate(state, reqBody) {
267
267
  };
268
268
  if (userInfo.tenantId) {
269
269
  errors_1.assert(state instanceof state_1.TenantProjectState && state.tenantId === userInfo.tenantId, "Tenant id in userInfo does not match the tenant id in request.");
270
+ }
271
+ if (state instanceof state_1.TenantProjectState) {
270
272
  fields.tenantId = state.tenantId;
271
273
  }
272
274
  if (userInfo.passwordHash) {
@@ -921,7 +923,7 @@ function signInWithCustomToken(state, reqBody) {
921
923
  else {
922
924
  const decoded = jsonwebtoken_1.decode(reqBody.token, { complete: true });
923
925
  if (state instanceof state_1.TenantProjectState) {
924
- errors_1.assert((decoded === null || decoded === void 0 ? void 0 : decoded.payload.tenantId) === state.tenantId, "TENANT_ID_MISMATCH");
926
+ errors_1.assert((decoded === null || decoded === void 0 ? void 0 : decoded.payload.tenant_id) === state.tenantId, "TENANT_ID_MISMATCH");
925
927
  }
926
928
  errors_1.assert(decoded, "INVALID_CUSTOM_TOKEN : Invalid assertion format");
927
929
  if (decoded.header.alg !== "none") {
@@ -1008,7 +1010,7 @@ function signInWithEmailLink(state, reqBody) {
1008
1010
  }
1009
1011
  }
1010
1012
  function signInWithIdp(state, reqBody) {
1011
- var _a, _b;
1013
+ var _a, _b, _c;
1012
1014
  errors_1.assert(!state.disableAuth, "PROJECT_DISABLED");
1013
1015
  errors_1.assert(state.usageMode !== state_1.UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION");
1014
1016
  if (reqBody.returnRefreshToken) {
@@ -1039,7 +1041,16 @@ function signInWithIdp(state, reqBody) {
1039
1041
  throw new errors_1.NotImplementedError("The Auth Emulator only supports sign-in with credentials (id_token required).");
1040
1042
  }
1041
1043
  }
1042
- 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);
1043
1054
  response.oauthAccessToken =
1044
1055
  oauthAccessToken || `FirebaseAuthEmulatorFakeAccessToken_${providerId}`;
1045
1056
  response.oauthIdToken = oauthIdToken;
@@ -1099,12 +1110,12 @@ function signInWithIdp(state, reqBody) {
1099
1110
  if (state instanceof state_1.TenantProjectState) {
1100
1111
  response.tenantId = state.tenantId;
1101
1112
  }
1102
- 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)) {
1103
1114
  return Object.assign(Object.assign({}, response), mfaPending(state, user, providerId));
1104
1115
  }
1105
1116
  else {
1106
1117
  user = state.updateUserByLocalId(user.localId, { lastLoginAt: Date.now().toString() });
1107
- return Object.assign(Object.assign({}, response), issueTokens(state, user, providerId));
1118
+ return Object.assign(Object.assign({}, response), issueTokens(state, user, providerId, { signInAttributes }));
1108
1119
  }
1109
1120
  }
1110
1121
  function signInWithPassword(state, reqBody) {
@@ -1389,7 +1400,7 @@ function redactPasswordHash(user) {
1389
1400
  function hashPassword(password, salt) {
1390
1401
  return `fakeHash:salt=${salt}:password=${password}`;
1391
1402
  }
1392
- function issueTokens(state, user, signInProvider, { extraClaims, secondFactor, } = {}) {
1403
+ function issueTokens(state, user, signInProvider, { extraClaims, secondFactor, signInAttributes, } = {}) {
1393
1404
  user = state.updateUserByLocalId(user.localId, { lastRefreshAt: new Date().toISOString() });
1394
1405
  const usageMode = state.usageMode === state_1.UsageMode.PASSTHROUGH ? "passthrough" : undefined;
1395
1406
  const tenantId = state instanceof state_1.TenantProjectState ? state.tenantId : undefined;
@@ -1402,6 +1413,7 @@ function issueTokens(state, user, signInProvider, { extraClaims, secondFactor, }
1402
1413
  secondFactor,
1403
1414
  usageMode,
1404
1415
  tenantId,
1416
+ signInAttributes,
1405
1417
  });
1406
1418
  const refreshToken = state.usageMode === state_1.UsageMode.DEFAULT
1407
1419
  ? state.createRefreshTokenFor(user, signInProvider, {
@@ -1433,7 +1445,7 @@ function parseIdToken(state, idToken) {
1433
1445
  const signInProvider = decoded.payload.firebase.sign_in_provider;
1434
1446
  return { user, signInProvider, payload: decoded.payload };
1435
1447
  }
1436
- function generateJwt(user, { projectId, signInProvider, expiresInSeconds, extraClaims = {}, secondFactor, usageMode, tenantId, }) {
1448
+ function generateJwt(user, { projectId, signInProvider, expiresInSeconds, extraClaims = {}, secondFactor, usageMode, tenantId, signInAttributes, }) {
1437
1449
  const identities = {};
1438
1450
  if (user.email) {
1439
1451
  identities["email"] = [user.email];
@@ -1457,6 +1469,7 @@ function generateJwt(user, { projectId, signInProvider, expiresInSeconds, extraC
1457
1469
  sign_in_second_factor: secondFactor === null || secondFactor === void 0 ? void 0 : secondFactor.provider,
1458
1470
  usage_mode: usageMode,
1459
1471
  tenant: tenantId,
1472
+ sign_in_attributes: signInAttributes,
1460
1473
  } });
1461
1474
  const jwtStr = jsonwebtoken_1.sign(customPayloadFields, "", {
1462
1475
  algorithm: "none",
@@ -1601,7 +1614,8 @@ function parseClaims(idTokenOrJsonClaims) {
1601
1614
  errors_1.assert(typeof claims.sub === "string", 'INVALID_IDP_RESPONSE : ((The "sub" field must be a string.))');
1602
1615
  return claims;
1603
1616
  }
1604
- function fakeFetchUserInfoFromIdp(providerId, claims) {
1617
+ function fakeFetchUserInfoFromIdp(providerId, claims, samlResponse) {
1618
+ var _a, _b, _c, _d, _e;
1605
1619
  const rawId = claims.sub;
1606
1620
  const email = claims.email ? utils_1.canonicalizeEmailAddress(claims.email) : undefined;
1607
1621
  const emailVerified = !!claims.email_verified;
@@ -1618,7 +1632,7 @@ function fakeFetchUserInfoFromIdp(providerId, claims) {
1618
1632
  emailVerified,
1619
1633
  photoUrl,
1620
1634
  };
1621
- let federatedId;
1635
+ let federatedId = rawId;
1622
1636
  switch (providerId) {
1623
1637
  case "google.com": {
1624
1638
  federatedId = `https://accounts.google.com/${rawId}`;
@@ -1641,8 +1655,14 @@ function fakeFetchUserInfoFromIdp(providerId, claims) {
1641
1655
  });
1642
1656
  break;
1643
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:
1644
1665
  default:
1645
- federatedId = rawId;
1646
1666
  response.rawUserInfo = JSON.stringify(claims);
1647
1667
  break;
1648
1668
  }
@@ -1798,16 +1818,24 @@ function parsePendingCredential(state, pendingCredential) {
1798
1818
  return { user, signInProvider };
1799
1819
  }
1800
1820
  function createTenant(state, reqBody) {
1821
+ var _a, _b, _c, _d, _e;
1801
1822
  if (!(state instanceof state_1.AgentProjectState)) {
1802
1823
  throw new errors_1.InternalError("INTERNAL_ERROR: Can only create tenant in agent project", "INTERNAL");
1803
1824
  }
1825
+ const mfaConfig = (_a = reqBody.mfaConfig) !== null && _a !== void 0 ? _a : {};
1826
+ if (!("state" in mfaConfig)) {
1827
+ mfaConfig.state = "DISABLED";
1828
+ }
1829
+ if (!("enabledProviders" in mfaConfig)) {
1830
+ mfaConfig.enabledProviders = [];
1831
+ }
1804
1832
  const tenant = {
1805
1833
  displayName: reqBody.displayName,
1806
- allowPasswordSignup: reqBody.allowPasswordSignup,
1807
- enableEmailLinkSignin: reqBody.enableEmailLinkSignin,
1808
- enableAnonymousUser: reqBody.enableAnonymousUser,
1809
- disableAuth: reqBody.disableAuth,
1810
- mfaConfig: reqBody.mfaConfig,
1834
+ allowPasswordSignup: (_b = reqBody.allowPasswordSignup) !== null && _b !== void 0 ? _b : false,
1835
+ enableEmailLinkSignin: (_c = reqBody.enableEmailLinkSignin) !== null && _c !== void 0 ? _c : false,
1836
+ enableAnonymousUser: (_d = reqBody.enableAnonymousUser) !== null && _d !== void 0 ? _d : false,
1837
+ disableAuth: (_e = reqBody.disableAuth) !== null && _e !== void 0 ? _e : false,
1838
+ mfaConfig: mfaConfig,
1811
1839
  tenantId: "",
1812
1840
  };
1813
1841
  return state.createTenant(tenant);
@@ -17,6 +17,7 @@ const lodash_1 = require("lodash");
17
17
  const handlers_1 = require("./handlers");
18
18
  const bodyParser = require("body-parser");
19
19
  const url_1 = require("url");
20
+ const jsonwebtoken_1 = require("jsonwebtoken");
20
21
  const apiSpec = apiSpec_1.default;
21
22
  const API_SPEC_PATH = "/emulator/openapi.json";
22
23
  const AUTH_HEADER_PREFIX = "bearer ";
@@ -70,6 +71,10 @@ async function createApp(defaultProjectId, projectStateForId = new Map()) {
70
71
  const app = express();
71
72
  app.set("json spaces", 2);
72
73
  app.use(cors({ origin: true }));
74
+ app.delete("*", (req, _, next) => {
75
+ delete req.headers["content-type"];
76
+ next();
77
+ });
73
78
  app.get("/", (req, res) => {
74
79
  return res.json({
75
80
  authEmulator: {
@@ -334,7 +339,7 @@ function toExegesisController(ops, getProjectStateById) {
334
339
  }
335
340
  function toExegesisOperation(operation) {
336
341
  return (ctx) => {
337
- var _a, _b, _c, _d, _e;
342
+ var _a, _b, _c, _d, _e, _f, _g;
338
343
  let targetProjectId = ctx.params.path.targetProjectId || ((_a = ctx.requestBody) === null || _a === void 0 ? void 0 : _a.targetProjectId);
339
344
  if (targetProjectId) {
340
345
  if ((_b = ctx.api.operationObject.security) === null || _b === void 0 ? void 0 : _b.some((sec) => sec.Oauth2)) {
@@ -344,10 +349,19 @@ function toExegesisController(ops, getProjectStateById) {
344
349
  else {
345
350
  targetProjectId = ctx.user;
346
351
  }
352
+ let targetTenantId = undefined;
347
353
  if (ctx.params.path.tenantId && ((_d = ctx.requestBody) === null || _d === void 0 ? void 0 : _d.tenantId)) {
348
354
  errors_2.assert(ctx.params.path.tenantId === ctx.requestBody.tenantId, "TENANT_ID_MISMATCH");
349
355
  }
350
- const targetTenantId = ctx.params.path.tenantId || ((_e = ctx.requestBody) === null || _e === void 0 ? void 0 : _e.tenantId);
356
+ targetTenantId = ctx.params.path.tenantId || ((_e = ctx.requestBody) === null || _e === void 0 ? void 0 : _e.tenantId);
357
+ if ((_f = ctx.requestBody) === null || _f === void 0 ? void 0 : _f.idToken) {
358
+ const idToken = (_g = ctx.requestBody) === null || _g === void 0 ? void 0 : _g.idToken;
359
+ const decoded = jsonwebtoken_1.decode(idToken, { complete: true });
360
+ if ((decoded === null || decoded === void 0 ? void 0 : decoded.payload.firebase.tenant) && targetTenantId) {
361
+ errors_2.assert((decoded === null || decoded === void 0 ? void 0 : decoded.payload.firebase.tenant) === targetTenantId, "TENANT_ID_MISMATCH");
362
+ }
363
+ targetTenantId = targetTenantId || (decoded === null || decoded === void 0 ? void 0 : decoded.payload.firebase.tenant);
364
+ }
351
365
  return operation(getProjectStateById(targetProjectId, targetTenantId), ctx.requestBody, ctx);
352
366
  };
353
367
  }
@@ -459,7 +459,17 @@ class AgentProjectState extends ProjectState {
459
459
  }
460
460
  getTenantProject(tenantId) {
461
461
  if (!this.tenantProjectForTenantId.has(tenantId)) {
462
- this.createTenantWithTenantId(tenantId, { tenantId });
462
+ this.createTenantWithTenantId(tenantId, {
463
+ tenantId,
464
+ allowPasswordSignup: true,
465
+ disableAuth: false,
466
+ mfaConfig: {
467
+ state: "ENABLED",
468
+ enabledProviders: ["PHONE_SMS"],
469
+ },
470
+ enableAnonymousUser: true,
471
+ enableEmailLinkSignin: true,
472
+ });
463
473
  }
464
474
  return this.tenantProjectForTenantId.get(tenantId);
465
475
  }
@@ -525,34 +535,43 @@ class TenantProjectState extends ProjectState {
525
535
  return this._tenantConfig;
526
536
  }
527
537
  get allowPasswordSignup() {
528
- var _a;
529
- return (_a = this._tenantConfig.allowPasswordSignup) !== null && _a !== void 0 ? _a : true;
538
+ return this._tenantConfig.allowPasswordSignup;
530
539
  }
531
540
  get disableAuth() {
532
- var _a;
533
- return (_a = this._tenantConfig.disableAuth) !== null && _a !== void 0 ? _a : false;
541
+ return this._tenantConfig.disableAuth;
534
542
  }
535
543
  get mfaConfig() {
536
- var _a;
537
- return ((_a = this._tenantConfig.mfaConfig) !== null && _a !== void 0 ? _a : {
538
- state: "ENABLED",
539
- enabledProviders: ["PHONE_SMS"],
540
- });
544
+ return this._tenantConfig.mfaConfig;
541
545
  }
542
546
  get enableAnonymousUser() {
543
- var _a;
544
- return (_a = this._tenantConfig.enableAnonymousUser) !== null && _a !== void 0 ? _a : true;
547
+ return this._tenantConfig.enableAnonymousUser;
545
548
  }
546
549
  get enableEmailLinkSignin() {
547
- var _a;
548
- return (_a = this._tenantConfig.enableEmailLinkSignin) !== null && _a !== void 0 ? _a : true;
550
+ return this._tenantConfig.enableEmailLinkSignin;
549
551
  }
550
552
  delete() {
551
553
  this.parentProject.deleteTenant(this.tenantId);
552
554
  }
553
555
  updateTenant(update, updateMask) {
556
+ var _a, _b, _c, _d, _e;
554
557
  if (!updateMask) {
555
- this._tenantConfig = Object.assign(Object.assign({}, update), { tenantId: this.tenantId, name: this.tenantConfig.name });
558
+ const mfaConfig = (_a = update.mfaConfig) !== null && _a !== void 0 ? _a : {};
559
+ if (!("state" in mfaConfig)) {
560
+ mfaConfig.state = "DISABLED";
561
+ }
562
+ if (!("enabledProviders" in mfaConfig)) {
563
+ mfaConfig.enabledProviders = [];
564
+ }
565
+ this._tenantConfig = {
566
+ tenantId: this.tenantId,
567
+ name: this.tenantConfig.name,
568
+ allowPasswordSignup: (_b = update.allowPasswordSignup) !== null && _b !== void 0 ? _b : false,
569
+ disableAuth: (_c = update.disableAuth) !== null && _c !== void 0 ? _c : false,
570
+ mfaConfig: mfaConfig,
571
+ enableAnonymousUser: (_d = update.enableAnonymousUser) !== null && _d !== void 0 ? _d : false,
572
+ enableEmailLinkSignin: (_e = update.enableEmailLinkSignin) !== null && _e !== void 0 ? _e : false,
573
+ displayName: update.displayName,
574
+ };
556
575
  return this.tenantConfig;
557
576
  }
558
577
  const paths = updateMask.split(",");
@@ -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.'
@@ -172,6 +173,19 @@ function finishWithUser(urlEncodedIdToken) {
172
173
  // Avoid URLSearchParams for browser compatibility.
173
174
  url += '?providerId=' + encodeURIComponent(providerId);
174
175
  url += '&id_token=' + urlEncodedIdToken;
176
+
177
+ // Save reasonable defaults for SAML providers
178
+ if (isSamlProvider) {
179
+ var email = document.getElementById('email-input').value;
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,
@@ -50,15 +50,15 @@ exports.DownloadDetails = {
50
50
  },
51
51
  },
52
52
  ui: {
53
- version: "1.6.3",
54
- downloadPath: path.join(CACHE_DIR, "ui-v1.6.3.zip"),
55
- unzipDir: path.join(CACHE_DIR, "ui-v1.6.3"),
56
- binaryPath: path.join(CACHE_DIR, "ui-v1.6.3", "server.bundle.js"),
53
+ version: "1.6.4",
54
+ downloadPath: path.join(CACHE_DIR, "ui-v1.6.4.zip"),
55
+ unzipDir: path.join(CACHE_DIR, "ui-v1.6.4"),
56
+ binaryPath: path.join(CACHE_DIR, "ui-v1.6.4", "server.bundle.js"),
57
57
  opts: {
58
58
  cacheDir: CACHE_DIR,
59
- remoteUrl: "https://storage.googleapis.com/firebase-preview-drop/emulator/ui-v1.6.3.zip",
60
- expectedSize: 3757268,
61
- expectedChecksum: "153090a46072545aadeb307397cc8f45",
59
+ remoteUrl: "https://storage.googleapis.com/firebase-preview-drop/emulator/ui-v1.6.4.zip",
60
+ expectedSize: 3757300,
61
+ expectedChecksum: "20d4ee71e4ff7527b1843b6a8636142e",
62
62
  namePrefix: "ui",
63
63
  },
64
64
  },