@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.mjs
CHANGED
|
@@ -62,6 +62,20 @@ function getProjectId() {
|
|
|
62
62
|
"Firebase project ID not configured. Either call initializeApp({ projectId: ... }) or set FIREBASE_PROJECT_ID environment variable."
|
|
63
63
|
);
|
|
64
64
|
}
|
|
65
|
+
function getFirebaseApiKey() {
|
|
66
|
+
if (globalConfig.apiKey) {
|
|
67
|
+
return globalConfig.apiKey;
|
|
68
|
+
}
|
|
69
|
+
if (typeof process !== "undefined" && process.env) {
|
|
70
|
+
const apiKey = process.env.FIREBASE_API_KEY || process.env.PUBLIC_FIREBASE_API_KEY;
|
|
71
|
+
if (apiKey) {
|
|
72
|
+
return apiKey;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw new Error(
|
|
76
|
+
"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."
|
|
77
|
+
);
|
|
78
|
+
}
|
|
65
79
|
|
|
66
80
|
// src/x509.ts
|
|
67
81
|
function decodeBase64(str) {
|
|
@@ -282,6 +296,105 @@ async function getUserFromToken(idToken) {
|
|
|
282
296
|
photoURL: decodedToken.picture || null
|
|
283
297
|
};
|
|
284
298
|
}
|
|
299
|
+
function base64UrlEncode(str) {
|
|
300
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
301
|
+
}
|
|
302
|
+
async function signWithPrivateKey(data, privateKey) {
|
|
303
|
+
const pemHeader = "-----BEGIN PRIVATE KEY-----";
|
|
304
|
+
const pemFooter = "-----END PRIVATE KEY-----";
|
|
305
|
+
const pemContents = privateKey.replace(pemHeader, "").replace(pemFooter, "").replace(/\s/g, "");
|
|
306
|
+
const binaryString = atob(pemContents);
|
|
307
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
308
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
309
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
310
|
+
}
|
|
311
|
+
const key = await crypto.subtle.importKey(
|
|
312
|
+
"pkcs8",
|
|
313
|
+
bytes,
|
|
314
|
+
{
|
|
315
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
316
|
+
hash: "SHA-256"
|
|
317
|
+
},
|
|
318
|
+
false,
|
|
319
|
+
["sign"]
|
|
320
|
+
);
|
|
321
|
+
const encoder = new TextEncoder();
|
|
322
|
+
const dataBytes = encoder.encode(data);
|
|
323
|
+
const signature = await crypto.subtle.sign(
|
|
324
|
+
"RSASSA-PKCS1-v1_5",
|
|
325
|
+
key,
|
|
326
|
+
dataBytes
|
|
327
|
+
);
|
|
328
|
+
return base64UrlEncode(String.fromCharCode(...new Uint8Array(signature)));
|
|
329
|
+
}
|
|
330
|
+
async function createCustomToken(uid, customClaims) {
|
|
331
|
+
const serviceAccount = getServiceAccount();
|
|
332
|
+
if (!uid || typeof uid !== "string") {
|
|
333
|
+
throw new Error("uid must be a non-empty string");
|
|
334
|
+
}
|
|
335
|
+
if (uid.length > 128) {
|
|
336
|
+
throw new Error("uid must be at most 128 characters");
|
|
337
|
+
}
|
|
338
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
339
|
+
const payload = {
|
|
340
|
+
iss: serviceAccount.client_email,
|
|
341
|
+
sub: serviceAccount.client_email,
|
|
342
|
+
aud: "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
|
|
343
|
+
iat: now,
|
|
344
|
+
exp: now + 3600,
|
|
345
|
+
// 1 hour
|
|
346
|
+
uid
|
|
347
|
+
};
|
|
348
|
+
if (customClaims) {
|
|
349
|
+
payload.claims = customClaims;
|
|
350
|
+
}
|
|
351
|
+
const header = {
|
|
352
|
+
alg: "RS256",
|
|
353
|
+
typ: "JWT"
|
|
354
|
+
};
|
|
355
|
+
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
356
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
357
|
+
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
|
|
358
|
+
const signature = await signWithPrivateKey(unsignedToken, serviceAccount.private_key);
|
|
359
|
+
return `${unsignedToken}.${signature}`;
|
|
360
|
+
}
|
|
361
|
+
async function signInWithCustomToken(customToken) {
|
|
362
|
+
if (!customToken || typeof customToken !== "string") {
|
|
363
|
+
throw new Error("customToken must be a non-empty string");
|
|
364
|
+
}
|
|
365
|
+
const apiKey = getFirebaseApiKey();
|
|
366
|
+
const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`;
|
|
367
|
+
const response = await fetch(url, {
|
|
368
|
+
method: "POST",
|
|
369
|
+
headers: {
|
|
370
|
+
"Content-Type": "application/json"
|
|
371
|
+
},
|
|
372
|
+
body: JSON.stringify({
|
|
373
|
+
token: customToken,
|
|
374
|
+
returnSecureToken: true
|
|
375
|
+
})
|
|
376
|
+
});
|
|
377
|
+
if (!response.ok) {
|
|
378
|
+
const errorText = await response.text();
|
|
379
|
+
let errorMessage = `Failed to sign in with custom token: ${response.status}`;
|
|
380
|
+
try {
|
|
381
|
+
const errorJson = JSON.parse(errorText);
|
|
382
|
+
if (errorJson.error && errorJson.error.message) {
|
|
383
|
+
errorMessage += ` - ${errorJson.error.message}`;
|
|
384
|
+
}
|
|
385
|
+
} catch {
|
|
386
|
+
errorMessage += ` - ${errorText}`;
|
|
387
|
+
}
|
|
388
|
+
throw new Error(errorMessage);
|
|
389
|
+
}
|
|
390
|
+
const result = await response.json();
|
|
391
|
+
return {
|
|
392
|
+
idToken: result.idToken,
|
|
393
|
+
refreshToken: result.refreshToken,
|
|
394
|
+
expiresIn: result.expiresIn,
|
|
395
|
+
localId: result.localId
|
|
396
|
+
};
|
|
397
|
+
}
|
|
285
398
|
function getAuth() {
|
|
286
399
|
return {
|
|
287
400
|
verifyIdToken
|
|
@@ -565,7 +678,7 @@ function removeFieldTransforms(data) {
|
|
|
565
678
|
}
|
|
566
679
|
|
|
567
680
|
// src/token-generation.ts
|
|
568
|
-
function
|
|
681
|
+
function base64UrlEncode2(str) {
|
|
569
682
|
return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
570
683
|
}
|
|
571
684
|
function base64UrlEncodeBuffer(buffer) {
|
|
@@ -586,8 +699,8 @@ async function createJWT(serviceAccount) {
|
|
|
586
699
|
exp: expiry,
|
|
587
700
|
scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
|
|
588
701
|
};
|
|
589
|
-
const encodedHeader =
|
|
590
|
-
const encodedPayload =
|
|
702
|
+
const encodedHeader = base64UrlEncode2(JSON.stringify(header));
|
|
703
|
+
const encodedPayload = base64UrlEncode2(JSON.stringify(payload));
|
|
591
704
|
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
|
|
592
705
|
const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
|
|
593
706
|
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
@@ -636,6 +749,26 @@ function clearTokenCache() {
|
|
|
636
749
|
tokenExpiry = 0;
|
|
637
750
|
}
|
|
638
751
|
|
|
752
|
+
// src/firestore/path-validation.ts
|
|
753
|
+
function validateCollectionPath(argumentName, collectionPath) {
|
|
754
|
+
const segments = collectionPath.split("/").filter((s) => s.length > 0);
|
|
755
|
+
if (segments.length % 2 === 0) {
|
|
756
|
+
throw new Error(
|
|
757
|
+
`Value for argument "${argumentName}" must point to a collection, but was "${collectionPath}". Your path does not contain an odd number of components.`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
function validateDocumentPath(argumentName, collectionPath, documentId) {
|
|
762
|
+
const collectionSegments = collectionPath.split("/").filter((s) => s.length > 0);
|
|
763
|
+
const totalSegments = collectionSegments.length + 1;
|
|
764
|
+
if (totalSegments % 2 !== 0) {
|
|
765
|
+
const fullPath = `${collectionPath}/${documentId}`;
|
|
766
|
+
throw new Error(
|
|
767
|
+
`Value for argument "${argumentName}" must point to a document, but was "${fullPath}". Your path does not contain an even number of components.`
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
639
772
|
// src/firestore/operations.ts
|
|
640
773
|
var FIRESTORE_API = "https://firestore.googleapis.com/v1";
|
|
641
774
|
async function commitWrites(writes) {
|
|
@@ -656,6 +789,7 @@ async function commitWrites(writes) {
|
|
|
656
789
|
}
|
|
657
790
|
}
|
|
658
791
|
async function setDocument(collectionPath, documentId, data, options) {
|
|
792
|
+
validateDocumentPath("collectionPath", collectionPath, documentId);
|
|
659
793
|
const projectId = getProjectId();
|
|
660
794
|
const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
|
|
661
795
|
const cleanData = removeFieldTransforms(data);
|
|
@@ -705,6 +839,7 @@ async function setDocument(collectionPath, documentId, data, options) {
|
|
|
705
839
|
}
|
|
706
840
|
}
|
|
707
841
|
async function addDocument(collectionPath, data, documentId) {
|
|
842
|
+
validateCollectionPath("collectionPath", collectionPath);
|
|
708
843
|
const accessToken = await getAdminAccessToken();
|
|
709
844
|
const projectId = getProjectId();
|
|
710
845
|
const baseUrl = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}`;
|
|
@@ -736,6 +871,7 @@ async function addDocument(collectionPath, data, documentId) {
|
|
|
736
871
|
};
|
|
737
872
|
}
|
|
738
873
|
async function getDocument(collectionPath, documentId) {
|
|
874
|
+
validateDocumentPath("collectionPath", collectionPath, documentId);
|
|
739
875
|
const accessToken = await getAdminAccessToken();
|
|
740
876
|
const projectId = getProjectId();
|
|
741
877
|
const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
|
|
@@ -755,6 +891,7 @@ async function getDocument(collectionPath, documentId) {
|
|
|
755
891
|
return convertFromFirestoreFormat(result.fields);
|
|
756
892
|
}
|
|
757
893
|
async function updateDocument(collectionPath, documentId, data) {
|
|
894
|
+
validateDocumentPath("collectionPath", collectionPath, documentId);
|
|
758
895
|
const projectId = getProjectId();
|
|
759
896
|
const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
|
|
760
897
|
const cleanData = removeFieldTransforms(data);
|
|
@@ -807,6 +944,7 @@ async function updateDocument(collectionPath, documentId, data) {
|
|
|
807
944
|
}
|
|
808
945
|
}
|
|
809
946
|
async function deleteDocument(collectionPath, documentId) {
|
|
947
|
+
validateDocumentPath("collectionPath", collectionPath, documentId);
|
|
810
948
|
const accessToken = await getAdminAccessToken();
|
|
811
949
|
const projectId = getProjectId();
|
|
812
950
|
const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
|
|
@@ -822,6 +960,7 @@ async function deleteDocument(collectionPath, documentId) {
|
|
|
822
960
|
}
|
|
823
961
|
}
|
|
824
962
|
async function queryDocuments(collectionPath, options) {
|
|
963
|
+
validateCollectionPath("collectionPath", collectionPath);
|
|
825
964
|
const accessToken = await getAdminAccessToken();
|
|
826
965
|
const projectId = getProjectId();
|
|
827
966
|
if (!options || Object.keys(options).length === 0) {
|
|
@@ -938,23 +1077,341 @@ async function batchWrite(operations) {
|
|
|
938
1077
|
}
|
|
939
1078
|
return await response.json();
|
|
940
1079
|
}
|
|
1080
|
+
|
|
1081
|
+
// src/storage/client.ts
|
|
1082
|
+
var STORAGE_API_BASE = "https://storage.googleapis.com/storage/v1";
|
|
1083
|
+
var UPLOAD_API_BASE = "https://storage.googleapis.com/upload/storage/v1";
|
|
1084
|
+
function getDefaultBucket() {
|
|
1085
|
+
const customBucket = process.env.FIREBASE_STORAGE_BUCKET;
|
|
1086
|
+
if (customBucket) {
|
|
1087
|
+
return customBucket;
|
|
1088
|
+
}
|
|
1089
|
+
const projectId = getProjectId();
|
|
1090
|
+
return `${projectId}.appspot.com`;
|
|
1091
|
+
}
|
|
1092
|
+
function detectContentType(filename) {
|
|
1093
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
1094
|
+
const mimeTypes = {
|
|
1095
|
+
"txt": "text/plain",
|
|
1096
|
+
"html": "text/html",
|
|
1097
|
+
"htm": "text/html",
|
|
1098
|
+
"css": "text/css",
|
|
1099
|
+
"js": "application/javascript",
|
|
1100
|
+
"json": "application/json",
|
|
1101
|
+
"xml": "application/xml",
|
|
1102
|
+
"jpg": "image/jpeg",
|
|
1103
|
+
"jpeg": "image/jpeg",
|
|
1104
|
+
"png": "image/png",
|
|
1105
|
+
"gif": "image/gif",
|
|
1106
|
+
"svg": "image/svg+xml",
|
|
1107
|
+
"webp": "image/webp",
|
|
1108
|
+
"pdf": "application/pdf",
|
|
1109
|
+
"zip": "application/zip",
|
|
1110
|
+
"mp4": "video/mp4",
|
|
1111
|
+
"mp3": "audio/mpeg",
|
|
1112
|
+
"wav": "audio/wav"
|
|
1113
|
+
};
|
|
1114
|
+
return mimeTypes[ext || ""] || "application/octet-stream";
|
|
1115
|
+
}
|
|
1116
|
+
async function uploadFile(path, data, options = {}) {
|
|
1117
|
+
const token = await getAdminAccessToken();
|
|
1118
|
+
const bucket = getDefaultBucket();
|
|
1119
|
+
const contentType = options.contentType || detectContentType(path);
|
|
1120
|
+
const url = `${UPLOAD_API_BASE}/b/${encodeURIComponent(bucket)}/o?uploadType=media&name=${encodeURIComponent(path)}`;
|
|
1121
|
+
let body;
|
|
1122
|
+
if (data instanceof Blob) {
|
|
1123
|
+
body = await data.arrayBuffer();
|
|
1124
|
+
} else if (data instanceof Uint8Array) {
|
|
1125
|
+
const buffer = new ArrayBuffer(data.byteLength);
|
|
1126
|
+
new Uint8Array(buffer).set(data);
|
|
1127
|
+
body = buffer;
|
|
1128
|
+
} else {
|
|
1129
|
+
body = data;
|
|
1130
|
+
}
|
|
1131
|
+
const response = await fetch(url, {
|
|
1132
|
+
method: "POST",
|
|
1133
|
+
headers: {
|
|
1134
|
+
"Authorization": `Bearer ${token}`,
|
|
1135
|
+
"Content-Type": contentType,
|
|
1136
|
+
"Content-Length": body.byteLength.toString()
|
|
1137
|
+
},
|
|
1138
|
+
body
|
|
1139
|
+
});
|
|
1140
|
+
if (!response.ok) {
|
|
1141
|
+
const errorText = await response.text();
|
|
1142
|
+
throw new Error(`Failed to upload file: ${response.status} ${errorText}`);
|
|
1143
|
+
}
|
|
1144
|
+
const result = await response.json();
|
|
1145
|
+
if (options.public) {
|
|
1146
|
+
await makeFilePublic(path);
|
|
1147
|
+
}
|
|
1148
|
+
if (options.metadata) {
|
|
1149
|
+
return await updateFileMetadata(path, options.metadata);
|
|
1150
|
+
}
|
|
1151
|
+
return result;
|
|
1152
|
+
}
|
|
1153
|
+
async function downloadFile(path, _options = {}) {
|
|
1154
|
+
const token = await getAdminAccessToken();
|
|
1155
|
+
const bucket = getDefaultBucket();
|
|
1156
|
+
const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}?alt=media`;
|
|
1157
|
+
const response = await fetch(url, {
|
|
1158
|
+
method: "GET",
|
|
1159
|
+
headers: {
|
|
1160
|
+
"Authorization": `Bearer ${token}`
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
if (!response.ok) {
|
|
1164
|
+
const errorText = await response.text();
|
|
1165
|
+
throw new Error(`Failed to download file: ${response.status} ${errorText}`);
|
|
1166
|
+
}
|
|
1167
|
+
return await response.arrayBuffer();
|
|
1168
|
+
}
|
|
1169
|
+
async function deleteFile(path) {
|
|
1170
|
+
const token = await getAdminAccessToken();
|
|
1171
|
+
const bucket = getDefaultBucket();
|
|
1172
|
+
const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}`;
|
|
1173
|
+
const response = await fetch(url, {
|
|
1174
|
+
method: "DELETE",
|
|
1175
|
+
headers: {
|
|
1176
|
+
"Authorization": `Bearer ${token}`
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
if (!response.ok) {
|
|
1180
|
+
const errorText = await response.text();
|
|
1181
|
+
throw new Error(`Failed to delete file: ${response.status} ${errorText}`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
async function getFileMetadata(path) {
|
|
1185
|
+
const token = await getAdminAccessToken();
|
|
1186
|
+
const bucket = getDefaultBucket();
|
|
1187
|
+
const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}`;
|
|
1188
|
+
const response = await fetch(url, {
|
|
1189
|
+
method: "GET",
|
|
1190
|
+
headers: {
|
|
1191
|
+
"Authorization": `Bearer ${token}`
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
if (!response.ok) {
|
|
1195
|
+
const errorText = await response.text();
|
|
1196
|
+
throw new Error(`Failed to get file metadata: ${response.status} ${errorText}`);
|
|
1197
|
+
}
|
|
1198
|
+
return await response.json();
|
|
1199
|
+
}
|
|
1200
|
+
async function updateFileMetadata(path, metadata) {
|
|
1201
|
+
const token = await getAdminAccessToken();
|
|
1202
|
+
const bucket = getDefaultBucket();
|
|
1203
|
+
const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}`;
|
|
1204
|
+
const response = await fetch(url, {
|
|
1205
|
+
method: "PATCH",
|
|
1206
|
+
headers: {
|
|
1207
|
+
"Authorization": `Bearer ${token}`,
|
|
1208
|
+
"Content-Type": "application/json"
|
|
1209
|
+
},
|
|
1210
|
+
body: JSON.stringify({ metadata })
|
|
1211
|
+
});
|
|
1212
|
+
if (!response.ok) {
|
|
1213
|
+
const errorText = await response.text();
|
|
1214
|
+
throw new Error(`Failed to update file metadata: ${response.status} ${errorText}`);
|
|
1215
|
+
}
|
|
1216
|
+
return await response.json();
|
|
1217
|
+
}
|
|
1218
|
+
async function makeFilePublic(path) {
|
|
1219
|
+
const token = await getAdminAccessToken();
|
|
1220
|
+
const bucket = getDefaultBucket();
|
|
1221
|
+
const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}/acl`;
|
|
1222
|
+
const response = await fetch(url, {
|
|
1223
|
+
method: "POST",
|
|
1224
|
+
headers: {
|
|
1225
|
+
"Authorization": `Bearer ${token}`,
|
|
1226
|
+
"Content-Type": "application/json"
|
|
1227
|
+
},
|
|
1228
|
+
body: JSON.stringify({
|
|
1229
|
+
entity: "allUsers",
|
|
1230
|
+
role: "READER"
|
|
1231
|
+
})
|
|
1232
|
+
});
|
|
1233
|
+
if (!response.ok) {
|
|
1234
|
+
const errorText = await response.text();
|
|
1235
|
+
throw new Error(`Failed to make file public: ${response.status} ${errorText}`);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
async function listFiles(options = {}) {
|
|
1239
|
+
const token = await getAdminAccessToken();
|
|
1240
|
+
const bucket = getDefaultBucket();
|
|
1241
|
+
const params = new URLSearchParams();
|
|
1242
|
+
if (options.prefix) params.append("prefix", options.prefix);
|
|
1243
|
+
if (options.delimiter) params.append("delimiter", options.delimiter);
|
|
1244
|
+
if (options.maxResults) params.append("maxResults", options.maxResults.toString());
|
|
1245
|
+
if (options.pageToken) params.append("pageToken", options.pageToken);
|
|
1246
|
+
const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o?${params.toString()}`;
|
|
1247
|
+
const response = await fetch(url, {
|
|
1248
|
+
method: "GET",
|
|
1249
|
+
headers: {
|
|
1250
|
+
"Authorization": `Bearer ${token}`
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
if (!response.ok) {
|
|
1254
|
+
const errorText = await response.text();
|
|
1255
|
+
throw new Error(`Failed to list files: ${response.status} ${errorText}`);
|
|
1256
|
+
}
|
|
1257
|
+
const result = await response.json();
|
|
1258
|
+
return {
|
|
1259
|
+
files: result.items || [],
|
|
1260
|
+
nextPageToken: result.nextPageToken
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
async function fileExists(path) {
|
|
1264
|
+
try {
|
|
1265
|
+
await getFileMetadata(path);
|
|
1266
|
+
return true;
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
if (error instanceof Error && error.message.includes("404")) {
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
throw error;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// src/storage/signed-urls.ts
|
|
1276
|
+
function getStorageBucket() {
|
|
1277
|
+
const customBucket = process.env.FIREBASE_STORAGE_BUCKET;
|
|
1278
|
+
if (customBucket) {
|
|
1279
|
+
return customBucket;
|
|
1280
|
+
}
|
|
1281
|
+
const projectId = getProjectId();
|
|
1282
|
+
return `${projectId}.appspot.com`;
|
|
1283
|
+
}
|
|
1284
|
+
function getExpirationTimestamp(expires) {
|
|
1285
|
+
if (expires instanceof Date) {
|
|
1286
|
+
return Math.floor(expires.getTime() / 1e3);
|
|
1287
|
+
}
|
|
1288
|
+
return Math.floor(Date.now() / 1e3) + expires;
|
|
1289
|
+
}
|
|
1290
|
+
function actionToMethod(action) {
|
|
1291
|
+
switch (action) {
|
|
1292
|
+
case "read":
|
|
1293
|
+
return "GET";
|
|
1294
|
+
case "write":
|
|
1295
|
+
return "PUT";
|
|
1296
|
+
case "delete":
|
|
1297
|
+
return "DELETE";
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function stringToHex(str) {
|
|
1301
|
+
const encoder = new TextEncoder();
|
|
1302
|
+
const bytes = encoder.encode(str);
|
|
1303
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1304
|
+
}
|
|
1305
|
+
async function signData(data, privateKey) {
|
|
1306
|
+
const pemHeader = "-----BEGIN PRIVATE KEY-----";
|
|
1307
|
+
const pemFooter = "-----END PRIVATE KEY-----";
|
|
1308
|
+
const pemContents = privateKey.replace(pemHeader, "").replace(pemFooter, "").replace(/\s/g, "");
|
|
1309
|
+
const binaryString = atob(pemContents);
|
|
1310
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
1311
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
1312
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
1313
|
+
}
|
|
1314
|
+
const key = await crypto.subtle.importKey(
|
|
1315
|
+
"pkcs8",
|
|
1316
|
+
bytes,
|
|
1317
|
+
{
|
|
1318
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
1319
|
+
hash: "SHA-256"
|
|
1320
|
+
},
|
|
1321
|
+
false,
|
|
1322
|
+
["sign"]
|
|
1323
|
+
);
|
|
1324
|
+
const encoder = new TextEncoder();
|
|
1325
|
+
const dataBytes = encoder.encode(data);
|
|
1326
|
+
const signature = await crypto.subtle.sign(
|
|
1327
|
+
"RSASSA-PKCS1-v1_5",
|
|
1328
|
+
key,
|
|
1329
|
+
dataBytes
|
|
1330
|
+
);
|
|
1331
|
+
const signatureArray = new Uint8Array(signature);
|
|
1332
|
+
return Array.from(signatureArray).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1333
|
+
}
|
|
1334
|
+
async function generateSignedUrl(path, options) {
|
|
1335
|
+
const serviceAccount = getServiceAccount();
|
|
1336
|
+
const bucket = getStorageBucket();
|
|
1337
|
+
const method = actionToMethod(options.action);
|
|
1338
|
+
const expiration = getExpirationTimestamp(options.expires);
|
|
1339
|
+
const now = /* @__PURE__ */ new Date();
|
|
1340
|
+
const timestamp = Math.floor(now.getTime() / 1e3);
|
|
1341
|
+
const isoString = now.toISOString();
|
|
1342
|
+
const datestamp = isoString.split("T")[0].replace(/-/g, "");
|
|
1343
|
+
const timeString = isoString.split("T")[1].replace(/[:.]/g, "").substring(0, 6);
|
|
1344
|
+
const dateTimeStamp = `${datestamp}T${timeString}Z`;
|
|
1345
|
+
const credentialScope = `${datestamp}/auto/storage/goog4_request`;
|
|
1346
|
+
const credential = `${serviceAccount.client_email}/${credentialScope}`;
|
|
1347
|
+
const canonicalHeaders = `host:storage.googleapis.com
|
|
1348
|
+
`;
|
|
1349
|
+
const signedHeaders = "host";
|
|
1350
|
+
const queryParams = {
|
|
1351
|
+
"X-Goog-Algorithm": "GOOG4-RSA-SHA256",
|
|
1352
|
+
"X-Goog-Credential": credential,
|
|
1353
|
+
"X-Goog-Date": dateTimeStamp,
|
|
1354
|
+
"X-Goog-Expires": (expiration - timestamp).toString(),
|
|
1355
|
+
"X-Goog-SignedHeaders": signedHeaders
|
|
1356
|
+
};
|
|
1357
|
+
if (options.contentType) {
|
|
1358
|
+
queryParams["response-content-type"] = options.contentType;
|
|
1359
|
+
}
|
|
1360
|
+
if (options.responseDisposition) {
|
|
1361
|
+
queryParams["response-content-disposition"] = options.responseDisposition;
|
|
1362
|
+
}
|
|
1363
|
+
if (options.responseType) {
|
|
1364
|
+
queryParams["response-content-type"] = options.responseType;
|
|
1365
|
+
}
|
|
1366
|
+
const sortedParams = Object.keys(queryParams).sort();
|
|
1367
|
+
const canonicalQueryString = sortedParams.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`).join("&");
|
|
1368
|
+
const encodedPath = path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
1369
|
+
const canonicalUri = `/${bucket}/${encodedPath}`;
|
|
1370
|
+
const canonicalRequest = [
|
|
1371
|
+
method,
|
|
1372
|
+
canonicalUri,
|
|
1373
|
+
canonicalQueryString,
|
|
1374
|
+
canonicalHeaders,
|
|
1375
|
+
signedHeaders,
|
|
1376
|
+
"UNSIGNED-PAYLOAD"
|
|
1377
|
+
].join("\n");
|
|
1378
|
+
const canonicalRequestHash = stringToHex(canonicalRequest);
|
|
1379
|
+
const stringToSign = [
|
|
1380
|
+
"GOOG4-RSA-SHA256",
|
|
1381
|
+
dateTimeStamp,
|
|
1382
|
+
credentialScope,
|
|
1383
|
+
canonicalRequestHash
|
|
1384
|
+
].join("\n");
|
|
1385
|
+
const signature = await signData(stringToSign, serviceAccount.private_key);
|
|
1386
|
+
const signedUrl = `https://storage.googleapis.com${canonicalUri}?${canonicalQueryString}&X-Goog-Signature=${signature}`;
|
|
1387
|
+
return signedUrl;
|
|
1388
|
+
}
|
|
941
1389
|
export {
|
|
942
1390
|
FieldValue,
|
|
943
1391
|
addDocument,
|
|
944
1392
|
batchWrite,
|
|
945
1393
|
clearConfig,
|
|
946
1394
|
clearTokenCache,
|
|
1395
|
+
createCustomToken,
|
|
947
1396
|
deleteDocument,
|
|
1397
|
+
deleteFile,
|
|
1398
|
+
downloadFile,
|
|
1399
|
+
fileExists,
|
|
1400
|
+
generateSignedUrl,
|
|
948
1401
|
getAdminAccessToken,
|
|
949
1402
|
getAuth,
|
|
950
1403
|
getConfig,
|
|
951
1404
|
getDocument,
|
|
1405
|
+
getFileMetadata,
|
|
952
1406
|
getProjectId,
|
|
953
1407
|
getServiceAccount,
|
|
954
1408
|
getUserFromToken,
|
|
955
1409
|
initializeApp,
|
|
1410
|
+
listFiles,
|
|
956
1411
|
queryDocuments,
|
|
957
1412
|
setDocument,
|
|
1413
|
+
signInWithCustomToken,
|
|
958
1414
|
updateDocument,
|
|
1415
|
+
uploadFile,
|
|
959
1416
|
verifyIdToken
|
|
960
1417
|
};
|
package/package.json
CHANGED
|
Binary file
|