@prmichaelsen/firebase-admin-sdk-v8 2.0.22 → 2.2.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 CHANGED
@@ -195,6 +195,7 @@ interface BatchWriteResult {
195
195
  interface SDKConfig {
196
196
  serviceAccount?: ServiceAccount | string;
197
197
  projectId?: string;
198
+ apiKey?: string;
198
199
  }
199
200
  /**
200
201
  * Initialize the Firebase Admin SDK with configuration
@@ -250,6 +251,21 @@ declare function getProjectId(): string;
250
251
  * ID token verification supporting both Firebase v9 and v10 token formats
251
252
  */
252
253
 
254
+ /**
255
+ * Custom claims for custom tokens
256
+ */
257
+ interface CustomClaims {
258
+ [key: string]: any;
259
+ }
260
+ /**
261
+ * Response from signInWithCustomToken
262
+ */
263
+ interface CustomTokenSignInResponse {
264
+ idToken: string;
265
+ refreshToken: string;
266
+ expiresIn: string;
267
+ isNewUser?: boolean;
268
+ }
253
269
  /**
254
270
  * Verify a Firebase ID token
255
271
  * Supports both Firebase v9 (securetoken) and v10 (session) token formats
@@ -280,6 +296,45 @@ declare function verifyIdToken(idToken: string): Promise<DecodedIdToken>;
280
296
  * ```
281
297
  */
282
298
  declare function getUserFromToken(idToken: string): Promise<UserInfo>;
299
+ /**
300
+ * Create a custom token for a user
301
+ *
302
+ * @param uid - User ID
303
+ * @param customClaims - Optional custom claims to include in token
304
+ * @returns Custom JWT token
305
+ *
306
+ * @example
307
+ * ```typescript
308
+ * // Create token with custom claims
309
+ * const token = await createCustomToken('user123', {
310
+ * role: 'admin',
311
+ * premium: true,
312
+ * });
313
+ *
314
+ * // Use token on client to sign in
315
+ * await signInWithCustomToken(auth, token);
316
+ * ```
317
+ */
318
+ declare function createCustomToken(uid: string, customClaims?: CustomClaims): Promise<string>;
319
+ /**
320
+ * Exchange a custom token for an ID token and refresh token
321
+ *
322
+ * @param customToken - Custom JWT token created with createCustomToken
323
+ * @returns User credentials (idToken, refreshToken, expiresIn, localId)
324
+ *
325
+ * @example
326
+ * ```typescript
327
+ * // Server-side: Create custom token
328
+ * const customToken = await createCustomToken('user123', { role: 'admin' });
329
+ *
330
+ * // Exchange for ID token
331
+ * const credentials = await signInWithCustomToken(customToken);
332
+ *
333
+ * // Use ID token for authenticated requests
334
+ * const user = await verifyIdToken(credentials.idToken);
335
+ * ```
336
+ */
337
+ declare function signInWithCustomToken(customToken: string): Promise<CustomTokenSignInResponse>;
283
338
  /**
284
339
  * Get Auth instance (for compatibility, but not used in new implementation)
285
340
  * @deprecated Use verifyIdToken directly
@@ -705,4 +760,4 @@ declare function getAdminAccessToken(): Promise<string>;
705
760
  */
706
761
  declare function clearTokenCache(): void;
707
762
 
708
- export { type BatchWrite, type BatchWriteResult, type DataObject, type DecodedIdToken, type DocumentReference, type DownloadOptions, FieldValue, type FieldValue$1 as FieldValueSentinel, FieldValueType, type FileMetadata, type FirestoreDocument, type FirestoreValue, type ListFilesResult, type ListOptions, type QueryFilter, type QueryOptions, type QueryOrder, type ServiceAccount, type SetOptions, type SignedUrlOptions, type TokenResponse, type UpdateOptions, type UploadOptions, type UserInfo, type WhereFilterOp, addDocument, batchWrite, clearConfig, clearTokenCache, deleteDocument, deleteFile, downloadFile, fileExists, generateSignedUrl, getAdminAccessToken, getAuth, getConfig, getDocument, getFileMetadata, getProjectId, getServiceAccount, getUserFromToken, initializeApp, listFiles, queryDocuments, setDocument, updateDocument, uploadFile, verifyIdToken };
763
+ export { type BatchWrite, type BatchWriteResult, type CustomClaims, type CustomTokenSignInResponse, type DataObject, type DecodedIdToken, type DocumentReference, type DownloadOptions, FieldValue, type FieldValue$1 as FieldValueSentinel, FieldValueType, type FileMetadata, type FirestoreDocument, type FirestoreValue, type ListFilesResult, type ListOptions, type QueryFilter, type QueryOptions, type QueryOrder, type ServiceAccount, type SetOptions, type SignedUrlOptions, type TokenResponse, type UpdateOptions, type UploadOptions, type UserInfo, type WhereFilterOp, addDocument, batchWrite, clearConfig, clearTokenCache, createCustomToken, deleteDocument, deleteFile, downloadFile, fileExists, generateSignedUrl, getAdminAccessToken, getAuth, getConfig, getDocument, getFileMetadata, getProjectId, getServiceAccount, getUserFromToken, initializeApp, listFiles, queryDocuments, setDocument, signInWithCustomToken, updateDocument, uploadFile, verifyIdToken };
package/dist/index.d.ts CHANGED
@@ -195,6 +195,7 @@ interface BatchWriteResult {
195
195
  interface SDKConfig {
196
196
  serviceAccount?: ServiceAccount | string;
197
197
  projectId?: string;
198
+ apiKey?: string;
198
199
  }
199
200
  /**
200
201
  * Initialize the Firebase Admin SDK with configuration
@@ -250,6 +251,21 @@ declare function getProjectId(): string;
250
251
  * ID token verification supporting both Firebase v9 and v10 token formats
251
252
  */
252
253
 
254
+ /**
255
+ * Custom claims for custom tokens
256
+ */
257
+ interface CustomClaims {
258
+ [key: string]: any;
259
+ }
260
+ /**
261
+ * Response from signInWithCustomToken
262
+ */
263
+ interface CustomTokenSignInResponse {
264
+ idToken: string;
265
+ refreshToken: string;
266
+ expiresIn: string;
267
+ isNewUser?: boolean;
268
+ }
253
269
  /**
254
270
  * Verify a Firebase ID token
255
271
  * Supports both Firebase v9 (securetoken) and v10 (session) token formats
@@ -280,6 +296,45 @@ declare function verifyIdToken(idToken: string): Promise<DecodedIdToken>;
280
296
  * ```
281
297
  */
282
298
  declare function getUserFromToken(idToken: string): Promise<UserInfo>;
299
+ /**
300
+ * Create a custom token for a user
301
+ *
302
+ * @param uid - User ID
303
+ * @param customClaims - Optional custom claims to include in token
304
+ * @returns Custom JWT token
305
+ *
306
+ * @example
307
+ * ```typescript
308
+ * // Create token with custom claims
309
+ * const token = await createCustomToken('user123', {
310
+ * role: 'admin',
311
+ * premium: true,
312
+ * });
313
+ *
314
+ * // Use token on client to sign in
315
+ * await signInWithCustomToken(auth, token);
316
+ * ```
317
+ */
318
+ declare function createCustomToken(uid: string, customClaims?: CustomClaims): Promise<string>;
319
+ /**
320
+ * Exchange a custom token for an ID token and refresh token
321
+ *
322
+ * @param customToken - Custom JWT token created with createCustomToken
323
+ * @returns User credentials (idToken, refreshToken, expiresIn, localId)
324
+ *
325
+ * @example
326
+ * ```typescript
327
+ * // Server-side: Create custom token
328
+ * const customToken = await createCustomToken('user123', { role: 'admin' });
329
+ *
330
+ * // Exchange for ID token
331
+ * const credentials = await signInWithCustomToken(customToken);
332
+ *
333
+ * // Use ID token for authenticated requests
334
+ * const user = await verifyIdToken(credentials.idToken);
335
+ * ```
336
+ */
337
+ declare function signInWithCustomToken(customToken: string): Promise<CustomTokenSignInResponse>;
283
338
  /**
284
339
  * Get Auth instance (for compatibility, but not used in new implementation)
285
340
  * @deprecated Use verifyIdToken directly
@@ -705,4 +760,4 @@ declare function getAdminAccessToken(): Promise<string>;
705
760
  */
706
761
  declare function clearTokenCache(): void;
707
762
 
708
- export { type BatchWrite, type BatchWriteResult, type DataObject, type DecodedIdToken, type DocumentReference, type DownloadOptions, FieldValue, type FieldValue$1 as FieldValueSentinel, FieldValueType, type FileMetadata, type FirestoreDocument, type FirestoreValue, type ListFilesResult, type ListOptions, type QueryFilter, type QueryOptions, type QueryOrder, type ServiceAccount, type SetOptions, type SignedUrlOptions, type TokenResponse, type UpdateOptions, type UploadOptions, type UserInfo, type WhereFilterOp, addDocument, batchWrite, clearConfig, clearTokenCache, deleteDocument, deleteFile, downloadFile, fileExists, generateSignedUrl, getAdminAccessToken, getAuth, getConfig, getDocument, getFileMetadata, getProjectId, getServiceAccount, getUserFromToken, initializeApp, listFiles, queryDocuments, setDocument, updateDocument, uploadFile, verifyIdToken };
763
+ export { type BatchWrite, type BatchWriteResult, type CustomClaims, type CustomTokenSignInResponse, type DataObject, type DecodedIdToken, type DocumentReference, type DownloadOptions, FieldValue, type FieldValue$1 as FieldValueSentinel, FieldValueType, type FileMetadata, type FirestoreDocument, type FirestoreValue, type ListFilesResult, type ListOptions, type QueryFilter, type QueryOptions, type QueryOrder, type ServiceAccount, type SetOptions, type SignedUrlOptions, type TokenResponse, type UpdateOptions, type UploadOptions, type UserInfo, type WhereFilterOp, addDocument, batchWrite, clearConfig, clearTokenCache, createCustomToken, deleteDocument, deleteFile, downloadFile, fileExists, generateSignedUrl, getAdminAccessToken, getAuth, getConfig, getDocument, getFileMetadata, getProjectId, getServiceAccount, getUserFromToken, initializeApp, listFiles, queryDocuments, setDocument, signInWithCustomToken, updateDocument, uploadFile, verifyIdToken };
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  batchWrite: () => batchWrite,
26
26
  clearConfig: () => clearConfig,
27
27
  clearTokenCache: () => clearTokenCache,
28
+ createCustomToken: () => createCustomToken,
28
29
  deleteDocument: () => deleteDocument,
29
30
  deleteFile: () => deleteFile,
30
31
  downloadFile: () => downloadFile,
@@ -42,6 +43,7 @@ __export(index_exports, {
42
43
  listFiles: () => listFiles,
43
44
  queryDocuments: () => queryDocuments,
44
45
  setDocument: () => setDocument,
46
+ signInWithCustomToken: () => signInWithCustomToken,
45
47
  updateDocument: () => updateDocument,
46
48
  uploadFile: () => uploadFile,
47
49
  verifyIdToken: () => verifyIdToken
@@ -112,6 +114,20 @@ function getProjectId() {
112
114
  "Firebase project ID not configured. Either call initializeApp({ projectId: ... }) or set FIREBASE_PROJECT_ID environment variable."
113
115
  );
114
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
+ }
115
131
 
116
132
  // src/x509.ts
117
133
  function decodeBase64(str) {
@@ -332,6 +348,105 @@ async function getUserFromToken(idToken) {
332
348
  photoURL: decodedToken.picture || null
333
349
  };
334
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
+ isNewUser: result.isNewUser
448
+ };
449
+ }
335
450
  function getAuth() {
336
451
  return {
337
452
  verifyIdToken
@@ -615,7 +730,7 @@ function removeFieldTransforms(data) {
615
730
  }
616
731
 
617
732
  // src/token-generation.ts
618
- function base64UrlEncode(str) {
733
+ function base64UrlEncode2(str) {
619
734
  return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
620
735
  }
621
736
  function base64UrlEncodeBuffer(buffer) {
@@ -636,8 +751,8 @@ async function createJWT(serviceAccount) {
636
751
  exp: expiry,
637
752
  scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
638
753
  };
639
- const encodedHeader = base64UrlEncode(JSON.stringify(header));
640
- const encodedPayload = base64UrlEncode(JSON.stringify(payload));
754
+ const encodedHeader = base64UrlEncode2(JSON.stringify(header));
755
+ const encodedPayload = base64UrlEncode2(JSON.stringify(payload));
641
756
  const unsignedToken = `${encodedHeader}.${encodedPayload}`;
642
757
  const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
643
758
  const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
@@ -1019,6 +1134,10 @@ async function batchWrite(operations) {
1019
1134
  var STORAGE_API_BASE = "https://storage.googleapis.com/storage/v1";
1020
1135
  var UPLOAD_API_BASE = "https://storage.googleapis.com/upload/storage/v1";
1021
1136
  function getDefaultBucket() {
1137
+ const customBucket = process.env.FIREBASE_STORAGE_BUCKET;
1138
+ if (customBucket) {
1139
+ return customBucket;
1140
+ }
1022
1141
  const projectId = getProjectId();
1023
1142
  return `${projectId}.appspot.com`;
1024
1143
  }
@@ -1206,6 +1325,14 @@ async function fileExists(path) {
1206
1325
  }
1207
1326
 
1208
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
+ }
1209
1336
  function getExpirationTimestamp(expires) {
1210
1337
  if (expires instanceof Date) {
1211
1338
  return Math.floor(expires.getTime() / 1e3);
@@ -1258,12 +1385,15 @@ async function signData(data, privateKey) {
1258
1385
  }
1259
1386
  async function generateSignedUrl(path, options) {
1260
1387
  const serviceAccount = getServiceAccount();
1261
- const projectId = getProjectId();
1262
- const bucket = `${projectId}.appspot.com`;
1388
+ const bucket = getStorageBucket();
1263
1389
  const method = actionToMethod(options.action);
1264
1390
  const expiration = getExpirationTimestamp(options.expires);
1265
- const timestamp = Math.floor(Date.now() / 1e3);
1266
- const datestamp = new Date(timestamp * 1e3).toISOString().split("T")[0].replace(/-/g, "");
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`;
1267
1397
  const credentialScope = `${datestamp}/auto/storage/goog4_request`;
1268
1398
  const credential = `${serviceAccount.client_email}/${credentialScope}`;
1269
1399
  const canonicalHeaders = `host:storage.googleapis.com
@@ -1272,7 +1402,7 @@ async function generateSignedUrl(path, options) {
1272
1402
  const queryParams = {
1273
1403
  "X-Goog-Algorithm": "GOOG4-RSA-SHA256",
1274
1404
  "X-Goog-Credential": credential,
1275
- "X-Goog-Date": `${datestamp}T000000Z`,
1405
+ "X-Goog-Date": dateTimeStamp,
1276
1406
  "X-Goog-Expires": (expiration - timestamp).toString(),
1277
1407
  "X-Goog-SignedHeaders": signedHeaders
1278
1408
  };
@@ -1287,7 +1417,8 @@ async function generateSignedUrl(path, options) {
1287
1417
  }
1288
1418
  const sortedParams = Object.keys(queryParams).sort();
1289
1419
  const canonicalQueryString = sortedParams.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`).join("&");
1290
- const canonicalUri = `/${bucket}/${path}`;
1420
+ const encodedPath = path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
1421
+ const canonicalUri = `/${bucket}/${encodedPath}`;
1291
1422
  const canonicalRequest = [
1292
1423
  method,
1293
1424
  canonicalUri,
@@ -1299,7 +1430,7 @@ async function generateSignedUrl(path, options) {
1299
1430
  const canonicalRequestHash = stringToHex(canonicalRequest);
1300
1431
  const stringToSign = [
1301
1432
  "GOOG4-RSA-SHA256",
1302
- `${datestamp}T000000Z`,
1433
+ dateTimeStamp,
1303
1434
  credentialScope,
1304
1435
  canonicalRequestHash
1305
1436
  ].join("\n");
@@ -1314,6 +1445,7 @@ async function generateSignedUrl(path, options) {
1314
1445
  batchWrite,
1315
1446
  clearConfig,
1316
1447
  clearTokenCache,
1448
+ createCustomToken,
1317
1449
  deleteDocument,
1318
1450
  deleteFile,
1319
1451
  downloadFile,
@@ -1331,6 +1463,7 @@ async function generateSignedUrl(path, options) {
1331
1463
  listFiles,
1332
1464
  queryDocuments,
1333
1465
  setDocument,
1466
+ signInWithCustomToken,
1334
1467
  updateDocument,
1335
1468
  uploadFile,
1336
1469
  verifyIdToken
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
+ isNewUser: result.isNewUser
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 base64UrlEncode(str) {
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 = base64UrlEncode(JSON.stringify(header));
590
- const encodedPayload = base64UrlEncode(JSON.stringify(payload));
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));
@@ -969,6 +1082,10 @@ async function batchWrite(operations) {
969
1082
  var STORAGE_API_BASE = "https://storage.googleapis.com/storage/v1";
970
1083
  var UPLOAD_API_BASE = "https://storage.googleapis.com/upload/storage/v1";
971
1084
  function getDefaultBucket() {
1085
+ const customBucket = process.env.FIREBASE_STORAGE_BUCKET;
1086
+ if (customBucket) {
1087
+ return customBucket;
1088
+ }
972
1089
  const projectId = getProjectId();
973
1090
  return `${projectId}.appspot.com`;
974
1091
  }
@@ -1156,6 +1273,14 @@ async function fileExists(path) {
1156
1273
  }
1157
1274
 
1158
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
+ }
1159
1284
  function getExpirationTimestamp(expires) {
1160
1285
  if (expires instanceof Date) {
1161
1286
  return Math.floor(expires.getTime() / 1e3);
@@ -1208,12 +1333,15 @@ async function signData(data, privateKey) {
1208
1333
  }
1209
1334
  async function generateSignedUrl(path, options) {
1210
1335
  const serviceAccount = getServiceAccount();
1211
- const projectId = getProjectId();
1212
- const bucket = `${projectId}.appspot.com`;
1336
+ const bucket = getStorageBucket();
1213
1337
  const method = actionToMethod(options.action);
1214
1338
  const expiration = getExpirationTimestamp(options.expires);
1215
- const timestamp = Math.floor(Date.now() / 1e3);
1216
- const datestamp = new Date(timestamp * 1e3).toISOString().split("T")[0].replace(/-/g, "");
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`;
1217
1345
  const credentialScope = `${datestamp}/auto/storage/goog4_request`;
1218
1346
  const credential = `${serviceAccount.client_email}/${credentialScope}`;
1219
1347
  const canonicalHeaders = `host:storage.googleapis.com
@@ -1222,7 +1350,7 @@ async function generateSignedUrl(path, options) {
1222
1350
  const queryParams = {
1223
1351
  "X-Goog-Algorithm": "GOOG4-RSA-SHA256",
1224
1352
  "X-Goog-Credential": credential,
1225
- "X-Goog-Date": `${datestamp}T000000Z`,
1353
+ "X-Goog-Date": dateTimeStamp,
1226
1354
  "X-Goog-Expires": (expiration - timestamp).toString(),
1227
1355
  "X-Goog-SignedHeaders": signedHeaders
1228
1356
  };
@@ -1237,7 +1365,8 @@ async function generateSignedUrl(path, options) {
1237
1365
  }
1238
1366
  const sortedParams = Object.keys(queryParams).sort();
1239
1367
  const canonicalQueryString = sortedParams.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`).join("&");
1240
- const canonicalUri = `/${bucket}/${path}`;
1368
+ const encodedPath = path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
1369
+ const canonicalUri = `/${bucket}/${encodedPath}`;
1241
1370
  const canonicalRequest = [
1242
1371
  method,
1243
1372
  canonicalUri,
@@ -1249,7 +1378,7 @@ async function generateSignedUrl(path, options) {
1249
1378
  const canonicalRequestHash = stringToHex(canonicalRequest);
1250
1379
  const stringToSign = [
1251
1380
  "GOOG4-RSA-SHA256",
1252
- `${datestamp}T000000Z`,
1381
+ dateTimeStamp,
1253
1382
  credentialScope,
1254
1383
  canonicalRequestHash
1255
1384
  ].join("\n");
@@ -1263,6 +1392,7 @@ export {
1263
1392
  batchWrite,
1264
1393
  clearConfig,
1265
1394
  clearTokenCache,
1395
+ createCustomToken,
1266
1396
  deleteDocument,
1267
1397
  deleteFile,
1268
1398
  downloadFile,
@@ -1280,6 +1410,7 @@ export {
1280
1410
  listFiles,
1281
1411
  queryDocuments,
1282
1412
  setDocument,
1413
+ signInWithCustomToken,
1283
1414
  updateDocument,
1284
1415
  uploadFile,
1285
1416
  verifyIdToken
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/firebase-admin-sdk-v8",
3
- "version": "2.0.22",
3
+ "version": "2.2.0",
4
4
  "description": "Firebase Admin SDK for Cloudflare Workers and edge runtimes using REST APIs",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",