@prmichaelsen/firebase-admin-sdk-v8 2.0.20 → 2.0.22

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.
@@ -31,11 +31,20 @@ jobs:
31
31
  run: |
32
32
  echo '${{ secrets.FIREBASE_SERVICE_ACCOUNT }}' > service-account.json
33
33
 
34
- - name: Run e2e tests
35
- run: npm run test:e2e
34
+ - name: Run e2e tests with coverage
35
+ run: npm run test:e2e -- --coverage
36
36
  env:
37
37
  NODE_ENV: test
38
38
 
39
+ - name: Upload e2e coverage to Codecov
40
+ uses: codecov/codecov-action@v4
41
+ with:
42
+ token: ${{ secrets.CODECOV_TOKEN }}
43
+ files: ./coverage/lcov.info
44
+ flags: e2etests
45
+ name: codecov-e2e
46
+ fail_ci_if_error: false
47
+
39
48
  - name: Clean up service account file
40
49
  if: always()
41
50
  run: rm -f service-account.json
package/README.md CHANGED
@@ -521,11 +521,132 @@ For better query performance:
521
521
  | Firestore Queries | ✅ | where, orderBy, limit, cursors |
522
522
  | Firestore Batch | ✅ | Up to 500 operations |
523
523
  | Firestore Transactions | ❌ | Not yet implemented |
524
+ | **Realtime Listeners** | **❌** | **See explanation below** |
524
525
  | Field Values | ✅ | increment, arrayUnion, serverTimestamp, etc. |
525
526
  | Realtime Database | ❌ | Not planned |
526
527
  | Cloud Storage | ❌ | Not yet implemented |
527
528
  | Cloud Messaging | ❌ | Not yet implemented |
528
529
 
530
+ ## ⚠️ Realtime Listeners Not Supported
531
+
532
+ This library **does not support** Firestore realtime listeners (`onSnapshot()`). Here's why:
533
+
534
+ ### Technical Limitation
535
+
536
+ **This library uses the Firestore REST API**, which is:
537
+ - ✅ Stateless (request/response only)
538
+ - ✅ Compatible with edge runtimes (Cloudflare Workers, Vercel Edge)
539
+ - ❌ **No persistent connections**
540
+ - ❌ **No server-push capabilities**
541
+ - ❌ **No streaming support**
542
+
543
+ **Realtime listeners require**:
544
+ - Persistent connections (WebSocket or gRPC)
545
+ - Bidirectional streaming
546
+ - Server-push architecture
547
+
548
+ The Firestore REST API simply doesn't provide these capabilities.
549
+
550
+ ### Why Not Implement gRPC?
551
+
552
+ While Firestore does offer a gRPC API with streaming support, implementing it would require:
553
+
554
+ 1. **Complex Protocol Implementation**
555
+ - HTTP/2 framing
556
+ - gRPC message framing
557
+ - Protobuf encoding/decoding
558
+ - Authentication flow
559
+ - Reconnection logic
560
+ - ~100+ hours of development
561
+
562
+ 2. **Runtime Limitations**
563
+ - Cloudflare Workers doesn't support full gRPC (only gRPC-Web)
564
+ - gRPC-Web requires a proxy server
565
+ - Can't connect directly to Firestore's gRPC endpoint
566
+ - Would only work in Durable Objects, not regular Workers
567
+
568
+ 3. **Maintenance Burden**
569
+ - Must keep up with Firestore protocol changes
570
+ - Complex debugging and error handling
571
+ - High ongoing maintenance cost
572
+
573
+ ### Alternatives
574
+
575
+ If you need realtime updates, consider these approaches:
576
+
577
+ #### 1. **Polling (Simple)**
578
+ ```typescript
579
+ // Poll for changes every 5 seconds
580
+ setInterval(async () => {
581
+ const doc = await getDocument('users', 'user123');
582
+ // Handle updates
583
+ }, 5000);
584
+ ```
585
+
586
+ **Pros**: Simple, works everywhere
587
+ **Cons**: 5-second delay, polling costs
588
+
589
+ #### 2. **Durable Objects + Polling (Better)**
590
+ ```typescript
591
+ // Durable Object polls once, broadcasts to many clients
592
+ export class FirestoreSync {
593
+ async poll() {
594
+ const doc = await getDocument('users', 'user123');
595
+ // Broadcast to all connected WebSocket clients
596
+ for (const ws of this.sessions) {
597
+ ws.send(JSON.stringify(doc));
598
+ }
599
+ }
600
+ }
601
+ ```
602
+
603
+ **Pros**: One poll serves many clients, WebSocket push to clients
604
+ **Cons**: Still polling-based, Cloudflare-specific
605
+
606
+ #### 3. **Hybrid Architecture (Best)**
607
+ ```typescript
608
+ // Use firebase-admin-node for realtime in Node.js
609
+ import admin from 'firebase-admin';
610
+
611
+ admin.firestore().collection('users').doc('user123')
612
+ .onSnapshot((snapshot) => {
613
+ // True realtime updates
614
+ console.log('Update:', snapshot.data());
615
+ });
616
+
617
+ // Use this library for CRUD in edge functions
618
+ import { getDocument } from '@prmichaelsen/firebase-admin-sdk-v8';
619
+ const doc = await getDocument('users', 'user123');
620
+ ```
621
+
622
+ **Pros**: True realtime where needed, edge performance for CRUD
623
+ **Cons**: Requires separate Node.js service
624
+
625
+ #### 4. **Firebase Client SDK (Frontend)**
626
+ ```typescript
627
+ // Use Firebase Client SDK in browser/mobile
628
+ import { onSnapshot, doc } from 'firebase/firestore';
629
+
630
+ onSnapshot(doc(db, 'users', 'user123'), (snapshot) => {
631
+ console.log('Update:', snapshot.data());
632
+ });
633
+ ```
634
+
635
+ **Pros**: True realtime, built-in, well-supported
636
+ **Cons**: Client-side only, requires Firebase Auth
637
+
638
+ ### Recommendation
639
+
640
+ - **For edge runtimes**: Use polling or Durable Objects pattern
641
+ - **For true realtime**: Use `firebase-admin-node` in Node.js
642
+ - **For client apps**: Use Firebase Client SDK
643
+ - **For hybrid**: Use this library for CRUD + Node.js for realtime
644
+
645
+ ### Related
646
+
647
+ - [firebase-admin-node](https://github.com/firebase/firebase-admin-node) - Full Admin SDK with realtime support
648
+ - [Firebase Client SDK](https://firebase.google.com/docs/firestore/query-data/listen) - Client-side realtime listeners
649
+
529
650
  ## 🗺️ Roadmap
530
651
 
531
652
  - [ ] Custom token creation
package/dist/index.d.mts CHANGED
@@ -425,6 +425,189 @@ declare function queryDocuments(collectionPath: string, options?: QueryOptions):
425
425
  */
426
426
  declare function batchWrite(operations: BatchWrite[]): Promise<BatchWriteResult>;
427
427
 
428
+ /**
429
+ * Firebase Storage client using Google Cloud Storage REST API
430
+ * Compatible with edge runtimes (Cloudflare Workers, Vercel Edge, etc.)
431
+ */
432
+ /**
433
+ * Options for uploading files
434
+ */
435
+ interface UploadOptions {
436
+ contentType?: string;
437
+ metadata?: Record<string, string>;
438
+ public?: boolean;
439
+ }
440
+ /**
441
+ * Options for downloading files
442
+ */
443
+ interface DownloadOptions {
444
+ responseType?: 'arraybuffer' | 'blob';
445
+ }
446
+ /**
447
+ * Options for listing files
448
+ */
449
+ interface ListOptions {
450
+ prefix?: string;
451
+ delimiter?: string;
452
+ maxResults?: number;
453
+ pageToken?: string;
454
+ }
455
+ /**
456
+ * File metadata returned by Storage API
457
+ */
458
+ interface FileMetadata {
459
+ name: string;
460
+ bucket: string;
461
+ size: string;
462
+ contentType: string;
463
+ timeCreated: string;
464
+ updated: string;
465
+ md5Hash: string;
466
+ metadata?: Record<string, string>;
467
+ }
468
+ /**
469
+ * Result of listing files
470
+ */
471
+ interface ListFilesResult {
472
+ files: FileMetadata[];
473
+ nextPageToken?: string;
474
+ }
475
+ /**
476
+ * Upload a file to Firebase Storage
477
+ *
478
+ * @param path - File path in storage (e.g., 'images/photo.jpg')
479
+ * @param data - File data as ArrayBuffer, Uint8Array, or Blob
480
+ * @param options - Upload options (contentType, metadata, public)
481
+ * @returns File metadata
482
+ *
483
+ * @example
484
+ * ```typescript
485
+ * const data = new TextEncoder().encode('Hello, Storage!');
486
+ * const metadata = await uploadFile('files/hello.txt', data, {
487
+ * contentType: 'text/plain',
488
+ * metadata: { userId: '123' },
489
+ * });
490
+ * ```
491
+ */
492
+ declare function uploadFile(path: string, data: ArrayBuffer | Uint8Array | Blob, options?: UploadOptions): Promise<FileMetadata>;
493
+ /**
494
+ * Download a file from Firebase Storage
495
+ *
496
+ * @param path - File path in storage
497
+ * @param options - Download options
498
+ * @returns File data as ArrayBuffer
499
+ *
500
+ * @example
501
+ * ```typescript
502
+ * const data = await downloadFile('files/hello.txt');
503
+ * const text = new TextDecoder().decode(data);
504
+ * console.log(text); // "Hello, Storage!"
505
+ * ```
506
+ */
507
+ declare function downloadFile(path: string, _options?: DownloadOptions): Promise<ArrayBuffer>;
508
+ /**
509
+ * Delete a file from Firebase Storage
510
+ *
511
+ * @param path - File path in storage
512
+ *
513
+ * @example
514
+ * ```typescript
515
+ * await deleteFile('files/hello.txt');
516
+ * ```
517
+ */
518
+ declare function deleteFile(path: string): Promise<void>;
519
+ /**
520
+ * Get file metadata from Firebase Storage
521
+ *
522
+ * @param path - File path in storage
523
+ * @returns File metadata
524
+ *
525
+ * @example
526
+ * ```typescript
527
+ * const metadata = await getFileMetadata('files/hello.txt');
528
+ * console.log(metadata.size, metadata.contentType);
529
+ * ```
530
+ */
531
+ declare function getFileMetadata(path: string): Promise<FileMetadata>;
532
+ /**
533
+ * List files in Firebase Storage
534
+ *
535
+ * @param options - List options (prefix, delimiter, maxResults, pageToken)
536
+ * @returns List of files and optional next page token
537
+ *
538
+ * @example
539
+ * ```typescript
540
+ * // List all files with prefix
541
+ * const { files } = await listFiles({ prefix: 'images/' });
542
+ *
543
+ * // List with pagination
544
+ * const { files, nextPageToken } = await listFiles({ maxResults: 10 });
545
+ * if (nextPageToken) {
546
+ * const nextPage = await listFiles({ pageToken: nextPageToken });
547
+ * }
548
+ * ```
549
+ */
550
+ declare function listFiles(options?: ListOptions): Promise<ListFilesResult>;
551
+ /**
552
+ * Check if a file exists in Firebase Storage
553
+ *
554
+ * @param path - File path in storage
555
+ * @returns True if file exists, false otherwise
556
+ *
557
+ * @example
558
+ * ```typescript
559
+ * if (await fileExists('files/hello.txt')) {
560
+ * console.log('File exists!');
561
+ * }
562
+ * ```
563
+ */
564
+ declare function fileExists(path: string): Promise<boolean>;
565
+
566
+ /**
567
+ * Generate signed URLs for temporary access to Firebase Storage files
568
+ * Uses Google Cloud Storage V4 signing process
569
+ */
570
+ /**
571
+ * Options for generating signed URLs
572
+ */
573
+ interface SignedUrlOptions {
574
+ action: 'read' | 'write' | 'delete';
575
+ expires: Date | number;
576
+ contentType?: string;
577
+ responseDisposition?: string;
578
+ responseType?: string;
579
+ }
580
+ /**
581
+ * Generate a signed URL for temporary access to a Storage file
582
+ *
583
+ * Uses Google Cloud Storage V4 signing process
584
+ *
585
+ * @param path - File path in storage (e.g., 'images/photo.jpg')
586
+ * @param options - Signed URL options
587
+ * @returns Signed URL that can be used without authentication
588
+ *
589
+ * @example
590
+ * ```typescript
591
+ * // Generate read URL valid for 1 hour
592
+ * const url = await generateSignedUrl('files/hello.txt', {
593
+ * action: 'read',
594
+ * expires: 3600,
595
+ * });
596
+ *
597
+ * // Generate write URL with content type
598
+ * const uploadUrl = await generateSignedUrl('files/upload.jpg', {
599
+ * action: 'write',
600
+ * expires: 1800, // 30 minutes
601
+ * contentType: 'image/jpeg',
602
+ * });
603
+ *
604
+ * // Use the URL (no auth needed)
605
+ * const response = await fetch(url);
606
+ * const data = await response.arrayBuffer();
607
+ * ```
608
+ */
609
+ declare function generateSignedUrl(path: string, options: SignedUrlOptions): Promise<string>;
610
+
428
611
  /**
429
612
  * Firebase Admin SDK v8 - Field Value Helpers
430
613
  * Special field values for Firestore operations
@@ -522,4 +705,4 @@ declare function getAdminAccessToken(): Promise<string>;
522
705
  */
523
706
  declare function clearTokenCache(): void;
524
707
 
525
- export { type BatchWrite, type BatchWriteResult, type DataObject, type DecodedIdToken, type DocumentReference, FieldValue, type FieldValue$1 as FieldValueSentinel, FieldValueType, type FirestoreDocument, type FirestoreValue, type QueryFilter, type QueryOptions, type QueryOrder, type ServiceAccount, type SetOptions, type TokenResponse, type UpdateOptions, type UserInfo, type WhereFilterOp, addDocument, batchWrite, clearConfig, clearTokenCache, deleteDocument, getAdminAccessToken, getAuth, getConfig, getDocument, getProjectId, getServiceAccount, getUserFromToken, initializeApp, queryDocuments, setDocument, updateDocument, verifyIdToken };
708
+ export { type BatchWrite, type BatchWriteResult, 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 ServiceAccount, type SetOptions, type SignedUrlOptions, type TokenResponse, type UpdateOptions, type UploadOptions, type UserInfo, type WhereFilterOp, addDocument, batchWrite, clearConfig, clearTokenCache, deleteDocument, deleteFile, downloadFile, fileExists, generateSignedUrl, getAdminAccessToken, getAuth, getConfig, getDocument, getFileMetadata, getProjectId, getServiceAccount, getUserFromToken, initializeApp, listFiles, queryDocuments, setDocument, updateDocument, uploadFile, verifyIdToken };
package/dist/index.d.ts CHANGED
@@ -425,6 +425,189 @@ declare function queryDocuments(collectionPath: string, options?: QueryOptions):
425
425
  */
426
426
  declare function batchWrite(operations: BatchWrite[]): Promise<BatchWriteResult>;
427
427
 
428
+ /**
429
+ * Firebase Storage client using Google Cloud Storage REST API
430
+ * Compatible with edge runtimes (Cloudflare Workers, Vercel Edge, etc.)
431
+ */
432
+ /**
433
+ * Options for uploading files
434
+ */
435
+ interface UploadOptions {
436
+ contentType?: string;
437
+ metadata?: Record<string, string>;
438
+ public?: boolean;
439
+ }
440
+ /**
441
+ * Options for downloading files
442
+ */
443
+ interface DownloadOptions {
444
+ responseType?: 'arraybuffer' | 'blob';
445
+ }
446
+ /**
447
+ * Options for listing files
448
+ */
449
+ interface ListOptions {
450
+ prefix?: string;
451
+ delimiter?: string;
452
+ maxResults?: number;
453
+ pageToken?: string;
454
+ }
455
+ /**
456
+ * File metadata returned by Storage API
457
+ */
458
+ interface FileMetadata {
459
+ name: string;
460
+ bucket: string;
461
+ size: string;
462
+ contentType: string;
463
+ timeCreated: string;
464
+ updated: string;
465
+ md5Hash: string;
466
+ metadata?: Record<string, string>;
467
+ }
468
+ /**
469
+ * Result of listing files
470
+ */
471
+ interface ListFilesResult {
472
+ files: FileMetadata[];
473
+ nextPageToken?: string;
474
+ }
475
+ /**
476
+ * Upload a file to Firebase Storage
477
+ *
478
+ * @param path - File path in storage (e.g., 'images/photo.jpg')
479
+ * @param data - File data as ArrayBuffer, Uint8Array, or Blob
480
+ * @param options - Upload options (contentType, metadata, public)
481
+ * @returns File metadata
482
+ *
483
+ * @example
484
+ * ```typescript
485
+ * const data = new TextEncoder().encode('Hello, Storage!');
486
+ * const metadata = await uploadFile('files/hello.txt', data, {
487
+ * contentType: 'text/plain',
488
+ * metadata: { userId: '123' },
489
+ * });
490
+ * ```
491
+ */
492
+ declare function uploadFile(path: string, data: ArrayBuffer | Uint8Array | Blob, options?: UploadOptions): Promise<FileMetadata>;
493
+ /**
494
+ * Download a file from Firebase Storage
495
+ *
496
+ * @param path - File path in storage
497
+ * @param options - Download options
498
+ * @returns File data as ArrayBuffer
499
+ *
500
+ * @example
501
+ * ```typescript
502
+ * const data = await downloadFile('files/hello.txt');
503
+ * const text = new TextDecoder().decode(data);
504
+ * console.log(text); // "Hello, Storage!"
505
+ * ```
506
+ */
507
+ declare function downloadFile(path: string, _options?: DownloadOptions): Promise<ArrayBuffer>;
508
+ /**
509
+ * Delete a file from Firebase Storage
510
+ *
511
+ * @param path - File path in storage
512
+ *
513
+ * @example
514
+ * ```typescript
515
+ * await deleteFile('files/hello.txt');
516
+ * ```
517
+ */
518
+ declare function deleteFile(path: string): Promise<void>;
519
+ /**
520
+ * Get file metadata from Firebase Storage
521
+ *
522
+ * @param path - File path in storage
523
+ * @returns File metadata
524
+ *
525
+ * @example
526
+ * ```typescript
527
+ * const metadata = await getFileMetadata('files/hello.txt');
528
+ * console.log(metadata.size, metadata.contentType);
529
+ * ```
530
+ */
531
+ declare function getFileMetadata(path: string): Promise<FileMetadata>;
532
+ /**
533
+ * List files in Firebase Storage
534
+ *
535
+ * @param options - List options (prefix, delimiter, maxResults, pageToken)
536
+ * @returns List of files and optional next page token
537
+ *
538
+ * @example
539
+ * ```typescript
540
+ * // List all files with prefix
541
+ * const { files } = await listFiles({ prefix: 'images/' });
542
+ *
543
+ * // List with pagination
544
+ * const { files, nextPageToken } = await listFiles({ maxResults: 10 });
545
+ * if (nextPageToken) {
546
+ * const nextPage = await listFiles({ pageToken: nextPageToken });
547
+ * }
548
+ * ```
549
+ */
550
+ declare function listFiles(options?: ListOptions): Promise<ListFilesResult>;
551
+ /**
552
+ * Check if a file exists in Firebase Storage
553
+ *
554
+ * @param path - File path in storage
555
+ * @returns True if file exists, false otherwise
556
+ *
557
+ * @example
558
+ * ```typescript
559
+ * if (await fileExists('files/hello.txt')) {
560
+ * console.log('File exists!');
561
+ * }
562
+ * ```
563
+ */
564
+ declare function fileExists(path: string): Promise<boolean>;
565
+
566
+ /**
567
+ * Generate signed URLs for temporary access to Firebase Storage files
568
+ * Uses Google Cloud Storage V4 signing process
569
+ */
570
+ /**
571
+ * Options for generating signed URLs
572
+ */
573
+ interface SignedUrlOptions {
574
+ action: 'read' | 'write' | 'delete';
575
+ expires: Date | number;
576
+ contentType?: string;
577
+ responseDisposition?: string;
578
+ responseType?: string;
579
+ }
580
+ /**
581
+ * Generate a signed URL for temporary access to a Storage file
582
+ *
583
+ * Uses Google Cloud Storage V4 signing process
584
+ *
585
+ * @param path - File path in storage (e.g., 'images/photo.jpg')
586
+ * @param options - Signed URL options
587
+ * @returns Signed URL that can be used without authentication
588
+ *
589
+ * @example
590
+ * ```typescript
591
+ * // Generate read URL valid for 1 hour
592
+ * const url = await generateSignedUrl('files/hello.txt', {
593
+ * action: 'read',
594
+ * expires: 3600,
595
+ * });
596
+ *
597
+ * // Generate write URL with content type
598
+ * const uploadUrl = await generateSignedUrl('files/upload.jpg', {
599
+ * action: 'write',
600
+ * expires: 1800, // 30 minutes
601
+ * contentType: 'image/jpeg',
602
+ * });
603
+ *
604
+ * // Use the URL (no auth needed)
605
+ * const response = await fetch(url);
606
+ * const data = await response.arrayBuffer();
607
+ * ```
608
+ */
609
+ declare function generateSignedUrl(path: string, options: SignedUrlOptions): Promise<string>;
610
+
428
611
  /**
429
612
  * Firebase Admin SDK v8 - Field Value Helpers
430
613
  * Special field values for Firestore operations
@@ -522,4 +705,4 @@ declare function getAdminAccessToken(): Promise<string>;
522
705
  */
523
706
  declare function clearTokenCache(): void;
524
707
 
525
- export { type BatchWrite, type BatchWriteResult, type DataObject, type DecodedIdToken, type DocumentReference, FieldValue, type FieldValue$1 as FieldValueSentinel, FieldValueType, type FirestoreDocument, type FirestoreValue, type QueryFilter, type QueryOptions, type QueryOrder, type ServiceAccount, type SetOptions, type TokenResponse, type UpdateOptions, type UserInfo, type WhereFilterOp, addDocument, batchWrite, clearConfig, clearTokenCache, deleteDocument, getAdminAccessToken, getAuth, getConfig, getDocument, getProjectId, getServiceAccount, getUserFromToken, initializeApp, queryDocuments, setDocument, updateDocument, verifyIdToken };
708
+ export { type BatchWrite, type BatchWriteResult, 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 ServiceAccount, type SetOptions, type SignedUrlOptions, type TokenResponse, type UpdateOptions, type UploadOptions, type UserInfo, type WhereFilterOp, addDocument, batchWrite, clearConfig, clearTokenCache, deleteDocument, deleteFile, downloadFile, fileExists, generateSignedUrl, getAdminAccessToken, getAuth, getConfig, getDocument, getFileMetadata, getProjectId, getServiceAccount, getUserFromToken, initializeApp, listFiles, queryDocuments, setDocument, updateDocument, uploadFile, verifyIdToken };
package/dist/index.js CHANGED
@@ -26,17 +26,24 @@ __export(index_exports, {
26
26
  clearConfig: () => clearConfig,
27
27
  clearTokenCache: () => clearTokenCache,
28
28
  deleteDocument: () => deleteDocument,
29
+ deleteFile: () => deleteFile,
30
+ downloadFile: () => downloadFile,
31
+ fileExists: () => fileExists,
32
+ generateSignedUrl: () => generateSignedUrl,
29
33
  getAdminAccessToken: () => getAdminAccessToken,
30
34
  getAuth: () => getAuth,
31
35
  getConfig: () => getConfig,
32
36
  getDocument: () => getDocument,
37
+ getFileMetadata: () => getFileMetadata,
33
38
  getProjectId: () => getProjectId,
34
39
  getServiceAccount: () => getServiceAccount,
35
40
  getUserFromToken: () => getUserFromToken,
36
41
  initializeApp: () => initializeApp,
42
+ listFiles: () => listFiles,
37
43
  queryDocuments: () => queryDocuments,
38
44
  setDocument: () => setDocument,
39
45
  updateDocument: () => updateDocument,
46
+ uploadFile: () => uploadFile,
40
47
  verifyIdToken: () => verifyIdToken
41
48
  });
42
49
  module.exports = __toCommonJS(index_exports);
@@ -679,6 +686,26 @@ function clearTokenCache() {
679
686
  tokenExpiry = 0;
680
687
  }
681
688
 
689
+ // src/firestore/path-validation.ts
690
+ function validateCollectionPath(argumentName, collectionPath) {
691
+ const segments = collectionPath.split("/").filter((s) => s.length > 0);
692
+ if (segments.length % 2 === 0) {
693
+ throw new Error(
694
+ `Value for argument "${argumentName}" must point to a collection, but was "${collectionPath}". Your path does not contain an odd number of components.`
695
+ );
696
+ }
697
+ }
698
+ function validateDocumentPath(argumentName, collectionPath, documentId) {
699
+ const collectionSegments = collectionPath.split("/").filter((s) => s.length > 0);
700
+ const totalSegments = collectionSegments.length + 1;
701
+ if (totalSegments % 2 !== 0) {
702
+ const fullPath = `${collectionPath}/${documentId}`;
703
+ throw new Error(
704
+ `Value for argument "${argumentName}" must point to a document, but was "${fullPath}". Your path does not contain an even number of components.`
705
+ );
706
+ }
707
+ }
708
+
682
709
  // src/firestore/operations.ts
683
710
  var FIRESTORE_API = "https://firestore.googleapis.com/v1";
684
711
  async function commitWrites(writes) {
@@ -699,6 +726,7 @@ async function commitWrites(writes) {
699
726
  }
700
727
  }
701
728
  async function setDocument(collectionPath, documentId, data, options) {
729
+ validateDocumentPath("collectionPath", collectionPath, documentId);
702
730
  const projectId = getProjectId();
703
731
  const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
704
732
  const cleanData = removeFieldTransforms(data);
@@ -748,6 +776,7 @@ async function setDocument(collectionPath, documentId, data, options) {
748
776
  }
749
777
  }
750
778
  async function addDocument(collectionPath, data, documentId) {
779
+ validateCollectionPath("collectionPath", collectionPath);
751
780
  const accessToken = await getAdminAccessToken();
752
781
  const projectId = getProjectId();
753
782
  const baseUrl = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}`;
@@ -779,6 +808,7 @@ async function addDocument(collectionPath, data, documentId) {
779
808
  };
780
809
  }
781
810
  async function getDocument(collectionPath, documentId) {
811
+ validateDocumentPath("collectionPath", collectionPath, documentId);
782
812
  const accessToken = await getAdminAccessToken();
783
813
  const projectId = getProjectId();
784
814
  const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
@@ -798,6 +828,7 @@ async function getDocument(collectionPath, documentId) {
798
828
  return convertFromFirestoreFormat(result.fields);
799
829
  }
800
830
  async function updateDocument(collectionPath, documentId, data) {
831
+ validateDocumentPath("collectionPath", collectionPath, documentId);
801
832
  const projectId = getProjectId();
802
833
  const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
803
834
  const cleanData = removeFieldTransforms(data);
@@ -850,6 +881,7 @@ async function updateDocument(collectionPath, documentId, data) {
850
881
  }
851
882
  }
852
883
  async function deleteDocument(collectionPath, documentId) {
884
+ validateDocumentPath("collectionPath", collectionPath, documentId);
853
885
  const accessToken = await getAdminAccessToken();
854
886
  const projectId = getProjectId();
855
887
  const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
@@ -865,6 +897,7 @@ async function deleteDocument(collectionPath, documentId) {
865
897
  }
866
898
  }
867
899
  async function queryDocuments(collectionPath, options) {
900
+ validateCollectionPath("collectionPath", collectionPath);
868
901
  const accessToken = await getAdminAccessToken();
869
902
  const projectId = getProjectId();
870
903
  if (!options || Object.keys(options).length === 0) {
@@ -981,6 +1014,299 @@ async function batchWrite(operations) {
981
1014
  }
982
1015
  return await response.json();
983
1016
  }
1017
+
1018
+ // src/storage/client.ts
1019
+ var STORAGE_API_BASE = "https://storage.googleapis.com/storage/v1";
1020
+ var UPLOAD_API_BASE = "https://storage.googleapis.com/upload/storage/v1";
1021
+ function getDefaultBucket() {
1022
+ const projectId = getProjectId();
1023
+ return `${projectId}.appspot.com`;
1024
+ }
1025
+ function detectContentType(filename) {
1026
+ const ext = filename.split(".").pop()?.toLowerCase();
1027
+ const mimeTypes = {
1028
+ "txt": "text/plain",
1029
+ "html": "text/html",
1030
+ "htm": "text/html",
1031
+ "css": "text/css",
1032
+ "js": "application/javascript",
1033
+ "json": "application/json",
1034
+ "xml": "application/xml",
1035
+ "jpg": "image/jpeg",
1036
+ "jpeg": "image/jpeg",
1037
+ "png": "image/png",
1038
+ "gif": "image/gif",
1039
+ "svg": "image/svg+xml",
1040
+ "webp": "image/webp",
1041
+ "pdf": "application/pdf",
1042
+ "zip": "application/zip",
1043
+ "mp4": "video/mp4",
1044
+ "mp3": "audio/mpeg",
1045
+ "wav": "audio/wav"
1046
+ };
1047
+ return mimeTypes[ext || ""] || "application/octet-stream";
1048
+ }
1049
+ async function uploadFile(path, data, options = {}) {
1050
+ const token = await getAdminAccessToken();
1051
+ const bucket = getDefaultBucket();
1052
+ const contentType = options.contentType || detectContentType(path);
1053
+ const url = `${UPLOAD_API_BASE}/b/${encodeURIComponent(bucket)}/o?uploadType=media&name=${encodeURIComponent(path)}`;
1054
+ let body;
1055
+ if (data instanceof Blob) {
1056
+ body = await data.arrayBuffer();
1057
+ } else if (data instanceof Uint8Array) {
1058
+ const buffer = new ArrayBuffer(data.byteLength);
1059
+ new Uint8Array(buffer).set(data);
1060
+ body = buffer;
1061
+ } else {
1062
+ body = data;
1063
+ }
1064
+ const response = await fetch(url, {
1065
+ method: "POST",
1066
+ headers: {
1067
+ "Authorization": `Bearer ${token}`,
1068
+ "Content-Type": contentType,
1069
+ "Content-Length": body.byteLength.toString()
1070
+ },
1071
+ body
1072
+ });
1073
+ if (!response.ok) {
1074
+ const errorText = await response.text();
1075
+ throw new Error(`Failed to upload file: ${response.status} ${errorText}`);
1076
+ }
1077
+ const result = await response.json();
1078
+ if (options.public) {
1079
+ await makeFilePublic(path);
1080
+ }
1081
+ if (options.metadata) {
1082
+ return await updateFileMetadata(path, options.metadata);
1083
+ }
1084
+ return result;
1085
+ }
1086
+ async function downloadFile(path, _options = {}) {
1087
+ const token = await getAdminAccessToken();
1088
+ const bucket = getDefaultBucket();
1089
+ const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}?alt=media`;
1090
+ const response = await fetch(url, {
1091
+ method: "GET",
1092
+ headers: {
1093
+ "Authorization": `Bearer ${token}`
1094
+ }
1095
+ });
1096
+ if (!response.ok) {
1097
+ const errorText = await response.text();
1098
+ throw new Error(`Failed to download file: ${response.status} ${errorText}`);
1099
+ }
1100
+ return await response.arrayBuffer();
1101
+ }
1102
+ async function deleteFile(path) {
1103
+ const token = await getAdminAccessToken();
1104
+ const bucket = getDefaultBucket();
1105
+ const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}`;
1106
+ const response = await fetch(url, {
1107
+ method: "DELETE",
1108
+ headers: {
1109
+ "Authorization": `Bearer ${token}`
1110
+ }
1111
+ });
1112
+ if (!response.ok) {
1113
+ const errorText = await response.text();
1114
+ throw new Error(`Failed to delete file: ${response.status} ${errorText}`);
1115
+ }
1116
+ }
1117
+ async function getFileMetadata(path) {
1118
+ const token = await getAdminAccessToken();
1119
+ const bucket = getDefaultBucket();
1120
+ const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}`;
1121
+ const response = await fetch(url, {
1122
+ method: "GET",
1123
+ headers: {
1124
+ "Authorization": `Bearer ${token}`
1125
+ }
1126
+ });
1127
+ if (!response.ok) {
1128
+ const errorText = await response.text();
1129
+ throw new Error(`Failed to get file metadata: ${response.status} ${errorText}`);
1130
+ }
1131
+ return await response.json();
1132
+ }
1133
+ async function updateFileMetadata(path, metadata) {
1134
+ const token = await getAdminAccessToken();
1135
+ const bucket = getDefaultBucket();
1136
+ const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}`;
1137
+ const response = await fetch(url, {
1138
+ method: "PATCH",
1139
+ headers: {
1140
+ "Authorization": `Bearer ${token}`,
1141
+ "Content-Type": "application/json"
1142
+ },
1143
+ body: JSON.stringify({ metadata })
1144
+ });
1145
+ if (!response.ok) {
1146
+ const errorText = await response.text();
1147
+ throw new Error(`Failed to update file metadata: ${response.status} ${errorText}`);
1148
+ }
1149
+ return await response.json();
1150
+ }
1151
+ async function makeFilePublic(path) {
1152
+ const token = await getAdminAccessToken();
1153
+ const bucket = getDefaultBucket();
1154
+ const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}/acl`;
1155
+ const response = await fetch(url, {
1156
+ method: "POST",
1157
+ headers: {
1158
+ "Authorization": `Bearer ${token}`,
1159
+ "Content-Type": "application/json"
1160
+ },
1161
+ body: JSON.stringify({
1162
+ entity: "allUsers",
1163
+ role: "READER"
1164
+ })
1165
+ });
1166
+ if (!response.ok) {
1167
+ const errorText = await response.text();
1168
+ throw new Error(`Failed to make file public: ${response.status} ${errorText}`);
1169
+ }
1170
+ }
1171
+ async function listFiles(options = {}) {
1172
+ const token = await getAdminAccessToken();
1173
+ const bucket = getDefaultBucket();
1174
+ const params = new URLSearchParams();
1175
+ if (options.prefix) params.append("prefix", options.prefix);
1176
+ if (options.delimiter) params.append("delimiter", options.delimiter);
1177
+ if (options.maxResults) params.append("maxResults", options.maxResults.toString());
1178
+ if (options.pageToken) params.append("pageToken", options.pageToken);
1179
+ const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o?${params.toString()}`;
1180
+ const response = await fetch(url, {
1181
+ method: "GET",
1182
+ headers: {
1183
+ "Authorization": `Bearer ${token}`
1184
+ }
1185
+ });
1186
+ if (!response.ok) {
1187
+ const errorText = await response.text();
1188
+ throw new Error(`Failed to list files: ${response.status} ${errorText}`);
1189
+ }
1190
+ const result = await response.json();
1191
+ return {
1192
+ files: result.items || [],
1193
+ nextPageToken: result.nextPageToken
1194
+ };
1195
+ }
1196
+ async function fileExists(path) {
1197
+ try {
1198
+ await getFileMetadata(path);
1199
+ return true;
1200
+ } catch (error) {
1201
+ if (error instanceof Error && error.message.includes("404")) {
1202
+ return false;
1203
+ }
1204
+ throw error;
1205
+ }
1206
+ }
1207
+
1208
+ // src/storage/signed-urls.ts
1209
+ function getExpirationTimestamp(expires) {
1210
+ if (expires instanceof Date) {
1211
+ return Math.floor(expires.getTime() / 1e3);
1212
+ }
1213
+ return Math.floor(Date.now() / 1e3) + expires;
1214
+ }
1215
+ function actionToMethod(action) {
1216
+ switch (action) {
1217
+ case "read":
1218
+ return "GET";
1219
+ case "write":
1220
+ return "PUT";
1221
+ case "delete":
1222
+ return "DELETE";
1223
+ }
1224
+ }
1225
+ function stringToHex(str) {
1226
+ const encoder = new TextEncoder();
1227
+ const bytes = encoder.encode(str);
1228
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1229
+ }
1230
+ async function signData(data, privateKey) {
1231
+ const pemHeader = "-----BEGIN PRIVATE KEY-----";
1232
+ const pemFooter = "-----END PRIVATE KEY-----";
1233
+ const pemContents = privateKey.replace(pemHeader, "").replace(pemFooter, "").replace(/\s/g, "");
1234
+ const binaryString = atob(pemContents);
1235
+ const bytes = new Uint8Array(binaryString.length);
1236
+ for (let i = 0; i < binaryString.length; i++) {
1237
+ bytes[i] = binaryString.charCodeAt(i);
1238
+ }
1239
+ const key = await crypto.subtle.importKey(
1240
+ "pkcs8",
1241
+ bytes,
1242
+ {
1243
+ name: "RSASSA-PKCS1-v1_5",
1244
+ hash: "SHA-256"
1245
+ },
1246
+ false,
1247
+ ["sign"]
1248
+ );
1249
+ const encoder = new TextEncoder();
1250
+ const dataBytes = encoder.encode(data);
1251
+ const signature = await crypto.subtle.sign(
1252
+ "RSASSA-PKCS1-v1_5",
1253
+ key,
1254
+ dataBytes
1255
+ );
1256
+ const signatureArray = new Uint8Array(signature);
1257
+ return Array.from(signatureArray).map((b) => b.toString(16).padStart(2, "0")).join("");
1258
+ }
1259
+ async function generateSignedUrl(path, options) {
1260
+ const serviceAccount = getServiceAccount();
1261
+ const projectId = getProjectId();
1262
+ const bucket = `${projectId}.appspot.com`;
1263
+ const method = actionToMethod(options.action);
1264
+ const expiration = getExpirationTimestamp(options.expires);
1265
+ const timestamp = Math.floor(Date.now() / 1e3);
1266
+ const datestamp = new Date(timestamp * 1e3).toISOString().split("T")[0].replace(/-/g, "");
1267
+ const credentialScope = `${datestamp}/auto/storage/goog4_request`;
1268
+ const credential = `${serviceAccount.client_email}/${credentialScope}`;
1269
+ const canonicalHeaders = `host:storage.googleapis.com
1270
+ `;
1271
+ const signedHeaders = "host";
1272
+ const queryParams = {
1273
+ "X-Goog-Algorithm": "GOOG4-RSA-SHA256",
1274
+ "X-Goog-Credential": credential,
1275
+ "X-Goog-Date": `${datestamp}T000000Z`,
1276
+ "X-Goog-Expires": (expiration - timestamp).toString(),
1277
+ "X-Goog-SignedHeaders": signedHeaders
1278
+ };
1279
+ if (options.contentType) {
1280
+ queryParams["response-content-type"] = options.contentType;
1281
+ }
1282
+ if (options.responseDisposition) {
1283
+ queryParams["response-content-disposition"] = options.responseDisposition;
1284
+ }
1285
+ if (options.responseType) {
1286
+ queryParams["response-content-type"] = options.responseType;
1287
+ }
1288
+ const sortedParams = Object.keys(queryParams).sort();
1289
+ const canonicalQueryString = sortedParams.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`).join("&");
1290
+ const canonicalUri = `/${bucket}/${path}`;
1291
+ const canonicalRequest = [
1292
+ method,
1293
+ canonicalUri,
1294
+ canonicalQueryString,
1295
+ canonicalHeaders,
1296
+ signedHeaders,
1297
+ "UNSIGNED-PAYLOAD"
1298
+ ].join("\n");
1299
+ const canonicalRequestHash = stringToHex(canonicalRequest);
1300
+ const stringToSign = [
1301
+ "GOOG4-RSA-SHA256",
1302
+ `${datestamp}T000000Z`,
1303
+ credentialScope,
1304
+ canonicalRequestHash
1305
+ ].join("\n");
1306
+ const signature = await signData(stringToSign, serviceAccount.private_key);
1307
+ const signedUrl = `https://storage.googleapis.com${canonicalUri}?${canonicalQueryString}&X-Goog-Signature=${signature}`;
1308
+ return signedUrl;
1309
+ }
984
1310
  // Annotate the CommonJS export names for ESM import in node:
985
1311
  0 && (module.exports = {
986
1312
  FieldValue,
@@ -989,16 +1315,23 @@ async function batchWrite(operations) {
989
1315
  clearConfig,
990
1316
  clearTokenCache,
991
1317
  deleteDocument,
1318
+ deleteFile,
1319
+ downloadFile,
1320
+ fileExists,
1321
+ generateSignedUrl,
992
1322
  getAdminAccessToken,
993
1323
  getAuth,
994
1324
  getConfig,
995
1325
  getDocument,
1326
+ getFileMetadata,
996
1327
  getProjectId,
997
1328
  getServiceAccount,
998
1329
  getUserFromToken,
999
1330
  initializeApp,
1331
+ listFiles,
1000
1332
  queryDocuments,
1001
1333
  setDocument,
1002
1334
  updateDocument,
1335
+ uploadFile,
1003
1336
  verifyIdToken
1004
1337
  });
package/dist/index.mjs CHANGED
@@ -636,6 +636,26 @@ function clearTokenCache() {
636
636
  tokenExpiry = 0;
637
637
  }
638
638
 
639
+ // src/firestore/path-validation.ts
640
+ function validateCollectionPath(argumentName, collectionPath) {
641
+ const segments = collectionPath.split("/").filter((s) => s.length > 0);
642
+ if (segments.length % 2 === 0) {
643
+ throw new Error(
644
+ `Value for argument "${argumentName}" must point to a collection, but was "${collectionPath}". Your path does not contain an odd number of components.`
645
+ );
646
+ }
647
+ }
648
+ function validateDocumentPath(argumentName, collectionPath, documentId) {
649
+ const collectionSegments = collectionPath.split("/").filter((s) => s.length > 0);
650
+ const totalSegments = collectionSegments.length + 1;
651
+ if (totalSegments % 2 !== 0) {
652
+ const fullPath = `${collectionPath}/${documentId}`;
653
+ throw new Error(
654
+ `Value for argument "${argumentName}" must point to a document, but was "${fullPath}". Your path does not contain an even number of components.`
655
+ );
656
+ }
657
+ }
658
+
639
659
  // src/firestore/operations.ts
640
660
  var FIRESTORE_API = "https://firestore.googleapis.com/v1";
641
661
  async function commitWrites(writes) {
@@ -656,6 +676,7 @@ async function commitWrites(writes) {
656
676
  }
657
677
  }
658
678
  async function setDocument(collectionPath, documentId, data, options) {
679
+ validateDocumentPath("collectionPath", collectionPath, documentId);
659
680
  const projectId = getProjectId();
660
681
  const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
661
682
  const cleanData = removeFieldTransforms(data);
@@ -705,6 +726,7 @@ async function setDocument(collectionPath, documentId, data, options) {
705
726
  }
706
727
  }
707
728
  async function addDocument(collectionPath, data, documentId) {
729
+ validateCollectionPath("collectionPath", collectionPath);
708
730
  const accessToken = await getAdminAccessToken();
709
731
  const projectId = getProjectId();
710
732
  const baseUrl = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}`;
@@ -736,6 +758,7 @@ async function addDocument(collectionPath, data, documentId) {
736
758
  };
737
759
  }
738
760
  async function getDocument(collectionPath, documentId) {
761
+ validateDocumentPath("collectionPath", collectionPath, documentId);
739
762
  const accessToken = await getAdminAccessToken();
740
763
  const projectId = getProjectId();
741
764
  const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
@@ -755,6 +778,7 @@ async function getDocument(collectionPath, documentId) {
755
778
  return convertFromFirestoreFormat(result.fields);
756
779
  }
757
780
  async function updateDocument(collectionPath, documentId, data) {
781
+ validateDocumentPath("collectionPath", collectionPath, documentId);
758
782
  const projectId = getProjectId();
759
783
  const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
760
784
  const cleanData = removeFieldTransforms(data);
@@ -807,6 +831,7 @@ async function updateDocument(collectionPath, documentId, data) {
807
831
  }
808
832
  }
809
833
  async function deleteDocument(collectionPath, documentId) {
834
+ validateDocumentPath("collectionPath", collectionPath, documentId);
810
835
  const accessToken = await getAdminAccessToken();
811
836
  const projectId = getProjectId();
812
837
  const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
@@ -822,6 +847,7 @@ async function deleteDocument(collectionPath, documentId) {
822
847
  }
823
848
  }
824
849
  async function queryDocuments(collectionPath, options) {
850
+ validateCollectionPath("collectionPath", collectionPath);
825
851
  const accessToken = await getAdminAccessToken();
826
852
  const projectId = getProjectId();
827
853
  if (!options || Object.keys(options).length === 0) {
@@ -938,6 +964,299 @@ async function batchWrite(operations) {
938
964
  }
939
965
  return await response.json();
940
966
  }
967
+
968
+ // src/storage/client.ts
969
+ var STORAGE_API_BASE = "https://storage.googleapis.com/storage/v1";
970
+ var UPLOAD_API_BASE = "https://storage.googleapis.com/upload/storage/v1";
971
+ function getDefaultBucket() {
972
+ const projectId = getProjectId();
973
+ return `${projectId}.appspot.com`;
974
+ }
975
+ function detectContentType(filename) {
976
+ const ext = filename.split(".").pop()?.toLowerCase();
977
+ const mimeTypes = {
978
+ "txt": "text/plain",
979
+ "html": "text/html",
980
+ "htm": "text/html",
981
+ "css": "text/css",
982
+ "js": "application/javascript",
983
+ "json": "application/json",
984
+ "xml": "application/xml",
985
+ "jpg": "image/jpeg",
986
+ "jpeg": "image/jpeg",
987
+ "png": "image/png",
988
+ "gif": "image/gif",
989
+ "svg": "image/svg+xml",
990
+ "webp": "image/webp",
991
+ "pdf": "application/pdf",
992
+ "zip": "application/zip",
993
+ "mp4": "video/mp4",
994
+ "mp3": "audio/mpeg",
995
+ "wav": "audio/wav"
996
+ };
997
+ return mimeTypes[ext || ""] || "application/octet-stream";
998
+ }
999
+ async function uploadFile(path, data, options = {}) {
1000
+ const token = await getAdminAccessToken();
1001
+ const bucket = getDefaultBucket();
1002
+ const contentType = options.contentType || detectContentType(path);
1003
+ const url = `${UPLOAD_API_BASE}/b/${encodeURIComponent(bucket)}/o?uploadType=media&name=${encodeURIComponent(path)}`;
1004
+ let body;
1005
+ if (data instanceof Blob) {
1006
+ body = await data.arrayBuffer();
1007
+ } else if (data instanceof Uint8Array) {
1008
+ const buffer = new ArrayBuffer(data.byteLength);
1009
+ new Uint8Array(buffer).set(data);
1010
+ body = buffer;
1011
+ } else {
1012
+ body = data;
1013
+ }
1014
+ const response = await fetch(url, {
1015
+ method: "POST",
1016
+ headers: {
1017
+ "Authorization": `Bearer ${token}`,
1018
+ "Content-Type": contentType,
1019
+ "Content-Length": body.byteLength.toString()
1020
+ },
1021
+ body
1022
+ });
1023
+ if (!response.ok) {
1024
+ const errorText = await response.text();
1025
+ throw new Error(`Failed to upload file: ${response.status} ${errorText}`);
1026
+ }
1027
+ const result = await response.json();
1028
+ if (options.public) {
1029
+ await makeFilePublic(path);
1030
+ }
1031
+ if (options.metadata) {
1032
+ return await updateFileMetadata(path, options.metadata);
1033
+ }
1034
+ return result;
1035
+ }
1036
+ async function downloadFile(path, _options = {}) {
1037
+ const token = await getAdminAccessToken();
1038
+ const bucket = getDefaultBucket();
1039
+ const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}?alt=media`;
1040
+ const response = await fetch(url, {
1041
+ method: "GET",
1042
+ headers: {
1043
+ "Authorization": `Bearer ${token}`
1044
+ }
1045
+ });
1046
+ if (!response.ok) {
1047
+ const errorText = await response.text();
1048
+ throw new Error(`Failed to download file: ${response.status} ${errorText}`);
1049
+ }
1050
+ return await response.arrayBuffer();
1051
+ }
1052
+ async function deleteFile(path) {
1053
+ const token = await getAdminAccessToken();
1054
+ const bucket = getDefaultBucket();
1055
+ const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}`;
1056
+ const response = await fetch(url, {
1057
+ method: "DELETE",
1058
+ headers: {
1059
+ "Authorization": `Bearer ${token}`
1060
+ }
1061
+ });
1062
+ if (!response.ok) {
1063
+ const errorText = await response.text();
1064
+ throw new Error(`Failed to delete file: ${response.status} ${errorText}`);
1065
+ }
1066
+ }
1067
+ async function getFileMetadata(path) {
1068
+ const token = await getAdminAccessToken();
1069
+ const bucket = getDefaultBucket();
1070
+ const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}`;
1071
+ const response = await fetch(url, {
1072
+ method: "GET",
1073
+ headers: {
1074
+ "Authorization": `Bearer ${token}`
1075
+ }
1076
+ });
1077
+ if (!response.ok) {
1078
+ const errorText = await response.text();
1079
+ throw new Error(`Failed to get file metadata: ${response.status} ${errorText}`);
1080
+ }
1081
+ return await response.json();
1082
+ }
1083
+ async function updateFileMetadata(path, metadata) {
1084
+ const token = await getAdminAccessToken();
1085
+ const bucket = getDefaultBucket();
1086
+ const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}`;
1087
+ const response = await fetch(url, {
1088
+ method: "PATCH",
1089
+ headers: {
1090
+ "Authorization": `Bearer ${token}`,
1091
+ "Content-Type": "application/json"
1092
+ },
1093
+ body: JSON.stringify({ metadata })
1094
+ });
1095
+ if (!response.ok) {
1096
+ const errorText = await response.text();
1097
+ throw new Error(`Failed to update file metadata: ${response.status} ${errorText}`);
1098
+ }
1099
+ return await response.json();
1100
+ }
1101
+ async function makeFilePublic(path) {
1102
+ const token = await getAdminAccessToken();
1103
+ const bucket = getDefaultBucket();
1104
+ const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o/${encodeURIComponent(path)}/acl`;
1105
+ const response = await fetch(url, {
1106
+ method: "POST",
1107
+ headers: {
1108
+ "Authorization": `Bearer ${token}`,
1109
+ "Content-Type": "application/json"
1110
+ },
1111
+ body: JSON.stringify({
1112
+ entity: "allUsers",
1113
+ role: "READER"
1114
+ })
1115
+ });
1116
+ if (!response.ok) {
1117
+ const errorText = await response.text();
1118
+ throw new Error(`Failed to make file public: ${response.status} ${errorText}`);
1119
+ }
1120
+ }
1121
+ async function listFiles(options = {}) {
1122
+ const token = await getAdminAccessToken();
1123
+ const bucket = getDefaultBucket();
1124
+ const params = new URLSearchParams();
1125
+ if (options.prefix) params.append("prefix", options.prefix);
1126
+ if (options.delimiter) params.append("delimiter", options.delimiter);
1127
+ if (options.maxResults) params.append("maxResults", options.maxResults.toString());
1128
+ if (options.pageToken) params.append("pageToken", options.pageToken);
1129
+ const url = `${STORAGE_API_BASE}/b/${encodeURIComponent(bucket)}/o?${params.toString()}`;
1130
+ const response = await fetch(url, {
1131
+ method: "GET",
1132
+ headers: {
1133
+ "Authorization": `Bearer ${token}`
1134
+ }
1135
+ });
1136
+ if (!response.ok) {
1137
+ const errorText = await response.text();
1138
+ throw new Error(`Failed to list files: ${response.status} ${errorText}`);
1139
+ }
1140
+ const result = await response.json();
1141
+ return {
1142
+ files: result.items || [],
1143
+ nextPageToken: result.nextPageToken
1144
+ };
1145
+ }
1146
+ async function fileExists(path) {
1147
+ try {
1148
+ await getFileMetadata(path);
1149
+ return true;
1150
+ } catch (error) {
1151
+ if (error instanceof Error && error.message.includes("404")) {
1152
+ return false;
1153
+ }
1154
+ throw error;
1155
+ }
1156
+ }
1157
+
1158
+ // src/storage/signed-urls.ts
1159
+ function getExpirationTimestamp(expires) {
1160
+ if (expires instanceof Date) {
1161
+ return Math.floor(expires.getTime() / 1e3);
1162
+ }
1163
+ return Math.floor(Date.now() / 1e3) + expires;
1164
+ }
1165
+ function actionToMethod(action) {
1166
+ switch (action) {
1167
+ case "read":
1168
+ return "GET";
1169
+ case "write":
1170
+ return "PUT";
1171
+ case "delete":
1172
+ return "DELETE";
1173
+ }
1174
+ }
1175
+ function stringToHex(str) {
1176
+ const encoder = new TextEncoder();
1177
+ const bytes = encoder.encode(str);
1178
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1179
+ }
1180
+ async function signData(data, privateKey) {
1181
+ const pemHeader = "-----BEGIN PRIVATE KEY-----";
1182
+ const pemFooter = "-----END PRIVATE KEY-----";
1183
+ const pemContents = privateKey.replace(pemHeader, "").replace(pemFooter, "").replace(/\s/g, "");
1184
+ const binaryString = atob(pemContents);
1185
+ const bytes = new Uint8Array(binaryString.length);
1186
+ for (let i = 0; i < binaryString.length; i++) {
1187
+ bytes[i] = binaryString.charCodeAt(i);
1188
+ }
1189
+ const key = await crypto.subtle.importKey(
1190
+ "pkcs8",
1191
+ bytes,
1192
+ {
1193
+ name: "RSASSA-PKCS1-v1_5",
1194
+ hash: "SHA-256"
1195
+ },
1196
+ false,
1197
+ ["sign"]
1198
+ );
1199
+ const encoder = new TextEncoder();
1200
+ const dataBytes = encoder.encode(data);
1201
+ const signature = await crypto.subtle.sign(
1202
+ "RSASSA-PKCS1-v1_5",
1203
+ key,
1204
+ dataBytes
1205
+ );
1206
+ const signatureArray = new Uint8Array(signature);
1207
+ return Array.from(signatureArray).map((b) => b.toString(16).padStart(2, "0")).join("");
1208
+ }
1209
+ async function generateSignedUrl(path, options) {
1210
+ const serviceAccount = getServiceAccount();
1211
+ const projectId = getProjectId();
1212
+ const bucket = `${projectId}.appspot.com`;
1213
+ const method = actionToMethod(options.action);
1214
+ const expiration = getExpirationTimestamp(options.expires);
1215
+ const timestamp = Math.floor(Date.now() / 1e3);
1216
+ const datestamp = new Date(timestamp * 1e3).toISOString().split("T")[0].replace(/-/g, "");
1217
+ const credentialScope = `${datestamp}/auto/storage/goog4_request`;
1218
+ const credential = `${serviceAccount.client_email}/${credentialScope}`;
1219
+ const canonicalHeaders = `host:storage.googleapis.com
1220
+ `;
1221
+ const signedHeaders = "host";
1222
+ const queryParams = {
1223
+ "X-Goog-Algorithm": "GOOG4-RSA-SHA256",
1224
+ "X-Goog-Credential": credential,
1225
+ "X-Goog-Date": `${datestamp}T000000Z`,
1226
+ "X-Goog-Expires": (expiration - timestamp).toString(),
1227
+ "X-Goog-SignedHeaders": signedHeaders
1228
+ };
1229
+ if (options.contentType) {
1230
+ queryParams["response-content-type"] = options.contentType;
1231
+ }
1232
+ if (options.responseDisposition) {
1233
+ queryParams["response-content-disposition"] = options.responseDisposition;
1234
+ }
1235
+ if (options.responseType) {
1236
+ queryParams["response-content-type"] = options.responseType;
1237
+ }
1238
+ const sortedParams = Object.keys(queryParams).sort();
1239
+ const canonicalQueryString = sortedParams.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`).join("&");
1240
+ const canonicalUri = `/${bucket}/${path}`;
1241
+ const canonicalRequest = [
1242
+ method,
1243
+ canonicalUri,
1244
+ canonicalQueryString,
1245
+ canonicalHeaders,
1246
+ signedHeaders,
1247
+ "UNSIGNED-PAYLOAD"
1248
+ ].join("\n");
1249
+ const canonicalRequestHash = stringToHex(canonicalRequest);
1250
+ const stringToSign = [
1251
+ "GOOG4-RSA-SHA256",
1252
+ `${datestamp}T000000Z`,
1253
+ credentialScope,
1254
+ canonicalRequestHash
1255
+ ].join("\n");
1256
+ const signature = await signData(stringToSign, serviceAccount.private_key);
1257
+ const signedUrl = `https://storage.googleapis.com${canonicalUri}?${canonicalQueryString}&X-Goog-Signature=${signature}`;
1258
+ return signedUrl;
1259
+ }
941
1260
  export {
942
1261
  FieldValue,
943
1262
  addDocument,
@@ -945,16 +1264,23 @@ export {
945
1264
  clearConfig,
946
1265
  clearTokenCache,
947
1266
  deleteDocument,
1267
+ deleteFile,
1268
+ downloadFile,
1269
+ fileExists,
1270
+ generateSignedUrl,
948
1271
  getAdminAccessToken,
949
1272
  getAuth,
950
1273
  getConfig,
951
1274
  getDocument,
1275
+ getFileMetadata,
952
1276
  getProjectId,
953
1277
  getServiceAccount,
954
1278
  getUserFromToken,
955
1279
  initializeApp,
1280
+ listFiles,
956
1281
  queryDocuments,
957
1282
  setDocument,
958
1283
  updateDocument,
1284
+ uploadFile,
959
1285
  verifyIdToken
960
1286
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/firebase-admin-sdk-v8",
3
- "version": "2.0.20",
3
+ "version": "2.0.22",
4
4
  "description": "Firebase Admin SDK for Cloudflare Workers and edge runtimes using REST APIs",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
Binary file