firebase-tools 15.22.3 → 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 (49) hide show
  1. package/lib/accountExporter.js +1 -0
  2. package/lib/accountImporter.js +1 -0
  3. package/lib/apiv2.js +91 -32
  4. package/lib/apphosting/backend.js +11 -4
  5. package/lib/apphosting/localbuilds.js +50 -0
  6. package/lib/commands/functions-delete.js +5 -3
  7. package/lib/commands/functions-lifecycle-list.js +94 -0
  8. package/lib/commands/functions-lifecycle-run.js +29 -0
  9. package/lib/commands/index.js +3 -0
  10. package/lib/database/import.js +3 -3
  11. package/lib/dataconnect/webhook.js +1 -2
  12. package/lib/deploy/apphosting/prepare.js +100 -1
  13. package/lib/deploy/functions/backend.js +15 -1
  14. package/lib/deploy/functions/build.js +28 -0
  15. package/lib/deploy/functions/prepare.js +91 -2
  16. package/lib/deploy/functions/prompts.js +62 -0
  17. package/lib/deploy/functions/release/fabricator.js +79 -4
  18. package/lib/deploy/functions/release/index.js +42 -23
  19. package/lib/deploy/functions/release/lifecycle.js +113 -0
  20. package/lib/deploy/functions/release/planner.js +41 -5
  21. package/lib/deploy/functions/runtimes/discovery/index.js +4 -3
  22. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +50 -0
  23. package/lib/deploy/functions/runtimes/node/index.js +1 -2
  24. package/lib/deploy/functions/runtimes/python/index.js +1 -2
  25. package/lib/deploy/functions/validate.js +44 -1
  26. package/lib/deploy/hosting/uploader.js +2 -3
  27. package/lib/downloadUtils.js +3 -1
  28. package/lib/emulator/auth/operations.js +18 -8
  29. package/lib/emulator/downloadableEmulatorInfo.json +31 -31
  30. package/lib/emulator/storage/apis/gcloud.js +61 -0
  31. package/lib/emulator/storage/multipart.js +82 -2
  32. package/lib/emulator/taskQueue.js +3 -11
  33. package/lib/extensions/extensionsHelper.js +6 -4
  34. package/lib/firestore/api.js +3 -1
  35. package/lib/gcp/cloudtasks.js +4 -0
  36. package/lib/gcp/iam.js +68 -2
  37. package/lib/gcp/knownRoles.json +99 -0
  38. package/lib/gcp/resourceManager.js +42 -1
  39. package/lib/gemini/fdcExperience.js +2 -2
  40. package/lib/hosting/initMiddleware.js +1 -1
  41. package/lib/hosting/proxy.js +42 -20
  42. package/lib/management/apps.js +4 -2
  43. package/lib/profiler.js +1 -2
  44. package/lib/streamUtils.js +24 -0
  45. package/lib/track.js +1 -2
  46. package/lib/tsconfig.compile.tsbuildinfo +1 -1
  47. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  48. package/lib/utils.js +4 -21
  49. package/package.json +2 -4
@@ -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) {
@@ -646,7 +646,9 @@ class FirestoreApi {
646
646
  pageSize: limit,
647
647
  },
648
648
  });
649
- return res.body;
649
+ return {
650
+ operations: res.body?.operations || [],
651
+ };
650
652
  }
651
653
  async describeOperation(project, databaseId, operationName) {
652
654
  const url = `/projects/${project}/databases/${databaseId}/operations/${operationName}`;
@@ -7,6 +7,7 @@ exports.updateQueue = updateQueue;
7
7
  exports.upsertQueue = upsertQueue;
8
8
  exports.purgeQueue = purgeQueue;
9
9
  exports.deleteQueue = deleteQueue;
10
+ exports.enqueueTask = enqueueTask;
10
11
  exports.setIamPolicy = setIamPolicy;
11
12
  exports.getIamPolicy = getIamPolicy;
12
13
  exports.setEnqueuer = setEnqueuer;
@@ -77,6 +78,9 @@ async function purgeQueue(name) {
77
78
  async function deleteQueue(name) {
78
79
  await client.delete(name);
79
80
  }
81
+ async function enqueueTask(queueName, task) {
82
+ await client.post(`${queueName}/tasks`, { task });
83
+ }
80
84
  async function setIamPolicy(name, policy) {
81
85
  const res = await client.post(`${name}:setIamPolicy`, {
82
86
  policy,
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);
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.proxyRequestHandler = proxyRequestHandler;
4
4
  exports.errorRequestHandler = errorRequestHandler;
5
5
  const lodash_1 = require("lodash");
6
- const node_fetch_1 = require("node-fetch");
7
6
  const stream_1 = require("stream");
8
7
  const url_1 = require("url");
9
8
  const apiv2_1 = require("../apiv2");
@@ -41,7 +40,7 @@ function proxyRequestHandler(url, rewriteIdentifier, options = {}) {
41
40
  passThrough = new stream_1.PassThrough();
42
41
  req.pipe(passThrough);
43
42
  }
44
- const headers = new node_fetch_1.Headers({
43
+ const headers = new Headers({
45
44
  "X-Forwarded-Host": req.headers.host || "",
46
45
  "X-Original-Url": req.url || "",
47
46
  Pragma: "no-cache",
@@ -83,13 +82,14 @@ function proxyRequestHandler(url, rewriteIdentifier, options = {}) {
83
82
  });
84
83
  }
85
84
  catch (err) {
85
+ logger_1.logger.error("[PROXY ERROR]", err);
86
86
  const isAbortError = err instanceof error_1.FirebaseError && err.original?.name.includes("AbortError");
87
87
  const isTimeoutError = err instanceof error_1.FirebaseError &&
88
- err.original instanceof node_fetch_1.FetchError &&
89
- err.original.code === "ETIMEDOUT";
88
+ (err.original?.code === "ETIMEDOUT" ||
89
+ err.original?.cause?.code === "ETIMEDOUT");
90
90
  const isSocketTimeoutError = err instanceof error_1.FirebaseError &&
91
- err.original instanceof node_fetch_1.FetchError &&
92
- err.original.code === "ESOCKETTIMEDOUT";
91
+ (err.original?.code === "ESOCKETTIMEDOUT" ||
92
+ err.original?.cause?.code === "ESOCKETTIMEDOUT");
93
93
  if (isAbortError || isTimeoutError || isSocketTimeoutError) {
94
94
  res.statusCode = 504;
95
95
  return res.end("Timed out waiting for function to respond.\n");
@@ -97,38 +97,60 @@ function proxyRequestHandler(url, rewriteIdentifier, options = {}) {
97
97
  res.statusCode = 500;
98
98
  return res.end(`An internal error occurred while proxying for ${rewriteIdentifier}\n`);
99
99
  }
100
+ finally {
101
+ if (passThrough) {
102
+ passThrough.resume();
103
+ }
104
+ }
105
+ const resHeaders = {};
106
+ for (const [key, value] of proxyRes.response.headers.entries()) {
107
+ resHeaders[key.toLowerCase()] = value;
108
+ }
100
109
  if (proxyRes.status === 404) {
101
- const cascade = proxyRes.response.headers.get("x-cascade");
102
- if (options.forceCascade || (cascade && cascade.toUpperCase() === "PASS")) {
110
+ const cascade = resHeaders["x-cascade"];
111
+ if (options.forceCascade ||
112
+ (typeof cascade === "string" && cascade.toUpperCase() === "PASS")) {
103
113
  return next();
104
114
  }
105
115
  }
106
- if (!proxyRes.response.headers.get("cache-control")) {
107
- proxyRes.response.headers.set("cache-control", "private");
116
+ if (!resHeaders["cache-control"]) {
117
+ resHeaders["cache-control"] = "private";
108
118
  }
109
- const cc = proxyRes.response.headers.get("cache-control");
110
- if (cc && !cc.includes("private")) {
111
- proxyRes.response.headers.delete("set-cookie");
119
+ const cc = resHeaders["cache-control"];
120
+ if (typeof cc === "string" && !cc.includes("private")) {
121
+ delete resHeaders["set-cookie"];
112
122
  }
113
- proxyRes.response.headers.set("vary", makeVary(proxyRes.response.headers.get("vary")));
114
- const location = proxyRes.response.headers.get("location");
115
- if (location) {
123
+ const vary = resHeaders["vary"];
124
+ resHeaders["vary"] = makeVary(typeof vary === "string" ? vary : null);
125
+ const location = resHeaders["location"];
126
+ if (typeof location === "string" && location) {
116
127
  try {
117
128
  const locationURL = new url_1.URL(location);
118
129
  if (locationURL.origin === u.origin) {
119
130
  const unborkedLocation = location.replace(locationURL.origin, "");
120
- proxyRes.response.headers.set("location", unborkedLocation);
131
+ resHeaders["location"] = unborkedLocation;
121
132
  }
122
133
  }
123
134
  catch (e) {
124
135
  logger_1.logger.debug(`[hosting] had trouble parsing location header, but this may be okay: "${location}"`);
125
136
  }
126
137
  }
127
- for (const [key, value] of Object.entries(proxyRes.response.headers.raw())) {
128
- res.setHeader(key, value);
138
+ for (const key of Object.keys(resHeaders)) {
139
+ const value = resHeaders[key];
140
+ if (key === "set-cookie") {
141
+ if (typeof proxyRes.response.headers.getSetCookie === "function") {
142
+ res.setHeader(key, proxyRes.response.headers.getSetCookie());
143
+ }
144
+ else {
145
+ res.setHeader(key, value);
146
+ }
147
+ }
148
+ else {
149
+ res.setHeader(key, value);
150
+ }
129
151
  }
130
152
  res.statusCode = proxyRes.status;
131
- proxyRes.response.body.pipe(res);
153
+ proxyRes.body.pipe(res);
132
154
  };
133
155
  }
134
156
  function errorRequestHandler(error) {
@@ -84,9 +84,11 @@ https://firebase.google.com/docs/flutter/setup
84
84
  async function initiateIosAppCreation(options) {
85
85
  if (!options.nonInteractive) {
86
86
  options.displayName = options.displayName || (await getDisplayName());
87
+ const promptAppStoreId = !options.bundleId;
87
88
  options.bundleId = options.bundleId || (await (0, prompt_1.input)("Please specify your iOS app bundle ID:"));
88
- options.appStoreId =
89
- options.appStoreId || (await (0, prompt_1.input)("Please specify your iOS app App Store ID:"));
89
+ if (promptAppStoreId && !options.appStoreId) {
90
+ options.appStoreId = await (0, prompt_1.input)("Please specify your iOS app App Store ID:");
91
+ }
90
92
  }
91
93
  if (!options.bundleId) {
92
94
  throw new error_1.FirebaseError("Bundle ID for iOS app cannot be empty");
package/lib/profiler.js CHANGED
@@ -5,7 +5,6 @@ const fs = require("fs");
5
5
  const ora = require("ora");
6
6
  const readline = require("readline");
7
7
  const tmp = require("tmp");
8
- const abort_controller_1 = require("abort-controller");
9
8
  const apiv2_1 = require("./apiv2");
10
9
  const api_1 = require("./database/api");
11
10
  const logger_1 = require("./logger");
@@ -26,7 +25,7 @@ async function profiler(options) {
26
25
  color: "yellow",
27
26
  });
28
27
  const outputFormat = options.raw ? "RAW" : options.parent.json ? "JSON" : "TXT";
29
- const controller = new abort_controller_1.default();
28
+ const controller = new AbortController();
30
29
  const generateReport = () => {
31
30
  rl.close();
32
31
  spinner.stop();
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stringToStream = stringToStream;
4
+ exports.streamToString = streamToString;
5
+ const stream_1 = require("stream");
6
+ function stringToStream(text) {
7
+ if (!text) {
8
+ return undefined;
9
+ }
10
+ const s = new stream_1.Readable();
11
+ s.push(text);
12
+ s.push(null);
13
+ return s;
14
+ }
15
+ function streamToString(s) {
16
+ return new Promise((resolve, reject) => {
17
+ let b = "";
18
+ s.on("error", reject);
19
+ s.on("data", (d) => {
20
+ b += d.toString();
21
+ });
22
+ s.once("end", () => resolve(b));
23
+ });
24
+ }