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.
- package/lib/accountExporter.js +1 -0
- package/lib/accountImporter.js +1 -0
- package/lib/apiv2.js +91 -32
- package/lib/apphosting/backend.js +11 -4
- package/lib/apphosting/localbuilds.js +50 -0
- package/lib/commands/functions-delete.js +5 -3
- package/lib/commands/functions-lifecycle-list.js +94 -0
- package/lib/commands/functions-lifecycle-run.js +29 -0
- package/lib/commands/index.js +3 -0
- package/lib/database/import.js +3 -3
- package/lib/dataconnect/webhook.js +1 -2
- package/lib/deploy/apphosting/prepare.js +100 -1
- package/lib/deploy/functions/backend.js +15 -1
- package/lib/deploy/functions/build.js +28 -0
- package/lib/deploy/functions/prepare.js +91 -2
- package/lib/deploy/functions/prompts.js +62 -0
- package/lib/deploy/functions/release/fabricator.js +79 -4
- package/lib/deploy/functions/release/index.js +42 -23
- package/lib/deploy/functions/release/lifecycle.js +113 -0
- package/lib/deploy/functions/release/planner.js +41 -5
- package/lib/deploy/functions/runtimes/discovery/index.js +4 -3
- package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +50 -0
- package/lib/deploy/functions/runtimes/node/index.js +1 -2
- package/lib/deploy/functions/runtimes/python/index.js +1 -2
- package/lib/deploy/functions/validate.js +44 -1
- package/lib/deploy/hosting/uploader.js +2 -3
- package/lib/downloadUtils.js +3 -1
- package/lib/emulator/auth/operations.js +18 -8
- package/lib/emulator/downloadableEmulatorInfo.json +31 -31
- package/lib/emulator/storage/apis/gcloud.js +61 -0
- package/lib/emulator/storage/multipart.js +82 -2
- package/lib/emulator/taskQueue.js +3 -11
- package/lib/extensions/extensionsHelper.js +6 -4
- package/lib/firestore/api.js +3 -1
- package/lib/gcp/cloudtasks.js +4 -0
- package/lib/gcp/iam.js +68 -2
- package/lib/gcp/knownRoles.json +99 -0
- package/lib/gcp/resourceManager.js +42 -1
- package/lib/gemini/fdcExperience.js +2 -2
- package/lib/hosting/initMiddleware.js +1 -1
- package/lib/hosting/proxy.js +42 -20
- package/lib/management/apps.js +4 -2
- package/lib/profiler.js +1 -2
- package/lib/streamUtils.js +24 -0
- package/lib/track.js +1 -2
- package/lib/tsconfig.compile.tsbuildinfo +1 -1
- package/lib/tsconfig.publish.tsbuildinfo +1 -1
- package/lib/utils.js +4 -21
- 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
|
|
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
|
|
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
|
|
208
|
-
const
|
|
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 (
|
|
518
|
-
if (response.ok) {
|
|
519
|
-
await
|
|
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/firestore/api.js
CHANGED
|
@@ -646,7 +646,9 @@ class FirestoreApi {
|
|
|
646
646
|
pageSize: limit,
|
|
647
647
|
},
|
|
648
648
|
});
|
|
649
|
-
return
|
|
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}`;
|
package/lib/gcp/cloudtasks.js
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
13
|
-
exports.PROMPT_GENERATE_SEED_DATA = "Create a mutation to populate the database with
|
|
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
|
|
34
|
+
for (const [key, value] of sdkRes.response.headers.entries()) {
|
|
35
35
|
res.setHeader(key, value);
|
|
36
36
|
}
|
|
37
37
|
sdkRes.body.pipe(res);
|
package/lib/hosting/proxy.js
CHANGED
|
@@ -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
|
|
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
|
|
89
|
-
|
|
88
|
+
(err.original?.code === "ETIMEDOUT" ||
|
|
89
|
+
err.original?.cause?.code === "ETIMEDOUT");
|
|
90
90
|
const isSocketTimeoutError = err instanceof error_1.FirebaseError &&
|
|
91
|
-
err.original
|
|
92
|
-
|
|
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 =
|
|
102
|
-
if (options.forceCascade ||
|
|
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 (!
|
|
107
|
-
|
|
116
|
+
if (!resHeaders["cache-control"]) {
|
|
117
|
+
resHeaders["cache-control"] = "private";
|
|
108
118
|
}
|
|
109
|
-
const cc =
|
|
110
|
-
if (cc && !cc.includes("private")) {
|
|
111
|
-
|
|
119
|
+
const cc = resHeaders["cache-control"];
|
|
120
|
+
if (typeof cc === "string" && !cc.includes("private")) {
|
|
121
|
+
delete resHeaders["set-cookie"];
|
|
112
122
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
|
128
|
-
|
|
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.
|
|
153
|
+
proxyRes.body.pipe(res);
|
|
132
154
|
};
|
|
133
155
|
}
|
|
134
156
|
function errorRequestHandler(error) {
|
package/lib/management/apps.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
+
}
|