firebase-tools 15.15.0 → 15.16.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 (54) hide show
  1. package/lib/agentSkills.js +3 -2
  2. package/lib/apphosting/backend.js +7 -19
  3. package/lib/apphosting/localbuilds.js +32 -4
  4. package/lib/apphosting/prompts.js +32 -11
  5. package/lib/apphosting/secrets/index.js +43 -0
  6. package/lib/apphosting/yaml.js +2 -4
  7. package/lib/bin/firebase.js +10 -0
  8. package/lib/bin/mcp.js +12 -1
  9. package/lib/commands/apphosting-backends-create.js +4 -10
  10. package/lib/commands/dataconnect-sdk-generate.js +2 -2
  11. package/lib/commands/deploy.js +6 -1
  12. package/lib/config.js +1 -0
  13. package/lib/dataconnect/build.js +6 -0
  14. package/lib/dataconnect/names.js +1 -1
  15. package/lib/deploy/apphosting/prepare.js +5 -2
  16. package/lib/deploy/firestore/prepare.js +1 -1
  17. package/lib/deploy/functions/backend.js +22 -14
  18. package/lib/deploy/functions/prepare.js +8 -2
  19. package/lib/deploy/functions/release/fabricator.js +26 -14
  20. package/lib/deploy/functions/runtimes/python/index.js +3 -0
  21. package/lib/deploy/functions/runtimes/supported/types.js +6 -0
  22. package/lib/deploy/hosting/prepare.js +1 -0
  23. package/lib/emulator/adminSdkConfig.js +2 -1
  24. package/lib/emulator/apphosting/serve.js +2 -39
  25. package/lib/emulator/commandUtils.js +4 -1
  26. package/lib/emulator/controller.js +4 -3
  27. package/lib/emulator/dataconnectEmulator.js +7 -4
  28. package/lib/emulator/downloadableEmulatorInfo.json +30 -30
  29. package/lib/emulator/functionsEmulator.js +12 -6
  30. package/lib/emulator/functionsEmulatorRuntime.js +12 -5
  31. package/lib/emulator/functionsRuntimeWorker.js +6 -3
  32. package/lib/experiments.js +6 -1
  33. package/lib/frameworks/angular/index.js +6 -1
  34. package/lib/gcp/rules.js +8 -4
  35. package/lib/init/features/agentSkills.js +2 -2
  36. package/lib/init/features/apphosting.js +2 -8
  37. package/lib/init/features/dataconnect/index.js +26 -15
  38. package/lib/init/features/dataconnect/sdk.js +8 -3
  39. package/lib/init/features/firestore/rules.js +2 -1
  40. package/lib/init/features/functions/dart.js +2 -0
  41. package/lib/init/features/storage/rules.js +2 -1
  42. package/lib/mcp/apps/update_environment/mcp-app.js +138 -0
  43. package/lib/mcp/index.js +57 -2
  44. package/lib/mcp/resources/index.js +2 -0
  45. package/lib/mcp/resources/update_environment_ui.js +32 -0
  46. package/lib/mcp/tools/core/get_security_rules.js +2 -1
  47. package/lib/mcp/util.js +19 -0
  48. package/lib/rulesDeploy.js +35 -16
  49. package/lib/tsconfig.compile.tsbuildinfo +1 -1
  50. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  51. package/package.json +1 -1
  52. package/schema/firebase-config.json +4 -2
  53. package/templates/init/functions/dart/analysis_options.yaml +30 -0
  54. package/templates/init/functions/dart/pubspec.yaml +1 -0
@@ -26,11 +26,11 @@ const cloudbilling_1 = require("../../../gcp/cloudbilling");
26
26
  exports.FDC_APP_FOLDER = "FDC_APP_FOLDER";
27
27
  exports.FDC_SDK_FRAMEWORKS_ENV = "FDC_SDK_FRAMEWORKS";
28
28
  exports.FDC_SDK_PLATFORM_ENV = "FDC_SDK_PLATFORM";
29
- async function askQuestions(setup) {
29
+ async function askQuestions(setup, config, options) {
30
30
  const info = {
31
31
  apps: [],
32
32
  };
33
- info.apps = await chooseApp();
33
+ info.apps = await chooseApp(options);
34
34
  if (!info.apps.length) {
35
35
  const npxMissingWarning = (0, utils_1.commandExistsSync)("npx")
36
36
  ? ""
@@ -46,6 +46,8 @@ async function askQuestions(setup) {
46
46
  { name: `Flutter${flutterMissingWarning}`, value: "flutter" },
47
47
  { name: "skip", value: "skip" },
48
48
  ],
49
+ default: "skip",
50
+ nonInteractive: options?.nonInteractive,
49
51
  });
50
52
  try {
51
53
  switch (choice) {
@@ -69,7 +71,7 @@ async function askQuestions(setup) {
69
71
  setup.featureInfo = setup.featureInfo || {};
70
72
  setup.featureInfo.dataconnectSdk = info;
71
73
  }
72
- async function chooseApp() {
74
+ async function chooseApp(options) {
73
75
  let apps = dedupeAppsByPlatformAndDirectory(await (0, appUtils_1.detectApps)(cwd));
74
76
  if (apps.length) {
75
77
  (0, utils_1.logLabeledSuccess)("dataconnect", `Detected existing apps ${apps.map((a) => (0, appUtils_1.appDescription)(a)).join(", ")}`);
@@ -108,9 +110,12 @@ async function chooseApp() {
108
110
  checked: a.directory === ".",
109
111
  };
110
112
  });
113
+ const defaultApps = choices.filter((c) => c.checked).map((c) => c.value);
111
114
  const pickedApps = await (0, prompt_1.checkbox)({
112
115
  message: "Which apps do you want to set up SQL Connect SDKs in?",
113
116
  choices,
117
+ default: defaultApps.length > 0 ? defaultApps : [choices[0].value],
118
+ nonInteractive: options?.nonInteractive,
114
119
  validate: (choices) => {
115
120
  if (choices.length === 0) {
116
121
  return "Please choose at least one app.";
@@ -39,7 +39,8 @@ async function initRules(setup, config, info) {
39
39
  info.writeRules = await config.confirmWriteProjectFile(info.rulesFilename, info.rules);
40
40
  }
41
41
  async function getRulesFromConsole(projectId) {
42
- const name = await gcp.rules.getLatestRulesetName(projectId, "cloud.firestore");
42
+ const releases = await gcp.rules.listAllReleases(projectId);
43
+ const name = await gcp.rules.getLatestRulesetName(projectId, "cloud.firestore", releases);
43
44
  if (!name) {
44
45
  return null;
45
46
  }
@@ -8,9 +8,11 @@ const templates_1 = require("../../../templates");
8
8
  const PUBSPEC_TEMPLATE = (0, templates_1.readTemplateSync)("init/functions/dart/pubspec.yaml");
9
9
  const MAIN_TEMPLATE = (0, templates_1.readTemplateSync)("init/functions/dart/server.dart");
10
10
  const GITIGNORE_TEMPLATE = (0, templates_1.readTemplateSync)("init/functions/dart/_gitignore");
11
+ const ANALYSIS_OPTIONS_TEMPLATE = (0, templates_1.readTemplateSync)("init/functions/dart/analysis_options.yaml");
11
12
  async function setup(setup, config) {
12
13
  await config.askWriteProjectFile(`${setup.functions.source}/pubspec.yaml`, PUBSPEC_TEMPLATE);
13
14
  await config.askWriteProjectFile(`${setup.functions.source}/.gitignore`, GITIGNORE_TEMPLATE);
15
+ await config.askWriteProjectFile(`${setup.functions.source}/analysis_options.yaml`, ANALYSIS_OPTIONS_TEMPLATE);
14
16
  await config.askWriteProjectFile(`${setup.functions.source}/bin/server.dart`, MAIN_TEMPLATE);
15
17
  config.set("functions.runtime", (0, supported_1.latest)("dart"));
16
18
  config.set("functions.ignore", [".dart_tool", "build"]);
@@ -5,7 +5,8 @@ const gcp = require("../../../gcp");
5
5
  const utils = require("../../../utils");
6
6
  async function getRulesFromConsole(projectId) {
7
7
  const defaultBucket = await gcp.storage.getDefaultBucket(projectId);
8
- const name = await gcp.rules.getLatestRulesetName(projectId, "firebase.storage", defaultBucket);
8
+ const releases = await gcp.rules.listAllReleases(projectId);
9
+ const name = await gcp.rules.getLatestRulesetName(projectId, "firebase.storage", releases, defaultBucket);
9
10
  if (!name) {
10
11
  return null;
11
12
  }
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ext_apps_1 = require("@modelcontextprotocol/ext-apps");
4
+ const app = new ext_apps_1.App({ name: "Update Firebase Environment", version: "1.0.0" });
5
+ const projectListContainer = document.getElementById("project-list");
6
+ const searchInput = document.getElementById("search-input");
7
+ const submitBtn = document.getElementById("submit-btn");
8
+ const statusBox = document.getElementById("status-box");
9
+ let projects = [];
10
+ let filteredProjects = [];
11
+ let selectedProjectId = null;
12
+ const envProjectIdEl = document.getElementById("env-project-id");
13
+ const envUserEl = document.getElementById("env-user");
14
+ function showStatus(message, type) {
15
+ statusBox.textContent = message;
16
+ statusBox.className = `status ${type}`;
17
+ statusBox.style.display = "block";
18
+ }
19
+ function renderProjects() {
20
+ projectListContainer.innerHTML = "";
21
+ if (filteredProjects.length === 0) {
22
+ projectListContainer.innerHTML = `
23
+ <div class="dropdown-item" style="cursor: default;">
24
+ <div class="item-name">No projects found.</div>
25
+ </div>
26
+ `;
27
+ return;
28
+ }
29
+ filteredProjects.forEach((p) => {
30
+ const item = document.createElement("div");
31
+ item.className = "dropdown-item";
32
+ if (p.projectId === selectedProjectId) {
33
+ item.classList.add("selected");
34
+ }
35
+ const displayName = p.displayName || p.projectId;
36
+ const projectId = p.projectId;
37
+ item.innerHTML = `
38
+ <div class="item-name">${displayName}</div>
39
+ <div class="item-id">${projectId}</div>
40
+ `;
41
+ item.onclick = () => {
42
+ selectedProjectId = projectId;
43
+ submitBtn.disabled = false;
44
+ renderProjects();
45
+ };
46
+ projectListContainer.appendChild(item);
47
+ });
48
+ }
49
+ searchInput.oninput = () => {
50
+ const query = searchInput.value.toLowerCase().trim();
51
+ if (query === "") {
52
+ filteredProjects = projects;
53
+ }
54
+ else {
55
+ filteredProjects = projects.filter((p) => {
56
+ const name = (p.displayName || p.projectId).toLowerCase();
57
+ const id = p.projectId.toLowerCase();
58
+ return name.includes(query) || id.includes(query);
59
+ });
60
+ }
61
+ renderProjects();
62
+ };
63
+ submitBtn.onclick = async () => {
64
+ if (!selectedProjectId)
65
+ return;
66
+ submitBtn.disabled = true;
67
+ showStatus(`Updating active project to ${selectedProjectId}...`, "info");
68
+ try {
69
+ const result = await app.callServerTool({
70
+ name: "firebase_update_environment",
71
+ arguments: { active_project: selectedProjectId },
72
+ });
73
+ const textContent = result.content?.find((c) => c.type === "text");
74
+ const text = textContent ? textContent.text : "Update complete.";
75
+ if (result.isError) {
76
+ showStatus(text, "error");
77
+ submitBtn.disabled = false;
78
+ }
79
+ else {
80
+ showStatus(text, "success");
81
+ }
82
+ }
83
+ catch (err) {
84
+ showStatus(`Error updating environment: ${err.message}`, "error");
85
+ submitBtn.disabled = false;
86
+ }
87
+ };
88
+ app.onhostcontextchanged = (ctx) => {
89
+ if (ctx.theme)
90
+ (0, ext_apps_1.applyDocumentTheme)(ctx.theme);
91
+ if (ctx.styles?.variables)
92
+ (0, ext_apps_1.applyHostStyleVariables)(ctx.styles.variables);
93
+ if (ctx.styles?.css?.fonts)
94
+ (0, ext_apps_1.applyHostFonts)(ctx.styles.css.fonts);
95
+ if (ctx.safeAreaInsets) {
96
+ const { top, right, bottom, left } = ctx.safeAreaInsets;
97
+ document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`;
98
+ }
99
+ };
100
+ (async () => {
101
+ try {
102
+ await app.connect();
103
+ showStatus("Connecting to server...", "info");
104
+ try {
105
+ const envResult = await app.callServerTool({
106
+ name: "firebase_get_environment",
107
+ arguments: {},
108
+ });
109
+ const envData = envResult.structuredContent;
110
+ if (envData) {
111
+ envProjectIdEl.textContent = envData.projectId || "<NONE>";
112
+ envUserEl.textContent = envData.authenticatedUser || "<NONE>";
113
+ }
114
+ }
115
+ catch (err) {
116
+ console.error("Failed to fetch environment:", err);
117
+ showStatus(`Failed to fetch environment: ${err.message}`, "error");
118
+ }
119
+ const result = await app.callServerTool({ name: "firebase_list_projects", arguments: {} });
120
+ const data = result.structuredContent;
121
+ if (data && data.projects) {
122
+ projects = data.projects;
123
+ filteredProjects = projects;
124
+ renderProjects();
125
+ showStatus("Projects loaded successfully.", "success");
126
+ setTimeout(() => {
127
+ if (statusBox.className === "status success")
128
+ statusBox.style.display = "none";
129
+ }, 3000);
130
+ }
131
+ else {
132
+ showStatus("No projects returned from server.", "error");
133
+ }
134
+ }
135
+ catch (err) {
136
+ showStatus(`Failed to load projects: ${err.message}`, "error");
137
+ }
138
+ })();
package/lib/mcp/index.js CHANGED
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FirebaseMcpServer = void 0;
4
4
  const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
5
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
7
+ const express_1 = require("express");
8
+ const cors_1 = require("cors");
6
9
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
10
  const crossSpawn = require("cross-spawn");
8
11
  const node_fs_1 = require("node:fs");
@@ -278,6 +281,9 @@ class FirebaseMcpServer {
278
281
  }
279
282
  const isBillingEnabled = projectId ? await this.safeCheckBillingEnabled(projectId) : false;
280
283
  const toolsCtx = this._createMcpContext(projectId, accountEmail, isBillingEnabled);
284
+ if (request.params._meta?.progressToken) {
285
+ toolsCtx.progressToken = request.params._meta.progressToken;
286
+ }
281
287
  try {
282
288
  const res = await tool.fn(toolArgs, toolsCtx);
283
289
  await this.trackGA4("mcp_tool_call", {
@@ -370,7 +376,56 @@ class FirebaseMcpServer {
370
376
  }
371
377
  return resolved.result;
372
378
  }
373
- async start() {
379
+ async start(options) {
380
+ if (options?.useSSE) {
381
+ const app = (0, express_1.default)();
382
+ app.use((0, cors_1.default)());
383
+ const port = options.port || 3000;
384
+ const transports = {};
385
+ app.get("/sse", async (req, res) => {
386
+ this.logger.debug(`[SSE] GET /sse connection attempt from ${req.ip}`);
387
+ try {
388
+ const transport = new sse_js_1.SSEServerTransport("/message", res);
389
+ const sessionId = transport.sessionId;
390
+ transports[sessionId] = transport;
391
+ this.logger.debug(`[SSE] Connected session ${sessionId}`);
392
+ await this.server.connect(transport);
393
+ this.logger.debug(`[SSE] Server connected to transport`);
394
+ await new Promise((resolve) => {
395
+ req.on("close", () => {
396
+ this.logger.debug(`[SSE] Session ${sessionId} disconnected`);
397
+ delete transports[sessionId];
398
+ resolve();
399
+ });
400
+ });
401
+ }
402
+ catch (err) {
403
+ this.logger.error(`[SSE] Connection error: ${err}`);
404
+ }
405
+ });
406
+ app.post("/message", async (req, res) => {
407
+ const sessionId = req.query.sessionId;
408
+ this.logger.debug(`[SSE] POST /message attempt for session ${sessionId}`);
409
+ const transport = transports[sessionId];
410
+ if (transport) {
411
+ try {
412
+ await transport.handlePostMessage(req, res);
413
+ this.logger.debug(`[SSE] Handled message for session ${sessionId}`);
414
+ }
415
+ catch (err) {
416
+ this.logger.error(`[SSE] Error handling message for session ${sessionId}: ${err}`);
417
+ }
418
+ }
419
+ else {
420
+ this.logger.error(`[SSE] Rejecting message: No active transport found for session ${sessionId}`);
421
+ res.status(400).send("No active SSE transport connection found for this session");
422
+ }
423
+ });
424
+ app.listen(port, "127.0.0.1", () => {
425
+ this.logger.info(`MCP Server running on HTTP/SSE mode at http://127.0.0.1:${port}`);
426
+ });
427
+ return;
428
+ }
374
429
  const transport = process.env.FIREBASE_MCP_DEBUG_LOG
375
430
  ? new logging_transport_1.LoggingStdioServerTransport(process.env.FIREBASE_MCP_DEBUG_LOG)
376
431
  : new stdio_js_1.StdioServerTransport();
@@ -384,7 +439,7 @@ class FirebaseMcpServer {
384
439
  this.logger.debug("[mcp] Error on billingInfo for " +
385
440
  projectId +
386
441
  ", failing open (assuming false): " +
387
- (e.message || e));
442
+ (e instanceof Error ? e.message : String(e)));
388
443
  return false;
389
444
  }
390
445
  }
@@ -15,6 +15,7 @@ const crashlytics_investigations_1 = require("./guides/crashlytics_investigation
15
15
  const track_1 = require("../../track");
16
16
  const crashlytics_issues_1 = require("./guides/crashlytics_issues");
17
17
  const crashlytics_reports_1 = require("./guides/crashlytics_reports");
18
+ const update_environment_ui_1 = require("./update_environment_ui");
18
19
  exports.resources = [
19
20
  app_id_1.app_id,
20
21
  crashlytics_investigations_1.crashlytics_investigations,
@@ -26,6 +27,7 @@ exports.resources = [
26
27
  init_firestore_rules_1.init_firestore_rules,
27
28
  init_auth_1.init_auth,
28
29
  init_hosting_1.init_hosting,
30
+ update_environment_ui_1.update_environment_ui,
29
31
  ];
30
32
  exports.resourceTemplates = [docs_1.docs];
31
33
  async function resolveResource(uri, ctx, track = true) {
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.update_environment_ui = void 0;
4
+ const resource_1 = require("../resource");
5
+ const path = require("path");
6
+ const fs = require("fs/promises");
7
+ const util_1 = require("../util");
8
+ const resourceUri = "ui://core/update_environment/mcp-app.html";
9
+ exports.update_environment_ui = (0, resource_1.resource)({
10
+ uri: resourceUri,
11
+ name: "Update Environment UI",
12
+ description: "Visual interface for selecting active Firebase project",
13
+ mimeType: util_1.RESOURCE_MIME_TYPE,
14
+ }, async () => {
15
+ try {
16
+ const htmlPath = path.join(__dirname, "../apps/update_environment/mcp-app.html");
17
+ const html = await fs.readFile(htmlPath, "utf-8");
18
+ return {
19
+ contents: [
20
+ {
21
+ uri: resourceUri,
22
+ mimeType: util_1.RESOURCE_MIME_TYPE,
23
+ text: html,
24
+ },
25
+ ],
26
+ };
27
+ }
28
+ catch (e) {
29
+ const message = e instanceof Error ? e.message : String(e);
30
+ throw new Error(`Failed to load Update Environment UI: ${message}`);
31
+ }
32
+ });
@@ -46,7 +46,8 @@ exports.get_security_rules = (0, tool_1.tool)("core", {
46
46
  storage: { productName: "Storage", releaseName: "firebase.storage" },
47
47
  };
48
48
  const { productName, releaseName } = serviceInfo[type];
49
- const rulesetName = await (0, rules_1.getLatestRulesetName)(projectId, releaseName);
49
+ const releases = await (0, rules_1.listAllReleases)(projectId);
50
+ const rulesetName = await (0, rules_1.getLatestRulesetName)(projectId, releaseName, releases);
50
51
  if (!rulesetName)
51
52
  return (0, util_1.mcpError)(`No active ${productName} rules were found in project '${projectId}'`);
52
53
  const rules = await (0, rules_1.getRulesetContent)(rulesetName);
package/lib/mcp/util.js CHANGED
@@ -1,10 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RESOURCE_MIME_TYPE = void 0;
3
4
  exports.toContent = toContent;
5
+ exports.applyAppMeta = applyAppMeta;
4
6
  exports.mcpError = mcpError;
5
7
  exports.checkFeatureActive = checkFeatureActive;
6
8
  exports.cleanSchema = cleanSchema;
7
9
  const js_yaml_1 = require("js-yaml");
10
+ const experiments = require("../experiments");
8
11
  const api_1 = require("../api");
9
12
  const ensureApiEnabled_1 = require("../ensureApiEnabled");
10
13
  const timeout_1 = require("../timeout");
@@ -25,8 +28,23 @@ function toContent(data, options) {
25
28
  const suffix = options?.contentSuffix || "";
26
29
  return {
27
30
  content: [{ type: "text", text: `${prefix}${text}${suffix}` }],
31
+ structuredContent: data,
28
32
  };
29
33
  }
34
+ function applyAppMeta(result, resourceUri) {
35
+ if (experiments.isEnabled("mcpapps")) {
36
+ return {
37
+ ...result,
38
+ _meta: {
39
+ ...result._meta,
40
+ ui: {
41
+ resourceUri,
42
+ },
43
+ },
44
+ };
45
+ }
46
+ return result;
47
+ }
30
48
  function mcpError(message, code) {
31
49
  let errorMessage = "unknown error";
32
50
  if (message instanceof Error) {
@@ -223,3 +241,4 @@ function cleanSchema(schema) {
223
241
  const result = deepClean(schema, true);
224
242
  return result === null ? {} : result;
225
243
  }
244
+ exports.RESOURCE_MIME_TYPE = "application/vnd.mcp.ext-app+html";
@@ -30,10 +30,10 @@ class RulesDeploy {
30
30
  this.options = options;
31
31
  this.type = type;
32
32
  this.project = options.project;
33
- this.rulesFiles = {};
33
+ this.rulesFiles = [];
34
34
  this.rulesetNames = {};
35
35
  }
36
- addFile(path) {
36
+ addFile(path, databaseId) {
37
37
  const fullPath = this.options.config.path(path);
38
38
  let src;
39
39
  try {
@@ -43,15 +43,15 @@ class RulesDeploy {
43
43
  logger_1.logger.debug("[rules read error]", e.stack);
44
44
  throw new error_1.FirebaseError(`Error reading rules file ${(0, colorette_1.bold)(path)}`);
45
45
  }
46
- this.rulesFiles[path] = [{ name: path, content: src }];
46
+ this.rulesFiles.push({ path, files: [{ name: path, content: src }], databaseId });
47
47
  }
48
48
  async compile() {
49
- await Promise.all(Object.keys(this.rulesFiles).map((filename) => {
50
- return this.compileRuleset(filename, this.rulesFiles[filename]);
49
+ await Promise.all(this.rulesFiles.map((entry) => {
50
+ return this.compileRuleset(entry.path, entry.files);
51
51
  }));
52
52
  }
53
- async getCurrentRules(service) {
54
- const latestName = await gcp.rules.getLatestRulesetName(this.options.project, service);
53
+ async getCurrentRules(service, releases, databaseId) {
54
+ const latestName = await gcp.rules.getLatestRulesetName(this.options.project, service, releases, databaseId);
55
55
  let latestContent = null;
56
56
  if (latestName) {
57
57
  latestContent = await gcp.rules.getRulesetContent(latestName);
@@ -88,24 +88,37 @@ class RulesDeploy {
88
88
  }
89
89
  async createRulesets(service) {
90
90
  const createdRulesetNames = [];
91
- const { latestName: latestRulesetName, latestContent: latestRulesetContent } = await this.getCurrentRules(service);
92
- const newRulesetsByFilename = new Map();
93
- for (const [filename, files] of Object.entries(this.rulesFiles)) {
91
+ const releases = await gcp.rules.listAllReleases(this.options.project);
92
+ const newRulesetsByKey = new Map();
93
+ for (const entry of this.rulesFiles) {
94
+ const { path: filename, files, databaseId } = entry;
95
+ const normalizedDatabaseId = databaseId === "(default)" ? undefined : databaseId;
96
+ const { latestName: latestRulesetName, latestContent: latestRulesetContent } = await this.getCurrentRules(service, releases, normalizedDatabaseId);
97
+ const key = this.type === RulesetServiceType.FIREBASE_STORAGE
98
+ ? filename
99
+ : `${filename}:${databaseId || ""}`;
94
100
  if (latestRulesetName && _.isEqual(files, latestRulesetContent)) {
95
101
  utils.logLabeledBullet(RulesetType[this.type], `latest version of ${(0, colorette_1.bold)(filename)} already up to date, skipping upload...`);
96
- this.rulesetNames[filename] = latestRulesetName;
102
+ this.rulesetNames[key] = latestRulesetName;
97
103
  continue;
98
104
  }
99
105
  if (service === RulesetServiceType.FIREBASE_STORAGE) {
100
106
  await this.checkStorageRulesIamPermissions(files[0]?.content);
101
107
  }
102
108
  utils.logLabeledBullet(RulesetType[this.type], `uploading rules ${(0, colorette_1.bold)(filename)}...`);
103
- newRulesetsByFilename.set(filename, gcp.rules.createRuleset(this.options.project, files));
109
+ let attachmentPoint;
110
+ if (this.type === RulesetServiceType.CLOUD_FIRESTORE &&
111
+ databaseId &&
112
+ databaseId !== "(default)") {
113
+ const projectNumber = await (0, getProjectNumber_1.getProjectNumber)(this.options);
114
+ attachmentPoint = `firestore.googleapis.com/projects/${projectNumber}/databases/${databaseId}`;
115
+ }
116
+ newRulesetsByKey.set(key, gcp.rules.createRuleset(this.options.project, files, attachmentPoint));
104
117
  }
105
118
  try {
106
- await Promise.all(newRulesetsByFilename.values());
107
- for (const [filename, rulesetName] of newRulesetsByFilename) {
108
- this.rulesetNames[filename] = await rulesetName;
119
+ await Promise.all(newRulesetsByKey.values());
120
+ for (const [key, rulesetName] of newRulesetsByKey) {
121
+ this.rulesetNames[key] = await rulesetName;
109
122
  createdRulesetNames.push(await rulesetName);
110
123
  }
111
124
  }
@@ -141,7 +154,13 @@ class RulesDeploy {
141
154
  if (resourceName === RulesetServiceType.FIREBASE_STORAGE && !subResourceName) {
142
155
  throw new error_1.FirebaseError(`Cannot release resource type "${resourceName}"`);
143
156
  }
144
- await gcp.rules.updateOrCreateRelease(this.options.project, this.rulesetNames[filename], subResourceName ? `${resourceName}/${subResourceName}` : resourceName);
157
+ const key = this.type === RulesetServiceType.FIREBASE_STORAGE
158
+ ? filename
159
+ : `${filename}:${subResourceName || ""}`;
160
+ const releaseName = subResourceName && subResourceName !== "(default)"
161
+ ? `${resourceName}/${subResourceName}`
162
+ : resourceName;
163
+ await gcp.rules.updateOrCreateRelease(this.options.project, this.rulesetNames[key], releaseName);
145
164
  utils.logLabeledSuccess(RulesetType[this.type], `released rules ${(0, colorette_1.bold)(filename)} to ${(0, colorette_1.bold)(resourceName)}`);
146
165
  }
147
166
  async compileRuleset(filename, files) {