@prmichaelsen/firebase-admin-sdk-v8 2.3.1 → 2.4.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/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.4.0] - 2026-02-15
11
+
12
+ ### Added
13
+ - **Session Cookie Support**: Added `createSessionCookie()` and `verifySessionCookie()`
14
+ - Long-lived authentication sessions (up to 14 days) instead of 1-hour ID tokens
15
+ - Session cookie verification with proper issuer validation
16
+ - 15 new unit tests for session cookie functionality
17
+ - 3 new E2E tests for session cookie creation and verification
18
+ - Comprehensive session cookie documentation in README
19
+
20
+ ### Changed
21
+ - Auth module coverage improved from 63.18% to 97.51%
22
+ - Total tests increased from 418 to 433 (+15 tests)
23
+
10
24
  ## [2.3.1] - 2026-02-14
11
25
 
12
26
  ### Fixed
package/README.md CHANGED
@@ -15,6 +15,7 @@ This library provides Firebase Admin SDK functionality for Cloudflare Workers an
15
15
  - ✅ **Zero Dependencies** - No external dependencies, pure Web APIs (crypto.subtle, fetch)
16
16
  - ✅ **JWT Token Generation** - Service account authentication
17
17
  - ✅ **ID Token Verification** - Verify Firebase ID tokens (supports v9 and v10 formats)
18
+ - ✅ **Session Cookies** - Create and verify long-lived session cookies (up to 14 days)
18
19
  - ✅ **Firebase v10 Compatible** - Supports both old and new token issuer formats
19
20
  - ✅ **Firestore REST API** - Full CRUD operations via REST
20
21
  - ✅ **Field Value Operations** - increment, arrayUnion, arrayRemove, serverTimestamp, delete
@@ -94,7 +95,26 @@ try {
94
95
  }
95
96
  ```
96
97
 
97
- ### 3. Basic Firestore Operations
98
+ ### 3. Session Cookies (Long-Lived Sessions)
99
+
100
+ ```typescript
101
+ import { createSessionCookie, verifySessionCookie } from '@prmichaelsen/firebase-admin-sdk-v8';
102
+
103
+ // Create 14-day session cookie from ID token
104
+ const sessionCookie = await createSessionCookie(idToken, {
105
+ expiresIn: 60 * 60 * 24 * 14 * 1000
106
+ });
107
+
108
+ // Set as HTTP-only cookie
109
+ response.headers.set('Set-Cookie',
110
+ `session=${sessionCookie}; Max-Age=1209600; HttpOnly; Secure; SameSite=Strict`
111
+ );
112
+
113
+ // Verify session cookie
114
+ const user = await verifySessionCookie(cookie);
115
+ ```
116
+
117
+ ### 4. Basic Firestore Operations
98
118
 
99
119
  ```typescript
100
120
  import { setDocument, getDocument, updateDocument, FieldValue } from '@prmichaelsen/firebase-admin-sdk-v8';
@@ -0,0 +1,161 @@
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
+ }
79
+
80
+ // src/token-generation.ts
81
+ function base64UrlEncode(str) {
82
+ return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
83
+ }
84
+ function base64UrlEncodeBuffer(buffer) {
85
+ return btoa(String.fromCharCode(...buffer)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
86
+ }
87
+ async function createJWT(serviceAccount) {
88
+ const now = Math.floor(Date.now() / 1e3);
89
+ const expiry = now + 3600;
90
+ const header = {
91
+ alg: "RS256",
92
+ typ: "JWT"
93
+ };
94
+ const payload = {
95
+ iss: serviceAccount.client_email,
96
+ sub: serviceAccount.client_email,
97
+ aud: serviceAccount.token_uri,
98
+ iat: now,
99
+ exp: expiry,
100
+ scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
101
+ };
102
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
103
+ const encodedPayload = base64UrlEncode(JSON.stringify(payload));
104
+ const unsignedToken = `${encodedHeader}.${encodedPayload}`;
105
+ const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
106
+ const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
107
+ const cryptoKey = await crypto.subtle.importKey(
108
+ "pkcs8",
109
+ binaryDer,
110
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
111
+ false,
112
+ ["sign"]
113
+ );
114
+ const signature = await crypto.subtle.sign(
115
+ "RSASSA-PKCS1-v1_5",
116
+ cryptoKey,
117
+ new TextEncoder().encode(unsignedToken)
118
+ );
119
+ const encodedSignature = base64UrlEncodeBuffer(new Uint8Array(signature));
120
+ return `${unsignedToken}.${encodedSignature}`;
121
+ }
122
+ var cachedAccessToken = null;
123
+ var tokenExpiry = 0;
124
+ async function getAdminAccessToken() {
125
+ if (cachedAccessToken && Date.now() < tokenExpiry) {
126
+ return cachedAccessToken;
127
+ }
128
+ const serviceAccount = getServiceAccount();
129
+ const jwt = await createJWT(serviceAccount);
130
+ const response = await fetch(serviceAccount.token_uri, {
131
+ method: "POST",
132
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
133
+ body: new URLSearchParams({
134
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
135
+ assertion: jwt
136
+ })
137
+ });
138
+ if (!response.ok) {
139
+ const errorText = await response.text();
140
+ throw new Error(`Failed to get access token: ${errorText}`);
141
+ }
142
+ const data = await response.json();
143
+ cachedAccessToken = data.access_token;
144
+ tokenExpiry = Date.now() + data.expires_in * 1e3 - 6e4;
145
+ return cachedAccessToken;
146
+ }
147
+ function clearTokenCache() {
148
+ cachedAccessToken = null;
149
+ tokenExpiry = 0;
150
+ }
151
+
152
+ export {
153
+ initializeApp,
154
+ getConfig,
155
+ clearConfig,
156
+ getServiceAccount,
157
+ getProjectId,
158
+ getFirebaseApiKey,
159
+ getAdminAccessToken,
160
+ clearTokenCache
161
+ };
package/dist/index.d.mts CHANGED
@@ -335,6 +335,60 @@ declare function createCustomToken(uid: string, customClaims?: CustomClaims): Pr
335
335
  * ```
336
336
  */
337
337
  declare function signInWithCustomToken(customToken: string): Promise<CustomTokenSignInResponse>;
338
+ /**
339
+ * Options for creating a session cookie
340
+ */
341
+ interface SessionCookieOptions {
342
+ /**
343
+ * Session duration in milliseconds
344
+ * Maximum: 14 days (1,209,600,000 ms)
345
+ * Minimum: 5 minutes (300,000 ms)
346
+ */
347
+ expiresIn: number;
348
+ }
349
+ /**
350
+ * Create a session cookie from an ID token
351
+ *
352
+ * Session cookies can have a maximum duration of 14 days and are
353
+ * useful for maintaining long-lived authentication sessions.
354
+ *
355
+ * @param idToken - Valid Firebase ID token
356
+ * @param options - Session cookie options
357
+ * @returns Session cookie string
358
+ *
359
+ * @example
360
+ * ```typescript
361
+ * // Create 14-day session cookie
362
+ * const sessionCookie = await createSessionCookie(idToken, {
363
+ * expiresIn: 60 * 60 * 24 * 14 * 1000
364
+ * });
365
+ *
366
+ * // Set as HTTP-only cookie
367
+ * response.headers.set('Set-Cookie',
368
+ * `session=${sessionCookie}; Max-Age=1209600; HttpOnly; Secure; SameSite=Strict`
369
+ * );
370
+ * ```
371
+ */
372
+ declare function createSessionCookie(idToken: string, options: SessionCookieOptions): Promise<string>;
373
+ /**
374
+ * Verify a Firebase session cookie
375
+ *
376
+ * Session cookies are verified similarly to ID tokens but have
377
+ * different expiration times (up to 14 days) and issuer format.
378
+ *
379
+ * @param sessionCookie - Session cookie string to verify
380
+ * @param checkRevoked - Whether to check if the token has been revoked (not yet implemented)
381
+ * @returns Decoded token claims
382
+ *
383
+ * @example
384
+ * ```typescript
385
+ * // Verify session cookie from request
386
+ * const sessionCookie = request.cookies.get('session');
387
+ * const decodedToken = await verifySessionCookie(sessionCookie);
388
+ * console.log('User ID:', decodedToken.uid);
389
+ * ```
390
+ */
391
+ declare function verifySessionCookie(sessionCookie: string, checkRevoked?: boolean): Promise<DecodedIdToken>;
338
392
  /**
339
393
  * Get Auth instance (for compatibility, but not used in new implementation)
340
394
  * @deprecated Use verifyIdToken directly
@@ -925,4 +979,4 @@ declare function getAdminAccessToken(): Promise<string>;
925
979
  */
926
980
  declare function clearTokenCache(): void;
927
981
 
928
- 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 ResumableUploadOptions, type ServiceAccount, type SetOptions, type SignedUrlOptions, type TokenResponse, type UpdateOptions, type UploadOptions, type UserInfo, type WhereFilterOp, addDocument, batchWrite, clearConfig, clearTokenCache, countDocuments, createCustomToken, deleteDocument, deleteFile, downloadFile, fileExists, generateSignedUrl, getAdminAccessToken, getAuth, getConfig, getDocument, getFileMetadata, getProjectId, getServiceAccount, getUserFromToken, initializeApp, iterateCollection, listDocuments, listFiles, queryDocuments, setDocument, signInWithCustomToken, updateDocument, uploadFile, uploadFileResumable, verifyIdToken };
982
+ 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 ResumableUploadOptions, type ServiceAccount, type SessionCookieOptions, type SetOptions, type SignedUrlOptions, type TokenResponse, type UpdateOptions, type UploadOptions, type UserInfo, type WhereFilterOp, addDocument, batchWrite, clearConfig, clearTokenCache, countDocuments, createCustomToken, createSessionCookie, deleteDocument, deleteFile, downloadFile, fileExists, generateSignedUrl, getAdminAccessToken, getAuth, getConfig, getDocument, getFileMetadata, getProjectId, getServiceAccount, getUserFromToken, initializeApp, iterateCollection, listDocuments, listFiles, queryDocuments, setDocument, signInWithCustomToken, updateDocument, uploadFile, uploadFileResumable, verifyIdToken, verifySessionCookie };
package/dist/index.d.ts CHANGED
@@ -335,6 +335,60 @@ declare function createCustomToken(uid: string, customClaims?: CustomClaims): Pr
335
335
  * ```
336
336
  */
337
337
  declare function signInWithCustomToken(customToken: string): Promise<CustomTokenSignInResponse>;
338
+ /**
339
+ * Options for creating a session cookie
340
+ */
341
+ interface SessionCookieOptions {
342
+ /**
343
+ * Session duration in milliseconds
344
+ * Maximum: 14 days (1,209,600,000 ms)
345
+ * Minimum: 5 minutes (300,000 ms)
346
+ */
347
+ expiresIn: number;
348
+ }
349
+ /**
350
+ * Create a session cookie from an ID token
351
+ *
352
+ * Session cookies can have a maximum duration of 14 days and are
353
+ * useful for maintaining long-lived authentication sessions.
354
+ *
355
+ * @param idToken - Valid Firebase ID token
356
+ * @param options - Session cookie options
357
+ * @returns Session cookie string
358
+ *
359
+ * @example
360
+ * ```typescript
361
+ * // Create 14-day session cookie
362
+ * const sessionCookie = await createSessionCookie(idToken, {
363
+ * expiresIn: 60 * 60 * 24 * 14 * 1000
364
+ * });
365
+ *
366
+ * // Set as HTTP-only cookie
367
+ * response.headers.set('Set-Cookie',
368
+ * `session=${sessionCookie}; Max-Age=1209600; HttpOnly; Secure; SameSite=Strict`
369
+ * );
370
+ * ```
371
+ */
372
+ declare function createSessionCookie(idToken: string, options: SessionCookieOptions): Promise<string>;
373
+ /**
374
+ * Verify a Firebase session cookie
375
+ *
376
+ * Session cookies are verified similarly to ID tokens but have
377
+ * different expiration times (up to 14 days) and issuer format.
378
+ *
379
+ * @param sessionCookie - Session cookie string to verify
380
+ * @param checkRevoked - Whether to check if the token has been revoked (not yet implemented)
381
+ * @returns Decoded token claims
382
+ *
383
+ * @example
384
+ * ```typescript
385
+ * // Verify session cookie from request
386
+ * const sessionCookie = request.cookies.get('session');
387
+ * const decodedToken = await verifySessionCookie(sessionCookie);
388
+ * console.log('User ID:', decodedToken.uid);
389
+ * ```
390
+ */
391
+ declare function verifySessionCookie(sessionCookie: string, checkRevoked?: boolean): Promise<DecodedIdToken>;
338
392
  /**
339
393
  * Get Auth instance (for compatibility, but not used in new implementation)
340
394
  * @deprecated Use verifyIdToken directly
@@ -925,4 +979,4 @@ declare function getAdminAccessToken(): Promise<string>;
925
979
  */
926
980
  declare function clearTokenCache(): void;
927
981
 
928
- 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 ResumableUploadOptions, type ServiceAccount, type SetOptions, type SignedUrlOptions, type TokenResponse, type UpdateOptions, type UploadOptions, type UserInfo, type WhereFilterOp, addDocument, batchWrite, clearConfig, clearTokenCache, countDocuments, createCustomToken, deleteDocument, deleteFile, downloadFile, fileExists, generateSignedUrl, getAdminAccessToken, getAuth, getConfig, getDocument, getFileMetadata, getProjectId, getServiceAccount, getUserFromToken, initializeApp, iterateCollection, listDocuments, listFiles, queryDocuments, setDocument, signInWithCustomToken, updateDocument, uploadFile, uploadFileResumable, verifyIdToken };
982
+ 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 ResumableUploadOptions, type ServiceAccount, type SessionCookieOptions, type SetOptions, type SignedUrlOptions, type TokenResponse, type UpdateOptions, type UploadOptions, type UserInfo, type WhereFilterOp, addDocument, batchWrite, clearConfig, clearTokenCache, countDocuments, createCustomToken, createSessionCookie, deleteDocument, deleteFile, downloadFile, fileExists, generateSignedUrl, getAdminAccessToken, getAuth, getConfig, getDocument, getFileMetadata, getProjectId, getServiceAccount, getUserFromToken, initializeApp, iterateCollection, listDocuments, listFiles, queryDocuments, setDocument, signInWithCustomToken, updateDocument, uploadFile, uploadFileResumable, verifyIdToken, verifySessionCookie };
package/dist/index.js CHANGED
@@ -3,6 +3,9 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ };
6
9
  var __export = (target, all) => {
7
10
  for (var name in all)
8
11
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -17,45 +20,7 @@ var __copyProps = (to, from, except, desc) => {
17
20
  };
18
21
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
22
 
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- FieldValue: () => FieldValue,
24
- addDocument: () => addDocument,
25
- batchWrite: () => batchWrite,
26
- clearConfig: () => clearConfig,
27
- clearTokenCache: () => clearTokenCache,
28
- countDocuments: () => countDocuments,
29
- createCustomToken: () => createCustomToken,
30
- deleteDocument: () => deleteDocument,
31
- deleteFile: () => deleteFile,
32
- downloadFile: () => downloadFile,
33
- fileExists: () => fileExists,
34
- generateSignedUrl: () => generateSignedUrl,
35
- getAdminAccessToken: () => getAdminAccessToken,
36
- getAuth: () => getAuth,
37
- getConfig: () => getConfig,
38
- getDocument: () => getDocument,
39
- getFileMetadata: () => getFileMetadata,
40
- getProjectId: () => getProjectId,
41
- getServiceAccount: () => getServiceAccount,
42
- getUserFromToken: () => getUserFromToken,
43
- initializeApp: () => initializeApp,
44
- iterateCollection: () => iterateCollection,
45
- listDocuments: () => listDocuments,
46
- listFiles: () => listFiles,
47
- queryDocuments: () => queryDocuments,
48
- setDocument: () => setDocument,
49
- signInWithCustomToken: () => signInWithCustomToken,
50
- updateDocument: () => updateDocument,
51
- uploadFile: () => uploadFile,
52
- uploadFileResumable: () => uploadFileResumable,
53
- verifyIdToken: () => verifyIdToken
54
- });
55
- module.exports = __toCommonJS(index_exports);
56
-
57
23
  // src/config.ts
58
- var globalConfig = {};
59
24
  function initializeApp(config) {
60
25
  globalConfig = { ...config };
61
26
  }
@@ -132,6 +97,149 @@ function getFirebaseApiKey() {
132
97
  "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."
133
98
  );
134
99
  }
100
+ var globalConfig;
101
+ var init_config = __esm({
102
+ "src/config.ts"() {
103
+ "use strict";
104
+ globalConfig = {};
105
+ }
106
+ });
107
+
108
+ // src/service-account.ts
109
+ var init_service_account = __esm({
110
+ "src/service-account.ts"() {
111
+ "use strict";
112
+ init_config();
113
+ }
114
+ });
115
+
116
+ // src/token-generation.ts
117
+ var token_generation_exports = {};
118
+ __export(token_generation_exports, {
119
+ clearTokenCache: () => clearTokenCache,
120
+ getAdminAccessToken: () => getAdminAccessToken
121
+ });
122
+ function base64UrlEncode(str) {
123
+ return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
124
+ }
125
+ function base64UrlEncodeBuffer(buffer) {
126
+ return btoa(String.fromCharCode(...buffer)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
127
+ }
128
+ async function createJWT(serviceAccount) {
129
+ const now = Math.floor(Date.now() / 1e3);
130
+ const expiry = now + 3600;
131
+ const header = {
132
+ alg: "RS256",
133
+ typ: "JWT"
134
+ };
135
+ const payload = {
136
+ iss: serviceAccount.client_email,
137
+ sub: serviceAccount.client_email,
138
+ aud: serviceAccount.token_uri,
139
+ iat: now,
140
+ exp: expiry,
141
+ scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
142
+ };
143
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
144
+ const encodedPayload = base64UrlEncode(JSON.stringify(payload));
145
+ const unsignedToken = `${encodedHeader}.${encodedPayload}`;
146
+ const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
147
+ const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
148
+ const cryptoKey = await crypto.subtle.importKey(
149
+ "pkcs8",
150
+ binaryDer,
151
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
152
+ false,
153
+ ["sign"]
154
+ );
155
+ const signature = await crypto.subtle.sign(
156
+ "RSASSA-PKCS1-v1_5",
157
+ cryptoKey,
158
+ new TextEncoder().encode(unsignedToken)
159
+ );
160
+ const encodedSignature = base64UrlEncodeBuffer(new Uint8Array(signature));
161
+ return `${unsignedToken}.${encodedSignature}`;
162
+ }
163
+ async function getAdminAccessToken() {
164
+ if (cachedAccessToken && Date.now() < tokenExpiry) {
165
+ return cachedAccessToken;
166
+ }
167
+ const serviceAccount = getServiceAccount();
168
+ const jwt = await createJWT(serviceAccount);
169
+ const response = await fetch(serviceAccount.token_uri, {
170
+ method: "POST",
171
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
172
+ body: new URLSearchParams({
173
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
174
+ assertion: jwt
175
+ })
176
+ });
177
+ if (!response.ok) {
178
+ const errorText = await response.text();
179
+ throw new Error(`Failed to get access token: ${errorText}`);
180
+ }
181
+ const data = await response.json();
182
+ cachedAccessToken = data.access_token;
183
+ tokenExpiry = Date.now() + data.expires_in * 1e3 - 6e4;
184
+ return cachedAccessToken;
185
+ }
186
+ function clearTokenCache() {
187
+ cachedAccessToken = null;
188
+ tokenExpiry = 0;
189
+ }
190
+ var cachedAccessToken, tokenExpiry;
191
+ var init_token_generation = __esm({
192
+ "src/token-generation.ts"() {
193
+ "use strict";
194
+ init_service_account();
195
+ cachedAccessToken = null;
196
+ tokenExpiry = 0;
197
+ }
198
+ });
199
+
200
+ // src/index.ts
201
+ var index_exports = {};
202
+ __export(index_exports, {
203
+ FieldValue: () => FieldValue,
204
+ addDocument: () => addDocument,
205
+ batchWrite: () => batchWrite,
206
+ clearConfig: () => clearConfig,
207
+ clearTokenCache: () => clearTokenCache,
208
+ countDocuments: () => countDocuments,
209
+ createCustomToken: () => createCustomToken,
210
+ createSessionCookie: () => createSessionCookie,
211
+ deleteDocument: () => deleteDocument,
212
+ deleteFile: () => deleteFile,
213
+ downloadFile: () => downloadFile,
214
+ fileExists: () => fileExists,
215
+ generateSignedUrl: () => generateSignedUrl,
216
+ getAdminAccessToken: () => getAdminAccessToken,
217
+ getAuth: () => getAuth,
218
+ getConfig: () => getConfig,
219
+ getDocument: () => getDocument,
220
+ getFileMetadata: () => getFileMetadata,
221
+ getProjectId: () => getProjectId,
222
+ getServiceAccount: () => getServiceAccount,
223
+ getUserFromToken: () => getUserFromToken,
224
+ initializeApp: () => initializeApp,
225
+ iterateCollection: () => iterateCollection,
226
+ listDocuments: () => listDocuments,
227
+ listFiles: () => listFiles,
228
+ queryDocuments: () => queryDocuments,
229
+ setDocument: () => setDocument,
230
+ signInWithCustomToken: () => signInWithCustomToken,
231
+ updateDocument: () => updateDocument,
232
+ uploadFile: () => uploadFile,
233
+ uploadFileResumable: () => uploadFileResumable,
234
+ verifyIdToken: () => verifyIdToken,
235
+ verifySessionCookie: () => verifySessionCookie
236
+ });
237
+ module.exports = __toCommonJS(index_exports);
238
+ init_config();
239
+
240
+ // src/auth.ts
241
+ init_service_account();
242
+ init_config();
135
243
 
136
244
  // src/x509.ts
137
245
  function decodeBase64(str) {
@@ -349,7 +457,7 @@ async function getUserFromToken(idToken) {
349
457
  photoURL: decodedToken.picture || null
350
458
  };
351
459
  }
352
- function base64UrlEncode(str) {
460
+ function base64UrlEncode2(str) {
353
461
  return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
354
462
  }
355
463
  async function signWithPrivateKey(data, privateKey) {
@@ -378,7 +486,7 @@ async function signWithPrivateKey(data, privateKey) {
378
486
  key,
379
487
  dataBytes
380
488
  );
381
- return base64UrlEncode(String.fromCharCode(...new Uint8Array(signature)));
489
+ return base64UrlEncode2(String.fromCharCode(...new Uint8Array(signature)));
382
490
  }
383
491
  async function createCustomToken(uid, customClaims) {
384
492
  const serviceAccount = getServiceAccount();
@@ -405,8 +513,8 @@ async function createCustomToken(uid, customClaims) {
405
513
  alg: "RS256",
406
514
  typ: "JWT"
407
515
  };
408
- const encodedHeader = base64UrlEncode(JSON.stringify(header));
409
- const encodedPayload = base64UrlEncode(JSON.stringify(payload));
516
+ const encodedHeader = base64UrlEncode2(JSON.stringify(header));
517
+ const encodedPayload = base64UrlEncode2(JSON.stringify(payload));
410
518
  const unsignedToken = `${encodedHeader}.${encodedPayload}`;
411
519
  const signature = await signWithPrivateKey(unsignedToken, serviceAccount.private_key);
412
520
  return `${unsignedToken}.${signature}`;
@@ -448,6 +556,136 @@ async function signInWithCustomToken(customToken) {
448
556
  isNewUser: result.isNewUser
449
557
  };
450
558
  }
559
+ async function createSessionCookie(idToken, options) {
560
+ if (!idToken || typeof idToken !== "string") {
561
+ throw new Error("idToken must be a non-empty string");
562
+ }
563
+ if (!options.expiresIn || typeof options.expiresIn !== "number") {
564
+ throw new Error("expiresIn must be a number");
565
+ }
566
+ const MIN_DURATION = 5 * 60 * 1e3;
567
+ const MAX_DURATION = 14 * 24 * 60 * 60 * 1e3;
568
+ if (options.expiresIn < MIN_DURATION) {
569
+ throw new Error(`expiresIn must be at least ${MIN_DURATION}ms (5 minutes)`);
570
+ }
571
+ if (options.expiresIn > MAX_DURATION) {
572
+ throw new Error(`expiresIn must be at most ${MAX_DURATION}ms (14 days)`);
573
+ }
574
+ const { getAdminAccessToken: getAdminAccessToken2 } = await Promise.resolve().then(() => (init_token_generation(), token_generation_exports));
575
+ const accessToken = await getAdminAccessToken2();
576
+ const projectId = getProjectId();
577
+ const url = `https://identitytoolkit.googleapis.com/v1/projects/${projectId}:createSessionCookie`;
578
+ const validDurationSeconds = Math.floor(options.expiresIn / 1e3);
579
+ const response = await fetch(url, {
580
+ method: "POST",
581
+ headers: {
582
+ "Authorization": `Bearer ${accessToken}`,
583
+ "Content-Type": "application/json"
584
+ },
585
+ body: JSON.stringify({
586
+ idToken,
587
+ validDuration: validDurationSeconds.toString()
588
+ // Must be string per API spec
589
+ })
590
+ });
591
+ if (!response.ok) {
592
+ const errorText = await response.text();
593
+ let errorMessage = `Failed to create session cookie: ${response.status}`;
594
+ try {
595
+ const errorJson = JSON.parse(errorText);
596
+ if (errorJson.error && errorJson.error.message) {
597
+ errorMessage += ` - ${errorJson.error.message}`;
598
+ }
599
+ } catch {
600
+ errorMessage += ` - ${errorText}`;
601
+ }
602
+ throw new Error(errorMessage);
603
+ }
604
+ const result = await response.json();
605
+ return result.sessionCookie;
606
+ }
607
+ async function verifySessionCookie(sessionCookie, checkRevoked = false) {
608
+ if (!sessionCookie || typeof sessionCookie !== "string") {
609
+ throw new Error("sessionCookie must be a non-empty string");
610
+ }
611
+ try {
612
+ const parts = sessionCookie.split(".");
613
+ if (parts.length !== 3) {
614
+ throw new Error("Invalid session cookie format");
615
+ }
616
+ const [headerB64, payloadB64] = parts;
617
+ const headerJson = atob(headerB64.replace(/-/g, "+").replace(/_/g, "/"));
618
+ const header = JSON.parse(headerJson);
619
+ const payloadJson = atob(payloadB64.replace(/-/g, "+").replace(/_/g, "/"));
620
+ const payload = JSON.parse(payloadJson);
621
+ const projectId = getProjectId();
622
+ const now = Math.floor(Date.now() / 1e3);
623
+ if (!payload.exp || payload.exp < now) {
624
+ throw new Error("Session cookie has expired");
625
+ }
626
+ if (!payload.iat || payload.iat > now) {
627
+ throw new Error("Session cookie issued in the future");
628
+ }
629
+ if (payload.aud !== projectId) {
630
+ throw new Error(`Session cookie has incorrect audience. Expected ${projectId}, got ${payload.aud}`);
631
+ }
632
+ const expectedIssuer = `https://session.firebase.google.com/${projectId}`;
633
+ if (payload.iss !== expectedIssuer) {
634
+ throw new Error(`Session cookie has incorrect issuer. Expected ${expectedIssuer}, got ${payload.iss}`);
635
+ }
636
+ if (!payload.sub || typeof payload.sub !== "string" || payload.sub.length === 0) {
637
+ throw new Error("Session cookie has no subject (user ID)");
638
+ }
639
+ await verifySessionCookieSignature(sessionCookie, header, payload);
640
+ if (checkRevoked) {
641
+ }
642
+ return {
643
+ uid: payload.sub,
644
+ aud: payload.aud,
645
+ auth_time: payload.auth_time,
646
+ exp: payload.exp,
647
+ iat: payload.iat,
648
+ iss: payload.iss,
649
+ sub: payload.sub,
650
+ email: payload.email,
651
+ email_verified: payload.email_verified,
652
+ firebase: payload.firebase,
653
+ ...payload
654
+ };
655
+ } catch (error) {
656
+ throw new Error(`Failed to verify session cookie: ${error.message}`);
657
+ }
658
+ }
659
+ async function verifySessionCookieSignature(jwt, header, payload) {
660
+ const keys = await fetchPublicKeys(payload.iss);
661
+ const kid = header.kid;
662
+ if (!kid || !keys[kid]) {
663
+ throw new Error("Session cookie has invalid key ID");
664
+ }
665
+ const publicKeyPem = keys[kid];
666
+ const publicKey = await importPublicKeyFromX509(publicKeyPem);
667
+ const [headerAndPayload, signature] = [
668
+ jwt.split(".").slice(0, 2).join("."),
669
+ jwt.split(".")[2]
670
+ ];
671
+ const encoder = new TextEncoder();
672
+ const data = encoder.encode(headerAndPayload);
673
+ const signatureBase64 = signature.replace(/-/g, "+").replace(/_/g, "/");
674
+ const signatureBinary = atob(signatureBase64);
675
+ const signatureBytes = new Uint8Array(signatureBinary.length);
676
+ for (let i = 0; i < signatureBinary.length; i++) {
677
+ signatureBytes[i] = signatureBinary.charCodeAt(i);
678
+ }
679
+ const isValid = await crypto.subtle.verify(
680
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
681
+ publicKey,
682
+ signatureBytes,
683
+ data
684
+ );
685
+ if (!isValid) {
686
+ throw new Error("Session cookie signature verification failed");
687
+ }
688
+ }
451
689
  function getAuth() {
452
690
  return {
453
691
  verifyIdToken
@@ -730,77 +968,9 @@ function removeFieldTransforms(data) {
730
968
  return result;
731
969
  }
732
970
 
733
- // src/token-generation.ts
734
- function base64UrlEncode2(str) {
735
- return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
736
- }
737
- function base64UrlEncodeBuffer(buffer) {
738
- return btoa(String.fromCharCode(...buffer)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
739
- }
740
- async function createJWT(serviceAccount) {
741
- const now = Math.floor(Date.now() / 1e3);
742
- const expiry = now + 3600;
743
- const header = {
744
- alg: "RS256",
745
- typ: "JWT"
746
- };
747
- const payload = {
748
- iss: serviceAccount.client_email,
749
- sub: serviceAccount.client_email,
750
- aud: serviceAccount.token_uri,
751
- iat: now,
752
- exp: expiry,
753
- scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
754
- };
755
- const encodedHeader = base64UrlEncode2(JSON.stringify(header));
756
- const encodedPayload = base64UrlEncode2(JSON.stringify(payload));
757
- const unsignedToken = `${encodedHeader}.${encodedPayload}`;
758
- const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
759
- const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
760
- const cryptoKey = await crypto.subtle.importKey(
761
- "pkcs8",
762
- binaryDer,
763
- { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
764
- false,
765
- ["sign"]
766
- );
767
- const signature = await crypto.subtle.sign(
768
- "RSASSA-PKCS1-v1_5",
769
- cryptoKey,
770
- new TextEncoder().encode(unsignedToken)
771
- );
772
- const encodedSignature = base64UrlEncodeBuffer(new Uint8Array(signature));
773
- return `${unsignedToken}.${encodedSignature}`;
774
- }
775
- var cachedAccessToken = null;
776
- var tokenExpiry = 0;
777
- async function getAdminAccessToken() {
778
- if (cachedAccessToken && Date.now() < tokenExpiry) {
779
- return cachedAccessToken;
780
- }
781
- const serviceAccount = getServiceAccount();
782
- const jwt = await createJWT(serviceAccount);
783
- const response = await fetch(serviceAccount.token_uri, {
784
- method: "POST",
785
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
786
- body: new URLSearchParams({
787
- grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
788
- assertion: jwt
789
- })
790
- });
791
- if (!response.ok) {
792
- const errorText = await response.text();
793
- throw new Error(`Failed to get access token: ${errorText}`);
794
- }
795
- const data = await response.json();
796
- cachedAccessToken = data.access_token;
797
- tokenExpiry = Date.now() + data.expires_in * 1e3 - 6e4;
798
- return cachedAccessToken;
799
- }
800
- function clearTokenCache() {
801
- cachedAccessToken = null;
802
- tokenExpiry = 0;
803
- }
971
+ // src/firestore/operations.ts
972
+ init_token_generation();
973
+ init_service_account();
804
974
 
805
975
  // src/firestore/path-validation.ts
806
976
  function validateCollectionPath(argumentName, collectionPath) {
@@ -1172,6 +1342,8 @@ async function countDocuments(collectionPath, options) {
1172
1342
  }
1173
1343
 
1174
1344
  // src/storage/client.ts
1345
+ init_token_generation();
1346
+ init_config();
1175
1347
  var STORAGE_API_BASE = "https://storage.googleapis.com/storage/v1";
1176
1348
  var UPLOAD_API_BASE = "https://storage.googleapis.com/upload/storage/v1";
1177
1349
  function getDefaultBucket() {
@@ -1366,6 +1538,7 @@ async function fileExists(path) {
1366
1538
  }
1367
1539
 
1368
1540
  // src/storage/signed-urls.ts
1541
+ init_config();
1369
1542
  function getStorageBucket() {
1370
1543
  const customBucket = process.env.FIREBASE_STORAGE_BUCKET;
1371
1544
  if (customBucket) {
@@ -1487,6 +1660,8 @@ UNSIGNED-PAYLOAD`;
1487
1660
  }
1488
1661
 
1489
1662
  // src/storage/resumable-upload.ts
1663
+ init_token_generation();
1664
+ init_config();
1490
1665
  var UPLOAD_API_BASE2 = "https://storage.googleapis.com/upload/storage/v1";
1491
1666
  function getDefaultBucket2() {
1492
1667
  const customBucket = process.env.FIREBASE_STORAGE_BUCKET;
@@ -1683,6 +1858,10 @@ async function uploadFromStream(bucket, path, stream, contentType, chunkSize, op
1683
1858
  reader.releaseLock();
1684
1859
  }
1685
1860
  }
1861
+
1862
+ // src/index.ts
1863
+ init_token_generation();
1864
+ init_service_account();
1686
1865
  // Annotate the CommonJS export names for ESM import in node:
1687
1866
  0 && (module.exports = {
1688
1867
  FieldValue,
@@ -1692,6 +1871,7 @@ async function uploadFromStream(bucket, path, stream, contentType, chunkSize, op
1692
1871
  clearTokenCache,
1693
1872
  countDocuments,
1694
1873
  createCustomToken,
1874
+ createSessionCookie,
1695
1875
  deleteDocument,
1696
1876
  deleteFile,
1697
1877
  downloadFile,
@@ -1715,5 +1895,6 @@ async function uploadFromStream(bucket, path, stream, contentType, chunkSize, op
1715
1895
  updateDocument,
1716
1896
  uploadFile,
1717
1897
  uploadFileResumable,
1718
- verifyIdToken
1898
+ verifyIdToken,
1899
+ verifySessionCookie
1719
1900
  });
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) {
@@ -392,6 +324,136 @@ async function signInWithCustomToken(customToken) {
392
324
  isNewUser: result.isNewUser
393
325
  };
394
326
  }
327
+ async function createSessionCookie(idToken, options) {
328
+ if (!idToken || typeof idToken !== "string") {
329
+ throw new Error("idToken must be a non-empty string");
330
+ }
331
+ if (!options.expiresIn || typeof options.expiresIn !== "number") {
332
+ throw new Error("expiresIn must be a number");
333
+ }
334
+ const MIN_DURATION = 5 * 60 * 1e3;
335
+ const MAX_DURATION = 14 * 24 * 60 * 60 * 1e3;
336
+ if (options.expiresIn < MIN_DURATION) {
337
+ throw new Error(`expiresIn must be at least ${MIN_DURATION}ms (5 minutes)`);
338
+ }
339
+ if (options.expiresIn > MAX_DURATION) {
340
+ throw new Error(`expiresIn must be at most ${MAX_DURATION}ms (14 days)`);
341
+ }
342
+ const { getAdminAccessToken: getAdminAccessToken2 } = await import("./token-generation-5K7K6T6U.mjs");
343
+ const accessToken = await getAdminAccessToken2();
344
+ const projectId = getProjectId();
345
+ const url = `https://identitytoolkit.googleapis.com/v1/projects/${projectId}:createSessionCookie`;
346
+ const validDurationSeconds = Math.floor(options.expiresIn / 1e3);
347
+ const response = await fetch(url, {
348
+ method: "POST",
349
+ headers: {
350
+ "Authorization": `Bearer ${accessToken}`,
351
+ "Content-Type": "application/json"
352
+ },
353
+ body: JSON.stringify({
354
+ idToken,
355
+ validDuration: validDurationSeconds.toString()
356
+ // Must be string per API spec
357
+ })
358
+ });
359
+ if (!response.ok) {
360
+ const errorText = await response.text();
361
+ let errorMessage = `Failed to create session cookie: ${response.status}`;
362
+ try {
363
+ const errorJson = JSON.parse(errorText);
364
+ if (errorJson.error && errorJson.error.message) {
365
+ errorMessage += ` - ${errorJson.error.message}`;
366
+ }
367
+ } catch {
368
+ errorMessage += ` - ${errorText}`;
369
+ }
370
+ throw new Error(errorMessage);
371
+ }
372
+ const result = await response.json();
373
+ return result.sessionCookie;
374
+ }
375
+ async function verifySessionCookie(sessionCookie, checkRevoked = false) {
376
+ if (!sessionCookie || typeof sessionCookie !== "string") {
377
+ throw new Error("sessionCookie must be a non-empty string");
378
+ }
379
+ try {
380
+ const parts = sessionCookie.split(".");
381
+ if (parts.length !== 3) {
382
+ throw new Error("Invalid session cookie format");
383
+ }
384
+ const [headerB64, payloadB64] = parts;
385
+ const headerJson = atob(headerB64.replace(/-/g, "+").replace(/_/g, "/"));
386
+ const header = JSON.parse(headerJson);
387
+ const payloadJson = atob(payloadB64.replace(/-/g, "+").replace(/_/g, "/"));
388
+ const payload = JSON.parse(payloadJson);
389
+ const projectId = getProjectId();
390
+ const now = Math.floor(Date.now() / 1e3);
391
+ if (!payload.exp || payload.exp < now) {
392
+ throw new Error("Session cookie has expired");
393
+ }
394
+ if (!payload.iat || payload.iat > now) {
395
+ throw new Error("Session cookie issued in the future");
396
+ }
397
+ if (payload.aud !== projectId) {
398
+ throw new Error(`Session cookie has incorrect audience. Expected ${projectId}, got ${payload.aud}`);
399
+ }
400
+ const expectedIssuer = `https://session.firebase.google.com/${projectId}`;
401
+ if (payload.iss !== expectedIssuer) {
402
+ throw new Error(`Session cookie has incorrect issuer. Expected ${expectedIssuer}, got ${payload.iss}`);
403
+ }
404
+ if (!payload.sub || typeof payload.sub !== "string" || payload.sub.length === 0) {
405
+ throw new Error("Session cookie has no subject (user ID)");
406
+ }
407
+ await verifySessionCookieSignature(sessionCookie, header, payload);
408
+ if (checkRevoked) {
409
+ }
410
+ return {
411
+ uid: payload.sub,
412
+ aud: payload.aud,
413
+ auth_time: payload.auth_time,
414
+ exp: payload.exp,
415
+ iat: payload.iat,
416
+ iss: payload.iss,
417
+ sub: payload.sub,
418
+ email: payload.email,
419
+ email_verified: payload.email_verified,
420
+ firebase: payload.firebase,
421
+ ...payload
422
+ };
423
+ } catch (error) {
424
+ throw new Error(`Failed to verify session cookie: ${error.message}`);
425
+ }
426
+ }
427
+ async function verifySessionCookieSignature(jwt, header, payload) {
428
+ const keys = await fetchPublicKeys(payload.iss);
429
+ const kid = header.kid;
430
+ if (!kid || !keys[kid]) {
431
+ throw new Error("Session cookie has invalid key ID");
432
+ }
433
+ const publicKeyPem = keys[kid];
434
+ const publicKey = await importPublicKeyFromX509(publicKeyPem);
435
+ const [headerAndPayload, signature] = [
436
+ jwt.split(".").slice(0, 2).join("."),
437
+ jwt.split(".")[2]
438
+ ];
439
+ const encoder = new TextEncoder();
440
+ const data = encoder.encode(headerAndPayload);
441
+ const signatureBase64 = signature.replace(/-/g, "+").replace(/_/g, "/");
442
+ const signatureBinary = atob(signatureBase64);
443
+ const signatureBytes = new Uint8Array(signatureBinary.length);
444
+ for (let i = 0; i < signatureBinary.length; i++) {
445
+ signatureBytes[i] = signatureBinary.charCodeAt(i);
446
+ }
447
+ const isValid = await crypto.subtle.verify(
448
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
449
+ publicKey,
450
+ signatureBytes,
451
+ data
452
+ );
453
+ if (!isValid) {
454
+ throw new Error("Session cookie signature verification failed");
455
+ }
456
+ }
395
457
  function getAuth() {
396
458
  return {
397
459
  verifyIdToken
@@ -674,78 +736,6 @@ function removeFieldTransforms(data) {
674
736
  return result;
675
737
  }
676
738
 
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
739
  // src/firestore/path-validation.ts
750
740
  function validateCollectionPath(argumentName, collectionPath) {
751
741
  const segments = collectionPath.split("/").filter((s) => s.length > 0);
@@ -1635,6 +1625,7 @@ export {
1635
1625
  clearTokenCache,
1636
1626
  countDocuments,
1637
1627
  createCustomToken,
1628
+ createSessionCookie,
1638
1629
  deleteDocument,
1639
1630
  deleteFile,
1640
1631
  downloadFile,
@@ -1658,5 +1649,6 @@ export {
1658
1649
  updateDocument,
1659
1650
  uploadFile,
1660
1651
  uploadFileResumable,
1661
- verifyIdToken
1652
+ verifyIdToken,
1653
+ verifySessionCookie
1662
1654
  };
@@ -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.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",