@prmichaelsen/firebase-admin-sdk-v8 2.3.0 → 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,28 @@ 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
+
24
+ ## [2.3.1] - 2026-02-14
25
+
26
+ ### Fixed
27
+ - **CRITICAL**: Fixed signed URL generation to match Google Cloud Storage SDK encoding
28
+ - Implemented `fixedEncodeURIComponent` to additionally encode `! * ' ( )` characters
29
+ - This fixes `SignatureDoesNotMatch` errors in production environments
30
+ - Path encoding now exactly matches official `@google-cloud/storage` SDK behavior
31
+
10
32
  ## [2.3.0] - 2026-02-14
11
33
 
12
34
  ### Added
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,13 +1538,14 @@ 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) {
1372
1545
  return customBucket;
1373
1546
  }
1374
1547
  const projectId = getProjectId();
1375
- return `${projectId}.appspot.com`;
1548
+ return `${projectId}.firebasestorage.app`;
1376
1549
  }
1377
1550
  function getExpirationTimestamp(expires) {
1378
1551
  if (expires instanceof Date) {
@@ -1390,10 +1563,18 @@ function actionToMethod(action) {
1390
1563
  return "DELETE";
1391
1564
  }
1392
1565
  }
1393
- function stringToHex(str) {
1566
+ function fixedEncodeURIComponent(str) {
1567
+ return encodeURIComponent(str).replace(
1568
+ /[!'()*]/g,
1569
+ (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
1570
+ );
1571
+ }
1572
+ async function sha256Hex(str) {
1394
1573
  const encoder = new TextEncoder();
1395
- const bytes = encoder.encode(str);
1396
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1574
+ const data = encoder.encode(str);
1575
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1576
+ const hashArray = new Uint8Array(hashBuffer);
1577
+ return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
1397
1578
  }
1398
1579
  async function signData(data, privateKey) {
1399
1580
  const pemHeader = "-----BEGIN PRIVATE KEY-----";
@@ -1458,17 +1639,15 @@ async function generateSignedUrl(path, options) {
1458
1639
  }
1459
1640
  const sortedParams = Object.keys(queryParams).sort();
1460
1641
  const canonicalQueryString = sortedParams.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`).join("&");
1461
- const encodedPath = path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
1642
+ const encodedPath = path.split("/").map((segment) => fixedEncodeURIComponent(segment)).join("/");
1462
1643
  const canonicalUri = `/${bucket}/${encodedPath}`;
1463
- const canonicalRequest = [
1464
- method,
1465
- canonicalUri,
1466
- canonicalQueryString,
1467
- canonicalHeaders,
1468
- signedHeaders,
1469
- "UNSIGNED-PAYLOAD"
1470
- ].join("\n");
1471
- const canonicalRequestHash = stringToHex(canonicalRequest);
1644
+ const canonicalRequest = `${method}
1645
+ ${canonicalUri}
1646
+ ${canonicalQueryString}
1647
+ ${canonicalHeaders}
1648
+ ${signedHeaders}
1649
+ UNSIGNED-PAYLOAD`;
1650
+ const canonicalRequestHash = await sha256Hex(canonicalRequest);
1472
1651
  const stringToSign = [
1473
1652
  "GOOG4-RSA-SHA256",
1474
1653
  dateTimeStamp,
@@ -1481,6 +1660,8 @@ async function generateSignedUrl(path, options) {
1481
1660
  }
1482
1661
 
1483
1662
  // src/storage/resumable-upload.ts
1663
+ init_token_generation();
1664
+ init_config();
1484
1665
  var UPLOAD_API_BASE2 = "https://storage.googleapis.com/upload/storage/v1";
1485
1666
  function getDefaultBucket2() {
1486
1667
  const customBucket = process.env.FIREBASE_STORAGE_BUCKET;
@@ -1677,6 +1858,10 @@ async function uploadFromStream(bucket, path, stream, contentType, chunkSize, op
1677
1858
  reader.releaseLock();
1678
1859
  }
1679
1860
  }
1861
+
1862
+ // src/index.ts
1863
+ init_token_generation();
1864
+ init_service_account();
1680
1865
  // Annotate the CommonJS export names for ESM import in node:
1681
1866
  0 && (module.exports = {
1682
1867
  FieldValue,
@@ -1686,6 +1871,7 @@ async function uploadFromStream(bucket, path, stream, contentType, chunkSize, op
1686
1871
  clearTokenCache,
1687
1872
  countDocuments,
1688
1873
  createCustomToken,
1874
+ createSessionCookie,
1689
1875
  deleteDocument,
1690
1876
  deleteFile,
1691
1877
  downloadFile,
@@ -1709,5 +1895,6 @@ async function uploadFromStream(bucket, path, stream, contentType, chunkSize, op
1709
1895
  updateDocument,
1710
1896
  uploadFile,
1711
1897
  uploadFileResumable,
1712
- verifyIdToken
1898
+ verifyIdToken,
1899
+ verifySessionCookie
1713
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);
@@ -1316,7 +1306,7 @@ function getStorageBucket() {
1316
1306
  return customBucket;
1317
1307
  }
1318
1308
  const projectId = getProjectId();
1319
- return `${projectId}.appspot.com`;
1309
+ return `${projectId}.firebasestorage.app`;
1320
1310
  }
1321
1311
  function getExpirationTimestamp(expires) {
1322
1312
  if (expires instanceof Date) {
@@ -1334,10 +1324,18 @@ function actionToMethod(action) {
1334
1324
  return "DELETE";
1335
1325
  }
1336
1326
  }
1337
- function stringToHex(str) {
1327
+ function fixedEncodeURIComponent(str) {
1328
+ return encodeURIComponent(str).replace(
1329
+ /[!'()*]/g,
1330
+ (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
1331
+ );
1332
+ }
1333
+ async function sha256Hex(str) {
1338
1334
  const encoder = new TextEncoder();
1339
- const bytes = encoder.encode(str);
1340
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1335
+ const data = encoder.encode(str);
1336
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1337
+ const hashArray = new Uint8Array(hashBuffer);
1338
+ return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
1341
1339
  }
1342
1340
  async function signData(data, privateKey) {
1343
1341
  const pemHeader = "-----BEGIN PRIVATE KEY-----";
@@ -1402,17 +1400,15 @@ async function generateSignedUrl(path, options) {
1402
1400
  }
1403
1401
  const sortedParams = Object.keys(queryParams).sort();
1404
1402
  const canonicalQueryString = sortedParams.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`).join("&");
1405
- const encodedPath = path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
1403
+ const encodedPath = path.split("/").map((segment) => fixedEncodeURIComponent(segment)).join("/");
1406
1404
  const canonicalUri = `/${bucket}/${encodedPath}`;
1407
- const canonicalRequest = [
1408
- method,
1409
- canonicalUri,
1410
- canonicalQueryString,
1411
- canonicalHeaders,
1412
- signedHeaders,
1413
- "UNSIGNED-PAYLOAD"
1414
- ].join("\n");
1415
- const canonicalRequestHash = stringToHex(canonicalRequest);
1405
+ const canonicalRequest = `${method}
1406
+ ${canonicalUri}
1407
+ ${canonicalQueryString}
1408
+ ${canonicalHeaders}
1409
+ ${signedHeaders}
1410
+ UNSIGNED-PAYLOAD`;
1411
+ const canonicalRequestHash = await sha256Hex(canonicalRequest);
1416
1412
  const stringToSign = [
1417
1413
  "GOOG4-RSA-SHA256",
1418
1414
  dateTimeStamp,
@@ -1629,6 +1625,7 @@ export {
1629
1625
  clearTokenCache,
1630
1626
  countDocuments,
1631
1627
  createCustomToken,
1628
+ createSessionCookie,
1632
1629
  deleteDocument,
1633
1630
  deleteFile,
1634
1631
  downloadFile,
@@ -1652,5 +1649,6 @@ export {
1652
1649
  updateDocument,
1653
1650
  uploadFile,
1654
1651
  uploadFileResumable,
1655
- verifyIdToken
1652
+ verifyIdToken,
1653
+ verifySessionCookie
1656
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.0",
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",