@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 +22 -0
- package/README.md +21 -1
- package/dist/chunk-5X465GLA.mjs +161 -0
- package/dist/index.d.mts +55 -1
- package/dist/index.d.ts +55 -1
- package/dist/index.js +315 -128
- package/dist/index.mjs +163 -165
- package/dist/token-generation-5K7K6T6U.mjs +8 -0
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
|
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 =
|
|
409
|
-
const encodedPayload =
|
|
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/
|
|
734
|
-
|
|
735
|
-
|
|
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}.
|
|
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
|
|
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
|
|
1396
|
-
|
|
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) =>
|
|
1642
|
+
const encodedPath = path.split("/").map((segment) => fixedEncodeURIComponent(segment)).join("/");
|
|
1462
1643
|
const canonicalUri = `/${bucket}/${encodedPath}`;
|
|
1463
|
-
const canonicalRequest =
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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}.
|
|
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
|
|
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
|
|
1340
|
-
|
|
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) =>
|
|
1403
|
+
const encodedPath = path.split("/").map((segment) => fixedEncodeURIComponent(segment)).join("/");
|
|
1406
1404
|
const canonicalUri = `/${bucket}/${encodedPath}`;
|
|
1407
|
-
const canonicalRequest =
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
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
|
};
|
package/package.json
CHANGED