@prmichaelsen/firebase-admin-sdk-v8 2.0.21 → 2.1.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/dist/index.d.mts +239 -1
- package/dist/index.d.ts +239 -1
- package/dist/index.js +469 -3
- package/dist/index.mjs +460 -3
- package/package.json +1 -1
- package/resources/puppy.png +0 -0
package/dist/index.js
CHANGED
|
@@ -25,18 +25,27 @@ __export(index_exports, {
|
|
|
25
25
|
batchWrite: () => batchWrite,
|
|
26
26
|
clearConfig: () => clearConfig,
|
|
27
27
|
clearTokenCache: () => clearTokenCache,
|
|
28
|
+
createCustomToken: () => createCustomToken,
|
|
28
29
|
deleteDocument: () => deleteDocument,
|
|
30
|
+
deleteFile: () => deleteFile,
|
|
31
|
+
downloadFile: () => downloadFile,
|
|
32
|
+
fileExists: () => fileExists,
|
|
33
|
+
generateSignedUrl: () => generateSignedUrl,
|
|
29
34
|
getAdminAccessToken: () => getAdminAccessToken,
|
|
30
35
|
getAuth: () => getAuth,
|
|
31
36
|
getConfig: () => getConfig,
|
|
32
37
|
getDocument: () => getDocument,
|
|
38
|
+
getFileMetadata: () => getFileMetadata,
|
|
33
39
|
getProjectId: () => getProjectId,
|
|
34
40
|
getServiceAccount: () => getServiceAccount,
|
|
35
41
|
getUserFromToken: () => getUserFromToken,
|
|
36
42
|
initializeApp: () => initializeApp,
|
|
43
|
+
listFiles: () => listFiles,
|
|
37
44
|
queryDocuments: () => queryDocuments,
|
|
38
45
|
setDocument: () => setDocument,
|
|
46
|
+
signInWithCustomToken: () => signInWithCustomToken,
|
|
39
47
|
updateDocument: () => updateDocument,
|
|
48
|
+
uploadFile: () => uploadFile,
|
|
40
49
|
verifyIdToken: () => verifyIdToken
|
|
41
50
|
});
|
|
42
51
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -105,6 +114,20 @@ function getProjectId() {
|
|
|
105
114
|
"Firebase project ID not configured. Either call initializeApp({ projectId: ... }) or set FIREBASE_PROJECT_ID environment variable."
|
|
106
115
|
);
|
|
107
116
|
}
|
|
117
|
+
function getFirebaseApiKey() {
|
|
118
|
+
if (globalConfig.apiKey) {
|
|
119
|
+
return globalConfig.apiKey;
|
|
120
|
+
}
|
|
121
|
+
if (typeof process !== "undefined" && process.env) {
|
|
122
|
+
const apiKey = process.env.FIREBASE_API_KEY || process.env.PUBLIC_FIREBASE_API_KEY;
|
|
123
|
+
if (apiKey) {
|
|
124
|
+
return apiKey;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
throw new Error(
|
|
128
|
+
"Firebase API key not configured. Either call initializeApp({ apiKey: ... }) or set FIREBASE_API_KEY environment variable. Find your API key in Firebase Console > Project Settings > Web API Key."
|
|
129
|
+
);
|
|
130
|
+
}
|
|
108
131
|
|
|
109
132
|
// src/x509.ts
|
|
110
133
|
function decodeBase64(str) {
|
|
@@ -325,6 +348,105 @@ async function getUserFromToken(idToken) {
|
|
|
325
348
|
photoURL: decodedToken.picture || null
|
|
326
349
|
};
|
|
327
350
|
}
|
|
351
|
+
function base64UrlEncode(str) {
|
|
352
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
353
|
+
}
|
|
354
|
+
async function signWithPrivateKey(data, privateKey) {
|
|
355
|
+
const pemHeader = "-----BEGIN PRIVATE KEY-----";
|
|
356
|
+
const pemFooter = "-----END PRIVATE KEY-----";
|
|
357
|
+
const pemContents = privateKey.replace(pemHeader, "").replace(pemFooter, "").replace(/\s/g, "");
|
|
358
|
+
const binaryString = atob(pemContents);
|
|
359
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
360
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
361
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
362
|
+
}
|
|
363
|
+
const key = await crypto.subtle.importKey(
|
|
364
|
+
"pkcs8",
|
|
365
|
+
bytes,
|
|
366
|
+
{
|
|
367
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
368
|
+
hash: "SHA-256"
|
|
369
|
+
},
|
|
370
|
+
false,
|
|
371
|
+
["sign"]
|
|
372
|
+
);
|
|
373
|
+
const encoder = new TextEncoder();
|
|
374
|
+
const dataBytes = encoder.encode(data);
|
|
375
|
+
const signature = await crypto.subtle.sign(
|
|
376
|
+
"RSASSA-PKCS1-v1_5",
|
|
377
|
+
key,
|
|
378
|
+
dataBytes
|
|
379
|
+
);
|
|
380
|
+
return base64UrlEncode(String.fromCharCode(...new Uint8Array(signature)));
|
|
381
|
+
}
|
|
382
|
+
async function createCustomToken(uid, customClaims) {
|
|
383
|
+
const serviceAccount = getServiceAccount();
|
|
384
|
+
if (!uid || typeof uid !== "string") {
|
|
385
|
+
throw new Error("uid must be a non-empty string");
|
|
386
|
+
}
|
|
387
|
+
if (uid.length > 128) {
|
|
388
|
+
throw new Error("uid must be at most 128 characters");
|
|
389
|
+
}
|
|
390
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
391
|
+
const payload = {
|
|
392
|
+
iss: serviceAccount.client_email,
|
|
393
|
+
sub: serviceAccount.client_email,
|
|
394
|
+
aud: "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
|
|
395
|
+
iat: now,
|
|
396
|
+
exp: now + 3600,
|
|
397
|
+
// 1 hour
|
|
398
|
+
uid
|
|
399
|
+
};
|
|
400
|
+
if (customClaims) {
|
|
401
|
+
payload.claims = customClaims;
|
|
402
|
+
}
|
|
403
|
+
const header = {
|
|
404
|
+
alg: "RS256",
|
|
405
|
+
typ: "JWT"
|
|
406
|
+
};
|
|
407
|
+
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
408
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
409
|
+
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
|
|
410
|
+
const signature = await signWithPrivateKey(unsignedToken, serviceAccount.private_key);
|
|
411
|
+
return `${unsignedToken}.${signature}`;
|
|
412
|
+
}
|
|
413
|
+
async function signInWithCustomToken(customToken) {
|
|
414
|
+
if (!customToken || typeof customToken !== "string") {
|
|
415
|
+
throw new Error("customToken must be a non-empty string");
|
|
416
|
+
}
|
|
417
|
+
const apiKey = getFirebaseApiKey();
|
|
418
|
+
const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`;
|
|
419
|
+
const response = await fetch(url, {
|
|
420
|
+
method: "POST",
|
|
421
|
+
headers: {
|
|
422
|
+
"Content-Type": "application/json"
|
|
423
|
+
},
|
|
424
|
+
body: JSON.stringify({
|
|
425
|
+
token: customToken,
|
|
426
|
+
returnSecureToken: true
|
|
427
|
+
})
|
|
428
|
+
});
|
|
429
|
+
if (!response.ok) {
|
|
430
|
+
const errorText = await response.text();
|
|
431
|
+
let errorMessage = `Failed to sign in with custom token: ${response.status}`;
|
|
432
|
+
try {
|
|
433
|
+
const errorJson = JSON.parse(errorText);
|
|
434
|
+
if (errorJson.error && errorJson.error.message) {
|
|
435
|
+
errorMessage += ` - ${errorJson.error.message}`;
|
|
436
|
+
}
|
|
437
|
+
} catch {
|
|
438
|
+
errorMessage += ` - ${errorText}`;
|
|
439
|
+
}
|
|
440
|
+
throw new Error(errorMessage);
|
|
441
|
+
}
|
|
442
|
+
const result = await response.json();
|
|
443
|
+
return {
|
|
444
|
+
idToken: result.idToken,
|
|
445
|
+
refreshToken: result.refreshToken,
|
|
446
|
+
expiresIn: result.expiresIn,
|
|
447
|
+
localId: result.localId
|
|
448
|
+
};
|
|
449
|
+
}
|
|
328
450
|
function getAuth() {
|
|
329
451
|
return {
|
|
330
452
|
verifyIdToken
|
|
@@ -608,7 +730,7 @@ function removeFieldTransforms(data) {
|
|
|
608
730
|
}
|
|
609
731
|
|
|
610
732
|
// src/token-generation.ts
|
|
611
|
-
function
|
|
733
|
+
function base64UrlEncode2(str) {
|
|
612
734
|
return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
613
735
|
}
|
|
614
736
|
function base64UrlEncodeBuffer(buffer) {
|
|
@@ -629,8 +751,8 @@ async function createJWT(serviceAccount) {
|
|
|
629
751
|
exp: expiry,
|
|
630
752
|
scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
|
|
631
753
|
};
|
|
632
|
-
const encodedHeader =
|
|
633
|
-
const encodedPayload =
|
|
754
|
+
const encodedHeader = base64UrlEncode2(JSON.stringify(header));
|
|
755
|
+
const encodedPayload = base64UrlEncode2(JSON.stringify(payload));
|
|
634
756
|
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
|
|
635
757
|
const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
|
|
636
758
|
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
@@ -679,6 +801,26 @@ function clearTokenCache() {
|
|
|
679
801
|
tokenExpiry = 0;
|
|
680
802
|
}
|
|
681
803
|
|
|
804
|
+
// src/firestore/path-validation.ts
|
|
805
|
+
function validateCollectionPath(argumentName, collectionPath) {
|
|
806
|
+
const segments = collectionPath.split("/").filter((s) => s.length > 0);
|
|
807
|
+
if (segments.length % 2 === 0) {
|
|
808
|
+
throw new Error(
|
|
809
|
+
`Value for argument "${argumentName}" must point to a collection, but was "${collectionPath}". Your path does not contain an odd number of components.`
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function validateDocumentPath(argumentName, collectionPath, documentId) {
|
|
814
|
+
const collectionSegments = collectionPath.split("/").filter((s) => s.length > 0);
|
|
815
|
+
const totalSegments = collectionSegments.length + 1;
|
|
816
|
+
if (totalSegments % 2 !== 0) {
|
|
817
|
+
const fullPath = `${collectionPath}/${documentId}`;
|
|
818
|
+
throw new Error(
|
|
819
|
+
`Value for argument "${argumentName}" must point to a document, but was "${fullPath}". Your path does not contain an even number of components.`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
682
824
|
// src/firestore/operations.ts
|
|
683
825
|
var FIRESTORE_API = "https://firestore.googleapis.com/v1";
|
|
684
826
|
async function commitWrites(writes) {
|
|
@@ -699,6 +841,7 @@ async function commitWrites(writes) {
|
|
|
699
841
|
}
|
|
700
842
|
}
|
|
701
843
|
async function setDocument(collectionPath, documentId, data, options) {
|
|
844
|
+
validateDocumentPath("collectionPath", collectionPath, documentId);
|
|
702
845
|
const projectId = getProjectId();
|
|
703
846
|
const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
|
|
704
847
|
const cleanData = removeFieldTransforms(data);
|
|
@@ -748,6 +891,7 @@ async function setDocument(collectionPath, documentId, data, options) {
|
|
|
748
891
|
}
|
|
749
892
|
}
|
|
750
893
|
async function addDocument(collectionPath, data, documentId) {
|
|
894
|
+
validateCollectionPath("collectionPath", collectionPath);
|
|
751
895
|
const accessToken = await getAdminAccessToken();
|
|
752
896
|
const projectId = getProjectId();
|
|
753
897
|
const baseUrl = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}`;
|
|
@@ -779,6 +923,7 @@ async function addDocument(collectionPath, data, documentId) {
|
|
|
779
923
|
};
|
|
780
924
|
}
|
|
781
925
|
async function getDocument(collectionPath, documentId) {
|
|
926
|
+
validateDocumentPath("collectionPath", collectionPath, documentId);
|
|
782
927
|
const accessToken = await getAdminAccessToken();
|
|
783
928
|
const projectId = getProjectId();
|
|
784
929
|
const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
|
|
@@ -798,6 +943,7 @@ async function getDocument(collectionPath, documentId) {
|
|
|
798
943
|
return convertFromFirestoreFormat(result.fields);
|
|
799
944
|
}
|
|
800
945
|
async function updateDocument(collectionPath, documentId, data) {
|
|
946
|
+
validateDocumentPath("collectionPath", collectionPath, documentId);
|
|
801
947
|
const projectId = getProjectId();
|
|
802
948
|
const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
|
|
803
949
|
const cleanData = removeFieldTransforms(data);
|
|
@@ -850,6 +996,7 @@ async function updateDocument(collectionPath, documentId, data) {
|
|
|
850
996
|
}
|
|
851
997
|
}
|
|
852
998
|
async function deleteDocument(collectionPath, documentId) {
|
|
999
|
+
validateDocumentPath("collectionPath", collectionPath, documentId);
|
|
853
1000
|
const accessToken = await getAdminAccessToken();
|
|
854
1001
|
const projectId = getProjectId();
|
|
855
1002
|
const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
|
|
@@ -865,6 +1012,7 @@ async function deleteDocument(collectionPath, documentId) {
|
|
|
865
1012
|
}
|
|
866
1013
|
}
|
|
867
1014
|
async function queryDocuments(collectionPath, options) {
|
|
1015
|
+
validateCollectionPath("collectionPath", collectionPath);
|
|
868
1016
|
const accessToken = await getAdminAccessToken();
|
|
869
1017
|
const projectId = getProjectId();
|
|
870
1018
|
if (!options || Object.keys(options).length === 0) {
|
|
@@ -981,6 +1129,315 @@ async function batchWrite(operations) {
|
|
|
981
1129
|
}
|
|
982
1130
|
return await response.json();
|
|
983
1131
|
}
|
|
1132
|
+
|
|
1133
|
+
// src/storage/client.ts
|
|
1134
|
+
var STORAGE_API_BASE = "https://storage.googleapis.com/storage/v1";
|
|
1135
|
+
var UPLOAD_API_BASE = "https://storage.googleapis.com/upload/storage/v1";
|
|
1136
|
+
function getDefaultBucket() {
|
|
1137
|
+
const customBucket = process.env.FIREBASE_STORAGE_BUCKET;
|
|
1138
|
+
if (customBucket) {
|
|
1139
|
+
return customBucket;
|
|
1140
|
+
}
|
|
1141
|
+
const projectId = getProjectId();
|
|
1142
|
+
return `${projectId}.appspot.com`;
|
|
1143
|
+
}
|
|
1144
|
+
function detectContentType(filename) {
|
|
1145
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
1146
|
+
const mimeTypes = {
|
|
1147
|
+
"txt": "text/plain",
|
|
1148
|
+
"html": "text/html",
|
|
1149
|
+
"htm": "text/html",
|
|
1150
|
+
"css": "text/css",
|
|
1151
|
+
"js": "application/javascript",
|
|
1152
|
+
"json": "application/json",
|
|
1153
|
+
"xml": "application/xml",
|
|
1154
|
+
"jpg": "image/jpeg",
|
|
1155
|
+
"jpeg": "image/jpeg",
|
|
1156
|
+
"png": "image/png",
|
|
1157
|
+
"gif": "image/gif",
|
|
1158
|
+
"svg": "image/svg+xml",
|
|
1159
|
+
"webp": "image/webp",
|
|
1160
|
+
"pdf": "application/pdf",
|
|
1161
|
+
"zip": "application/zip",
|
|
1162
|
+
"mp4": "video/mp4",
|
|
1163
|
+
"mp3": "audio/mpeg",
|
|
1164
|
+
"wav": "audio/wav"
|
|
1165
|
+
};
|
|
1166
|
+
return mimeTypes[ext || ""] || "application/octet-stream";
|
|
1167
|
+
}
|
|
1168
|
+
async function uploadFile(path, data, options = {}) {
|
|
1169
|
+
const token = await getAdminAccessToken();
|
|
1170
|
+
const bucket = getDefaultBucket();
|
|
1171
|
+
const contentType = options.contentType || detectContentType(path);
|
|
1172
|
+
const url = `${UPLOAD_API_BASE}/b/${encodeURIComponent(bucket)}/o?uploadType=media&name=${encodeURIComponent(path)}`;
|
|
1173
|
+
let body;
|
|
1174
|
+
if (data instanceof Blob) {
|
|
1175
|
+
body = await data.arrayBuffer();
|
|
1176
|
+
} else if (data instanceof Uint8Array) {
|
|
1177
|
+
const buffer = new ArrayBuffer(data.byteLength);
|
|
1178
|
+
new Uint8Array(buffer).set(data);
|
|
1179
|
+
body = buffer;
|
|
1180
|
+
} else {
|
|
1181
|
+
body = data;
|
|
1182
|
+
}
|
|
1183
|
+
const response = await fetch(url, {
|
|
1184
|
+
method: "POST",
|
|
1185
|
+
headers: {
|
|
1186
|
+
"Authorization": `Bearer ${token}`,
|
|
1187
|
+
"Content-Type": contentType,
|
|
1188
|
+
"Content-Length": body.byteLength.toString()
|
|
1189
|
+
},
|
|
1190
|
+
body
|
|
1191
|
+
});
|
|
1192
|
+
if (!response.ok) {
|
|
1193
|
+
const errorText = await response.text();
|
|
1194
|
+
throw new Error(`Failed to upload file: ${response.status} ${errorText}`);
|
|
1195
|
+
}
|
|
1196
|
+
const result = await response.json();
|
|
1197
|
+
if (options.public) {
|
|
1198
|
+
await makeFilePublic(path);
|
|
1199
|
+
}
|
|
1200
|
+
if (options.metadata) {
|
|
1201
|
+
return await updateFileMetadata(path, options.metadata);
|
|
1202
|
+
}
|
|
1203
|
+
return result;
|
|
1204
|
+
}
|
|
1205
|
+
async function downloadFile(path, _options = {}) {
|
|
1206
|
+
const token = await getAdminAccessToken();
|
|
1207
|
+
const bucket = getDefaultBucket();
|
|
1208
|
+
const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}?alt=media`;
|
|
1209
|
+
const response = await fetch(url, {
|
|
1210
|
+
method: "GET",
|
|
1211
|
+
headers: {
|
|
1212
|
+
"Authorization": `Bearer ${token}`
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
if (!response.ok) {
|
|
1216
|
+
const errorText = await response.text();
|
|
1217
|
+
throw new Error(`Failed to download file: ${response.status} ${errorText}`);
|
|
1218
|
+
}
|
|
1219
|
+
return await response.arrayBuffer();
|
|
1220
|
+
}
|
|
1221
|
+
async function deleteFile(path) {
|
|
1222
|
+
const token = await getAdminAccessToken();
|
|
1223
|
+
const bucket = getDefaultBucket();
|
|
1224
|
+
const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}`;
|
|
1225
|
+
const response = await fetch(url, {
|
|
1226
|
+
method: "DELETE",
|
|
1227
|
+
headers: {
|
|
1228
|
+
"Authorization": `Bearer ${token}`
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
if (!response.ok) {
|
|
1232
|
+
const errorText = await response.text();
|
|
1233
|
+
throw new Error(`Failed to delete file: ${response.status} ${errorText}`);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
async function getFileMetadata(path) {
|
|
1237
|
+
const token = await getAdminAccessToken();
|
|
1238
|
+
const bucket = getDefaultBucket();
|
|
1239
|
+
const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}`;
|
|
1240
|
+
const response = await fetch(url, {
|
|
1241
|
+
method: "GET",
|
|
1242
|
+
headers: {
|
|
1243
|
+
"Authorization": `Bearer ${token}`
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
if (!response.ok) {
|
|
1247
|
+
const errorText = await response.text();
|
|
1248
|
+
throw new Error(`Failed to get file metadata: ${response.status} ${errorText}`);
|
|
1249
|
+
}
|
|
1250
|
+
return await response.json();
|
|
1251
|
+
}
|
|
1252
|
+
async function updateFileMetadata(path, metadata) {
|
|
1253
|
+
const token = await getAdminAccessToken();
|
|
1254
|
+
const bucket = getDefaultBucket();
|
|
1255
|
+
const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}`;
|
|
1256
|
+
const response = await fetch(url, {
|
|
1257
|
+
method: "PATCH",
|
|
1258
|
+
headers: {
|
|
1259
|
+
"Authorization": `Bearer ${token}`,
|
|
1260
|
+
"Content-Type": "application/json"
|
|
1261
|
+
},
|
|
1262
|
+
body: JSON.stringify({ metadata })
|
|
1263
|
+
});
|
|
1264
|
+
if (!response.ok) {
|
|
1265
|
+
const errorText = await response.text();
|
|
1266
|
+
throw new Error(`Failed to update file metadata: ${response.status} ${errorText}`);
|
|
1267
|
+
}
|
|
1268
|
+
return await response.json();
|
|
1269
|
+
}
|
|
1270
|
+
async function makeFilePublic(path) {
|
|
1271
|
+
const token = await getAdminAccessToken();
|
|
1272
|
+
const bucket = getDefaultBucket();
|
|
1273
|
+
const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}/acl`;
|
|
1274
|
+
const response = await fetch(url, {
|
|
1275
|
+
method: "POST",
|
|
1276
|
+
headers: {
|
|
1277
|
+
"Authorization": `Bearer ${token}`,
|
|
1278
|
+
"Content-Type": "application/json"
|
|
1279
|
+
},
|
|
1280
|
+
body: JSON.stringify({
|
|
1281
|
+
entity: "allUsers",
|
|
1282
|
+
role: "READER"
|
|
1283
|
+
})
|
|
1284
|
+
});
|
|
1285
|
+
if (!response.ok) {
|
|
1286
|
+
const errorText = await response.text();
|
|
1287
|
+
throw new Error(`Failed to make file public: ${response.status} ${errorText}`);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
async function listFiles(options = {}) {
|
|
1291
|
+
const token = await getAdminAccessToken();
|
|
1292
|
+
const bucket = getDefaultBucket();
|
|
1293
|
+
const params = new URLSearchParams();
|
|
1294
|
+
if (options.prefix) params.append("prefix", options.prefix);
|
|
1295
|
+
if (options.delimiter) params.append("delimiter", options.delimiter);
|
|
1296
|
+
if (options.maxResults) params.append("maxResults", options.maxResults.toString());
|
|
1297
|
+
if (options.pageToken) params.append("pageToken", options.pageToken);
|
|
1298
|
+
const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o?${params.toString()}`;
|
|
1299
|
+
const response = await fetch(url, {
|
|
1300
|
+
method: "GET",
|
|
1301
|
+
headers: {
|
|
1302
|
+
"Authorization": `Bearer ${token}`
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
if (!response.ok) {
|
|
1306
|
+
const errorText = await response.text();
|
|
1307
|
+
throw new Error(`Failed to list files: ${response.status} ${errorText}`);
|
|
1308
|
+
}
|
|
1309
|
+
const result = await response.json();
|
|
1310
|
+
return {
|
|
1311
|
+
files: result.items || [],
|
|
1312
|
+
nextPageToken: result.nextPageToken
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
async function fileExists(path) {
|
|
1316
|
+
try {
|
|
1317
|
+
await getFileMetadata(path);
|
|
1318
|
+
return true;
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
if (error instanceof Error && error.message.includes("404")) {
|
|
1321
|
+
return false;
|
|
1322
|
+
}
|
|
1323
|
+
throw error;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// src/storage/signed-urls.ts
|
|
1328
|
+
function getStorageBucket() {
|
|
1329
|
+
const customBucket = process.env.FIREBASE_STORAGE_BUCKET;
|
|
1330
|
+
if (customBucket) {
|
|
1331
|
+
return customBucket;
|
|
1332
|
+
}
|
|
1333
|
+
const projectId = getProjectId();
|
|
1334
|
+
return `${projectId}.appspot.com`;
|
|
1335
|
+
}
|
|
1336
|
+
function getExpirationTimestamp(expires) {
|
|
1337
|
+
if (expires instanceof Date) {
|
|
1338
|
+
return Math.floor(expires.getTime() / 1e3);
|
|
1339
|
+
}
|
|
1340
|
+
return Math.floor(Date.now() / 1e3) + expires;
|
|
1341
|
+
}
|
|
1342
|
+
function actionToMethod(action) {
|
|
1343
|
+
switch (action) {
|
|
1344
|
+
case "read":
|
|
1345
|
+
return "GET";
|
|
1346
|
+
case "write":
|
|
1347
|
+
return "PUT";
|
|
1348
|
+
case "delete":
|
|
1349
|
+
return "DELETE";
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
function stringToHex(str) {
|
|
1353
|
+
const encoder = new TextEncoder();
|
|
1354
|
+
const bytes = encoder.encode(str);
|
|
1355
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1356
|
+
}
|
|
1357
|
+
async function signData(data, privateKey) {
|
|
1358
|
+
const pemHeader = "-----BEGIN PRIVATE KEY-----";
|
|
1359
|
+
const pemFooter = "-----END PRIVATE KEY-----";
|
|
1360
|
+
const pemContents = privateKey.replace(pemHeader, "").replace(pemFooter, "").replace(/\s/g, "");
|
|
1361
|
+
const binaryString = atob(pemContents);
|
|
1362
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
1363
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
1364
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
1365
|
+
}
|
|
1366
|
+
const key = await crypto.subtle.importKey(
|
|
1367
|
+
"pkcs8",
|
|
1368
|
+
bytes,
|
|
1369
|
+
{
|
|
1370
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
1371
|
+
hash: "SHA-256"
|
|
1372
|
+
},
|
|
1373
|
+
false,
|
|
1374
|
+
["sign"]
|
|
1375
|
+
);
|
|
1376
|
+
const encoder = new TextEncoder();
|
|
1377
|
+
const dataBytes = encoder.encode(data);
|
|
1378
|
+
const signature = await crypto.subtle.sign(
|
|
1379
|
+
"RSASSA-PKCS1-v1_5",
|
|
1380
|
+
key,
|
|
1381
|
+
dataBytes
|
|
1382
|
+
);
|
|
1383
|
+
const signatureArray = new Uint8Array(signature);
|
|
1384
|
+
return Array.from(signatureArray).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1385
|
+
}
|
|
1386
|
+
async function generateSignedUrl(path, options) {
|
|
1387
|
+
const serviceAccount = getServiceAccount();
|
|
1388
|
+
const bucket = getStorageBucket();
|
|
1389
|
+
const method = actionToMethod(options.action);
|
|
1390
|
+
const expiration = getExpirationTimestamp(options.expires);
|
|
1391
|
+
const now = /* @__PURE__ */ new Date();
|
|
1392
|
+
const timestamp = Math.floor(now.getTime() / 1e3);
|
|
1393
|
+
const isoString = now.toISOString();
|
|
1394
|
+
const datestamp = isoString.split("T")[0].replace(/-/g, "");
|
|
1395
|
+
const timeString = isoString.split("T")[1].replace(/[:.]/g, "").substring(0, 6);
|
|
1396
|
+
const dateTimeStamp = `${datestamp}T${timeString}Z`;
|
|
1397
|
+
const credentialScope = `${datestamp}/auto/storage/goog4_request`;
|
|
1398
|
+
const credential = `${serviceAccount.client_email}/${credentialScope}`;
|
|
1399
|
+
const canonicalHeaders = `host:storage.googleapis.com
|
|
1400
|
+
`;
|
|
1401
|
+
const signedHeaders = "host";
|
|
1402
|
+
const queryParams = {
|
|
1403
|
+
"X-Goog-Algorithm": "GOOG4-RSA-SHA256",
|
|
1404
|
+
"X-Goog-Credential": credential,
|
|
1405
|
+
"X-Goog-Date": dateTimeStamp,
|
|
1406
|
+
"X-Goog-Expires": (expiration - timestamp).toString(),
|
|
1407
|
+
"X-Goog-SignedHeaders": signedHeaders
|
|
1408
|
+
};
|
|
1409
|
+
if (options.contentType) {
|
|
1410
|
+
queryParams["response-content-type"] = options.contentType;
|
|
1411
|
+
}
|
|
1412
|
+
if (options.responseDisposition) {
|
|
1413
|
+
queryParams["response-content-disposition"] = options.responseDisposition;
|
|
1414
|
+
}
|
|
1415
|
+
if (options.responseType) {
|
|
1416
|
+
queryParams["response-content-type"] = options.responseType;
|
|
1417
|
+
}
|
|
1418
|
+
const sortedParams = Object.keys(queryParams).sort();
|
|
1419
|
+
const canonicalQueryString = sortedParams.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`).join("&");
|
|
1420
|
+
const encodedPath = path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
1421
|
+
const canonicalUri = `/${bucket}/${encodedPath}`;
|
|
1422
|
+
const canonicalRequest = [
|
|
1423
|
+
method,
|
|
1424
|
+
canonicalUri,
|
|
1425
|
+
canonicalQueryString,
|
|
1426
|
+
canonicalHeaders,
|
|
1427
|
+
signedHeaders,
|
|
1428
|
+
"UNSIGNED-PAYLOAD"
|
|
1429
|
+
].join("\n");
|
|
1430
|
+
const canonicalRequestHash = stringToHex(canonicalRequest);
|
|
1431
|
+
const stringToSign = [
|
|
1432
|
+
"GOOG4-RSA-SHA256",
|
|
1433
|
+
dateTimeStamp,
|
|
1434
|
+
credentialScope,
|
|
1435
|
+
canonicalRequestHash
|
|
1436
|
+
].join("\n");
|
|
1437
|
+
const signature = await signData(stringToSign, serviceAccount.private_key);
|
|
1438
|
+
const signedUrl = `https://storage.googleapis.com${canonicalUri}?${canonicalQueryString}&X-Goog-Signature=${signature}`;
|
|
1439
|
+
return signedUrl;
|
|
1440
|
+
}
|
|
984
1441
|
// Annotate the CommonJS export names for ESM import in node:
|
|
985
1442
|
0 && (module.exports = {
|
|
986
1443
|
FieldValue,
|
|
@@ -988,17 +1445,26 @@ async function batchWrite(operations) {
|
|
|
988
1445
|
batchWrite,
|
|
989
1446
|
clearConfig,
|
|
990
1447
|
clearTokenCache,
|
|
1448
|
+
createCustomToken,
|
|
991
1449
|
deleteDocument,
|
|
1450
|
+
deleteFile,
|
|
1451
|
+
downloadFile,
|
|
1452
|
+
fileExists,
|
|
1453
|
+
generateSignedUrl,
|
|
992
1454
|
getAdminAccessToken,
|
|
993
1455
|
getAuth,
|
|
994
1456
|
getConfig,
|
|
995
1457
|
getDocument,
|
|
1458
|
+
getFileMetadata,
|
|
996
1459
|
getProjectId,
|
|
997
1460
|
getServiceAccount,
|
|
998
1461
|
getUserFromToken,
|
|
999
1462
|
initializeApp,
|
|
1463
|
+
listFiles,
|
|
1000
1464
|
queryDocuments,
|
|
1001
1465
|
setDocument,
|
|
1466
|
+
signInWithCustomToken,
|
|
1002
1467
|
updateDocument,
|
|
1468
|
+
uploadFile,
|
|
1003
1469
|
verifyIdToken
|
|
1004
1470
|
});
|