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.
- package/lib/apiv2.js +91 -32
- package/lib/apphosting/backend.js +11 -4
- 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/functions/backend.js +9 -0
- package/lib/deploy/functions/build.js +3 -0
- package/lib/deploy/functions/prepare.js +90 -1
- package/lib/deploy/functions/prompts.js +62 -0
- package/lib/deploy/functions/release/fabricator.js +79 -4
- package/lib/deploy/functions/release/index.js +40 -25
- package/lib/deploy/functions/release/lifecycle.js +44 -51
- 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 +18 -1
- package/lib/deploy/functions/runtimes/node/index.js +1 -2
- package/lib/deploy/functions/runtimes/python/index.js +1 -2
- 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 +24 -24
- 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/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
package/lib/downloadUtils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
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.
|
|
58
|
-
"expectedSize":
|
|
59
|
-
"expectedChecksum": "
|
|
60
|
-
"expectedChecksumSHA256": "
|
|
61
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-amd64-v3.4.
|
|
62
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.
|
|
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.
|
|
66
|
-
"expectedSize":
|
|
67
|
-
"expectedChecksum": "
|
|
68
|
-
"expectedChecksumSHA256": "
|
|
69
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-arm64-v3.4.
|
|
70
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.
|
|
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.
|
|
74
|
-
"expectedSize":
|
|
75
|
-
"expectedChecksum": "
|
|
76
|
-
"expectedChecksumSHA256": "
|
|
77
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-amd64-v3.4.
|
|
78
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.
|
|
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.
|
|
82
|
-
"expectedSize":
|
|
83
|
-
"expectedChecksum": "
|
|
84
|
-
"expectedChecksumSHA256": "
|
|
85
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-amd64-v3.4.
|
|
86
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.
|
|
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
|
|
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/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);
|