@prmichaelsen/firebase-admin-sdk-v8 2.0.17 → 2.0.20
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/.github/workflows/e2e-tests.yml +41 -0
- package/.github/workflows/test.yml +44 -0
- package/README.md +10 -7
- package/dist/index.d.mts +26 -26
- package/dist/index.d.ts +26 -26
- package/dist/index.js +73 -73
- package/dist/index.mjs +73 -73
- package/package.json +1 -1
- package/firebase.json +0 -9
- package/firestore.indexes.json +0 -33
- package/firestore.rules +0 -11
- package/jest.config.js +0 -12
- package/jest.e2e.config.js +0 -14
- package/service-account.json +0 -13
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: E2E Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ mainline ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ mainline ]
|
|
8
|
+
workflow_dispatch: # Allow manual trigger
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
e2e-tests:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout code
|
|
16
|
+
uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Setup Node.js
|
|
19
|
+
uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: '20'
|
|
22
|
+
cache: 'npm'
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: npm ci
|
|
26
|
+
|
|
27
|
+
- name: Run unit tests
|
|
28
|
+
run: npm test
|
|
29
|
+
|
|
30
|
+
- name: Create service account file
|
|
31
|
+
run: |
|
|
32
|
+
echo '${{ secrets.FIREBASE_SERVICE_ACCOUNT }}' > service-account.json
|
|
33
|
+
|
|
34
|
+
- name: Run e2e tests
|
|
35
|
+
run: npm run test:e2e
|
|
36
|
+
env:
|
|
37
|
+
NODE_ENV: test
|
|
38
|
+
|
|
39
|
+
- name: Clean up service account file
|
|
40
|
+
if: always()
|
|
41
|
+
run: rm -f service-account.json
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Unit Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ mainline, develop ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ mainline, develop ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
node-version: [18, 20, 22]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout code
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Setup Node.js ${{ matrix.node-version }}
|
|
22
|
+
uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: ${{ matrix.node-version }}
|
|
25
|
+
cache: 'npm'
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: npm ci
|
|
29
|
+
|
|
30
|
+
- name: Run unit tests
|
|
31
|
+
run: npm test
|
|
32
|
+
|
|
33
|
+
- name: Run build
|
|
34
|
+
run: npm run build
|
|
35
|
+
|
|
36
|
+
- name: Upload coverage to Codecov (Node 20 only)
|
|
37
|
+
if: matrix.node-version == 20
|
|
38
|
+
uses: codecov/codecov-action@v4
|
|
39
|
+
with:
|
|
40
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
41
|
+
files: ./coverage/lcov.info
|
|
42
|
+
flags: unittests
|
|
43
|
+
name: codecov-umbrella
|
|
44
|
+
fail_ci_if_error: false
|
package/README.md
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
> Firebase Admin SDK for Cloudflare Workers and edge runtimes using REST APIs
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/firebase-admin-sdk-v8)
|
|
5
|
+
[](https://www.npmjs.com/package/@prmichaelsen/firebase-admin-sdk-v8)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://github.com/prmichaelsen/firebase-admin-sdk-v8/actions/workflows/test.yml)
|
|
8
|
+
[](https://github.com/prmichaelsen/firebase-admin-sdk-v8/actions/workflows/e2e-tests.yml)
|
|
9
|
+
[](https://codecov.io/gh/prmichaelsen/firebase-admin-sdk-v8)
|
|
7
10
|
|
|
8
11
|
This library provides Firebase Admin SDK functionality for Cloudflare Workers and other edge runtimes. It uses REST APIs and JWT token generation instead of the Node.js Admin SDK, making it compatible with environments that don't support Node.js.
|
|
9
12
|
|
|
@@ -25,7 +28,7 @@ This library provides Firebase Admin SDK functionality for Cloudflare Workers an
|
|
|
25
28
|
## 📦 Installation
|
|
26
29
|
|
|
27
30
|
```bash
|
|
28
|
-
npm install firebase-admin-sdk-v8
|
|
31
|
+
npm install @prmichaelsen/firebase-admin-sdk-v8
|
|
29
32
|
```
|
|
30
33
|
|
|
31
34
|
## 🚀 Quick Start
|
|
@@ -78,7 +81,7 @@ FIREBASE_PROJECT_ID=your-project-id
|
|
|
78
81
|
### 2. Verify ID Tokens
|
|
79
82
|
|
|
80
83
|
```typescript
|
|
81
|
-
import { verifyIdToken, getUserFromToken } from 'firebase-admin-sdk-v8';
|
|
84
|
+
import { verifyIdToken, getUserFromToken } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
82
85
|
|
|
83
86
|
const authHeader = request.headers.get('authorization');
|
|
84
87
|
const idToken = authHeader?.split('Bearer ')[1];
|
|
@@ -94,7 +97,7 @@ try {
|
|
|
94
97
|
### 3. Basic Firestore Operations
|
|
95
98
|
|
|
96
99
|
```typescript
|
|
97
|
-
import { setDocument, getDocument, updateDocument, FieldValue } from 'firebase-admin-sdk-v8';
|
|
100
|
+
import { setDocument, getDocument, updateDocument, FieldValue } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
98
101
|
|
|
99
102
|
// Set a document (create or overwrite)
|
|
100
103
|
await setDocument('users', 'user123', {
|
|
@@ -116,7 +119,7 @@ await updateDocument('users', 'user123', {
|
|
|
116
119
|
### 4. Advanced Queries
|
|
117
120
|
|
|
118
121
|
```typescript
|
|
119
|
-
import { queryDocuments } from 'firebase-admin-sdk-v8';
|
|
122
|
+
import { queryDocuments } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
120
123
|
|
|
121
124
|
const activeUsers = await queryDocuments('users', {
|
|
122
125
|
where: [
|
|
@@ -397,7 +400,7 @@ export default {
|
|
|
397
400
|
### Leaderboard Example
|
|
398
401
|
|
|
399
402
|
```typescript
|
|
400
|
-
import { queryDocuments, updateDocument, FieldValue } from 'firebase-admin-sdk-v8';
|
|
403
|
+
import { queryDocuments, updateDocument, FieldValue } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
401
404
|
|
|
402
405
|
async function getTopPlayers(limit = 10) {
|
|
403
406
|
return await queryDocuments('players', {
|
|
@@ -418,7 +421,7 @@ async function updatePlayerScore(playerId: string, points: number) {
|
|
|
418
421
|
### Bulk Operations Example
|
|
419
422
|
|
|
420
423
|
```typescript
|
|
421
|
-
import { batchWrite, FieldValue } from 'firebase-admin-sdk-v8';
|
|
424
|
+
import { batchWrite, FieldValue } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
422
425
|
|
|
423
426
|
async function bulkUpdateUsers(userIds: string[], updates: any) {
|
|
424
427
|
const operations = userIds.map(userId => ({
|
package/dist/index.d.mts
CHANGED
|
@@ -287,18 +287,18 @@ declare function getUserFromToken(idToken: string): Promise<UserInfo>;
|
|
|
287
287
|
declare function getAuth(): any;
|
|
288
288
|
|
|
289
289
|
/**
|
|
290
|
-
* Firebase Admin SDK v8 - Firestore
|
|
291
|
-
*
|
|
290
|
+
* Firebase Admin SDK v8 - Firestore CRUD Operations
|
|
291
|
+
* All Firestore document operations using REST API
|
|
292
292
|
*/
|
|
293
293
|
|
|
294
294
|
/**
|
|
295
295
|
* Set a document in Firestore (create or overwrite)
|
|
296
296
|
*
|
|
297
|
-
* @param
|
|
298
|
-
* @param
|
|
299
|
-
* @param
|
|
300
|
-
* @param
|
|
301
|
-
* @returns
|
|
297
|
+
* @param collectionPath - Collection path
|
|
298
|
+
* @param documentId - Document ID
|
|
299
|
+
* @param data - Document data
|
|
300
|
+
* @param options - Set options (merge, mergeFields)
|
|
301
|
+
* @returns Promise that resolves when document is set
|
|
302
302
|
* @throws {Error} If the operation fails
|
|
303
303
|
*
|
|
304
304
|
* @example
|
|
@@ -317,10 +317,10 @@ declare function setDocument(collectionPath: string, documentId: string, data: D
|
|
|
317
317
|
/**
|
|
318
318
|
* Add a document to Firestore collection
|
|
319
319
|
*
|
|
320
|
-
* @param
|
|
321
|
-
* @param
|
|
322
|
-
* @param
|
|
323
|
-
* @returns
|
|
320
|
+
* @param collectionPath - Collection path (e.g., 'users' or 'users/uid/posts')
|
|
321
|
+
* @param data - Document data
|
|
322
|
+
* @param documentId - Optional document ID (auto-generated if not provided)
|
|
323
|
+
* @returns Document reference with id and path
|
|
324
324
|
* @throws {Error} If the operation fails
|
|
325
325
|
*
|
|
326
326
|
* @example
|
|
@@ -337,9 +337,9 @@ declare function addDocument(collectionPath: string, data: DataObject, documentI
|
|
|
337
337
|
/**
|
|
338
338
|
* Get a document from Firestore
|
|
339
339
|
*
|
|
340
|
-
* @param
|
|
341
|
-
* @param
|
|
342
|
-
* @returns
|
|
340
|
+
* @param collectionPath - Collection path
|
|
341
|
+
* @param documentId - Document ID
|
|
342
|
+
* @returns Document data or null if not found
|
|
343
343
|
* @throws {Error} If the operation fails
|
|
344
344
|
*
|
|
345
345
|
* @example
|
|
@@ -354,10 +354,10 @@ declare function getDocument(collectionPath: string, documentId: string): Promis
|
|
|
354
354
|
/**
|
|
355
355
|
* Update a document in Firestore
|
|
356
356
|
*
|
|
357
|
-
* @param
|
|
358
|
-
* @param
|
|
359
|
-
* @param
|
|
360
|
-
* @returns
|
|
357
|
+
* @param collectionPath - Collection path
|
|
358
|
+
* @param documentId - Document ID
|
|
359
|
+
* @param data - Data to update
|
|
360
|
+
* @returns Promise that resolves when document is updated
|
|
361
361
|
* @throws {Error} If the operation fails
|
|
362
362
|
*
|
|
363
363
|
* @example
|
|
@@ -372,9 +372,9 @@ declare function updateDocument(collectionPath: string, documentId: string, data
|
|
|
372
372
|
/**
|
|
373
373
|
* Delete a document from Firestore
|
|
374
374
|
*
|
|
375
|
-
* @param
|
|
376
|
-
* @param
|
|
377
|
-
* @returns
|
|
375
|
+
* @param collectionPath - Collection path
|
|
376
|
+
* @param documentId - Document ID
|
|
377
|
+
* @returns Promise that resolves when document is deleted
|
|
378
378
|
* @throws {Error} If the operation fails
|
|
379
379
|
*
|
|
380
380
|
* @example
|
|
@@ -386,9 +386,9 @@ declare function deleteDocument(collectionPath: string, documentId: string): Pro
|
|
|
386
386
|
/**
|
|
387
387
|
* Query documents in a collection with advanced filtering
|
|
388
388
|
*
|
|
389
|
-
* @param
|
|
390
|
-
* @param
|
|
391
|
-
* @returns
|
|
389
|
+
* @param collectionPath - Collection path
|
|
390
|
+
* @param options - Query options (where, orderBy, limit, etc.)
|
|
391
|
+
* @returns Array of documents with id and data
|
|
392
392
|
* @throws {Error} If the operation fails
|
|
393
393
|
*
|
|
394
394
|
* @example
|
|
@@ -410,8 +410,8 @@ declare function queryDocuments(collectionPath: string, options?: QueryOptions):
|
|
|
410
410
|
/**
|
|
411
411
|
* Perform batch write operations (set, update, delete)
|
|
412
412
|
*
|
|
413
|
-
* @param
|
|
414
|
-
* @returns
|
|
413
|
+
* @param operations - Array of batch operations
|
|
414
|
+
* @returns Batch write result
|
|
415
415
|
* @throws {Error} If the operation fails
|
|
416
416
|
*
|
|
417
417
|
* @example
|
package/dist/index.d.ts
CHANGED
|
@@ -287,18 +287,18 @@ declare function getUserFromToken(idToken: string): Promise<UserInfo>;
|
|
|
287
287
|
declare function getAuth(): any;
|
|
288
288
|
|
|
289
289
|
/**
|
|
290
|
-
* Firebase Admin SDK v8 - Firestore
|
|
291
|
-
*
|
|
290
|
+
* Firebase Admin SDK v8 - Firestore CRUD Operations
|
|
291
|
+
* All Firestore document operations using REST API
|
|
292
292
|
*/
|
|
293
293
|
|
|
294
294
|
/**
|
|
295
295
|
* Set a document in Firestore (create or overwrite)
|
|
296
296
|
*
|
|
297
|
-
* @param
|
|
298
|
-
* @param
|
|
299
|
-
* @param
|
|
300
|
-
* @param
|
|
301
|
-
* @returns
|
|
297
|
+
* @param collectionPath - Collection path
|
|
298
|
+
* @param documentId - Document ID
|
|
299
|
+
* @param data - Document data
|
|
300
|
+
* @param options - Set options (merge, mergeFields)
|
|
301
|
+
* @returns Promise that resolves when document is set
|
|
302
302
|
* @throws {Error} If the operation fails
|
|
303
303
|
*
|
|
304
304
|
* @example
|
|
@@ -317,10 +317,10 @@ declare function setDocument(collectionPath: string, documentId: string, data: D
|
|
|
317
317
|
/**
|
|
318
318
|
* Add a document to Firestore collection
|
|
319
319
|
*
|
|
320
|
-
* @param
|
|
321
|
-
* @param
|
|
322
|
-
* @param
|
|
323
|
-
* @returns
|
|
320
|
+
* @param collectionPath - Collection path (e.g., 'users' or 'users/uid/posts')
|
|
321
|
+
* @param data - Document data
|
|
322
|
+
* @param documentId - Optional document ID (auto-generated if not provided)
|
|
323
|
+
* @returns Document reference with id and path
|
|
324
324
|
* @throws {Error} If the operation fails
|
|
325
325
|
*
|
|
326
326
|
* @example
|
|
@@ -337,9 +337,9 @@ declare function addDocument(collectionPath: string, data: DataObject, documentI
|
|
|
337
337
|
/**
|
|
338
338
|
* Get a document from Firestore
|
|
339
339
|
*
|
|
340
|
-
* @param
|
|
341
|
-
* @param
|
|
342
|
-
* @returns
|
|
340
|
+
* @param collectionPath - Collection path
|
|
341
|
+
* @param documentId - Document ID
|
|
342
|
+
* @returns Document data or null if not found
|
|
343
343
|
* @throws {Error} If the operation fails
|
|
344
344
|
*
|
|
345
345
|
* @example
|
|
@@ -354,10 +354,10 @@ declare function getDocument(collectionPath: string, documentId: string): Promis
|
|
|
354
354
|
/**
|
|
355
355
|
* Update a document in Firestore
|
|
356
356
|
*
|
|
357
|
-
* @param
|
|
358
|
-
* @param
|
|
359
|
-
* @param
|
|
360
|
-
* @returns
|
|
357
|
+
* @param collectionPath - Collection path
|
|
358
|
+
* @param documentId - Document ID
|
|
359
|
+
* @param data - Data to update
|
|
360
|
+
* @returns Promise that resolves when document is updated
|
|
361
361
|
* @throws {Error} If the operation fails
|
|
362
362
|
*
|
|
363
363
|
* @example
|
|
@@ -372,9 +372,9 @@ declare function updateDocument(collectionPath: string, documentId: string, data
|
|
|
372
372
|
/**
|
|
373
373
|
* Delete a document from Firestore
|
|
374
374
|
*
|
|
375
|
-
* @param
|
|
376
|
-
* @param
|
|
377
|
-
* @returns
|
|
375
|
+
* @param collectionPath - Collection path
|
|
376
|
+
* @param documentId - Document ID
|
|
377
|
+
* @returns Promise that resolves when document is deleted
|
|
378
378
|
* @throws {Error} If the operation fails
|
|
379
379
|
*
|
|
380
380
|
* @example
|
|
@@ -386,9 +386,9 @@ declare function deleteDocument(collectionPath: string, documentId: string): Pro
|
|
|
386
386
|
/**
|
|
387
387
|
* Query documents in a collection with advanced filtering
|
|
388
388
|
*
|
|
389
|
-
* @param
|
|
390
|
-
* @param
|
|
391
|
-
* @returns
|
|
389
|
+
* @param collectionPath - Collection path
|
|
390
|
+
* @param options - Query options (where, orderBy, limit, etc.)
|
|
391
|
+
* @returns Array of documents with id and data
|
|
392
392
|
* @throws {Error} If the operation fails
|
|
393
393
|
*
|
|
394
394
|
* @example
|
|
@@ -410,8 +410,8 @@ declare function queryDocuments(collectionPath: string, options?: QueryOptions):
|
|
|
410
410
|
/**
|
|
411
411
|
* Perform batch write operations (set, update, delete)
|
|
412
412
|
*
|
|
413
|
-
* @param
|
|
414
|
-
* @returns
|
|
413
|
+
* @param operations - Array of batch operations
|
|
414
|
+
* @returns Batch write result
|
|
415
415
|
* @throws {Error} If the operation fails
|
|
416
416
|
*
|
|
417
417
|
* @example
|
package/dist/index.js
CHANGED
|
@@ -331,78 +331,6 @@ function getAuth() {
|
|
|
331
331
|
};
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
// src/token-generation.ts
|
|
335
|
-
function base64UrlEncode(str) {
|
|
336
|
-
return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
337
|
-
}
|
|
338
|
-
function base64UrlEncodeBuffer(buffer) {
|
|
339
|
-
return btoa(String.fromCharCode(...buffer)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
340
|
-
}
|
|
341
|
-
async function createJWT(serviceAccount) {
|
|
342
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
343
|
-
const expiry = now + 3600;
|
|
344
|
-
const header = {
|
|
345
|
-
alg: "RS256",
|
|
346
|
-
typ: "JWT"
|
|
347
|
-
};
|
|
348
|
-
const payload = {
|
|
349
|
-
iss: serviceAccount.client_email,
|
|
350
|
-
sub: serviceAccount.client_email,
|
|
351
|
-
aud: serviceAccount.token_uri,
|
|
352
|
-
iat: now,
|
|
353
|
-
exp: expiry,
|
|
354
|
-
scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
|
|
355
|
-
};
|
|
356
|
-
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
357
|
-
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
358
|
-
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
|
|
359
|
-
const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
|
|
360
|
-
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
361
|
-
const cryptoKey = await crypto.subtle.importKey(
|
|
362
|
-
"pkcs8",
|
|
363
|
-
binaryDer,
|
|
364
|
-
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
365
|
-
false,
|
|
366
|
-
["sign"]
|
|
367
|
-
);
|
|
368
|
-
const signature = await crypto.subtle.sign(
|
|
369
|
-
"RSASSA-PKCS1-v1_5",
|
|
370
|
-
cryptoKey,
|
|
371
|
-
new TextEncoder().encode(unsignedToken)
|
|
372
|
-
);
|
|
373
|
-
const encodedSignature = base64UrlEncodeBuffer(new Uint8Array(signature));
|
|
374
|
-
return `${unsignedToken}.${encodedSignature}`;
|
|
375
|
-
}
|
|
376
|
-
var cachedAccessToken = null;
|
|
377
|
-
var tokenExpiry = 0;
|
|
378
|
-
async function getAdminAccessToken() {
|
|
379
|
-
if (cachedAccessToken && Date.now() < tokenExpiry) {
|
|
380
|
-
return cachedAccessToken;
|
|
381
|
-
}
|
|
382
|
-
const serviceAccount = getServiceAccount();
|
|
383
|
-
const jwt = await createJWT(serviceAccount);
|
|
384
|
-
const response = await fetch(serviceAccount.token_uri, {
|
|
385
|
-
method: "POST",
|
|
386
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
387
|
-
body: new URLSearchParams({
|
|
388
|
-
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
389
|
-
assertion: jwt
|
|
390
|
-
})
|
|
391
|
-
});
|
|
392
|
-
if (!response.ok) {
|
|
393
|
-
const errorText = await response.text();
|
|
394
|
-
throw new Error(`Failed to get access token: ${errorText}`);
|
|
395
|
-
}
|
|
396
|
-
const data = await response.json();
|
|
397
|
-
cachedAccessToken = data.access_token;
|
|
398
|
-
tokenExpiry = Date.now() + data.expires_in * 1e3 - 6e4;
|
|
399
|
-
return cachedAccessToken;
|
|
400
|
-
}
|
|
401
|
-
function clearTokenCache() {
|
|
402
|
-
cachedAccessToken = null;
|
|
403
|
-
tokenExpiry = 0;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
334
|
// src/field-value.ts
|
|
407
335
|
function serverTimestamp() {
|
|
408
336
|
return {
|
|
@@ -679,7 +607,79 @@ function removeFieldTransforms(data) {
|
|
|
679
607
|
return result;
|
|
680
608
|
}
|
|
681
609
|
|
|
682
|
-
// src/
|
|
610
|
+
// src/token-generation.ts
|
|
611
|
+
function base64UrlEncode(str) {
|
|
612
|
+
return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
613
|
+
}
|
|
614
|
+
function base64UrlEncodeBuffer(buffer) {
|
|
615
|
+
return btoa(String.fromCharCode(...buffer)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
616
|
+
}
|
|
617
|
+
async function createJWT(serviceAccount) {
|
|
618
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
619
|
+
const expiry = now + 3600;
|
|
620
|
+
const header = {
|
|
621
|
+
alg: "RS256",
|
|
622
|
+
typ: "JWT"
|
|
623
|
+
};
|
|
624
|
+
const payload = {
|
|
625
|
+
iss: serviceAccount.client_email,
|
|
626
|
+
sub: serviceAccount.client_email,
|
|
627
|
+
aud: serviceAccount.token_uri,
|
|
628
|
+
iat: now,
|
|
629
|
+
exp: expiry,
|
|
630
|
+
scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
|
|
631
|
+
};
|
|
632
|
+
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
633
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
634
|
+
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
|
|
635
|
+
const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
|
|
636
|
+
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
637
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
638
|
+
"pkcs8",
|
|
639
|
+
binaryDer,
|
|
640
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
641
|
+
false,
|
|
642
|
+
["sign"]
|
|
643
|
+
);
|
|
644
|
+
const signature = await crypto.subtle.sign(
|
|
645
|
+
"RSASSA-PKCS1-v1_5",
|
|
646
|
+
cryptoKey,
|
|
647
|
+
new TextEncoder().encode(unsignedToken)
|
|
648
|
+
);
|
|
649
|
+
const encodedSignature = base64UrlEncodeBuffer(new Uint8Array(signature));
|
|
650
|
+
return `${unsignedToken}.${encodedSignature}`;
|
|
651
|
+
}
|
|
652
|
+
var cachedAccessToken = null;
|
|
653
|
+
var tokenExpiry = 0;
|
|
654
|
+
async function getAdminAccessToken() {
|
|
655
|
+
if (cachedAccessToken && Date.now() < tokenExpiry) {
|
|
656
|
+
return cachedAccessToken;
|
|
657
|
+
}
|
|
658
|
+
const serviceAccount = getServiceAccount();
|
|
659
|
+
const jwt = await createJWT(serviceAccount);
|
|
660
|
+
const response = await fetch(serviceAccount.token_uri, {
|
|
661
|
+
method: "POST",
|
|
662
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
663
|
+
body: new URLSearchParams({
|
|
664
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
665
|
+
assertion: jwt
|
|
666
|
+
})
|
|
667
|
+
});
|
|
668
|
+
if (!response.ok) {
|
|
669
|
+
const errorText = await response.text();
|
|
670
|
+
throw new Error(`Failed to get access token: ${errorText}`);
|
|
671
|
+
}
|
|
672
|
+
const data = await response.json();
|
|
673
|
+
cachedAccessToken = data.access_token;
|
|
674
|
+
tokenExpiry = Date.now() + data.expires_in * 1e3 - 6e4;
|
|
675
|
+
return cachedAccessToken;
|
|
676
|
+
}
|
|
677
|
+
function clearTokenCache() {
|
|
678
|
+
cachedAccessToken = null;
|
|
679
|
+
tokenExpiry = 0;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/firestore/operations.ts
|
|
683
683
|
var FIRESTORE_API = "https://firestore.googleapis.com/v1";
|
|
684
684
|
async function commitWrites(writes) {
|
|
685
685
|
const accessToken = await getAdminAccessToken();
|
package/dist/index.mjs
CHANGED
|
@@ -288,78 +288,6 @@ function getAuth() {
|
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
// src/token-generation.ts
|
|
292
|
-
function base64UrlEncode(str) {
|
|
293
|
-
return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
294
|
-
}
|
|
295
|
-
function base64UrlEncodeBuffer(buffer) {
|
|
296
|
-
return btoa(String.fromCharCode(...buffer)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
297
|
-
}
|
|
298
|
-
async function createJWT(serviceAccount) {
|
|
299
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
300
|
-
const expiry = now + 3600;
|
|
301
|
-
const header = {
|
|
302
|
-
alg: "RS256",
|
|
303
|
-
typ: "JWT"
|
|
304
|
-
};
|
|
305
|
-
const payload = {
|
|
306
|
-
iss: serviceAccount.client_email,
|
|
307
|
-
sub: serviceAccount.client_email,
|
|
308
|
-
aud: serviceAccount.token_uri,
|
|
309
|
-
iat: now,
|
|
310
|
-
exp: expiry,
|
|
311
|
-
scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
|
|
312
|
-
};
|
|
313
|
-
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
314
|
-
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
315
|
-
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
|
|
316
|
-
const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
|
|
317
|
-
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
318
|
-
const cryptoKey = await crypto.subtle.importKey(
|
|
319
|
-
"pkcs8",
|
|
320
|
-
binaryDer,
|
|
321
|
-
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
322
|
-
false,
|
|
323
|
-
["sign"]
|
|
324
|
-
);
|
|
325
|
-
const signature = await crypto.subtle.sign(
|
|
326
|
-
"RSASSA-PKCS1-v1_5",
|
|
327
|
-
cryptoKey,
|
|
328
|
-
new TextEncoder().encode(unsignedToken)
|
|
329
|
-
);
|
|
330
|
-
const encodedSignature = base64UrlEncodeBuffer(new Uint8Array(signature));
|
|
331
|
-
return `${unsignedToken}.${encodedSignature}`;
|
|
332
|
-
}
|
|
333
|
-
var cachedAccessToken = null;
|
|
334
|
-
var tokenExpiry = 0;
|
|
335
|
-
async function getAdminAccessToken() {
|
|
336
|
-
if (cachedAccessToken && Date.now() < tokenExpiry) {
|
|
337
|
-
return cachedAccessToken;
|
|
338
|
-
}
|
|
339
|
-
const serviceAccount = getServiceAccount();
|
|
340
|
-
const jwt = await createJWT(serviceAccount);
|
|
341
|
-
const response = await fetch(serviceAccount.token_uri, {
|
|
342
|
-
method: "POST",
|
|
343
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
344
|
-
body: new URLSearchParams({
|
|
345
|
-
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
346
|
-
assertion: jwt
|
|
347
|
-
})
|
|
348
|
-
});
|
|
349
|
-
if (!response.ok) {
|
|
350
|
-
const errorText = await response.text();
|
|
351
|
-
throw new Error(`Failed to get access token: ${errorText}`);
|
|
352
|
-
}
|
|
353
|
-
const data = await response.json();
|
|
354
|
-
cachedAccessToken = data.access_token;
|
|
355
|
-
tokenExpiry = Date.now() + data.expires_in * 1e3 - 6e4;
|
|
356
|
-
return cachedAccessToken;
|
|
357
|
-
}
|
|
358
|
-
function clearTokenCache() {
|
|
359
|
-
cachedAccessToken = null;
|
|
360
|
-
tokenExpiry = 0;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
291
|
// src/field-value.ts
|
|
364
292
|
function serverTimestamp() {
|
|
365
293
|
return {
|
|
@@ -636,7 +564,79 @@ function removeFieldTransforms(data) {
|
|
|
636
564
|
return result;
|
|
637
565
|
}
|
|
638
566
|
|
|
639
|
-
// src/
|
|
567
|
+
// src/token-generation.ts
|
|
568
|
+
function base64UrlEncode(str) {
|
|
569
|
+
return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
570
|
+
}
|
|
571
|
+
function base64UrlEncodeBuffer(buffer) {
|
|
572
|
+
return btoa(String.fromCharCode(...buffer)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
573
|
+
}
|
|
574
|
+
async function createJWT(serviceAccount) {
|
|
575
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
576
|
+
const expiry = now + 3600;
|
|
577
|
+
const header = {
|
|
578
|
+
alg: "RS256",
|
|
579
|
+
typ: "JWT"
|
|
580
|
+
};
|
|
581
|
+
const payload = {
|
|
582
|
+
iss: serviceAccount.client_email,
|
|
583
|
+
sub: serviceAccount.client_email,
|
|
584
|
+
aud: serviceAccount.token_uri,
|
|
585
|
+
iat: now,
|
|
586
|
+
exp: expiry,
|
|
587
|
+
scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
|
|
588
|
+
};
|
|
589
|
+
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
590
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
591
|
+
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
|
|
592
|
+
const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
|
|
593
|
+
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
594
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
595
|
+
"pkcs8",
|
|
596
|
+
binaryDer,
|
|
597
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
598
|
+
false,
|
|
599
|
+
["sign"]
|
|
600
|
+
);
|
|
601
|
+
const signature = await crypto.subtle.sign(
|
|
602
|
+
"RSASSA-PKCS1-v1_5",
|
|
603
|
+
cryptoKey,
|
|
604
|
+
new TextEncoder().encode(unsignedToken)
|
|
605
|
+
);
|
|
606
|
+
const encodedSignature = base64UrlEncodeBuffer(new Uint8Array(signature));
|
|
607
|
+
return `${unsignedToken}.${encodedSignature}`;
|
|
608
|
+
}
|
|
609
|
+
var cachedAccessToken = null;
|
|
610
|
+
var tokenExpiry = 0;
|
|
611
|
+
async function getAdminAccessToken() {
|
|
612
|
+
if (cachedAccessToken && Date.now() < tokenExpiry) {
|
|
613
|
+
return cachedAccessToken;
|
|
614
|
+
}
|
|
615
|
+
const serviceAccount = getServiceAccount();
|
|
616
|
+
const jwt = await createJWT(serviceAccount);
|
|
617
|
+
const response = await fetch(serviceAccount.token_uri, {
|
|
618
|
+
method: "POST",
|
|
619
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
620
|
+
body: new URLSearchParams({
|
|
621
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
622
|
+
assertion: jwt
|
|
623
|
+
})
|
|
624
|
+
});
|
|
625
|
+
if (!response.ok) {
|
|
626
|
+
const errorText = await response.text();
|
|
627
|
+
throw new Error(`Failed to get access token: ${errorText}`);
|
|
628
|
+
}
|
|
629
|
+
const data = await response.json();
|
|
630
|
+
cachedAccessToken = data.access_token;
|
|
631
|
+
tokenExpiry = Date.now() + data.expires_in * 1e3 - 6e4;
|
|
632
|
+
return cachedAccessToken;
|
|
633
|
+
}
|
|
634
|
+
function clearTokenCache() {
|
|
635
|
+
cachedAccessToken = null;
|
|
636
|
+
tokenExpiry = 0;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/firestore/operations.ts
|
|
640
640
|
var FIRESTORE_API = "https://firestore.googleapis.com/v1";
|
|
641
641
|
async function commitWrites(writes) {
|
|
642
642
|
const accessToken = await getAdminAccessToken();
|
package/package.json
CHANGED
package/firebase.json
DELETED
package/firestore.indexes.json
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"indexes": [
|
|
3
|
-
{
|
|
4
|
-
"collectionGroup": "e2e-tests",
|
|
5
|
-
"queryScope": "COLLECTION",
|
|
6
|
-
"fields": [
|
|
7
|
-
{
|
|
8
|
-
"fieldPath": "_test",
|
|
9
|
-
"order": "ASCENDING"
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"fieldPath": "age",
|
|
13
|
-
"order": "ASCENDING"
|
|
14
|
-
}
|
|
15
|
-
]
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
"collectionGroup": "messages",
|
|
19
|
-
"queryScope": "COLLECTION",
|
|
20
|
-
"fields": [
|
|
21
|
-
{
|
|
22
|
-
"fieldPath": "_test",
|
|
23
|
-
"order": "ASCENDING"
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"fieldPath": "timestamp",
|
|
27
|
-
"order": "ASCENDING"
|
|
28
|
-
}
|
|
29
|
-
]
|
|
30
|
-
}
|
|
31
|
-
],
|
|
32
|
-
"fieldOverrides": []
|
|
33
|
-
}
|
package/firestore.rules
DELETED
package/jest.config.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
preset: 'ts-jest',
|
|
3
|
-
testEnvironment: 'node',
|
|
4
|
-
roots: ['<rootDir>/src'],
|
|
5
|
-
testMatch: ['**/*.spec.ts'],
|
|
6
|
-
moduleFileExtensions: ['ts', 'js'],
|
|
7
|
-
collectCoverageFrom: [
|
|
8
|
-
'src/**/*.ts',
|
|
9
|
-
'!src/**/*.d.ts',
|
|
10
|
-
'!src/**/*.spec.ts',
|
|
11
|
-
],
|
|
12
|
-
};
|
package/jest.e2e.config.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
preset: 'ts-jest',
|
|
3
|
-
testEnvironment: 'node',
|
|
4
|
-
testMatch: ['**/*.e2e.ts'],
|
|
5
|
-
testTimeout: 30000, // 30 seconds for real API calls
|
|
6
|
-
roots: ['<rootDir>/src'],
|
|
7
|
-
collectCoverageFrom: [
|
|
8
|
-
'src/**/*.ts',
|
|
9
|
-
'!src/**/*.spec.ts',
|
|
10
|
-
'!src/**/*.e2e.ts',
|
|
11
|
-
'!src/types.ts',
|
|
12
|
-
'!src/index.ts',
|
|
13
|
-
],
|
|
14
|
-
};
|
package/service-account.json
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"type": "service_account",
|
|
3
|
-
"project_id": "prmichaelsen-firebase-e2e",
|
|
4
|
-
"private_key_id": "84caabb8515ec5fab4caa53a0f85405c270d5cd4",
|
|
5
|
-
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/0n8V7m7vCQ9/\n5FGYL5V20pQxliUMF/ycNBSV/xHrXAvTMxyMEjFK8Tg9sBLdYhuxC488lnwm3/o+\nn5JkckQ2AyzXX6mi+camwxdLXmT97kXqdFLisO4PY7Fv6HvvMgRQqysj7aJkyiz7\nrAs0DpQq1L70sBFdVLiScPH9FFnuV0EeyOTuVgYwGYk1L76/U7bb3WLNCmcmtG+Y\nt4BNzyMqFgtIdXCQFVBQf9HXy3x5xk8a1rrkf7pEuiwK0PXCDIVH7GYX4xCiQ1qs\nYF4IunNFhcwu1hk5XTIDSVrIn/qLPMZvEcBqHxIJd5JvePjx5hPP81b1mrGUVske\nsaRimjxxAgMBAAECggEAAWNKVG7KslkMRH5y5q56yYbMLVsAPp5SehDYZfNtU5jG\nucr2CxS7SDxcOKS0UOdmUDlyBQaztED3mbS5hZeGuHtSZjvaHtnpdDMXe+NIHhw3\nY520LHwKOjtG69/bPFz4x1qjBRoG4Zgi4NlFpXqbhinO4zdTkNYi6xBSd+R0oshP\nSo59lQvs8e3bMD4f4uTl7JxUifjVM64R3gCkH6AbGqU01wg8UzsllCf0fQM04w8i\nq1oVjhBVWYYVE7w2H+EU+LdiRSRET+Vrk5dtBL3vI6yckvGmxTBMnkK2gTFAAR6F\nnN4NKlUsiFol9WVRY3tdRNOnStjlORDyerSEFQaqhwKBgQDfAa5ddxBVbxUS3E2a\nnGJRA44fYlRyZf9KnJdz8uWsCvuG7UYbe2bp7TxlpEXdTG6Vqu/hhYWd/msmhupZ\nc+YFg1HWE1Ee+nQv7iwAD+okfncnKGIC65XjwaSEMRGrmtxBL6Qq4yxn/Yug/8YR\nniGRQKuMtJ2ZA0p41vI6Mkt7kwKBgQDcM7eQQ5482FcXuOfUlPoiWnd+pF5mNrU1\ngKoxdsXMYRKyTGMPLrOKqsf6kisQUlA48vWRCQOa9QBSjHktfOt2EZ3mNQz5k3AR\n/dIjCGf+ATr2P9Sc0uo7sA20XB62wmG+t2A/vxf9WzBAgdDk6nENB4nz6lL9xcak\ngvVt2vXSawKBgQCV6JRk4f/J3oVFC3DjaRKyMPid4kSwLh6B8mfhGrwHfc59cgz5\ntmeFAuPh057fV1zTIXhlmpMqlPdEi9cHUOCkfhVKGewjLetiuPE9DXWxGI5SdVQF\ncIZu9yH3duDRAaXj7/mklteoBAmTrbxg5XLdKKLpUBTM4ihyuNNWCa8yHwKBgCYK\noTnBFMM6NMGaZiKpohTxQBeW2eAar2+QzNZCyKUoWAyJecuTq9zW6Dl3qwzky4sr\nHhVyUzcgAHBCaGTdYehB3t94ZsdvGztgeD8pIp4VJFSKbnaxUVoCbjusdnnoVu6V\ny4D3yHMyn8FlK+uAPQudM835u2CwHEMrhK731uQFAoGBAKO0crVWuY1EP1RLSyLK\nIS3uUgbauBbQlE4ZPBTi84vXCZjN/53obFgkPHJ5tdjxj6C8x58jgHg/Hufyvbib\ngOQQxadM90UBumlzA8lV7A9jvL2ryfop9ketnVO5hPvB4O8iw9Z1R+25FeM9ZFJI\nqmEjiy9Nhmvpvur+FZ85qcRm\n-----END PRIVATE KEY-----\n",
|
|
6
|
-
"client_email": "firebase-adminsdk-fbsvc@prmichaelsen-firebase-e2e.iam.gserviceaccount.com",
|
|
7
|
-
"client_id": "103889460014910902622",
|
|
8
|
-
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
9
|
-
"token_uri": "https://oauth2.googleapis.com/token",
|
|
10
|
-
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
|
11
|
-
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40prmichaelsen-firebase-e2e.iam.gserviceaccount.com",
|
|
12
|
-
"universe_domain": "googleapis.com"
|
|
13
|
-
}
|