@prmichaelsen/firebase-admin-sdk-v8 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -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 +295 -114
- package/dist/index.mjs +143 -151
- package/dist/token-generation-5K7K6T6U.mjs +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.4.0] - 2026-02-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Session Cookie Support**: Added `createSessionCookie()` and `verifySessionCookie()`
|
|
14
|
+
- Long-lived authentication sessions (up to 14 days) instead of 1-hour ID tokens
|
|
15
|
+
- Session cookie verification with proper issuer validation
|
|
16
|
+
- 15 new unit tests for session cookie functionality
|
|
17
|
+
- 3 new E2E tests for session cookie creation and verification
|
|
18
|
+
- Comprehensive session cookie documentation in README
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Auth module coverage improved from 63.18% to 97.51%
|
|
22
|
+
- Total tests increased from 418 to 433 (+15 tests)
|
|
23
|
+
|
|
10
24
|
## [2.3.1] - 2026-02-14
|
|
11
25
|
|
|
12
26
|
### Fixed
|
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ This library provides Firebase Admin SDK functionality for Cloudflare Workers an
|
|
|
15
15
|
- ✅ **Zero Dependencies** - No external dependencies, pure Web APIs (crypto.subtle, fetch)
|
|
16
16
|
- ✅ **JWT Token Generation** - Service account authentication
|
|
17
17
|
- ✅ **ID Token Verification** - Verify Firebase ID tokens (supports v9 and v10 formats)
|
|
18
|
+
- ✅ **Session Cookies** - Create and verify long-lived session cookies (up to 14 days)
|
|
18
19
|
- ✅ **Firebase v10 Compatible** - Supports both old and new token issuer formats
|
|
19
20
|
- ✅ **Firestore REST API** - Full CRUD operations via REST
|
|
20
21
|
- ✅ **Field Value Operations** - increment, arrayUnion, arrayRemove, serverTimestamp, delete
|
|
@@ -94,7 +95,26 @@ try {
|
|
|
94
95
|
}
|
|
95
96
|
```
|
|
96
97
|
|
|
97
|
-
### 3.
|
|
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,6 +1538,7 @@ async function fileExists(path) {
|
|
|
1366
1538
|
}
|
|
1367
1539
|
|
|
1368
1540
|
// src/storage/signed-urls.ts
|
|
1541
|
+
init_config();
|
|
1369
1542
|
function getStorageBucket() {
|
|
1370
1543
|
const customBucket = process.env.FIREBASE_STORAGE_BUCKET;
|
|
1371
1544
|
if (customBucket) {
|
|
@@ -1487,6 +1660,8 @@ UNSIGNED-PAYLOAD`;
|
|
|
1487
1660
|
}
|
|
1488
1661
|
|
|
1489
1662
|
// src/storage/resumable-upload.ts
|
|
1663
|
+
init_token_generation();
|
|
1664
|
+
init_config();
|
|
1490
1665
|
var UPLOAD_API_BASE2 = "https://storage.googleapis.com/upload/storage/v1";
|
|
1491
1666
|
function getDefaultBucket2() {
|
|
1492
1667
|
const customBucket = process.env.FIREBASE_STORAGE_BUCKET;
|
|
@@ -1683,6 +1858,10 @@ async function uploadFromStream(bucket, path, stream, contentType, chunkSize, op
|
|
|
1683
1858
|
reader.releaseLock();
|
|
1684
1859
|
}
|
|
1685
1860
|
}
|
|
1861
|
+
|
|
1862
|
+
// src/index.ts
|
|
1863
|
+
init_token_generation();
|
|
1864
|
+
init_service_account();
|
|
1686
1865
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1687
1866
|
0 && (module.exports = {
|
|
1688
1867
|
FieldValue,
|
|
@@ -1692,6 +1871,7 @@ async function uploadFromStream(bucket, path, stream, contentType, chunkSize, op
|
|
|
1692
1871
|
clearTokenCache,
|
|
1693
1872
|
countDocuments,
|
|
1694
1873
|
createCustomToken,
|
|
1874
|
+
createSessionCookie,
|
|
1695
1875
|
deleteDocument,
|
|
1696
1876
|
deleteFile,
|
|
1697
1877
|
downloadFile,
|
|
@@ -1715,5 +1895,6 @@ async function uploadFromStream(bucket, path, stream, contentType, chunkSize, op
|
|
|
1715
1895
|
updateDocument,
|
|
1716
1896
|
uploadFile,
|
|
1717
1897
|
uploadFileResumable,
|
|
1718
|
-
verifyIdToken
|
|
1898
|
+
verifyIdToken,
|
|
1899
|
+
verifySessionCookie
|
|
1719
1900
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,81 +1,13 @@
|
|
|
1
|
-
|
|
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);
|
|
@@ -1635,6 +1625,7 @@ export {
|
|
|
1635
1625
|
clearTokenCache,
|
|
1636
1626
|
countDocuments,
|
|
1637
1627
|
createCustomToken,
|
|
1628
|
+
createSessionCookie,
|
|
1638
1629
|
deleteDocument,
|
|
1639
1630
|
deleteFile,
|
|
1640
1631
|
downloadFile,
|
|
@@ -1658,5 +1649,6 @@ export {
|
|
|
1658
1649
|
updateDocument,
|
|
1659
1650
|
uploadFile,
|
|
1660
1651
|
uploadFileResumable,
|
|
1661
|
-
verifyIdToken
|
|
1652
|
+
verifyIdToken,
|
|
1653
|
+
verifySessionCookie
|
|
1662
1654
|
};
|
package/package.json
CHANGED