@prmichaelsen/firebase-admin-sdk-v8 2.3.1 → 2.4.2

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.mjs CHANGED
@@ -1,81 +1,13 @@
1
- // src/config.ts
2
- var globalConfig = {};
3
- function initializeApp(config) {
4
- globalConfig = { ...config };
5
- }
6
- function getConfig() {
7
- return globalConfig;
8
- }
9
- function clearConfig() {
10
- globalConfig = {};
11
- }
12
- function getServiceAccount() {
13
- if (globalConfig.serviceAccount) {
14
- if (typeof globalConfig.serviceAccount === "string") {
15
- return JSON.parse(globalConfig.serviceAccount);
16
- }
17
- return globalConfig.serviceAccount;
18
- }
19
- const key = typeof process !== "undefined" && process.env?.FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY;
20
- if (!key) {
21
- throw new Error(
22
- "Firebase service account not configured. Either call initializeApp({ serviceAccount: ... }) or set FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY environment variable."
23
- );
24
- }
25
- try {
26
- const serviceAccount = JSON.parse(key);
27
- const requiredFields = [
28
- "type",
29
- "project_id",
30
- "private_key_id",
31
- "private_key",
32
- "client_email",
33
- "client_id",
34
- "token_uri"
35
- ];
36
- for (const field of requiredFields) {
37
- if (!(field in serviceAccount)) {
38
- throw new Error(`Service account is missing required field: ${field}`);
39
- }
40
- }
41
- return serviceAccount;
42
- } catch (error) {
43
- if (error instanceof SyntaxError) {
44
- throw new Error(
45
- "Failed to parse FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY. Ensure it contains valid JSON."
46
- );
47
- }
48
- throw error;
49
- }
50
- }
51
- function getProjectId() {
52
- if (globalConfig.projectId) {
53
- return globalConfig.projectId;
54
- }
55
- if (typeof process !== "undefined" && process.env) {
56
- const projectId = process.env.FIREBASE_PROJECT_ID || process.env.PUBLIC_FIREBASE_PROJECT_ID;
57
- if (projectId) {
58
- return projectId;
59
- }
60
- }
61
- throw new Error(
62
- "Firebase project ID not configured. Either call initializeApp({ projectId: ... }) or set FIREBASE_PROJECT_ID environment variable."
63
- );
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
- }
1
+ import {
2
+ clearConfig,
3
+ clearTokenCache,
4
+ getAdminAccessToken,
5
+ getConfig,
6
+ getFirebaseApiKey,
7
+ getProjectId,
8
+ getServiceAccount,
9
+ initializeApp
10
+ } from "./chunk-5X465GLA.mjs";
79
11
 
80
12
  // src/x509.ts
81
13
  function decodeBase64(str) {
@@ -166,23 +98,41 @@ async function importPublicKeyFromX509(pem) {
166
98
  }
167
99
 
168
100
  // src/auth.ts
169
- var publicKeysCache = null;
170
- var publicKeysCacheExpiry = 0;
101
+ var idTokenKeysCache = null;
102
+ var idTokenKeysCacheExpiry = 0;
103
+ var sessionKeysCache = null;
104
+ var sessionKeysCacheExpiry = 0;
171
105
  async function fetchPublicKeys(issuer) {
172
- let endpoint = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com";
173
- if (issuer && issuer.includes("session.firebase.google.com")) {
106
+ const isSessionCookie = issuer && issuer.includes("session.firebase.google.com");
107
+ let endpoint;
108
+ let cache;
109
+ let cacheExpiry;
110
+ if (isSessionCookie) {
174
111
  endpoint = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys";
112
+ cache = sessionKeysCache;
113
+ cacheExpiry = sessionKeysCacheExpiry;
114
+ } else {
115
+ endpoint = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com";
116
+ cache = idTokenKeysCache;
117
+ cacheExpiry = idTokenKeysCacheExpiry;
175
118
  }
176
- if (publicKeysCache && Date.now() < publicKeysCacheExpiry) {
177
- return publicKeysCache;
119
+ if (cache && Date.now() < cacheExpiry) {
120
+ return cache;
178
121
  }
179
122
  const response = await fetch(endpoint);
180
123
  if (!response.ok) {
181
124
  throw new Error(`Failed to fetch Firebase public keys from ${endpoint}`);
182
125
  }
183
- publicKeysCache = await response.json();
184
- publicKeysCacheExpiry = Date.now() + 36e5;
185
- return publicKeysCache;
126
+ const keys = await response.json();
127
+ const newExpiry = Date.now() + 36e5;
128
+ if (isSessionCookie) {
129
+ sessionKeysCache = keys;
130
+ sessionKeysCacheExpiry = newExpiry;
131
+ } else {
132
+ idTokenKeysCache = keys;
133
+ idTokenKeysCacheExpiry = newExpiry;
134
+ }
135
+ return keys;
186
136
  }
187
137
  function base64UrlDecode(str) {
188
138
  let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
@@ -257,8 +207,14 @@ async function verifyIdToken(idToken) {
257
207
  let publicKeys = await fetchPublicKeys(payload.iss);
258
208
  let publicKeyPem = publicKeys[header.kid];
259
209
  if (!publicKeyPem) {
260
- publicKeysCache = null;
261
- publicKeysCacheExpiry = 0;
210
+ const isSessionCookie = payload.iss && payload.iss.includes("session.firebase.google.com");
211
+ if (isSessionCookie) {
212
+ sessionKeysCache = null;
213
+ sessionKeysCacheExpiry = 0;
214
+ } else {
215
+ idTokenKeysCache = null;
216
+ idTokenKeysCacheExpiry = 0;
217
+ }
262
218
  publicKeys = await fetchPublicKeys(payload.iss);
263
219
  publicKeyPem = publicKeys[header.kid];
264
220
  if (!publicKeyPem) {
@@ -392,6 +348,136 @@ async function signInWithCustomToken(customToken) {
392
348
  isNewUser: result.isNewUser
393
349
  };
394
350
  }
351
+ async function createSessionCookie(idToken, options) {
352
+ if (!idToken || typeof idToken !== "string") {
353
+ throw new Error("idToken must be a non-empty string");
354
+ }
355
+ if (!options.expiresIn || typeof options.expiresIn !== "number") {
356
+ throw new Error("expiresIn must be a number");
357
+ }
358
+ const MIN_DURATION = 5 * 60 * 1e3;
359
+ const MAX_DURATION = 14 * 24 * 60 * 60 * 1e3;
360
+ if (options.expiresIn < MIN_DURATION) {
361
+ throw new Error(`expiresIn must be at least ${MIN_DURATION}ms (5 minutes)`);
362
+ }
363
+ if (options.expiresIn > MAX_DURATION) {
364
+ throw new Error(`expiresIn must be at most ${MAX_DURATION}ms (14 days)`);
365
+ }
366
+ const { getAdminAccessToken: getAdminAccessToken2 } = await import("./token-generation-5K7K6T6U.mjs");
367
+ const accessToken = await getAdminAccessToken2();
368
+ const projectId = getProjectId();
369
+ const url = `https://identitytoolkit.googleapis.com/v1/projects/${projectId}:createSessionCookie`;
370
+ const validDurationSeconds = Math.floor(options.expiresIn / 1e3);
371
+ const response = await fetch(url, {
372
+ method: "POST",
373
+ headers: {
374
+ "Authorization": `Bearer ${accessToken}`,
375
+ "Content-Type": "application/json"
376
+ },
377
+ body: JSON.stringify({
378
+ idToken,
379
+ validDuration: validDurationSeconds.toString()
380
+ // Must be string per API spec
381
+ })
382
+ });
383
+ if (!response.ok) {
384
+ const errorText = await response.text();
385
+ let errorMessage = `Failed to create session cookie: ${response.status}`;
386
+ try {
387
+ const errorJson = JSON.parse(errorText);
388
+ if (errorJson.error && errorJson.error.message) {
389
+ errorMessage += ` - ${errorJson.error.message}`;
390
+ }
391
+ } catch {
392
+ errorMessage += ` - ${errorText}`;
393
+ }
394
+ throw new Error(errorMessage);
395
+ }
396
+ const result = await response.json();
397
+ return result.sessionCookie;
398
+ }
399
+ async function verifySessionCookie(sessionCookie, checkRevoked = false) {
400
+ if (!sessionCookie || typeof sessionCookie !== "string") {
401
+ throw new Error("sessionCookie must be a non-empty string");
402
+ }
403
+ try {
404
+ const parts = sessionCookie.split(".");
405
+ if (parts.length !== 3) {
406
+ throw new Error("Invalid session cookie format");
407
+ }
408
+ const [headerB64, payloadB64] = parts;
409
+ const headerJson = atob(headerB64.replace(/-/g, "+").replace(/_/g, "/"));
410
+ const header = JSON.parse(headerJson);
411
+ const payloadJson = atob(payloadB64.replace(/-/g, "+").replace(/_/g, "/"));
412
+ const payload = JSON.parse(payloadJson);
413
+ const projectId = getProjectId();
414
+ const now = Math.floor(Date.now() / 1e3);
415
+ if (!payload.exp || payload.exp < now) {
416
+ throw new Error("Session cookie has expired");
417
+ }
418
+ if (!payload.iat || payload.iat > now) {
419
+ throw new Error("Session cookie issued in the future");
420
+ }
421
+ if (payload.aud !== projectId) {
422
+ throw new Error(`Session cookie has incorrect audience. Expected ${projectId}, got ${payload.aud}`);
423
+ }
424
+ const expectedIssuer = `https://session.firebase.google.com/${projectId}`;
425
+ if (payload.iss !== expectedIssuer) {
426
+ throw new Error(`Session cookie has incorrect issuer. Expected ${expectedIssuer}, got ${payload.iss}`);
427
+ }
428
+ if (!payload.sub || typeof payload.sub !== "string" || payload.sub.length === 0) {
429
+ throw new Error("Session cookie has no subject (user ID)");
430
+ }
431
+ await verifySessionCookieSignature(sessionCookie, header, payload);
432
+ if (checkRevoked) {
433
+ }
434
+ return {
435
+ uid: payload.sub,
436
+ aud: payload.aud,
437
+ auth_time: payload.auth_time,
438
+ exp: payload.exp,
439
+ iat: payload.iat,
440
+ iss: payload.iss,
441
+ sub: payload.sub,
442
+ email: payload.email,
443
+ email_verified: payload.email_verified,
444
+ firebase: payload.firebase,
445
+ ...payload
446
+ };
447
+ } catch (error) {
448
+ throw new Error(`Failed to verify session cookie: ${error.message}`);
449
+ }
450
+ }
451
+ async function verifySessionCookieSignature(jwt, header, payload) {
452
+ const keys = await fetchPublicKeys(payload.iss);
453
+ const kid = header.kid;
454
+ if (!kid || !keys[kid]) {
455
+ throw new Error("Session cookie has invalid key ID");
456
+ }
457
+ const publicKeyPem = keys[kid];
458
+ const publicKey = await importPublicKeyFromX509(publicKeyPem);
459
+ const [headerAndPayload, signature] = [
460
+ jwt.split(".").slice(0, 2).join("."),
461
+ jwt.split(".")[2]
462
+ ];
463
+ const encoder = new TextEncoder();
464
+ const data = encoder.encode(headerAndPayload);
465
+ const signatureBase64 = signature.replace(/-/g, "+").replace(/_/g, "/");
466
+ const signatureBinary = atob(signatureBase64);
467
+ const signatureBytes = new Uint8Array(signatureBinary.length);
468
+ for (let i = 0; i < signatureBinary.length; i++) {
469
+ signatureBytes[i] = signatureBinary.charCodeAt(i);
470
+ }
471
+ const isValid = await crypto.subtle.verify(
472
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
473
+ publicKey,
474
+ signatureBytes,
475
+ data
476
+ );
477
+ if (!isValid) {
478
+ throw new Error("Session cookie signature verification failed");
479
+ }
480
+ }
395
481
  function getAuth() {
396
482
  return {
397
483
  verifyIdToken
@@ -674,78 +760,6 @@ function removeFieldTransforms(data) {
674
760
  return result;
675
761
  }
676
762
 
677
- // src/token-generation.ts
678
- function base64UrlEncode2(str) {
679
- return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
680
- }
681
- function base64UrlEncodeBuffer(buffer) {
682
- return btoa(String.fromCharCode(...buffer)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
683
- }
684
- async function createJWT(serviceAccount) {
685
- const now = Math.floor(Date.now() / 1e3);
686
- const expiry = now + 3600;
687
- const header = {
688
- alg: "RS256",
689
- typ: "JWT"
690
- };
691
- const payload = {
692
- iss: serviceAccount.client_email,
693
- sub: serviceAccount.client_email,
694
- aud: serviceAccount.token_uri,
695
- iat: now,
696
- exp: expiry,
697
- scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
698
- };
699
- const encodedHeader = base64UrlEncode2(JSON.stringify(header));
700
- const encodedPayload = base64UrlEncode2(JSON.stringify(payload));
701
- const unsignedToken = `${encodedHeader}.${encodedPayload}`;
702
- const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
703
- const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
704
- const cryptoKey = await crypto.subtle.importKey(
705
- "pkcs8",
706
- binaryDer,
707
- { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
708
- false,
709
- ["sign"]
710
- );
711
- const signature = await crypto.subtle.sign(
712
- "RSASSA-PKCS1-v1_5",
713
- cryptoKey,
714
- new TextEncoder().encode(unsignedToken)
715
- );
716
- const encodedSignature = base64UrlEncodeBuffer(new Uint8Array(signature));
717
- return `${unsignedToken}.${encodedSignature}`;
718
- }
719
- var cachedAccessToken = null;
720
- var tokenExpiry = 0;
721
- async function getAdminAccessToken() {
722
- if (cachedAccessToken && Date.now() < tokenExpiry) {
723
- return cachedAccessToken;
724
- }
725
- const serviceAccount = getServiceAccount();
726
- const jwt = await createJWT(serviceAccount);
727
- const response = await fetch(serviceAccount.token_uri, {
728
- method: "POST",
729
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
730
- body: new URLSearchParams({
731
- grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
732
- assertion: jwt
733
- })
734
- });
735
- if (!response.ok) {
736
- const errorText = await response.text();
737
- throw new Error(`Failed to get access token: ${errorText}`);
738
- }
739
- const data = await response.json();
740
- cachedAccessToken = data.access_token;
741
- tokenExpiry = Date.now() + data.expires_in * 1e3 - 6e4;
742
- return cachedAccessToken;
743
- }
744
- function clearTokenCache() {
745
- cachedAccessToken = null;
746
- tokenExpiry = 0;
747
- }
748
-
749
763
  // src/firestore/path-validation.ts
750
764
  function validateCollectionPath(argumentName, collectionPath) {
751
765
  const segments = collectionPath.split("/").filter((s) => s.length > 0);
@@ -1635,6 +1649,7 @@ export {
1635
1649
  clearTokenCache,
1636
1650
  countDocuments,
1637
1651
  createCustomToken,
1652
+ createSessionCookie,
1638
1653
  deleteDocument,
1639
1654
  deleteFile,
1640
1655
  downloadFile,
@@ -1658,5 +1673,6 @@ export {
1658
1673
  updateDocument,
1659
1674
  uploadFile,
1660
1675
  uploadFileResumable,
1661
- verifyIdToken
1676
+ verifyIdToken,
1677
+ verifySessionCookie
1662
1678
  };
@@ -0,0 +1,8 @@
1
+ import {
2
+ clearTokenCache,
3
+ getAdminAccessToken
4
+ } from "./chunk-5X465GLA.mjs";
5
+ export {
6
+ clearTokenCache,
7
+ getAdminAccessToken
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/firebase-admin-sdk-v8",
3
- "version": "2.3.1",
3
+ "version": "2.4.2",
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",
@@ -26,8 +26,8 @@
26
26
  "typecheck": "tsc --noEmit",
27
27
  "test": "jest",
28
28
  "test:watch": "jest --watch",
29
- "test:e2e": "jest --config jest.e2e.config.js",
30
- "test:e2e:watch": "jest --config jest.e2e.config.js --watch",
29
+ "test:e2e": "node --env-file=.env node_modules/.bin/jest --config jest.e2e.config.js",
30
+ "test:e2e:watch": "node --env-file=.env node_modules/.bin/jest --config jest.e2e.config.js --watch",
31
31
  "test:all": "npm test && npm run test:e2e",
32
32
  "prepublishOnly": "npm run build"
33
33
  },