@prmichaelsen/firebase-admin-sdk-v8 2.0.16 → 2.0.19

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/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 REST API
291
- * Provides CRUD operations for Firestore using REST API
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 {string} collectionPath - Collection path
298
- * @param {string} documentId - Document ID
299
- * @param {DataObject} data - Document data
300
- * @param {SetOptions} [options] - Set options (merge, mergeFields)
301
- * @returns {Promise<void>}
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 {string} collectionPath - Collection path (e.g., 'users' or 'users/uid/posts')
321
- * @param {DataObject} data - Document data
322
- * @param {string} [documentId] - Optional document ID (auto-generated if not provided)
323
- * @returns {Promise<DocumentReference>} Document reference with id and path
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 {string} collectionPath - Collection path
341
- * @param {string} documentId - Document ID
342
- * @returns {Promise<DataObject | null>} Document data or null if not found
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 {string} collectionPath - Collection path
358
- * @param {string} documentId - Document ID
359
- * @param {DataObject} data - Data to update
360
- * @returns {Promise<void>}
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 {string} collectionPath - Collection path
376
- * @param {string} documentId - Document ID
377
- * @returns {Promise<void>}
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 {string} collectionPath - Collection path
390
- * @param {QueryOptions} [options] - Query options (where, orderBy, limit, etc.)
391
- * @returns {Promise<Array<{ id: string; data: DataObject }>>} Array of documents
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 {BatchWrite[]} operations - Array of batch operations
414
- * @returns {Promise<BatchWriteResult>} Batch write result
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 REST API
291
- * Provides CRUD operations for Firestore using REST API
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 {string} collectionPath - Collection path
298
- * @param {string} documentId - Document ID
299
- * @param {DataObject} data - Document data
300
- * @param {SetOptions} [options] - Set options (merge, mergeFields)
301
- * @returns {Promise<void>}
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 {string} collectionPath - Collection path (e.g., 'users' or 'users/uid/posts')
321
- * @param {DataObject} data - Document data
322
- * @param {string} [documentId] - Optional document ID (auto-generated if not provided)
323
- * @returns {Promise<DocumentReference>} Document reference with id and path
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 {string} collectionPath - Collection path
341
- * @param {string} documentId - Document ID
342
- * @returns {Promise<DataObject | null>} Document data or null if not found
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 {string} collectionPath - Collection path
358
- * @param {string} documentId - Document ID
359
- * @param {DataObject} data - Data to update
360
- * @returns {Promise<void>}
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 {string} collectionPath - Collection path
376
- * @param {string} documentId - Document ID
377
- * @returns {Promise<void>}
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 {string} collectionPath - Collection path
390
- * @param {QueryOptions} [options] - Query options (where, orderBy, limit, etc.)
391
- * @returns {Promise<Array<{ id: string; data: DataObject }>>} Array of documents
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 {BatchWrite[]} operations - Array of batch operations
414
- * @returns {Promise<BatchWriteResult>} Batch write result
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 {
@@ -443,8 +371,7 @@ var FieldValue = {
443
371
  delete: deleteField
444
372
  };
445
373
 
446
- // src/firestore-rest.ts
447
- var FIRESTORE_API = "https://firestore.googleapis.com/v1";
374
+ // src/firestore/converters.ts
448
375
  function toFirestoreValue(value) {
449
376
  if (value === null || value === void 0) {
450
377
  return { nullValue: null };
@@ -504,58 +431,6 @@ function convertToFirestoreFormat(data) {
504
431
  }
505
432
  return result;
506
433
  }
507
- function extractFieldTransforms(data, fieldPrefix = "") {
508
- const transforms = [];
509
- for (const [key, value] of Object.entries(data)) {
510
- const fieldPath = fieldPrefix ? `${fieldPrefix}.${key}` : key;
511
- if (isFieldValue(value)) {
512
- switch (value._type) {
513
- case "serverTimestamp":
514
- transforms.push({
515
- fieldPath,
516
- setToServerValue: "REQUEST_TIME"
517
- });
518
- break;
519
- case "increment":
520
- transforms.push({
521
- fieldPath,
522
- increment: toFirestoreValue(value._value)
523
- });
524
- break;
525
- case "arrayUnion":
526
- transforms.push({
527
- fieldPath,
528
- appendMissingElements: {
529
- values: value._value.map((v) => toFirestoreValue(v))
530
- }
531
- });
532
- break;
533
- case "arrayRemove":
534
- transforms.push({
535
- fieldPath,
536
- removeAllFromArray: {
537
- values: value._value.map((v) => toFirestoreValue(v))
538
- }
539
- });
540
- break;
541
- }
542
- }
543
- }
544
- return transforms;
545
- }
546
- function removeFieldTransforms(data) {
547
- const result = {};
548
- for (const [key, value] of Object.entries(data)) {
549
- if (isFieldValue(value)) {
550
- if (value._type === "delete") {
551
- continue;
552
- }
553
- continue;
554
- }
555
- result[key] = value;
556
- }
557
- return result;
558
- }
559
434
  function fromFirestoreValue(value) {
560
435
  if ("stringValue" in value) {
561
436
  return value.stringValue;
@@ -584,12 +459,17 @@ function fromFirestoreValue(value) {
584
459
  return null;
585
460
  }
586
461
  function convertFromFirestoreFormat(fields) {
462
+ if (!fields) {
463
+ return {};
464
+ }
587
465
  const result = {};
588
466
  for (const [key, value] of Object.entries(fields)) {
589
467
  result[key] = fromFirestoreValue(value);
590
468
  }
591
469
  return result;
592
470
  }
471
+
472
+ // src/firestore/query-builder.ts
593
473
  function buildStructuredQuery(collectionPath, options) {
594
474
  const pathSegments = collectionPath.split("/");
595
475
  const collectionId = pathSegments[pathSegments.length - 1];
@@ -672,14 +552,181 @@ function mapWhereOp(op) {
672
552
  };
673
553
  return opMap[op] || "EQUAL";
674
554
  }
675
- async function setDocument(collectionPath, documentId, data, options) {
555
+
556
+ // src/firestore/transforms.ts
557
+ function extractFieldTransforms(data, fieldPrefix = "") {
558
+ const transforms = [];
559
+ for (const [key, value] of Object.entries(data)) {
560
+ const fieldPath = fieldPrefix ? `${fieldPrefix}.${key}` : key;
561
+ if (isFieldValue(value)) {
562
+ switch (value._type) {
563
+ case "serverTimestamp":
564
+ transforms.push({
565
+ fieldPath,
566
+ setToServerValue: "REQUEST_TIME"
567
+ });
568
+ break;
569
+ case "increment":
570
+ transforms.push({
571
+ fieldPath,
572
+ increment: toFirestoreValue(value._value)
573
+ });
574
+ break;
575
+ case "arrayUnion":
576
+ transforms.push({
577
+ fieldPath,
578
+ appendMissingElements: {
579
+ values: value._value.map((v) => toFirestoreValue(v))
580
+ }
581
+ });
582
+ break;
583
+ case "arrayRemove":
584
+ transforms.push({
585
+ fieldPath,
586
+ removeAllFromArray: {
587
+ values: value._value.map((v) => toFirestoreValue(v))
588
+ }
589
+ });
590
+ break;
591
+ }
592
+ }
593
+ }
594
+ return transforms;
595
+ }
596
+ function removeFieldTransforms(data) {
597
+ const result = {};
598
+ for (const [key, value] of Object.entries(data)) {
599
+ if (isFieldValue(value)) {
600
+ if (value._type === "delete") {
601
+ continue;
602
+ }
603
+ continue;
604
+ }
605
+ result[key] = value;
606
+ }
607
+ return result;
608
+ }
609
+
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
+ var FIRESTORE_API = "https://firestore.googleapis.com/v1";
684
+ async function commitWrites(writes) {
676
685
  const accessToken = await getAdminAccessToken();
677
686
  const projectId = getProjectId();
678
- const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
687
+ const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents:commit`;
688
+ const response = await fetch(url, {
689
+ method: "POST",
690
+ headers: {
691
+ "Authorization": `Bearer ${accessToken}`,
692
+ "Content-Type": "application/json"
693
+ },
694
+ body: JSON.stringify({ writes })
695
+ });
696
+ if (!response.ok) {
697
+ const errorText = await response.text();
698
+ throw new Error(`Failed to commit writes: ${errorText}`);
699
+ }
700
+ }
701
+ async function setDocument(collectionPath, documentId, data, options) {
702
+ const projectId = getProjectId();
703
+ const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
679
704
  const cleanData = removeFieldTransforms(data);
680
705
  const firestoreData = convertToFirestoreFormat(cleanData);
681
706
  const transforms = extractFieldTransforms(data);
682
- const body = { fields: firestoreData };
707
+ if (transforms.length > 0) {
708
+ const updateWrite = {
709
+ update: {
710
+ name: documentPath,
711
+ fields: firestoreData
712
+ },
713
+ updateTransforms: transforms
714
+ };
715
+ const nonTransformFields = Object.keys(cleanData);
716
+ if (nonTransformFields.length > 0) {
717
+ if (options?.merge) {
718
+ updateWrite.updateMask = { fieldPaths: ["*"] };
719
+ } else if (options?.mergeFields && options.mergeFields.length > 0) {
720
+ updateWrite.updateMask = { fieldPaths: options.mergeFields };
721
+ } else {
722
+ updateWrite.updateMask = { fieldPaths: nonTransformFields };
723
+ }
724
+ }
725
+ await commitWrites([updateWrite]);
726
+ return;
727
+ }
728
+ const accessToken = await getAdminAccessToken();
729
+ const url = `${FIRESTORE_API}/${documentPath}`;
683
730
  let queryParams = "";
684
731
  if (options?.merge) {
685
732
  queryParams = "?updateMask.fieldPaths=*";
@@ -687,16 +734,13 @@ async function setDocument(collectionPath, documentId, data, options) {
687
734
  const fieldPaths = options.mergeFields.join("&updateMask.fieldPaths=");
688
735
  queryParams = `?updateMask.fieldPaths=${fieldPaths}`;
689
736
  }
690
- if (transforms.length > 0) {
691
- body.transforms = transforms;
692
- }
693
737
  const response = await fetch(`${url}${queryParams}`, {
694
738
  method: "PATCH",
695
739
  headers: {
696
740
  "Authorization": `Bearer ${accessToken}`,
697
741
  "Content-Type": "application/json"
698
742
  },
699
- body: JSON.stringify(body)
743
+ body: JSON.stringify({ fields: firestoreData })
700
744
  });
701
745
  if (!response.ok) {
702
746
  const errorText = await response.text();
@@ -754,25 +798,51 @@ async function getDocument(collectionPath, documentId) {
754
798
  return convertFromFirestoreFormat(result.fields);
755
799
  }
756
800
  async function updateDocument(collectionPath, documentId, data) {
757
- const accessToken = await getAdminAccessToken();
758
801
  const projectId = getProjectId();
759
- const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
802
+ const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
760
803
  const cleanData = removeFieldTransforms(data);
761
804
  const firestoreData = convertToFirestoreFormat(cleanData);
762
805
  const transforms = extractFieldTransforms(data);
763
- const updateMaskFields = Object.keys(data).filter((key) => !isFieldValue(data[key]) || data[key]._type !== "delete");
764
- const updateMaskParams = updateMaskFields.map((field) => `updateMask.fieldPaths=${encodeURIComponent(field)}`).join("&");
765
- const body = { fields: firestoreData };
806
+ const updateMaskFields = Object.keys(data).filter((key) => {
807
+ if (!isFieldValue(data[key])) {
808
+ return true;
809
+ }
810
+ return data[key]._type === "delete";
811
+ });
766
812
  if (transforms.length > 0) {
767
- body.transforms = transforms;
813
+ const nonTransformFields = Object.keys(cleanData);
814
+ if (nonTransformFields.length === 0) {
815
+ const transformWrite = {
816
+ transform: {
817
+ document: documentPath,
818
+ fieldTransforms: transforms
819
+ }
820
+ };
821
+ await commitWrites([transformWrite]);
822
+ return;
823
+ }
824
+ const updateWrite = {
825
+ update: {
826
+ name: documentPath,
827
+ fields: firestoreData
828
+ },
829
+ updateMask: { fieldPaths: updateMaskFields },
830
+ updateTransforms: transforms,
831
+ currentDocument: { exists: true }
832
+ };
833
+ await commitWrites([updateWrite]);
834
+ return;
768
835
  }
836
+ const accessToken = await getAdminAccessToken();
837
+ const url = `${FIRESTORE_API}/${documentPath}`;
838
+ const updateMaskParams = updateMaskFields.map((field) => `updateMask.fieldPaths=${encodeURIComponent(field)}`).join("&");
769
839
  const response = await fetch(`${url}?${updateMaskParams}&currentDocument.exists=true`, {
770
840
  method: "PATCH",
771
841
  headers: {
772
842
  "Authorization": `Bearer ${accessToken}`,
773
843
  "Content-Type": "application/json"
774
844
  },
775
- body: JSON.stringify(body)
845
+ body: JSON.stringify({ fields: firestoreData })
776
846
  });
777
847
  if (!response.ok) {
778
848
  const errorText = await response.text();
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 {
@@ -400,8 +328,7 @@ var FieldValue = {
400
328
  delete: deleteField
401
329
  };
402
330
 
403
- // src/firestore-rest.ts
404
- var FIRESTORE_API = "https://firestore.googleapis.com/v1";
331
+ // src/firestore/converters.ts
405
332
  function toFirestoreValue(value) {
406
333
  if (value === null || value === void 0) {
407
334
  return { nullValue: null };
@@ -461,58 +388,6 @@ function convertToFirestoreFormat(data) {
461
388
  }
462
389
  return result;
463
390
  }
464
- function extractFieldTransforms(data, fieldPrefix = "") {
465
- const transforms = [];
466
- for (const [key, value] of Object.entries(data)) {
467
- const fieldPath = fieldPrefix ? `${fieldPrefix}.${key}` : key;
468
- if (isFieldValue(value)) {
469
- switch (value._type) {
470
- case "serverTimestamp":
471
- transforms.push({
472
- fieldPath,
473
- setToServerValue: "REQUEST_TIME"
474
- });
475
- break;
476
- case "increment":
477
- transforms.push({
478
- fieldPath,
479
- increment: toFirestoreValue(value._value)
480
- });
481
- break;
482
- case "arrayUnion":
483
- transforms.push({
484
- fieldPath,
485
- appendMissingElements: {
486
- values: value._value.map((v) => toFirestoreValue(v))
487
- }
488
- });
489
- break;
490
- case "arrayRemove":
491
- transforms.push({
492
- fieldPath,
493
- removeAllFromArray: {
494
- values: value._value.map((v) => toFirestoreValue(v))
495
- }
496
- });
497
- break;
498
- }
499
- }
500
- }
501
- return transforms;
502
- }
503
- function removeFieldTransforms(data) {
504
- const result = {};
505
- for (const [key, value] of Object.entries(data)) {
506
- if (isFieldValue(value)) {
507
- if (value._type === "delete") {
508
- continue;
509
- }
510
- continue;
511
- }
512
- result[key] = value;
513
- }
514
- return result;
515
- }
516
391
  function fromFirestoreValue(value) {
517
392
  if ("stringValue" in value) {
518
393
  return value.stringValue;
@@ -541,12 +416,17 @@ function fromFirestoreValue(value) {
541
416
  return null;
542
417
  }
543
418
  function convertFromFirestoreFormat(fields) {
419
+ if (!fields) {
420
+ return {};
421
+ }
544
422
  const result = {};
545
423
  for (const [key, value] of Object.entries(fields)) {
546
424
  result[key] = fromFirestoreValue(value);
547
425
  }
548
426
  return result;
549
427
  }
428
+
429
+ // src/firestore/query-builder.ts
550
430
  function buildStructuredQuery(collectionPath, options) {
551
431
  const pathSegments = collectionPath.split("/");
552
432
  const collectionId = pathSegments[pathSegments.length - 1];
@@ -629,14 +509,181 @@ function mapWhereOp(op) {
629
509
  };
630
510
  return opMap[op] || "EQUAL";
631
511
  }
632
- async function setDocument(collectionPath, documentId, data, options) {
512
+
513
+ // src/firestore/transforms.ts
514
+ function extractFieldTransforms(data, fieldPrefix = "") {
515
+ const transforms = [];
516
+ for (const [key, value] of Object.entries(data)) {
517
+ const fieldPath = fieldPrefix ? `${fieldPrefix}.${key}` : key;
518
+ if (isFieldValue(value)) {
519
+ switch (value._type) {
520
+ case "serverTimestamp":
521
+ transforms.push({
522
+ fieldPath,
523
+ setToServerValue: "REQUEST_TIME"
524
+ });
525
+ break;
526
+ case "increment":
527
+ transforms.push({
528
+ fieldPath,
529
+ increment: toFirestoreValue(value._value)
530
+ });
531
+ break;
532
+ case "arrayUnion":
533
+ transforms.push({
534
+ fieldPath,
535
+ appendMissingElements: {
536
+ values: value._value.map((v) => toFirestoreValue(v))
537
+ }
538
+ });
539
+ break;
540
+ case "arrayRemove":
541
+ transforms.push({
542
+ fieldPath,
543
+ removeAllFromArray: {
544
+ values: value._value.map((v) => toFirestoreValue(v))
545
+ }
546
+ });
547
+ break;
548
+ }
549
+ }
550
+ }
551
+ return transforms;
552
+ }
553
+ function removeFieldTransforms(data) {
554
+ const result = {};
555
+ for (const [key, value] of Object.entries(data)) {
556
+ if (isFieldValue(value)) {
557
+ if (value._type === "delete") {
558
+ continue;
559
+ }
560
+ continue;
561
+ }
562
+ result[key] = value;
563
+ }
564
+ return result;
565
+ }
566
+
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
+ var FIRESTORE_API = "https://firestore.googleapis.com/v1";
641
+ async function commitWrites(writes) {
633
642
  const accessToken = await getAdminAccessToken();
634
643
  const projectId = getProjectId();
635
- const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
644
+ const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents:commit`;
645
+ const response = await fetch(url, {
646
+ method: "POST",
647
+ headers: {
648
+ "Authorization": `Bearer ${accessToken}`,
649
+ "Content-Type": "application/json"
650
+ },
651
+ body: JSON.stringify({ writes })
652
+ });
653
+ if (!response.ok) {
654
+ const errorText = await response.text();
655
+ throw new Error(`Failed to commit writes: ${errorText}`);
656
+ }
657
+ }
658
+ async function setDocument(collectionPath, documentId, data, options) {
659
+ const projectId = getProjectId();
660
+ const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
636
661
  const cleanData = removeFieldTransforms(data);
637
662
  const firestoreData = convertToFirestoreFormat(cleanData);
638
663
  const transforms = extractFieldTransforms(data);
639
- const body = { fields: firestoreData };
664
+ if (transforms.length > 0) {
665
+ const updateWrite = {
666
+ update: {
667
+ name: documentPath,
668
+ fields: firestoreData
669
+ },
670
+ updateTransforms: transforms
671
+ };
672
+ const nonTransformFields = Object.keys(cleanData);
673
+ if (nonTransformFields.length > 0) {
674
+ if (options?.merge) {
675
+ updateWrite.updateMask = { fieldPaths: ["*"] };
676
+ } else if (options?.mergeFields && options.mergeFields.length > 0) {
677
+ updateWrite.updateMask = { fieldPaths: options.mergeFields };
678
+ } else {
679
+ updateWrite.updateMask = { fieldPaths: nonTransformFields };
680
+ }
681
+ }
682
+ await commitWrites([updateWrite]);
683
+ return;
684
+ }
685
+ const accessToken = await getAdminAccessToken();
686
+ const url = `${FIRESTORE_API}/${documentPath}`;
640
687
  let queryParams = "";
641
688
  if (options?.merge) {
642
689
  queryParams = "?updateMask.fieldPaths=*";
@@ -644,16 +691,13 @@ async function setDocument(collectionPath, documentId, data, options) {
644
691
  const fieldPaths = options.mergeFields.join("&updateMask.fieldPaths=");
645
692
  queryParams = `?updateMask.fieldPaths=${fieldPaths}`;
646
693
  }
647
- if (transforms.length > 0) {
648
- body.transforms = transforms;
649
- }
650
694
  const response = await fetch(`${url}${queryParams}`, {
651
695
  method: "PATCH",
652
696
  headers: {
653
697
  "Authorization": `Bearer ${accessToken}`,
654
698
  "Content-Type": "application/json"
655
699
  },
656
- body: JSON.stringify(body)
700
+ body: JSON.stringify({ fields: firestoreData })
657
701
  });
658
702
  if (!response.ok) {
659
703
  const errorText = await response.text();
@@ -711,25 +755,51 @@ async function getDocument(collectionPath, documentId) {
711
755
  return convertFromFirestoreFormat(result.fields);
712
756
  }
713
757
  async function updateDocument(collectionPath, documentId, data) {
714
- const accessToken = await getAdminAccessToken();
715
758
  const projectId = getProjectId();
716
- const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
759
+ const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
717
760
  const cleanData = removeFieldTransforms(data);
718
761
  const firestoreData = convertToFirestoreFormat(cleanData);
719
762
  const transforms = extractFieldTransforms(data);
720
- const updateMaskFields = Object.keys(data).filter((key) => !isFieldValue(data[key]) || data[key]._type !== "delete");
721
- const updateMaskParams = updateMaskFields.map((field) => `updateMask.fieldPaths=${encodeURIComponent(field)}`).join("&");
722
- const body = { fields: firestoreData };
763
+ const updateMaskFields = Object.keys(data).filter((key) => {
764
+ if (!isFieldValue(data[key])) {
765
+ return true;
766
+ }
767
+ return data[key]._type === "delete";
768
+ });
723
769
  if (transforms.length > 0) {
724
- body.transforms = transforms;
770
+ const nonTransformFields = Object.keys(cleanData);
771
+ if (nonTransformFields.length === 0) {
772
+ const transformWrite = {
773
+ transform: {
774
+ document: documentPath,
775
+ fieldTransforms: transforms
776
+ }
777
+ };
778
+ await commitWrites([transformWrite]);
779
+ return;
780
+ }
781
+ const updateWrite = {
782
+ update: {
783
+ name: documentPath,
784
+ fields: firestoreData
785
+ },
786
+ updateMask: { fieldPaths: updateMaskFields },
787
+ updateTransforms: transforms,
788
+ currentDocument: { exists: true }
789
+ };
790
+ await commitWrites([updateWrite]);
791
+ return;
725
792
  }
793
+ const accessToken = await getAdminAccessToken();
794
+ const url = `${FIRESTORE_API}/${documentPath}`;
795
+ const updateMaskParams = updateMaskFields.map((field) => `updateMask.fieldPaths=${encodeURIComponent(field)}`).join("&");
726
796
  const response = await fetch(`${url}?${updateMaskParams}&currentDocument.exists=true`, {
727
797
  method: "PATCH",
728
798
  headers: {
729
799
  "Authorization": `Bearer ${accessToken}`,
730
800
  "Content-Type": "application/json"
731
801
  },
732
- body: JSON.stringify(body)
802
+ body: JSON.stringify({ fields: firestoreData })
733
803
  });
734
804
  if (!response.ok) {
735
805
  const errorText = await response.text();
package/firebase.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "projects": {
3
+ "default": "prmichaelsen-firebase-e2e"
4
+ },
5
+ "firestore": {
6
+ "rules": "firestore.rules",
7
+ "indexes": "firestore.indexes.json"
8
+ }
9
+ }
@@ -0,0 +1,33 @@
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
+ }
@@ -0,0 +1,11 @@
1
+ rules_version = '2';
2
+
3
+ service cloud.firestore {
4
+ match /databases/{database}/documents {
5
+ // Allow all reads and writes for testing
6
+ // WARNING: These rules are for testing only!
7
+ match /{document=**} {
8
+ allow read, write: if true;
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,14 @@
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/firebase-admin-sdk-v8",
3
- "version": "2.0.16",
3
+ "version": "2.0.19",
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",
@@ -26,7 +26,10 @@
26
26
  "typecheck": "tsc --noEmit",
27
27
  "test": "jest",
28
28
  "test:watch": "jest --watch",
29
- "publish": "npm run build && npm publish"
29
+ "test:e2e": "jest --config jest.e2e.config.js",
30
+ "test:e2e:watch": "jest --config jest.e2e.config.js --watch",
31
+ "test:all": "npm test && npm run test:e2e",
32
+ "prepublishOnly": "npm run build"
30
33
  },
31
34
  "keywords": [
32
35
  "firebase",
@@ -0,0 +1,13 @@
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
+ }