@oxyhq/services 5.16.37 → 5.16.38

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.
@@ -23,6 +23,7 @@ export interface TransferState {
23
23
  markRestored: () => void;
24
24
  cleanupExpired: () => void;
25
25
  reset: () => void;
26
+ clearAll: () => void;
26
27
  }
27
28
  export declare const useTransferStore: import("zustand").UseBoundStore<import("zustand").StoreApi<TransferState>>;
28
29
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"transferStore.d.ts","sourceRoot":"","sources":["../../../../src/ui/stores/transferStore.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa;IAE5B,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAGhD,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAGhC,UAAU,EAAE,OAAO,CAAC;IAGpB,iBAAiB,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAChH,eAAe,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,gBAAgB,GAAG,IAAI,CAAC;IACjE,iBAAiB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,mBAAmB,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,KAAK,IAAI,CAAC;IAC7F,sBAAsB,EAAE,MAAM,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,gBAAgB,CAAA;KAAE,CAAC,CAAC;IACpF,mBAAmB,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACzC,mBAAmB,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzD,kBAAkB,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAAE,gBAAgB,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACvG,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAUD,eAAO,MAAM,gBAAgB,4EAyI1B,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,8BAA8B;;;CAO1C,CAAC"}
1
+ {"version":3,"file":"transferStore.d.ts","sourceRoot":"","sources":["../../../../src/ui/stores/transferStore.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa;IAE5B,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAGhD,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAGhC,UAAU,EAAE,OAAO,CAAC;IAGpB,iBAAiB,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAChH,eAAe,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,gBAAgB,GAAG,IAAI,CAAC;IACjE,iBAAiB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,mBAAmB,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,KAAK,IAAI,CAAC;IAC7F,sBAAsB,EAAE,MAAM,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,gBAAgB,CAAA;KAAE,CAAC,CAAC;IACpF,mBAAmB,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACzC,mBAAmB,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzD,kBAAkB,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAAE,gBAAgB,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACvG,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAUD,eAAO,MAAM,gBAAgB,4EA8I1B,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,8BAA8B;;;CAO1C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/services",
3
- "version": "5.16.37",
3
+ "version": "5.16.38",
4
4
  "description": "Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -4,6 +4,7 @@ import {
4
4
  useCallback,
5
5
  useContext,
6
6
  useEffect,
7
+ useLayoutEffect,
7
8
  useMemo,
8
9
  useRef,
9
10
  useState,
@@ -211,6 +212,15 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
211
212
 
212
213
  const { storage, isReady: isStorageReady } = useStorage({ onError, logger });
213
214
 
215
+ // Invalidate KeyManager cache on mount to prevent stale cache issues
216
+ // useLayoutEffect runs synchronously after render but before paint, ensuring cache
217
+ // invalidation happens before any async operations start
218
+ useLayoutEffect(() => {
219
+ if (Platform.OS !== 'web') {
220
+ KeyManager.invalidateCache();
221
+ }
222
+ }, []);
223
+
214
224
  // Identity integrity check and auto-restore on startup
215
225
  // Skip on web platform - identity storage is only available on native platforms
216
226
  useEffect(() => {
@@ -219,9 +229,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
219
229
 
220
230
  const checkAndRestoreIdentity = async () => {
221
231
  try {
222
- // CRITICAL: Invalidate cache on app startup to ensure fresh state check
223
- // This prevents stale cache from previous session from showing incorrect state
224
- KeyManager.invalidateCache();
232
+ // Cache was already invalidated synchronously above
225
233
 
226
234
  // Check if identity exists and verify integrity
227
235
  const hasIdentity = await KeyManager.hasIdentity();
@@ -352,9 +360,17 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
352
360
  [importIdentityBase]
353
361
  );
354
362
 
363
+ // Storage keys for transfer codes
364
+ const TRANSFER_CODES_STORAGE_KEY = `${storageKeyPrefix}_transfer_codes`;
365
+ const ACTIVE_TRANSFER_STORAGE_KEY = `${storageKeyPrefix}_active_transfer_id`;
366
+
355
367
  // Clear all account data when identity is lost (for accounts app)
356
368
  // In accounts app, identity = account, so losing identity means losing everything
357
369
  const clearAllAccountData = useCallback(async (): Promise<void> => {
370
+ if (__DEV__) {
371
+ logger('Clearing all account data - identity changed or lost');
372
+ }
373
+
358
374
  // Clear TanStack Query cache (in-memory)
359
375
  queryClient.clear();
360
376
 
@@ -386,9 +402,30 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
386
402
  // Reset account store
387
403
  useAccountStore.getState().reset();
388
404
 
405
+ // CRITICAL: Clear ALL transfer codes and active transfer state
406
+ // This prevents transfer state from previous identity from lingering
407
+ if (storage) {
408
+ try {
409
+ await storage.removeItem(TRANSFER_CODES_STORAGE_KEY);
410
+ await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
411
+
412
+ // Also clear Zustand transfer store
413
+ useTransferStore.getState().clearAll();
414
+
415
+ if (__DEV__) {
416
+ logger('Cleared all transfer state');
417
+ }
418
+ } catch (error) {
419
+ logger('Failed to clear transfer state', error);
420
+ }
421
+ }
422
+
389
423
  // Clear HTTP service cache
390
424
  oxyServices.clearCache();
391
- }, [queryClient, storage, clearSessionState, logger, oxyServices]);
425
+
426
+ // Force KeyManager cache invalidation
427
+ KeyManager.invalidateCache();
428
+ }, [queryClient, storage, clearSessionState, logger, oxyServices, TRANSFER_CODES_STORAGE_KEY, ACTIVE_TRANSFER_STORAGE_KEY]);
392
429
 
393
430
  // Extract Zustand store functions early (before they're used in callbacks)
394
431
  const getAllPendingTransfersStore = useTransferStore((state) => state.getAllPendingTransfers);
@@ -582,6 +619,8 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
582
619
  try {
583
620
  // CRITICAL: Get current identity's public key first
584
621
  // Only restore sessions that belong to this identity
622
+ // Force cache invalidation to ensure we get the actual current identity
623
+ KeyManager.invalidateCache();
585
624
  const currentPublicKey = await KeyManager.getPublicKey().catch(() => null);
586
625
 
587
626
  const storedSessionIdsJson = await storage.getItem(storageKeys.sessionIds);
@@ -591,13 +630,21 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
591
630
  // If no identity exists, clear all sessions and return
592
631
  if (!currentPublicKey) {
593
632
  if (storedSessionIds.length > 0 || storedActiveSessionId) {
633
+ if (__DEV__) {
634
+ logger('No identity found - clearing all stored sessions to prevent cross-identity data leak');
635
+ }
594
636
  await clearSessionState();
595
637
  }
596
638
  setTokenReady(true);
597
639
  return;
598
640
  }
599
641
 
642
+ if (__DEV__) {
643
+ logger('Restoring sessions for identity', { publicKey: currentPublicKey.substring(0, 16) + '...', sessionCount: storedSessionIds.length });
644
+ }
645
+
600
646
  const validSessions: ClientSession[] = [];
647
+ const invalidSessionIds: string[] = []; // Track invalid sessions for batch removal
601
648
 
602
649
  if (storedSessionIds.length > 0) {
603
650
  for (const sessionId of storedSessionIds) {
@@ -605,14 +652,18 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
605
652
  const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation: true });
606
653
  if (validation?.valid && validation.user) {
607
654
  // CRITICAL: Verify session belongs to current identity
608
- // IMPORTANT: In OxyAccounts, user.id is set to the publicKey (as confirmed by line 754 comment below)
609
- // This is different from the JWT's userId field which contains MongoDB ObjectId
610
- // We compare user.id (publicKey) to currentPublicKey to ensure session ownership
611
- if (validation.user.id !== currentPublicKey) {
612
- // Session belongs to different identity - skip it
655
+ // Compare session's publicKey to current identity's publicKey
656
+ if (validation.user.publicKey !== currentPublicKey) {
657
+ // Session belongs to different identity - skip it and log warning
613
658
  if (__DEV__) {
614
- logger('Skipping session from different identity during restoration');
659
+ logger('CRITICAL: Skipping session from different identity during restoration', {
660
+ sessionPublicKey: validation.user.publicKey?.substring(0, 16) + '...',
661
+ currentPublicKey: currentPublicKey.substring(0, 16) + '...',
662
+ sessionId: sessionId.substring(0, 16) + '...',
663
+ });
615
664
  }
665
+ // Mark for batch removal
666
+ invalidSessionIds.push(sessionId);
616
667
  continue;
617
668
  }
618
669
 
@@ -638,6 +689,24 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
638
689
  }
639
690
  }
640
691
 
692
+ // Batch remove invalid sessions from storage (performance optimization)
693
+ if (invalidSessionIds.length > 0) {
694
+ try {
695
+ // Use Set for O(n) lookup instead of O(n²) with includes()
696
+ const invalidSessionSet = new Set(invalidSessionIds);
697
+ const updatedIds = storedSessionIds.filter(id => !invalidSessionSet.has(id));
698
+ await storage.setItem(storageKeys.sessionIds, JSON.stringify(updatedIds));
699
+ if (__DEV__) {
700
+ logger('Removed invalid sessions from storage', { count: invalidSessionIds.length });
701
+ }
702
+ } catch (cleanupError) {
703
+ // Ignore cleanup errors - will be cleaned on next restart
704
+ if (__DEV__) {
705
+ logger('Failed to remove invalid sessions from storage', cleanupError);
706
+ }
707
+ }
708
+ }
709
+
641
710
  if (validSessions.length > 0) {
642
711
  updateSessions(validSessions, { merge: false });
643
712
  }
@@ -752,13 +821,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
752
821
  }, [isAuthenticatedFromStore, isAuthenticatedFromSessions]);
753
822
 
754
823
  // Get userId from JWT token (MongoDB ObjectId) for socket room matching
755
- // user.id is set to publicKey for compatibility, but socket rooms use MongoDB ObjectId
756
- // The JWT token's userId field contains the MongoDB ObjectId
824
+ // user.id contains the MongoDB ObjectId, user.publicKey contains the cryptographic public key
757
825
  const userId = oxyServices.getCurrentUserId() || user?.id;
758
826
 
759
827
  // Use Zustand store for transfer state management
760
- const TRANSFER_CODES_STORAGE_KEY = `${storageKeyPrefix}_transfer_codes`;
761
- const ACTIVE_TRANSFER_STORAGE_KEY = `${storageKeyPrefix}_active_transfer_id`;
828
+ // Storage keys are defined above (TRANSFER_CODES_STORAGE_KEY, ACTIVE_TRANSFER_STORAGE_KEY)
762
829
  const isRestored = useTransferStore((state) => state.isRestored);
763
830
  const restoreFromStorage = useTransferStore((state) => state.restoreFromStorage);
764
831
  const markRestored = useTransferStore((state) => state.markRestored);
@@ -91,13 +91,28 @@ export const useAuthOperations = ({
91
91
  const clearSessionsIfIdentityChanged = useCallback(
92
92
  async (oldPublicKey: string | null, newPublicKey: string): Promise<void> => {
93
93
  if (oldPublicKey && oldPublicKey !== newPublicKey) {
94
+ if (__DEV__ && logger) {
95
+ logger('CRITICAL: Identity changed - clearing all session data', {
96
+ oldPublicKey: oldPublicKey.substring(0, 16) + '...',
97
+ newPublicKey: newPublicKey.substring(0, 16) + '...',
98
+ });
99
+ }
100
+
94
101
  // Clear all session state to prevent old identity's data from showing up
95
102
  await clearSessionState();
96
- // Logout from auth store
103
+
104
+ // Logout from auth store (clears user, isAuthenticated, etc.)
97
105
  logoutStore();
106
+
107
+ // Force KeyManager cache invalidation
108
+ KeyManager.invalidateCache();
109
+
110
+ if (__DEV__ && logger) {
111
+ logger('Session state cleared for new identity');
112
+ }
98
113
  }
99
114
  },
100
- [clearSessionState, logoutStore]
115
+ [clearSessionState, logoutStore, logger]
101
116
  );
102
117
 
103
118
  /**
@@ -311,10 +326,18 @@ export const useAuthOperations = ({
311
326
  // CRITICAL: Get old public key before creating new identity
312
327
  // If identity changes, we must clear all session data to prevent data leakage
313
328
  const oldPublicKey = await KeyManager.getPublicKey().catch(() => null);
329
+
330
+ if (__DEV__ && logger) {
331
+ logger('Creating new identity', { hadPreviousIdentity: !!oldPublicKey });
332
+ }
314
333
 
315
334
  // Generate new key pair directly (works offline)
316
335
  const { publicKey, privateKey } = await KeyManager.generateKeyPair();
317
336
  await KeyManager.importKeyPair(privateKey);
337
+
338
+ if (__DEV__ && logger) {
339
+ logger('Identity keys generated', { publicKey: publicKey.substring(0, 16) + '...' });
340
+ }
318
341
 
319
342
  // Clear sessions if identity changed (prevents data leakage)
320
343
  await clearSessionsIfIdentityChanged(oldPublicKey, publicKey);
@@ -331,14 +354,18 @@ export const useAuthOperations = ({
331
354
  // Mark as synced (Zustand store + storage)
332
355
  await storage.setItem('oxy_identity_synced', 'true');
333
356
  setIdentitySynced(true);
357
+
358
+ if (__DEV__ && logger) {
359
+ logger('Identity synced with server successfully');
360
+ }
334
361
 
335
362
  return {
336
363
  synced: true,
337
364
  };
338
365
  } catch (syncError) {
339
366
  // Offline or server error - identity is created locally but not synced
340
- if (__DEV__) {
341
- console.log('[Auth] Identity created locally, will sync when online:', syncError);
367
+ if (__DEV__ && logger) {
368
+ logger('Identity created locally (offline), will sync when online', syncError);
342
369
  }
343
370
 
344
371
  return {
@@ -489,6 +516,13 @@ export const useAuthOperations = ({
489
516
  // CRITICAL: Get old public key before importing new identity
490
517
  // If identity changes, we must clear all session data to prevent data leakage
491
518
  const oldPublicKey = await KeyManager.getPublicKey().catch(() => null);
519
+
520
+ if (__DEV__ && logger) {
521
+ logger('Importing identity from backup', {
522
+ hadPreviousIdentity: !!oldPublicKey,
523
+ backupPublicKey: backupData.publicKey.substring(0, 16) + '...'
524
+ });
525
+ }
492
526
 
493
527
  // Decrypt private key from backup data
494
528
  const Crypto = await import('expo-crypto');
@@ -525,6 +559,10 @@ export const useAuthOperations = ({
525
559
 
526
560
  // Import the key pair
527
561
  const publicKey = await KeyManager.importKeyPair(privateKey);
562
+
563
+ if (__DEV__ && logger) {
564
+ logger('Identity keys imported', { publicKey: publicKey.substring(0, 16) + '...' });
565
+ }
528
566
 
529
567
  // Verify public key matches
530
568
  if (publicKey !== backupData.publicKey) {
@@ -31,6 +31,7 @@ export interface TransferState {
31
31
  markRestored: () => void;
32
32
  cleanupExpired: () => void;
33
33
  reset: () => void;
34
+ clearAll: () => void; // Alias for reset - use when clearing on identity change for semantic clarity
34
35
  }
35
36
 
36
37
  const FIFTEEN_MINUTES = 15 * 60 * 1000;
@@ -178,6 +179,11 @@ export const useTransferStore = create<TransferState>((set, get) => ({
178
179
  reset: () => {
179
180
  set(initialState);
180
181
  },
182
+
183
+ clearAll: () => {
184
+ // Delegate to reset to maintain single source of truth
185
+ get().reset();
186
+ },
181
187
  }));
182
188
 
183
189
  /**