firebase-tools 15.22.4 → 15.23.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 (42) hide show
  1. package/lib/apiv2.js +91 -32
  2. package/lib/apphosting/backend.js +11 -4
  3. package/lib/commands/functions-delete.js +5 -3
  4. package/lib/commands/functions-lifecycle-list.js +94 -0
  5. package/lib/commands/functions-lifecycle-run.js +29 -0
  6. package/lib/commands/index.js +3 -0
  7. package/lib/database/import.js +3 -3
  8. package/lib/dataconnect/webhook.js +1 -2
  9. package/lib/deploy/functions/backend.js +9 -0
  10. package/lib/deploy/functions/build.js +3 -0
  11. package/lib/deploy/functions/prepare.js +90 -1
  12. package/lib/deploy/functions/prompts.js +62 -0
  13. package/lib/deploy/functions/release/fabricator.js +79 -4
  14. package/lib/deploy/functions/release/index.js +40 -25
  15. package/lib/deploy/functions/release/lifecycle.js +44 -51
  16. package/lib/deploy/functions/release/planner.js +41 -5
  17. package/lib/deploy/functions/runtimes/discovery/index.js +4 -3
  18. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +18 -1
  19. package/lib/deploy/functions/runtimes/node/index.js +1 -2
  20. package/lib/deploy/functions/runtimes/python/index.js +1 -2
  21. package/lib/deploy/hosting/uploader.js +2 -3
  22. package/lib/downloadUtils.js +3 -1
  23. package/lib/emulator/auth/operations.js +18 -8
  24. package/lib/emulator/downloadableEmulatorInfo.json +24 -24
  25. package/lib/emulator/storage/apis/gcloud.js +61 -0
  26. package/lib/emulator/storage/multipart.js +82 -2
  27. package/lib/emulator/taskQueue.js +3 -11
  28. package/lib/extensions/extensionsHelper.js +6 -4
  29. package/lib/gcp/iam.js +68 -2
  30. package/lib/gcp/knownRoles.json +99 -0
  31. package/lib/gcp/resourceManager.js +42 -1
  32. package/lib/gemini/fdcExperience.js +2 -2
  33. package/lib/hosting/initMiddleware.js +1 -1
  34. package/lib/hosting/proxy.js +42 -20
  35. package/lib/management/apps.js +4 -2
  36. package/lib/profiler.js +1 -2
  37. package/lib/streamUtils.js +24 -0
  38. package/lib/track.js +1 -2
  39. package/lib/tsconfig.compile.tsbuildinfo +1 -1
  40. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  41. package/lib/utils.js +4 -21
  42. package/package.json +2 -4
@@ -10,6 +10,7 @@ const ProgressBar = require("progress");
10
10
  const tmp = require("tmp");
11
11
  const apiv2_1 = require("./apiv2");
12
12
  const error_1 = require("./error");
13
+ const streamUtils_1 = require("./streamUtils");
13
14
  async function downloadToTmp(remoteUrl, auth = false) {
14
15
  const u = new url_1.URL(remoteUrl);
15
16
  const c = new apiv2_1.Client({ urlPrefix: u.origin, auth });
@@ -24,7 +25,8 @@ async function downloadToTmp(remoteUrl, auth = false) {
24
25
  resolveOnHTTPError: true,
25
26
  });
26
27
  if (res.status !== 200) {
27
- throw new error_1.FirebaseError(`download failed, status ${res.status}: ${await res.response.text()}`, {
28
+ const errorText = await (0, streamUtils_1.streamToString)(res.body);
29
+ throw new error_1.FirebaseError(`download failed, status ${res.status}: ${errorText}`, {
28
30
  status: res.status,
29
31
  });
30
32
  }
@@ -6,8 +6,6 @@ exports.setAccountInfoImpl = setAccountInfoImpl;
6
6
  exports.parseBlockingFunctionJwt = parseBlockingFunctionJwt;
7
7
  const url_1 = require("url");
8
8
  const jsonwebtoken_1 = require("jsonwebtoken");
9
- const node_fetch_1 = require("node-fetch");
10
- const abort_controller_1 = require("abort-controller");
11
9
  const utils_1 = require("./utils");
12
10
  const errors_1 = require("./errors");
13
11
  const types_1 = require("../types");
@@ -2183,7 +2181,7 @@ async function fetchBlockingFunction(state, event, user, options = {}, oauthToke
2183
2181
  jwt,
2184
2182
  },
2185
2183
  };
2186
- const controller = new abort_controller_1.default();
2184
+ const controller = new AbortController();
2187
2185
  const timeout = setTimeout(() => {
2188
2186
  controller.abort();
2189
2187
  }, timeoutMs);
@@ -2193,11 +2191,23 @@ async function fetchBlockingFunction(state, event, user, options = {}, oauthToke
2193
2191
  let text;
2194
2192
  try {
2195
2193
  const signal = controller.signal;
2196
- signal.reason = "";
2197
- signal.throwIfAborted = () => {
2198
- throw new error_1.FirebaseError("Aborted");
2199
- };
2200
- const res = await (0, node_fetch_1.default)(url, {
2194
+ try {
2195
+ if (!("reason" in signal)) {
2196
+ signal.reason = "";
2197
+ }
2198
+ }
2199
+ catch (e) {
2200
+ }
2201
+ try {
2202
+ if (!("throwIfAborted" in signal)) {
2203
+ signal.throwIfAborted = () => {
2204
+ throw new error_1.FirebaseError("Aborted");
2205
+ };
2206
+ }
2207
+ }
2208
+ catch (e) {
2209
+ }
2210
+ const res = await fetch(url, {
2201
2211
  method: "POST",
2202
2212
  headers: { "Content-Type": "application/json" },
2203
2213
  body: JSON.stringify(reqBody),
@@ -54,36 +54,36 @@
54
54
  },
55
55
  "dataconnect": {
56
56
  "darwin": {
57
- "version": "3.4.14",
58
- "expectedSize": 32468976,
59
- "expectedChecksum": "d62fc079187a1adc2ceef7dc0e1a1e44",
60
- "expectedChecksumSHA256": "4bbf0ae04ca83dcc765804549d30f94d02c7a8b6baeef5f3c52104e771627e43",
61
- "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-amd64-v3.4.14",
62
- "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.14"
57
+ "version": "3.4.15",
58
+ "expectedSize": 32502016,
59
+ "expectedChecksum": "00f4b06ef1a6a70fbba7b2b8c1cd8eac",
60
+ "expectedChecksumSHA256": "8ea50a699023e5464b3306689e464716b8df7c51cadfe4a3db5d346981e1a916",
61
+ "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-amd64-v3.4.15",
62
+ "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.15"
63
63
  },
64
64
  "darwin_arm64": {
65
- "version": "3.4.14",
66
- "expectedSize": 30604754,
67
- "expectedChecksum": "306e88ebe314eec82ae44842aa87d20e",
68
- "expectedChecksumSHA256": "3eeaf0dbfd48d3d7cf761e256e8850bc6159dccc128dd59f87bbeeaaaa6a62c6",
69
- "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-arm64-v3.4.14",
70
- "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.14"
65
+ "version": "3.4.15",
66
+ "expectedSize": 30638050,
67
+ "expectedChecksum": "5b824b0d725e87f4f1820f8c3d50f2ce",
68
+ "expectedChecksumSHA256": "563ea6788e2291b82ea1cd0744f2594f0661b79277bc13e4da190b939a99293d",
69
+ "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-arm64-v3.4.15",
70
+ "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.15"
71
71
  },
72
72
  "win32": {
73
- "version": "3.4.14",
74
- "expectedSize": 32509952,
75
- "expectedChecksum": "c5cbd9482be394bf466d4328b975d88e",
76
- "expectedChecksumSHA256": "b9f14587c5d5f87bccd093e1dddc9f5bb2a77be007201028d9ee8339e79219f0",
77
- "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-amd64-v3.4.14",
78
- "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.14.exe"
73
+ "version": "3.4.15",
74
+ "expectedSize": 32543232,
75
+ "expectedChecksum": "dbdbdca5482d386a142cd602177e811d",
76
+ "expectedChecksumSHA256": "01b1408750b2f1612d3a99e89702a600803f9d8f77669e5ad2502651ed560796",
77
+ "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-amd64-v3.4.15",
78
+ "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.15.exe"
79
79
  },
80
80
  "linux": {
81
- "version": "3.4.14",
82
- "expectedSize": 31625400,
83
- "expectedChecksum": "506a09ca12b61df98f705728047219dd",
84
- "expectedChecksumSHA256": "257d7aa4f4dd1e4de9c9d3db9f7b7834323f0d53e658e6814a933b1ec73ca27d",
85
- "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-amd64-v3.4.14",
86
- "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.14"
81
+ "version": "3.4.15",
82
+ "expectedSize": 31654072,
83
+ "expectedChecksum": "b49d21a4704ddf9420fa58d0c233821d",
84
+ "expectedChecksumSHA256": "b152edccdf50f3e8e3d6904d6cd1437eb88cb7a447ede3e3dd5092d12262bdc1",
85
+ "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-amd64-v3.4.15",
86
+ "downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.15"
87
87
  }
88
88
  }
89
89
  }
@@ -320,6 +320,67 @@ function createCloudEndpoints(emulator) {
320
320
  }
321
321
  return (0, shared_1.sendFileBytes)(getObjectResponse.metadata, getObjectResponse.data, req, res);
322
322
  });
323
+ gcloudStorageAPI.post("/:bucketId", (req, res, next) => {
324
+ adminStorageLayer.createBucket(req.params.bucketId);
325
+ next();
326
+ }, async (req, res) => {
327
+ const contentTypeHeader = req.header("content-type");
328
+ if (!contentTypeHeader?.includes("multipart/form-data")) {
329
+ return res.status(400).send("Content-Type must be multipart/form-data");
330
+ }
331
+ try {
332
+ const bodyBuffer = await (0, request_1.reqBodyToBuffer)(req);
333
+ const formData = (0, multipart_1.parseFormDataMultipartRequest)(contentTypeHeader, bodyBuffer);
334
+ const keyPart = formData.find((p) => p.name === "key");
335
+ const filePart = formData.find((p) => p.type === "file");
336
+ if (keyPart?.type !== "field" || filePart?.type !== "file") {
337
+ return res.status(400).send("Missing 'key' or file.");
338
+ }
339
+ const metadata = {
340
+ contentType: filePart.contentType,
341
+ metadata: {},
342
+ };
343
+ const HEADER_MAP = {
344
+ "content-type": "contentType",
345
+ "cache-control": "cacheControl",
346
+ "content-disposition": "contentDisposition",
347
+ "content-encoding": "contentEncoding",
348
+ "content-language": "contentLanguage",
349
+ };
350
+ for (const part of formData) {
351
+ if (part.type === "file" || part.name === "key")
352
+ continue;
353
+ const key = part.name.toLowerCase();
354
+ if (key.startsWith("x-goog-meta-")) {
355
+ metadata.metadata[key.substring(12)] = part.value;
356
+ }
357
+ else if (HEADER_MAP[key]) {
358
+ metadata[HEADER_MAP[key]] = part.value.trim();
359
+ }
360
+ }
361
+ const upload = uploadService.multipartUpload({
362
+ bucketId: req.params.bucketId,
363
+ objectId: keyPart.value,
364
+ dataRaw: filePart.data,
365
+ metadata: metadata,
366
+ authorization: req.header("authorization"),
367
+ });
368
+ await adminStorageLayer.uploadObject(upload);
369
+ return res.sendStatus(204);
370
+ }
371
+ catch (err) {
372
+ if (err instanceof errors_1.ForbiddenError) {
373
+ return res.sendStatus(403);
374
+ }
375
+ if (err instanceof errors_1.NotFoundError) {
376
+ return res.sendStatus(404);
377
+ }
378
+ if (err instanceof Error) {
379
+ return res.status(400).send(err.message);
380
+ }
381
+ throw err;
382
+ }
383
+ });
323
384
  gcloudStorageAPI.post("/b/:bucketId/o/:objectId/:method(rewriteTo|copyTo)/b/:destBucketId/o/:destObjectId", (req, res, next) => {
324
385
  if (req.params.method === "rewriteTo" && req.query.rewriteToken) {
325
386
  return next();
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseObjectUploadMultipartRequest = parseObjectUploadMultipartRequest;
4
+ exports.parseFormDataMultipartRequest = parseFormDataMultipartRequest;
4
5
  const LINE_SEPARATOR = `\r\n`;
6
+ const LINE_SEPARATOR_BUFFER = Buffer.from(LINE_SEPARATOR);
5
7
  function splitBufferByDelimiter(buffer, delimiter, maxResults = -1) {
6
8
  let offset = 0;
7
9
  let nextDelimiterIndex = buffer.indexOf(delimiter, offset);
@@ -22,14 +24,21 @@ function splitBufferByDelimiter(buffer, delimiter, maxResults = -1) {
22
24
  bufferParts.push(Buffer.from(buffer.slice(offset)));
23
25
  return bufferParts;
24
26
  }
25
- function parseMultipartRequestBody(boundaryId, body) {
27
+ function splitMultipartIntoParts(boundaryId, body) {
26
28
  const cleanBoundaryId = boundaryId.replace(/^["'](.+(?=["']$))["']$/, "$1");
27
29
  const boundaryString = `--${cleanBoundaryId}`;
28
30
  const bodyParts = splitBufferByDelimiter(body, boundaryString).map((buf) => {
29
31
  return Buffer.from(buf.slice(2));
30
32
  });
33
+ if (bodyParts.length < 2) {
34
+ return [];
35
+ }
36
+ return bodyParts.slice(1, bodyParts.length - 1);
37
+ }
38
+ function parseMultipartRequestBody(boundaryId, body) {
39
+ const rawParts = splitMultipartIntoParts(boundaryId, body);
31
40
  const parsedParts = [];
32
- for (const bodyPart of bodyParts.slice(1, bodyParts.length - 1)) {
41
+ for (const bodyPart of rawParts) {
33
42
  parsedParts.push(parseMultipartRequestBodyPart(bodyPart));
34
43
  }
35
44
  return parsedParts;
@@ -60,3 +69,74 @@ function parseObjectUploadMultipartRequest(contentTypeHeader, body) {
60
69
  dataRaw: Buffer.from(parsedBody[1].dataRaw),
61
70
  };
62
71
  }
72
+ function parseMultipartFormDataBody(boundaryId, body) {
73
+ const rawParts = splitMultipartIntoParts(boundaryId, body);
74
+ const parsedParts = [];
75
+ for (const bodyPart of rawParts) {
76
+ parsedParts.push(parseMultipartFormDataBodyPart(bodyPart));
77
+ }
78
+ return parsedParts;
79
+ }
80
+ function parseMultipartFormDataBodyPart(part) {
81
+ const doubleCRLF = `${LINE_SEPARATOR}${LINE_SEPARATOR}`;
82
+ const sections = splitBufferByDelimiter(part, doubleCRLF, 2);
83
+ if (sections.length < 2) {
84
+ throw new Error("Failed to parse multipart part: Missing header-body separator.");
85
+ }
86
+ const headerBuffer = sections[0];
87
+ const bodyBuffer = sections[1];
88
+ if (bodyBuffer.byteLength < LINE_SEPARATOR_BUFFER.byteLength ||
89
+ !bodyBuffer
90
+ .slice(bodyBuffer.byteLength - LINE_SEPARATOR_BUFFER.byteLength)
91
+ .equals(LINE_SEPARATOR_BUFFER)) {
92
+ throw new Error("Failed to parse multipart part: Missing trailing line separator.");
93
+ }
94
+ const dataRaw = bodyBuffer.slice(0, bodyBuffer.byteLength - LINE_SEPARATOR_BUFFER.byteLength);
95
+ const headers = headerBuffer.toString().split(LINE_SEPARATOR);
96
+ let name = "";
97
+ let filename;
98
+ let contentType;
99
+ for (const h of headers) {
100
+ const lowerH = h.toLowerCase();
101
+ if (lowerH.startsWith("content-disposition:")) {
102
+ name = extractParam(h, "name") ?? name;
103
+ filename = extractParam(h, "filename") ?? filename;
104
+ }
105
+ else if (lowerH.startsWith("content-type:")) {
106
+ contentType = h.substring("content-type:".length).trim();
107
+ }
108
+ }
109
+ if (!name) {
110
+ throw new Error("Failed to parse multipart form-data. Missing 'name' in Content-Disposition header.");
111
+ }
112
+ if (filename) {
113
+ return {
114
+ type: "file",
115
+ name,
116
+ filename,
117
+ contentType: contentType || "application/octet-stream",
118
+ data: dataRaw,
119
+ };
120
+ }
121
+ else {
122
+ return {
123
+ type: "field",
124
+ name,
125
+ value: dataRaw.toString(),
126
+ };
127
+ }
128
+ }
129
+ function extractParam(header, param) {
130
+ const match = header.match(new RegExp(`\\b${param}=["']?([^"';]+)["']?`, "i"));
131
+ return match ? match[1] : undefined;
132
+ }
133
+ function parseFormDataMultipartRequest(contentTypeHeader, body) {
134
+ if (!contentTypeHeader.startsWith("multipart/form-data")) {
135
+ throw new Error(`Bad content type. ${contentTypeHeader}`);
136
+ }
137
+ const boundaryId = contentTypeHeader.split("boundary=")[1]?.split(";")[0]?.trim();
138
+ if (!boundaryId) {
139
+ throw new Error(`Bad content type. ${contentTypeHeader}`);
140
+ }
141
+ return parseMultipartFormDataBody(boundaryId, body);
142
+ }
@@ -1,11 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TaskQueue = exports.TaskStatus = exports.Queue = void 0;
4
- const abort_controller_1 = require("abort-controller");
5
4
  const emulatorLogger_1 = require("./emulatorLogger");
6
5
  const types_1 = require("./types");
7
- const error_1 = require("../error");
8
- const node_fetch_1 = require("node-fetch");
9
6
  class Node {
10
7
  constructor(data) {
11
8
  this.data = data;
@@ -204,17 +201,12 @@ class TaskQueue {
204
201
  if (emulatedTask.metadata.previousResponse) {
205
202
  headers["X-CloudTasks-TaskPreviousResponse"] = `${emulatedTask.metadata.previousResponse}`;
206
203
  }
207
- const controller = new abort_controller_1.default();
208
- const signal = controller.signal;
209
- signal.reason = "";
210
- signal.throwIfAborted = () => {
211
- throw new error_1.FirebaseError("Aborted");
212
- };
213
- const request = (0, node_fetch_1.default)(emulatedTask.task.httpRequest.url, {
204
+ const controller = new AbortController();
205
+ const request = fetch(emulatedTask.task.httpRequest.url, {
214
206
  method: "POST",
215
207
  headers: headers,
216
208
  body: JSON.stringify(emulatedTask.task.httpRequest.body),
217
- signal: signal,
209
+ signal: controller.signal,
218
210
  });
219
211
  const dispatchDeadline = emulatedTask.task.dispatchDeadline;
220
212
  const dispatchDeadlineSeconds = dispatchDeadline
@@ -34,8 +34,8 @@ const ora = require("ora");
34
34
  const semver = require("semver");
35
35
  const tmp = require("tmp");
36
36
  const fs = require("fs-extra");
37
- const node_fetch_1 = require("node-fetch");
38
37
  const path = require("path");
38
+ const stream_1 = require("stream");
39
39
  const marked_1 = require("marked");
40
40
  const marked_terminal_1 = require("marked-terminal");
41
41
  const unzip_1 = require("./../unzip");
@@ -514,9 +514,11 @@ async function fetchExtensionSource(repoUri, sourceRef, extensionRoot) {
514
514
  const tempDirectory = tmp.dirSync({ unsafeCleanup: true });
515
515
  const archiveErrorMessage = `Failed to extract archive from ${clc.bold(archiveUri)}. Please check that the repo is public and that the source ref is valid.`;
516
516
  try {
517
- const response = await (0, node_fetch_1.default)(archiveUri);
518
- if (response.ok) {
519
- await response.body.pipe((0, unzip_1.createUnzipTransform)(tempDirectory.name)).promise();
517
+ const response = await fetch(archiveUri);
518
+ if (response.ok && response.body) {
519
+ await stream_1.Readable.fromWeb(response.body)
520
+ .pipe((0, unzip_1.createUnzipTransform)(tempDirectory.name))
521
+ .promise();
520
522
  }
521
523
  }
522
524
  catch (err) {
package/lib/gcp/iam.js CHANGED
@@ -11,10 +11,16 @@ exports.testResourceIamPermissions = testResourceIamPermissions;
11
11
  exports.testIamPermissions = testIamPermissions;
12
12
  exports.mergeBindings = mergeBindings;
13
13
  exports.printManualIamConfig = printManualIamConfig;
14
+ exports.getRoleName = getRoleName;
15
+ exports.generateManagedServiceAccountName = generateManagedServiceAccountName;
16
+ exports.computeRolesEtag = computeRolesEtag;
14
17
  const api_1 = require("../api");
15
18
  const logger_1 = require("../logger");
16
19
  const apiv2_1 = require("../apiv2");
17
20
  const utils = require("../utils");
21
+ const error_1 = require("../error");
22
+ const knownRoles = require("./knownRoles.json");
23
+ const crypto = require("crypto");
18
24
  const apiClient = new apiv2_1.Client({ urlPrefix: (0, api_1.iamOrigin)(), apiVersion: "v1" });
19
25
  function getDefaultCloudBuildServiceAgent(projectNumber) {
20
26
  return `${projectNumber}@cloudbuild.gserviceaccount.com`;
@@ -30,11 +36,17 @@ async function createServiceAccount(projectId, accountId, description, displayNa
30
36
  return response.body;
31
37
  }
32
38
  async function getServiceAccount(projectId, serviceAccountName) {
33
- const response = await apiClient.get(`/projects/${projectId}/serviceAccounts/${serviceAccountName}@${projectId}.iam.gserviceaccount.com`);
39
+ const email = serviceAccountName.includes("@")
40
+ ? serviceAccountName
41
+ : `${serviceAccountName}@${projectId}.iam.gserviceaccount.com`;
42
+ const response = await apiClient.get(`/projects/${projectId}/serviceAccounts/${email}`);
34
43
  return response.body;
35
44
  }
36
45
  async function createServiceAccountKey(projectId, serviceAccountName) {
37
- const response = await apiClient.post(`/projects/${projectId}/serviceAccounts/${serviceAccountName}@${projectId}.iam.gserviceaccount.com/keys`, {
46
+ const email = serviceAccountName.includes("@")
47
+ ? serviceAccountName
48
+ : `${serviceAccountName}@${projectId}.iam.gserviceaccount.com`;
49
+ const response = await apiClient.post(`/projects/${projectId}/serviceAccounts/${email}/keys`, {
38
50
  keyAlgorithm: "KEY_ALG_UNSPECIFIED",
39
51
  privateKeyType: "TYPE_GOOGLE_CREDENTIALS_FILE",
40
52
  });
@@ -109,3 +121,57 @@ function printManualIamConfig(requiredBindings, projectId, prefix) {
109
121
  }
110
122
  }
111
123
  }
124
+ async function getRoleName(role) {
125
+ const map = knownRoles;
126
+ const title = map[role];
127
+ if (title) {
128
+ return title;
129
+ }
130
+ try {
131
+ const roleDetails = await getRole(role);
132
+ return roleDetails.title || role;
133
+ }
134
+ catch (err) {
135
+ return role;
136
+ }
137
+ }
138
+ async function generateManagedServiceAccountName(projectId, prefix) {
139
+ const maxAttempts = 10;
140
+ for (let i = 0; i < maxAttempts; i++) {
141
+ const randomSuffix = Math.floor(Math.random() * 10000000000)
142
+ .toString()
143
+ .padStart(10, "0");
144
+ const accountId = `${prefix}-${randomSuffix}`;
145
+ try {
146
+ await getServiceAccount(projectId, accountId);
147
+ }
148
+ catch (err) {
149
+ if ((0, error_1.getErrStatus)(err) === 404) {
150
+ return accountId;
151
+ }
152
+ throw err;
153
+ }
154
+ await utils.sleep(200);
155
+ }
156
+ throw new error_1.FirebaseError("Failed to generate a unique service account name after 10 attempts.");
157
+ }
158
+ function computeRolesEtag(roles, existingSalt) {
159
+ const BASE38_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_";
160
+ let salt = existingSalt;
161
+ if (!salt) {
162
+ salt = String.fromCharCode(97 + Math.floor(Math.random() * 26));
163
+ for (let i = 0; i < 9; i++) {
164
+ salt += BASE38_CHARS[Math.floor(Math.random() * 38)];
165
+ }
166
+ }
167
+ const sorted = Array.from(roles).sort();
168
+ const hashBuffer = crypto
169
+ .createHash("sha256")
170
+ .update(salt + sorted.join(","))
171
+ .digest();
172
+ let hashStr = "";
173
+ for (const byte of hashBuffer) {
174
+ hashStr += BASE38_CHARS[byte % 38];
175
+ }
176
+ return `${salt}-${hashStr.substring(0, 52)}`;
177
+ }
@@ -0,0 +1,99 @@
1
+ {
2
+ "roles/actions.Admin": "Actions Admin",
3
+ "roles/actions.Viewer": "Actions Viewer",
4
+ "roles/bigquery.dataEditor": "BigQuery Data Editor",
5
+ "roles/bigquery.user": "BigQuery User",
6
+ "roles/cloudconfig.admin": "Firebase Remote Config Admin",
7
+ "roles/cloudconfig.viewer": "Firebase Remote Config Viewer",
8
+ "roles/clouddeploymentmanager.serviceAgent": "Cloud Deployment Manager Service Agent",
9
+ "roles/cloudfunctions.admin": "Cloud Functions Admin",
10
+ "roles/cloudfunctions.developer": "Cloud Functions Developer",
11
+ "roles/cloudfunctions.invoker": "Cloud Functions Invoker",
12
+ "roles/cloudtasks.admin": "Cloud Tasks Admin",
13
+ "roles/cloudtasks.enqueuer": "Cloud Tasks Enqueuer",
14
+ "roles/cloudtasks.viewer": "Cloud Tasks Viewer",
15
+ "roles/cloudtestservice.testAdmin": "Firebase Test Lab Admin",
16
+ "roles/cloudtestservice.testViewer": "Firebase Test Lab Viewer",
17
+ "roles/composer.serviceAgent": "Cloud Composer API Service Agent",
18
+ "roles/dataflow.serviceAgent": "Cloud Dataflow Service Agent",
19
+ "roles/datafusion.serviceAgent": "Cloud Data Fusion API Service Agent",
20
+ "roles/datapipelines.serviceAgent": "Datapipelines Service Agent",
21
+ "roles/dataplex.serviceAgent": "Cloud Dataplex Service Agent",
22
+ "roles/dataproc.serviceAgent": "Dataproc Service Agent",
23
+ "roles/datastore.user": "Cloud Datastore User",
24
+ "roles/dlp.serviceAgent": "DLP API Service Agent",
25
+ "roles/editor": "Editor",
26
+ "roles/eventarc.admin": "Eventarc Admin",
27
+ "roles/eventarc.eventReceiver": "Eventarc Event Receiver",
28
+ "roles/eventarc.viewer": "Eventarc Viewer",
29
+ "roles/firebase.admin": "Firebase Admin",
30
+ "roles/firebase.analyticsAdmin": "Firebase Analytics Admin",
31
+ "roles/firebase.analyticsViewer": "Firebase Analytics Viewer",
32
+ "roles/firebase.developAdmin": "Firebase Develop Admin",
33
+ "roles/firebase.developViewer": "Firebase Develop Viewer",
34
+ "roles/firebase.editor": "Firebase Editor",
35
+ "roles/firebase.growthAdmin": "Firebase Grow Admin",
36
+ "roles/firebase.growthViewer": "Firebase Grow Viewer",
37
+ "roles/firebase.managementServiceAgent": "Firebase Service Management Service Agent",
38
+ "roles/firebase.qualityAdmin": "Firebase Quality Admin",
39
+ "roles/firebase.qualityViewer": "Firebase Quality Viewer",
40
+ "roles/firebase.sdkAdminServiceAgent": "Firebase Admin SDK Administrator Service Agent",
41
+ "roles/firebase.sdkProvisioningServiceAgent": "Firebase SDK Provisioning Service Agent",
42
+ "roles/firebase.viewer": "Firebase Viewer",
43
+ "roles/firebaseabt.admin": "Firebase A/B Testing Admin",
44
+ "roles/firebaseabt.viewer": "Firebase A/B Testing Viewer",
45
+ "roles/firebaseappdistro.admin": "Firebase App Distribution Admin",
46
+ "roles/firebaseappdistro.viewer": "Firebase App Distribution Viewer",
47
+ "roles/firebaseauth.admin": "Firebase Authentication Admin",
48
+ "roles/firebaseauth.editor": "Firebase Authentication editor",
49
+ "roles/firebaseauth.viewer": "Firebase Authentication Viewer",
50
+ "roles/firebasecloudmessaging.viewer": "Firebase Cloud Messaging API Viewer",
51
+ "roles/firebasecrash.symbolMappingsAdmin": "Firebase Crash Symbol Uploader",
52
+ "roles/firebasecrashlytics.admin": "Firebase Crashlytics Admin",
53
+ "roles/firebasecrashlytics.viewer": "Firebase Crashlytics Viewer",
54
+ "roles/firebasedatabase.admin": "Firebase Realtime Database Admin",
55
+ "roles/firebasedatabase.viewer": "Firebase Realtime Database Viewer",
56
+ "roles/firebasedynamiclinks.admin": "Firebase Dynamic Links Admin",
57
+ "roles/firebasedynamiclinks.editor": "Firebasedynamiclinks Editor",
58
+ "roles/firebasedynamiclinks.viewer": "Firebase Dynamic Links Viewer",
59
+ "roles/firebaseextensions.developer": "Firebase Extensions Developer",
60
+ "roles/firebaseextensions.editor": "Firebaseextensions Editor",
61
+ "roles/firebaseextensions.viewer": "Firebase Extensions Viewer",
62
+ "roles/firebaseextensionspublisher.admin": "Firebaseextensionspublisher Admin",
63
+ "roles/firebaseextensionspublisher.extensionsAdmin": "Firebase Extensions Publisher - Extensions Admin",
64
+ "roles/firebaseextensionspublisher.extensionsViewer": "Firebase Extensions Publisher - Extensions Viewer",
65
+ "roles/firebaseextensionspublisher.viewer": "Firebaseextensionspublisher Viewer",
66
+ "roles/firebasehosting.admin": "Firebase Hosting Admin",
67
+ "roles/firebasehosting.viewer": "Firebase Hosting Viewer",
68
+ "roles/firebaseinappmessaging.admin": "Firebase In-App Messaging Admin",
69
+ "roles/firebaseinappmessaging.viewer": "Firebase In-App Messaging Viewer",
70
+ "roles/firebaseml.admin": "Firebase ML Kit Admin",
71
+ "roles/firebaseml.viewer": "Firebase ML Kit Viewer",
72
+ "roles/firebasenotifications.admin": "Firebase Cloud Messaging Admin",
73
+ "roles/firebasenotifications.viewer": "Firebase Cloud Messaging Viewer",
74
+ "roles/firebaseperformance.admin": "Firebase Performance Reporting Admin",
75
+ "roles/firebaseperformance.viewer": "Firebase Performance Reporting Viewer",
76
+ "roles/firebasestorage.admin": "Cloud Storage for Firebase Admin",
77
+ "roles/iam.databasesAdmin": "Databases Admin",
78
+ "roles/iam.infrastructureAdmin": "Infrastructure Administrator",
79
+ "roles/iam.securityAdmin": "Security Admin",
80
+ "roles/iam.securityAuditor": "Security Auditor",
81
+ "roles/iam.securityReviewer": "Security Reviewer",
82
+ "roles/iam.supportUser": "Support User",
83
+ "roles/identitytoolkit.editor": "Identity Toolkit editor",
84
+ "roles/ml.serviceAgent": "AI Platform Service Agent",
85
+ "roles/oauthconfig.editor": "OAuth Config Editor",
86
+ "roles/oauthconfig.viewer": "OAuth Config Viewer",
87
+ "roles/owner": "Owner",
88
+ "roles/pubsub.admin": "Pub/Sub Admin",
89
+ "roles/pubsub.publisher": "Pub/Sub Publisher",
90
+ "roles/pubsub.subscriber": "Pub/Sub Subscriber",
91
+ "roles/pubsub.viewer": "Pub/Sub Viewer",
92
+ "roles/storage.admin": "Storage Admin",
93
+ "roles/storage.hmacKeyAdmin": "Storage HMAC Key Admin",
94
+ "roles/storage.objectAdmin": "Storage Object Admin",
95
+ "roles/storage.objectCreator": "Storage Object Creator",
96
+ "roles/storage.objectViewer": "Storage Object Viewer",
97
+ "roles/viewer": "Viewer",
98
+ "roles/visualinspection.serviceAgent": "Visual Inspection AI Service Agent"
99
+ }
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.firebaseRoles = void 0;
3
+ exports.addServiceAccountRoles = exports.firebaseRoles = void 0;
4
4
  exports.getIamPolicy = getIamPolicy;
5
5
  exports.setIamPolicy = setIamPolicy;
6
6
  exports.addServiceAccountToRoles = addServiceAccountToRoles;
7
7
  exports.serviceAccountHasRoles = serviceAccountHasRoles;
8
+ exports.removeServiceAccountRoles = removeServiceAccountRoles;
9
+ exports.getServiceAccountRoles = getServiceAccountRoles;
8
10
  const lodash_1 = require("lodash");
9
11
  const api_1 = require("../api");
10
12
  const apiv2_1 = require("../apiv2");
@@ -37,6 +39,7 @@ async function addServiceAccountToRoles(projectId, serviceAccountName, roles, sk
37
39
  : (0, iam_1.getServiceAccount)(projectId, serviceAccountName),
38
40
  getIamPolicy(projectId),
39
41
  ]);
42
+ projectPolicy.bindings = projectPolicy.bindings || [];
40
43
  const newMemberName = `serviceAccount:${fullServiceAccountName.split("/").pop()}`;
41
44
  roles.forEach((roleName) => {
42
45
  let bindingIndex = (0, lodash_1.findIndex)(projectPolicy.bindings, (binding) => binding.role === roleName);
@@ -61,6 +64,7 @@ async function serviceAccountHasRoles(projectId, serviceAccountName, roles, skip
61
64
  : (0, iam_1.getServiceAccount)(projectId, serviceAccountName),
62
65
  getIamPolicy(projectId),
63
66
  ]);
67
+ projectPolicy.bindings = projectPolicy.bindings || [];
64
68
  const memberName = `serviceAccount:${fullServiceAccountName.split("/").pop()}`;
65
69
  for (const roleName of roles) {
66
70
  const binding = projectPolicy.bindings.find((b) => b.role === roleName);
@@ -73,3 +77,40 @@ async function serviceAccountHasRoles(projectId, serviceAccountName, roles, skip
73
77
  }
74
78
  return true;
75
79
  }
80
+ async function removeServiceAccountRoles(projectId, serviceAccountEmail, rolesToRemove) {
81
+ const projectPolicy = await getIamPolicy(projectId);
82
+ const bindings = projectPolicy.bindings || [];
83
+ const memberName = `serviceAccount:${serviceAccountEmail}`;
84
+ let updated = false;
85
+ for (let i = bindings.length - 1; i >= 0; i--) {
86
+ const binding = bindings[i];
87
+ if (rolesToRemove.includes(binding.role)) {
88
+ const memberIndex = binding.members.indexOf(memberName);
89
+ if (memberIndex !== -1) {
90
+ binding.members.splice(memberIndex, 1);
91
+ updated = true;
92
+ if (binding.members.length === 0) {
93
+ bindings.splice(i, 1);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ if (updated) {
99
+ projectPolicy.bindings = bindings;
100
+ return await setIamPolicy(projectId, projectPolicy, "bindings");
101
+ }
102
+ return projectPolicy;
103
+ }
104
+ async function getServiceAccountRoles(projectId, serviceAccountEmail) {
105
+ const projectPolicy = await getIamPolicy(projectId);
106
+ const memberName = `serviceAccount:${serviceAccountEmail}`;
107
+ const roles = [];
108
+ const bindings = projectPolicy.bindings || [];
109
+ for (const binding of bindings) {
110
+ if (binding.members.includes(memberName)) {
111
+ roles.push(binding.role);
112
+ }
113
+ }
114
+ return roles;
115
+ }
116
+ exports.addServiceAccountRoles = addServiceAccountToRoles;
@@ -9,8 +9,8 @@ const api_1 = require("../api");
9
9
  const error_1 = require("../error");
10
10
  const logger_1 = require("../logger");
11
11
  const apiClient = new apiv2_1.Client({ urlPrefix: (0, api_1.dataconnectOrigin)(), auth: true });
12
- exports.PROMPT_GENERATE_CONNECTOR = "Create 4 operations for an app using the instance schema with proper authentication.";
13
- exports.PROMPT_GENERATE_SEED_DATA = "Create a mutation to populate the database with some seed data.";
12
+ exports.PROMPT_GENERATE_CONNECTOR = 'Generate at least one create (insert/upsert), delete, update, get (read by key), and list operation for each table defined in the schema. For user-specific data, use the \'auth.uid\' server value (e.g., id_expr: "auth.uid") or correct filters to secure user-owned data and require authentication using @auth(level: USER). For publicly accessible data, use @auth(level: PUBLIC, insecureReason: "why is it OK to be public?")';
13
+ exports.PROMPT_GENERATE_SEED_DATA = "Create a single mutation wrapped in a @transaction to populate the database with seed data. The mutation should call the insertMany action (e.g., table_insertMany) for all tables/entities defined in the schema, using fake data. Generate appropriate fake records (3 to 5 per table) conforming to the schema and including proper relationships/foreign keys.";
14
14
  function logCurl(method, path, body) {
15
15
  const url = `${(0, api_1.dataconnectOrigin)()}${path}`;
16
16
  const headers = [
@@ -31,7 +31,7 @@ function initMiddleware(init) {
31
31
  if (sdkRes.status === 404) {
32
32
  return next();
33
33
  }
34
- for (const [key, value] of Object.entries(sdkRes.response.headers.raw())) {
34
+ for (const [key, value] of sdkRes.response.headers.entries()) {
35
35
  res.setHeader(key, value);
36
36
  }
37
37
  sdkRes.body.pipe(res);