@seaverse/data-service-sdk 0.10.0 → 0.10.2

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/README.md CHANGED
@@ -53,7 +53,7 @@ SeaVerse organizes your Firestore data into three permission levels:
53
53
  |-----------------|--------------|-------------|--------------|----------|
54
54
  | **publicRead** | `appData/{app_id}/publicRead/_data/{collection}/{docId}` | All authenticated users | Admin only | System configs, announcements, static content |
55
55
  | **publicData** | `appData/{app_id}/publicData/_data/{collection}/{docId}` | All authenticated users | All authenticated users | User posts, comments, shared content |
56
- | **userData** | `appData/{app_id}/userData/{user_id}/{collection}/{docId}` | Owner only | Owner only | User settings, private notes, personal data |
56
+ | **userData** | `appData/{app_id}/userData/{user_id}/_data/{collection}/{docId}` | Owner only | Owner only | User settings, private notes, personal data |
57
57
 
58
58
  ### 🚨 CRITICAL: Firestore Path Rules (For LLM)
59
59
 
@@ -88,7 +88,7 @@ const postsRef = collection(db, `appData/${appId}/publicData/_data/posts`);
88
88
  await addDoc(postsRef, { ...data }); // Firestore adds document ID
89
89
 
90
90
  // User Private Data (owner only)
91
- const notesRef = collection(db, `appData/${appId}/userData/${userId}/notes`);
91
+ const notesRef = collection(db, `appData/${appId}/userData/${userId}/_data/notes`);
92
92
  await addDoc(notesRef, { ...data });
93
93
 
94
94
  // Public Read-Only (everyone can read, admin can write)
@@ -97,9 +97,10 @@ await getDocs(configRef);
97
97
  ```
98
98
 
99
99
  **The pattern is always:**
100
- - `appData` → `{app_id}` → `{permission_layer}` → `_data` → `{collection}` → (auto-generated doc ID)
101
- - Collection: 5 segments (odd) ✅
102
- - Document: 6 segments (even) ✅
100
+ - `appData` → `{app_id}` → `{permission_layer}` → (`{user_id}` for userData) → `_data` → `{collection}` → (auto-generated doc ID)
101
+ - publicRead/publicData Collection: 5 segments (odd) ✅
102
+ - userData Collection: 6 segments (even) ✅ (includes userId)
103
+ - Document paths: add 1 more segment for docId
103
104
 
104
105
  ### Required Fields
105
106
 
@@ -257,17 +258,17 @@ collection(db, `apps/${appId}/users/${userId}/articles`) // Not matching securi
257
258
  import { getPublicDataPath, getUserDataPath } from '@seaverse/data-service-sdk';
258
259
 
259
260
  // ✅ CORRECT - Guaranteed to match security rules
260
- collection(db, getPublicDataPath(appId, 'posts')) // → appData/{appId}/publicData/posts
261
- collection(db, getUserDataPath(appId, userId, 'notes')) // → appData/{appId}/userData/{userId}/notes
261
+ collection(db, getPublicDataPath(appId, 'posts')) // → appData/{appId}/publicData/_data/posts
262
+ collection(db, getUserDataPath(appId, userId, 'notes')) // → appData/{appId}/userData/{userId}/_data/notes
262
263
  ```
263
264
 
264
265
  ### Available Path Helpers
265
266
 
266
267
  ```typescript
267
268
  import {
268
- getPublicReadPath, // For read-only public data
269
- getPublicDataPath, // For public read/write data
270
- getUserDataPath, // For private user data
269
+ getPublicReadPath, // Returns: appData/{appId}/publicRead/_data/{collection}
270
+ getPublicDataPath, // Returns: appData/{appId}/publicData/_data/{collection}
271
+ getUserDataPath, // Returns: appData/{appId}/userData/{userId}/_data/{collection}
271
272
  getPublicReadDocPath, // For specific public document
272
273
  getPublicDataDocPath, // For specific public data document
273
274
  getUserDataDocPath, // For specific user document
@@ -324,7 +325,7 @@ const docPath = builder.publicData('posts').doc('post-123').build();
324
325
 
325
326
  // Build user data path
326
327
  const userPath = builder.userData(userId, 'notes').build();
327
- // Returns: 'appData/my-app/userData/user-123/notes'
328
+ // Returns: 'appData/my-app/userData/user-123/_data/notes'
328
329
  ```
329
330
 
330
331
  ### Error Prevention
@@ -631,9 +632,31 @@ initializeWithToken(tokenResponse: FirestoreTokenResponse): Promise<{
631
632
  db: Firestore;
632
633
  userId: string;
633
634
  appId: string;
635
+ helper: FirestoreHelper;
634
636
  }>
635
637
  ```
636
638
 
639
+ **⚠️ IMPORTANT: Safe for Multiple Calls**
640
+
641
+ This function has been updated to safely handle multiple calls:
642
+ - ✅ **Reuses existing Firebase app** if already initialized
643
+ - ✅ **Re-authenticates with new token** for token refresh scenarios
644
+ - ✅ **Handles errors gracefully** with automatic cleanup
645
+ - ✅ **Validates app_id** before initialization
646
+
647
+ **Common Use Cases:**
648
+ ```typescript
649
+ // ✅ SAFE: First-time initialization
650
+ const { helper } = await initializeWithToken(tokenResponse);
651
+
652
+ // ✅ SAFE: Token refresh (reuses existing app)
653
+ const newTokenResponse = await client.generateFirestoreToken({ ... });
654
+ const { helper: newHelper } = await initializeWithToken(newTokenResponse);
655
+
656
+ // ✅ SAFE: User re-login (reuses existing app)
657
+ const { helper } = await initializeWithToken(newUserTokenResponse);
658
+ ```
659
+
637
660
  **Example:**
638
661
 
639
662
  ```typescript
@@ -642,7 +665,7 @@ import { initializeWithToken } from '@seaverse/data-service-sdk';
642
665
  const tokenResponse = await client.generateGuestFirestoreToken({ app_id: 'my-app' });
643
666
 
644
667
  // One line to get everything!
645
- const { db, appId, userId } = await initializeWithToken(tokenResponse);
668
+ const { db, appId, userId, helper } = await initializeWithToken(tokenResponse);
646
669
 
647
670
  // Ready to use Firestore
648
671
  await addDoc(collection(db, `appData/${appId}/publicData/_data/posts`), { ... });
@@ -653,6 +676,22 @@ await addDoc(collection(db, `appData/${appId}/publicData/_data/posts`), { ... })
653
676
  npm install firebase
654
677
  ```
655
678
 
679
+ **Error Handling:**
680
+
681
+ ```typescript
682
+ try {
683
+ const { helper } = await initializeWithToken(tokenResponse);
684
+ } catch (error) {
685
+ if (error.message.includes('app_id is required')) {
686
+ // Handle missing app_id in token response
687
+ } else if (error.message.includes('Firebase SDK not found')) {
688
+ // Handle missing Firebase installation
689
+ } else if (error.message.includes('Failed to initialize Firebase')) {
690
+ // Handle authentication or initialization errors
691
+ }
692
+ }
693
+ ```
694
+
656
695
  ## Common Use Cases
657
696
 
658
697
  ### Use Case 1: Public Forum with Comments
@@ -678,7 +717,7 @@ const comments = await getDocs(
678
717
 
679
718
  ```typescript
680
719
  // Only the user can write to their own settings
681
- await setDoc(doc(db, `appData/${appId}/userData/${userId}/settings/preferences`), {
720
+ await setDoc(doc(db, `appData/${appId}/userData/${userId}/_data/settings/preferences`), {
682
721
  _appId: appId,
683
722
  _createdAt: serverTimestamp(),
684
723
  _createdBy: userId,
@@ -689,7 +728,7 @@ await setDoc(doc(db, `appData/${appId}/userData/${userId}/settings/preferences`)
689
728
 
690
729
  // Only the user can read their own settings
691
730
  const settings = await getDoc(
692
- doc(db, `appData/${appId}/userData/${userId}/settings/preferences`)
731
+ doc(db, `appData/${appId}/userData/${userId}/_data/settings/preferences`)
693
732
  );
694
733
  ```
695
734
 
@@ -730,7 +769,7 @@ const snapshot = await getDocs(userPosts);
730
769
  ✅ **Authenticated Users:**
731
770
  - Read `publicRead` data
732
771
  - Read and write `publicData` (all users' data)
733
- - Read and write their own `userData/{userId}` only
772
+ - Read and write their own `userData/{userId}/_data/` only
734
773
  - Update/delete documents where `_createdBy == userId`
735
774
 
736
775
  ✅ **Guest Users:**
@@ -790,6 +829,264 @@ import type {
790
829
  } from '@seaverse/data-service-sdk';
791
830
  ```
792
831
 
832
+ ## Production Usage Recommendations
833
+
834
+ ### Token Expiration Management
835
+
836
+ Firestore tokens expire after 1 hour (3600 seconds). For production applications, you should implement token refresh logic:
837
+
838
+ ```typescript
839
+ import { DataServiceClient, initializeWithToken } from '@seaverse/data-service-sdk';
840
+
841
+ class FirestoreManager {
842
+ private tokenExpiryTime: number | null = null;
843
+ private dataClient = new DataServiceClient();
844
+ private userToken: string | null = null;
845
+ private appId: string;
846
+
847
+ constructor(appId: string) {
848
+ this.appId = appId;
849
+ }
850
+
851
+ async initialize(userToken: string) {
852
+ this.userToken = userToken;
853
+
854
+ // Generate Firestore token
855
+ const tokenResponse = await this.dataClient.generateFirestoreToken({
856
+ token: userToken,
857
+ app_id: this.appId
858
+ });
859
+
860
+ // Initialize Firebase (safe to call multiple times)
861
+ const result = await initializeWithToken(tokenResponse);
862
+
863
+ // Track token expiry (expires_in is in seconds)
864
+ this.tokenExpiryTime = Date.now() + (tokenResponse.expires_in * 1000);
865
+
866
+ return result;
867
+ }
868
+
869
+ isTokenExpiringSoon(): boolean {
870
+ if (!this.tokenExpiryTime) return true;
871
+
872
+ // Check if token expires within 5 minutes
873
+ const fiveMinutes = 5 * 60 * 1000;
874
+ return Date.now() + fiveMinutes >= this.tokenExpiryTime;
875
+ }
876
+
877
+ async refreshTokenIfNeeded() {
878
+ if (this.isTokenExpiringSoon() && this.userToken) {
879
+ console.log('Token expiring soon, refreshing...');
880
+ return await this.initialize(this.userToken);
881
+ }
882
+ }
883
+ }
884
+
885
+ // Usage
886
+ const manager = new FirestoreManager('my-app-123');
887
+
888
+ // Initialize
889
+ const { helper } = await manager.initialize(userJwtToken);
890
+
891
+ // Before making Firestore operations, check token
892
+ await manager.refreshTokenIfNeeded();
893
+ await helper.addToPublicData('posts', { title: 'My Post' });
894
+ ```
895
+
896
+ ### Advanced: Singleton Pattern with React
897
+
898
+ For React applications, you can create a singleton manager with hooks:
899
+
900
+ ```typescript
901
+ // firestore-manager.ts
902
+ class FirestoreManager {
903
+ private static instance: FirestoreManager | null = null;
904
+ private helper: FirestoreHelper | null = null;
905
+ private db: Firestore | null = null;
906
+ private appId: string | null = null;
907
+ private userId: string | null = null;
908
+ private tokenExpiryTime: number | null = null;
909
+
910
+ static getInstance(): FirestoreManager {
911
+ if (!FirestoreManager.instance) {
912
+ FirestoreManager.instance = new FirestoreManager();
913
+ }
914
+ return FirestoreManager.instance;
915
+ }
916
+
917
+ async initialize(tokenResponse: FirestoreTokenResponse) {
918
+ // Always call initializeWithToken - it's safe for multiple calls
919
+ const result = await initializeWithToken(tokenResponse);
920
+
921
+ this.db = result.db;
922
+ this.appId = result.appId;
923
+ this.userId = result.userId;
924
+ this.helper = result.helper;
925
+ this.tokenExpiryTime = Date.now() + (tokenResponse.expires_in * 1000);
926
+
927
+ return result;
928
+ }
929
+
930
+ getHelper(): FirestoreHelper | null {
931
+ return this.helper;
932
+ }
933
+
934
+ isTokenExpiringSoon(): boolean {
935
+ if (!this.tokenExpiryTime) return true;
936
+ const fiveMinutes = 5 * 60 * 1000;
937
+ return Date.now() + fiveMinutes >= this.tokenExpiryTime;
938
+ }
939
+
940
+ reset() {
941
+ this.db = null;
942
+ this.appId = null;
943
+ this.userId = null;
944
+ this.helper = null;
945
+ this.tokenExpiryTime = null;
946
+ }
947
+ }
948
+
949
+ export default FirestoreManager;
950
+
951
+ // useFirestore.ts (React Hook)
952
+ import { useState, useEffect } from 'react';
953
+ import { DataServiceClient } from '@seaverse/data-service-sdk';
954
+ import FirestoreManager from './firestore-manager';
955
+
956
+ export function useFirestore(userToken: string, appId: string) {
957
+ const [helper, setHelper] = useState<FirestoreHelper | null>(null);
958
+ const [loading, setLoading] = useState(true);
959
+ const [error, setError] = useState<Error | null>(null);
960
+
961
+ useEffect(() => {
962
+ let mounted = true;
963
+
964
+ async function init() {
965
+ try {
966
+ const dataClient = new DataServiceClient();
967
+ const tokenResponse = await dataClient.generateFirestoreToken({
968
+ token: userToken,
969
+ app_id: appId
970
+ });
971
+
972
+ const manager = FirestoreManager.getInstance();
973
+ const { helper } = await manager.initialize(tokenResponse);
974
+
975
+ if (mounted) {
976
+ setHelper(helper);
977
+ setLoading(false);
978
+ }
979
+ } catch (err) {
980
+ if (mounted) {
981
+ setError(err as Error);
982
+ setLoading(false);
983
+ }
984
+ }
985
+ }
986
+
987
+ init();
988
+
989
+ return () => {
990
+ mounted = false;
991
+ };
992
+ }, [userToken, appId]);
993
+
994
+ return { helper, loading, error };
995
+ }
996
+
997
+ // Usage in component
998
+ function MyComponent() {
999
+ const { helper, loading, error } = useFirestore(userToken, 'my-app-123');
1000
+
1001
+ if (loading) return <div>Loading...</div>;
1002
+ if (error) return <div>Error: {error.message}</div>;
1003
+
1004
+ const handleAddPost = async () => {
1005
+ await helper.addToPublicData('posts', {
1006
+ title: 'My Post',
1007
+ content: 'Hello World'
1008
+ });
1009
+ };
1010
+
1011
+ return <button onClick={handleAddPost}>Add Post</button>;
1012
+ }
1013
+ ```
1014
+
1015
+ ### Background Token Refresh
1016
+
1017
+ For long-running applications, set up automatic background token refresh:
1018
+
1019
+ ```typescript
1020
+ class FirestoreManager {
1021
+ private refreshTimer: NodeJS.Timeout | null = null;
1022
+
1023
+ async initialize(userToken: string, appId: string) {
1024
+ const dataClient = new DataServiceClient();
1025
+ const tokenResponse = await dataClient.generateFirestoreToken({
1026
+ token: userToken,
1027
+ app_id: appId
1028
+ });
1029
+
1030
+ const result = await initializeWithToken(tokenResponse);
1031
+
1032
+ // Schedule token refresh 5 minutes before expiry
1033
+ const refreshIn = (tokenResponse.expires_in - 5 * 60) * 1000;
1034
+ this.scheduleRefresh(userToken, appId, refreshIn);
1035
+
1036
+ return result;
1037
+ }
1038
+
1039
+ private scheduleRefresh(userToken: string, appId: string, delay: number) {
1040
+ if (this.refreshTimer) {
1041
+ clearTimeout(this.refreshTimer);
1042
+ }
1043
+
1044
+ this.refreshTimer = setTimeout(async () => {
1045
+ console.log('Auto-refreshing Firestore token...');
1046
+ try {
1047
+ await this.initialize(userToken, appId);
1048
+ } catch (error) {
1049
+ console.error('Failed to refresh token:', error);
1050
+ }
1051
+ }, delay);
1052
+ }
1053
+
1054
+ cleanup() {
1055
+ if (this.refreshTimer) {
1056
+ clearTimeout(this.refreshTimer);
1057
+ this.refreshTimer = null;
1058
+ }
1059
+ }
1060
+ }
1061
+ ```
1062
+
1063
+ ### Error Recovery
1064
+
1065
+ Implement proper error recovery for network failures:
1066
+
1067
+ ```typescript
1068
+ async function initializeWithRetry(
1069
+ tokenResponse: FirestoreTokenResponse,
1070
+ maxRetries = 3
1071
+ ) {
1072
+ for (let i = 0; i < maxRetries; i++) {
1073
+ try {
1074
+ return await initializeWithToken(tokenResponse);
1075
+ } catch (error) {
1076
+ console.error(`Initialization attempt ${i + 1} failed:`, error);
1077
+
1078
+ if (i === maxRetries - 1) {
1079
+ throw error; // Last attempt failed
1080
+ }
1081
+
1082
+ // Wait before retry (exponential backoff)
1083
+ const delay = Math.pow(2, i) * 1000;
1084
+ await new Promise(resolve => setTimeout(resolve, delay));
1085
+ }
1086
+ }
1087
+ }
1088
+ ```
1089
+
793
1090
  ## Best Practices for LLM
794
1091
 
795
1092
  When using this SDK with LLM-generated code:
@@ -933,7 +1230,7 @@ async function completeExample() {
933
1230
 
934
1231
  // 7. Save user preferences (private)
935
1232
  await addDoc(
936
- collection(db, `appData/${appId}/userData/${userId}/preferences`),
1233
+ collection(db, `appData/${appId}/userData/${userId}/_data/preferences`),
937
1234
  {
938
1235
  _appId: appId,
939
1236
  _createdAt: serverTimestamp(),
package/dist/browser.js CHANGED
@@ -3941,9 +3941,9 @@ const ENDPOINTS = {
3941
3941
  * ---------------------------
3942
3942
  * Your Firestore data is organized in three permission levels:
3943
3943
  *
3944
- * 1. publicRead/ - System configs, announcements (Read: Everyone, Write: Admin only)
3945
- * 2. publicData/ - User posts, shared content (Read: Everyone, Write: Everyone)
3946
- * 3. userData/{userId}/ - Private user data (Read/Write: Owner only)
3944
+ * 1. publicRead/_data/ - System configs, announcements (Read: Everyone, Write: Admin only)
3945
+ * 2. publicData/_data/ - User posts, shared content (Read: Everyone, Write: Everyone)
3946
+ * 3. userData/{userId}/_data/ - Private user data (Read/Write: Owner only)
3947
3947
  *
3948
3948
  * QUICK START FOR LLM:
3949
3949
  * -------------------
@@ -3995,7 +3995,7 @@ const ENDPOINTS = {
3995
3995
  * const snapshot = await getDocs(collection(db, `appData/${appId}/publicData/posts`));
3996
3996
  *
3997
3997
  * // Write to userData (only owner can write)
3998
- * await addDoc(collection(db, `appData/${appId}/userData/${userId}/notes`), {
3998
+ * await addDoc(collection(db, `appData/${appId}/userData/${userId}/_data/notes`), {
3999
3999
  * _appId: appId, // REQUIRED
4000
4000
  * _createdAt: serverTimestamp(), // REQUIRED
4001
4001
  * _createdBy: userId, // REQUIRED
@@ -4248,7 +4248,7 @@ function getPublicDataPath(appId, collectionName) {
4248
4248
  * ```typescript
4249
4249
  * // Write private user notes
4250
4250
  * const path = getUserDataPath('my-app', 'user-123', 'notes');
4251
- * // Returns: 'appData/my-app/userData/user-123/notes'
4251
+ * // Returns: 'appData/my-app/userData/user-123/_data/notes'
4252
4252
  *
4253
4253
  * await addDoc(collection(db, path), {
4254
4254
  * _appId: appId,
@@ -4262,7 +4262,7 @@ function getUserDataPath(appId, userId, collectionName) {
4262
4262
  validateSegment('appId', appId);
4263
4263
  validateSegment('userId', userId);
4264
4264
  validateSegment('collectionName', collectionName);
4265
- return `appData/${appId}/userData/${userId}/${collectionName}`;
4265
+ return `appData/${appId}/userData/${userId}/_data/${collectionName}`;
4266
4266
  }
4267
4267
  /**
4268
4268
  * Generate path for a specific document in publicRead
@@ -4320,7 +4320,7 @@ function getPublicDataDocPath(appId, collectionName, docId) {
4320
4320
  * @example
4321
4321
  * ```typescript
4322
4322
  * const path = getUserDataDocPath('my-app', 'user-123', 'notes', 'note-456');
4323
- * // Returns: 'appData/my-app/userData/user-123/notes/note-456'
4323
+ * // Returns: 'appData/my-app/userData/user-123/_data/notes/note-456'
4324
4324
  *
4325
4325
  * const docSnap = await getDoc(doc(db, path));
4326
4326
  * ```
@@ -4330,7 +4330,7 @@ function getUserDataDocPath(appId, userId, collectionName, docId) {
4330
4330
  validateSegment('userId', userId);
4331
4331
  validateSegment('collectionName', collectionName);
4332
4332
  validateSegment('docId', docId);
4333
- return `appData/${appId}/userData/${userId}/${collectionName}/${docId}`;
4333
+ return `appData/${appId}/userData/${userId}/_data/${collectionName}/${docId}`;
4334
4334
  }
4335
4335
  /**
4336
4336
  * Validate a path segment to ensure it doesn't contain invalid characters
@@ -4395,7 +4395,7 @@ class PathBuilder {
4395
4395
  userData(userId, collectionName) {
4396
4396
  validateSegment('userId', userId);
4397
4397
  validateSegment('collectionName', collectionName);
4398
- this.segments.push('userData', userId, collectionName);
4398
+ this.segments.push('userData', userId, '_data', collectionName);
4399
4399
  return this;
4400
4400
  }
4401
4401
  /**
@@ -5014,7 +5014,9 @@ class FirestoreHelper {
5014
5014
  }
5015
5015
  else {
5016
5016
  // Filter out soft-deleted documents
5017
- const q = query(colRef, where('_deleted', '==', false));
5017
+ // Use '!=' to include documents without _deleted field (new documents)
5018
+ // This will return documents where _deleted is missing, null, undefined, or false
5019
+ const q = query(colRef, where('_deleted', '!=', true));
5018
5020
  return getDocs(q);
5019
5021
  }
5020
5022
  }
@@ -5147,7 +5149,7 @@ function getFirebaseConfig(tokenResponse) {
5147
5149
  *
5148
5150
  * This is a convenience function that automatically:
5149
5151
  * 1. Creates Firebase config from token response
5150
- * 2. Initializes Firebase app
5152
+ * 2. Initializes Firebase app (or reuses existing one)
5151
5153
  * 3. Signs in with the custom token
5152
5154
  * 4. Creates FirestoreHelper for LLM-friendly operations
5153
5155
  * 5. Returns authenticated Firebase instances
@@ -5155,11 +5157,20 @@ function getFirebaseConfig(tokenResponse) {
5155
5157
  * IMPORTANT: This function requires Firebase SDK to be installed separately:
5156
5158
  * npm install firebase
5157
5159
  *
5160
+ * ⚠️ IMPORTANT: This function can be called multiple times safely. It will:
5161
+ * - Reuse existing Firebase app if already initialized
5162
+ * - Re-authenticate with new token if needed
5163
+ * - Handle token refresh scenarios
5164
+ *
5158
5165
  * 🎯 LLM RECOMMENDED: Use the `helper` object for simplified operations
5159
5166
  *
5160
5167
  * @param tokenResponse - The Firestore token response from SDK
5161
5168
  * @returns Object containing initialized Firebase instances and LLM-friendly helper
5162
5169
  *
5170
+ * @throws {Error} If app_id is missing from token response
5171
+ * @throws {Error} If Firebase SDK is not installed
5172
+ * @throws {Error} If authentication fails
5173
+ *
5163
5174
  * @example
5164
5175
  * ```typescript
5165
5176
  * import { initializeWithToken } from '@seaverse/data-service-sdk';
@@ -5178,17 +5189,25 @@ function getFirebaseConfig(tokenResponse) {
5178
5189
  * ```
5179
5190
  */
5180
5191
  async function initializeWithToken(tokenResponse) {
5192
+ // ✅ FIX #3: Validate appId FIRST before any initialization
5193
+ const appId = tokenResponse.app_id;
5194
+ if (!appId) {
5195
+ throw new Error('app_id is required in token response. ' +
5196
+ 'Make sure your token response includes a valid app_id field.');
5197
+ }
5181
5198
  // Check if Firebase SDK is available
5182
5199
  let initializeApp;
5183
5200
  let getAuth;
5184
5201
  let signInWithCustomToken;
5185
5202
  let getFirestore;
5203
+ let getApps;
5186
5204
  try {
5187
5205
  // Try to import Firebase modules
5188
5206
  const firebaseApp = await import('firebase/app');
5189
5207
  const firebaseAuth = await import('firebase/auth');
5190
5208
  const firebaseFirestore = await import('firebase/firestore');
5191
5209
  initializeApp = firebaseApp.initializeApp;
5210
+ getApps = firebaseApp.getApps;
5192
5211
  getAuth = firebaseAuth.getAuth;
5193
5212
  signInWithCustomToken = firebaseAuth.signInWithCustomToken;
5194
5213
  getFirestore = firebaseFirestore.getFirestore;
@@ -5197,17 +5216,57 @@ async function initializeWithToken(tokenResponse) {
5197
5216
  throw new Error('Firebase SDK not found. Please install it: npm install firebase\n' +
5198
5217
  'Or import manually and use getFirebaseConfig() helper instead.');
5199
5218
  }
5200
- // Initialize Firebase
5201
- const config = getFirebaseConfig(tokenResponse);
5202
- const app = initializeApp(config);
5203
- // Sign in with custom token
5204
- const auth = getAuth(app);
5205
- await signInWithCustomToken(auth, tokenResponse.custom_token);
5206
- // Get Firestore instance with correct database ID
5207
- // IMPORTANT: Must specify database_id, not just use default!
5208
- const db = getFirestore(app, tokenResponse.database_id);
5219
+ // FIX #1: Check if Firebase app already exists
5220
+ const existingApps = getApps();
5221
+ const existingApp = existingApps.find((app) => app.name === '[DEFAULT]');
5222
+ let app;
5223
+ let auth;
5224
+ let db;
5225
+ if (existingApp) {
5226
+ // ✅ FIX #2: Reuse existing app instead of creating new one
5227
+ console.log('[SeaVerse SDK] Reusing existing Firebase app');
5228
+ app = existingApp;
5229
+ auth = getAuth(app);
5230
+ db = getFirestore(app, tokenResponse.database_id);
5231
+ // ✅ FIX #4: Re-authenticate with new token (for token refresh scenarios)
5232
+ try {
5233
+ await signInWithCustomToken(auth, tokenResponse.custom_token);
5234
+ console.log('[SeaVerse SDK] Re-authenticated with new token');
5235
+ }
5236
+ catch (error) {
5237
+ // If already signed in with same token, ignore error
5238
+ // Only throw if it's a real authentication failure
5239
+ if (error?.code !== 'auth/invalid-credential' && error?.code !== 'auth/network-request-failed') {
5240
+ console.warn('[SeaVerse SDK] Re-authentication warning:', error?.message || error);
5241
+ }
5242
+ }
5243
+ }
5244
+ else {
5245
+ // ✅ FIX #4: First-time initialization with proper error handling
5246
+ console.log('[SeaVerse SDK] Initializing new Firebase app');
5247
+ try {
5248
+ const config = getFirebaseConfig(tokenResponse);
5249
+ app = initializeApp(config);
5250
+ auth = getAuth(app);
5251
+ await signInWithCustomToken(auth, tokenResponse.custom_token);
5252
+ // IMPORTANT: Must specify database_id, not just use default!
5253
+ db = getFirestore(app, tokenResponse.database_id);
5254
+ }
5255
+ catch (error) {
5256
+ // ✅ FIX #4: Cleanup on failure to prevent half-initialized state
5257
+ if (app) {
5258
+ try {
5259
+ await app.delete();
5260
+ console.log('[SeaVerse SDK] Cleaned up failed Firebase app initialization');
5261
+ }
5262
+ catch (cleanupError) {
5263
+ console.warn('[SeaVerse SDK] Failed to cleanup app:', cleanupError);
5264
+ }
5265
+ }
5266
+ throw new Error(`Failed to initialize Firebase: ${error instanceof Error ? error.message : String(error)}`);
5267
+ }
5268
+ }
5209
5269
  const userId = tokenResponse.user_id;
5210
- const appId = tokenResponse.app_id || '';
5211
5270
  // Create LLM-friendly helper
5212
5271
  const helper = new FirestoreHelper(db, appId, userId);
5213
5272
  return {