@oxyhq/services 5.16.30 → 5.16.32

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.
Files changed (63) hide show
  1. package/lib/commonjs/core/services/SessionService.js +2 -1
  2. package/lib/commonjs/core/services/SessionService.js.map +1 -1
  3. package/lib/commonjs/core/services/TokenService.js +17 -9
  4. package/lib/commonjs/core/services/TokenService.js.map +1 -1
  5. package/lib/commonjs/models/interfaces.js +9 -8
  6. package/lib/commonjs/models/interfaces.js.map +1 -1
  7. package/lib/commonjs/ui/context/OxyContext.js +40 -4
  8. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  9. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +25 -14
  10. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  11. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +12 -4
  12. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -1
  13. package/lib/commonjs/ui/hooks/useSessionManagement.js +8 -0
  14. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  15. package/lib/commonjs/ui/utils/sessionHelpers.js +26 -11
  16. package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
  17. package/lib/commonjs/utils/sessionUtils.js +8 -1
  18. package/lib/commonjs/utils/sessionUtils.js.map +1 -1
  19. package/lib/module/core/services/SessionService.js +2 -1
  20. package/lib/module/core/services/SessionService.js.map +1 -1
  21. package/lib/module/core/services/TokenService.js +17 -9
  22. package/lib/module/core/services/TokenService.js.map +1 -1
  23. package/lib/module/models/interfaces.js +9 -8
  24. package/lib/module/models/interfaces.js.map +1 -1
  25. package/lib/module/ui/context/OxyContext.js +40 -4
  26. package/lib/module/ui/context/OxyContext.js.map +1 -1
  27. package/lib/module/ui/context/hooks/useAuthOperations.js +25 -14
  28. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  29. package/lib/module/ui/hooks/queries/useServicesQueries.js +13 -5
  30. package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -1
  31. package/lib/module/ui/hooks/useSessionManagement.js +8 -0
  32. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  33. package/lib/module/ui/utils/sessionHelpers.js +26 -11
  34. package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
  35. package/lib/module/utils/sessionUtils.js +8 -1
  36. package/lib/module/utils/sessionUtils.js.map +1 -1
  37. package/lib/typescript/core/services/SessionService.d.ts +4 -2
  38. package/lib/typescript/core/services/SessionService.d.ts.map +1 -1
  39. package/lib/typescript/core/services/TokenService.d.ts +8 -3
  40. package/lib/typescript/core/services/TokenService.d.ts.map +1 -1
  41. package/lib/typescript/models/interfaces.d.ts +9 -8
  42. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  43. package/lib/typescript/models/session.d.ts +4 -2
  44. package/lib/typescript/models/session.d.ts.map +1 -1
  45. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  46. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  47. package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
  48. package/lib/typescript/ui/hooks/useSessionManagement.d.ts.map +1 -1
  49. package/lib/typescript/ui/utils/sessionHelpers.d.ts +6 -2
  50. package/lib/typescript/ui/utils/sessionHelpers.d.ts.map +1 -1
  51. package/lib/typescript/utils/sessionUtils.d.ts.map +1 -1
  52. package/package.json +1 -1
  53. package/src/core/services/SessionService.ts +4 -2
  54. package/src/core/services/TokenService.ts +18 -10
  55. package/src/models/interfaces.ts +11 -10
  56. package/src/models/session.ts +5 -3
  57. package/src/ui/context/OxyContext.tsx +56 -20
  58. package/src/ui/context/hooks/useAuthOperations.ts +23 -15
  59. package/src/ui/hooks/auth/index.ts +1 -0
  60. package/src/ui/hooks/queries/useServicesQueries.ts +8 -3
  61. package/src/ui/hooks/useSessionManagement.ts +8 -1
  62. package/src/ui/utils/sessionHelpers.ts +32 -15
  63. package/src/utils/sessionUtils.ts +8 -1
@@ -222,7 +222,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
222
222
  // CRITICAL: Invalidate cache on app startup to ensure fresh state check
223
223
  // This prevents stale cache from previous session from showing incorrect state
224
224
  KeyManager.invalidateCache();
225
-
225
+
226
226
  // Check if identity exists and verify integrity
227
227
  const hasIdentity = await KeyManager.hasIdentity();
228
228
  if (hasIdentity) {
@@ -421,7 +421,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
421
421
  if (pendingTransfers.length > 0) {
422
422
  const activeTransferId = getActiveTransferId();
423
423
  const hasActiveTransfer = activeTransferId && pendingTransfers.some((t: { transferId: string; data: any }) => t.transferId === activeTransferId);
424
-
424
+
425
425
  if (hasActiveTransfer) {
426
426
  throw new Error(
427
427
  'Cannot delete identity: An active identity transfer is in progress. ' +
@@ -485,11 +485,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
485
485
  if (wasOffline) {
486
486
  const now = Date.now();
487
487
  const timeSinceLastLog = now - lastReconnectionLog;
488
-
488
+
489
489
  if (timeSinceLastLog >= RECONNECTION_LOG_DEBOUNCE_MS) {
490
490
  logger('Network reconnected, checking identity sync...');
491
491
  lastReconnectionLog = now;
492
-
492
+
493
493
  // Sync identity first (if not synced)
494
494
  try {
495
495
  const hasIdentityValue = await hasIdentity();
@@ -520,7 +520,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
520
520
  // This is handled by useCheckPendingTransfers hook which runs automatically
521
521
  // when authenticated and online
522
522
  }
523
-
523
+
524
524
  // TanStack Query will automatically retry pending mutations
525
525
  // Reset flag immediately after processing (whether logged or not)
526
526
  wasOffline = false;
@@ -580,6 +580,22 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
580
580
  setTokenReady(false);
581
581
 
582
582
  try {
583
+ // CRITICAL: Get current local identity (publicKey) before restoring sessions
584
+ // Sessions must belong to the current local identity stored in Accounts app
585
+ let currentPublicKey: string | null = null;
586
+ try {
587
+ if (Platform.OS !== 'web') {
588
+ // Only check identity on native platforms (web doesn't have local identity)
589
+ // KeyManager is already imported at the top of the file
590
+ currentPublicKey = await KeyManager.getPublicKey();
591
+ }
592
+ } catch (identityError) {
593
+ if (__DEV__) {
594
+ logger('Failed to get current local identity during session restoration', identityError);
595
+ }
596
+ // Continue without identity check if it fails (e.g., on web platform)
597
+ }
598
+
583
599
  const storedSessionIdsJson = await storage.getItem(storageKeys.sessionIds);
584
600
  const storedSessionIds: string[] = storedSessionIdsJson ? JSON.parse(storedSessionIdsJson) : [];
585
601
  const storedActiveSessionId = await storage.getItem(storageKeys.activeSessionId);
@@ -592,12 +608,25 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
592
608
  const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation: true });
593
609
  if (validation?.valid && validation.user) {
594
610
  const now = new Date();
611
+ // user.id is now publicKey (canonical identity)
612
+ const sessionPublicKey = validation.user.publicKey || validation.user.id || '';
613
+
614
+ // IDENTITY BINDING: Only restore sessions that match current local identity
615
+ // If we have a current local identity, filter out sessions that don't match
616
+ if (currentPublicKey && sessionPublicKey !== currentPublicKey) {
617
+ if (__DEV__) {
618
+ logger(`Skipping session ${sessionId.substring(0, 8)}... - belongs to different identity (${sessionPublicKey.substring(0, 16)}... vs ${currentPublicKey.substring(0, 16)}...)`);
619
+ }
620
+ continue; // Skip this session - it belongs to a different identity
621
+ }
622
+
595
623
  validSessions.push({
596
624
  sessionId,
597
625
  deviceId: '',
598
626
  expiresAt: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
599
627
  lastActive: now.toISOString(),
600
- userId: validation.user.id?.toString() ?? '',
628
+ publicKey: sessionPublicKey, // Canonical user identity
629
+ userId: validation.user.id?.toString(), // Optional MongoDB ObjectId for backward compatibility
601
630
  isCurrent: sessionId === storedActiveSessionId,
602
631
  });
603
632
  }
@@ -615,6 +644,13 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
615
644
 
616
645
  if (validSessions.length > 0) {
617
646
  updateSessions(validSessions, { merge: false });
647
+ } else if (currentPublicKey && storedSessionIds.length > 0) {
648
+ // All sessions were filtered out due to identity mismatch - clear stored sessions
649
+ if (__DEV__) {
650
+ logger('All stored sessions belong to different identity - clearing session storage');
651
+ }
652
+ await storage.removeItem(storageKeys.sessionIds);
653
+ await storage.removeItem(storageKeys.activeSessionId);
618
654
  }
619
655
  }
620
656
 
@@ -726,9 +762,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
726
762
  return isAuthenticatedFromStore || isAuthenticatedFromSessions;
727
763
  }, [isAuthenticatedFromStore, isAuthenticatedFromSessions]);
728
764
 
729
- // Get userId from JWT token (MongoDB ObjectId) for socket room matching
730
- // user.id is set to publicKey for compatibility, but socket rooms use MongoDB ObjectId
731
- // The JWT token's userId field contains the MongoDB ObjectId
765
+ // Get userId from JWT token for socket room matching
766
+ // Note: JWT token may contain MongoDB ObjectId internally, but user.id is publicKey (canonical identity)
767
+ // Socket rooms may need internal mapping from publicKey to MongoDB ObjectId if backend requires it
732
768
  const userId = oxyServices.getCurrentUserId() || user?.id;
733
769
 
734
770
  // Use Zustand store for transfer state management
@@ -748,16 +784,16 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
748
784
  // Load transfer codes
749
785
  const storedCodes = await storage.getItem(TRANSFER_CODES_STORAGE_KEY);
750
786
  const storedActiveTransferId = await storage.getItem(ACTIVE_TRANSFER_STORAGE_KEY);
751
-
787
+
752
788
  const parsedCodes = storedCodes ? JSON.parse(storedCodes) : {};
753
789
  const activeTransferId = storedActiveTransferId || null;
754
-
790
+
755
791
  // Restore to Zustand store (store handles validation and expiration)
756
792
  restoreFromStorage(parsedCodes, activeTransferId);
757
793
  markRestored();
758
-
794
+
759
795
  if (__DEV__ && Object.keys(parsedCodes).length > 0) {
760
- logger('Restored transfer codes from storage', {
796
+ logger('Restored transfer codes from storage', {
761
797
  count: Object.keys(parsedCodes).length,
762
798
  hasActiveTransfer: !!activeTransferId,
763
799
  });
@@ -782,7 +818,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
782
818
  const persistTransferCodes = async () => {
783
819
  try {
784
820
  await storage.setItem(TRANSFER_CODES_STORAGE_KEY, JSON.stringify(transferCodes));
785
-
821
+
786
822
  if (activeTransferId) {
787
823
  await storage.setItem(ACTIVE_TRANSFER_STORAGE_KEY, activeTransferId);
788
824
  } else {
@@ -810,7 +846,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
810
846
  // Transfer code management functions using Zustand store
811
847
  const storeTransferCode = useCallback(async (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => {
812
848
  storeTransferCodeStore(transferId, code, sourceDeviceId, publicKey);
813
-
849
+
814
850
  if (__DEV__) {
815
851
  logger('Stored transfer code', { transferId, sourceDeviceId, publicKey: publicKey.substring(0, 16) + '...' });
816
852
  }
@@ -822,7 +858,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
822
858
 
823
859
  const updateTransferState = useCallback(async (transferId: string, state: 'pending' | 'completed' | 'failed') => {
824
860
  updateTransferStateStore(transferId, state);
825
-
861
+
826
862
  if (__DEV__) {
827
863
  logger('Updated transfer state', { transferId, state });
828
864
  }
@@ -830,7 +866,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
830
866
 
831
867
  const clearTransferCode = useCallback(async (transferId: string) => {
832
868
  clearTransferCodeStore(transferId);
833
-
869
+
834
870
  if (__DEV__) {
835
871
  logger('Cleared transfer code', { transferId });
836
872
  }
@@ -872,7 +908,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
872
908
  const storedTransfer = getTransferCode(data.transferId);
873
909
 
874
910
  if (!storedTransfer) {
875
- logger('Transfer code not found for transferId', {
911
+ logger('Transfer code not found for transferId', {
876
912
  transferId: data.transferId,
877
913
  });
878
914
  toast.error('Transfer verification failed: Code not found. Identity will not be deleted.');
@@ -894,7 +930,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
894
930
  // Verify deviceId matches - very lenient since publicKey is the critical check
895
931
  // If publicKey matches, we allow deletion even if deviceId doesn't match exactly
896
932
  // This handles cases where deviceId might not be available or slightly different
897
- const deviceIdMatches =
933
+ const deviceIdMatches =
898
934
  // Exact match
899
935
  (data.sourceDeviceId && data.sourceDeviceId === currentDeviceId) ||
900
936
  // Stored sourceDeviceId matches current deviceId
@@ -974,7 +1010,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
974
1010
  }
975
1011
 
976
1012
  await deleteIdentityAndClearAccount(false, false, true);
977
-
1013
+
978
1014
  // Verify identity was actually deleted
979
1015
  const identityDeleted = !(await KeyManager.hasIdentity());
980
1016
  if (!identityDeleted) {
@@ -146,9 +146,9 @@ export const useAuthOperations = ({
146
146
  const localDeviceId = `device_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
147
147
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // 7 days
148
148
 
149
- // Create minimal user object with publicKey as id
149
+ // Create minimal user object with publicKey as id (canonical identity)
150
150
  fullUser = {
151
- id: publicKey, // Use publicKey as id (per migration document)
151
+ id: publicKey, // publicKey is the canonical user identity
152
152
  publicKey,
153
153
  username: '',
154
154
  privacySettings: {},
@@ -170,7 +170,7 @@ export const useAuthOperations = ({
170
170
  deviceId: localDeviceId,
171
171
  expiresAt,
172
172
  lastActive: new Date().toISOString(),
173
- userId: publicKey,
173
+ publicKey, // Canonical user identity
174
174
  isCurrent: true,
175
175
  };
176
176
 
@@ -204,17 +204,10 @@ export const useAuthOperations = ({
204
204
  // Get full user data
205
205
  fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
206
206
 
207
- // IMPORTANT: user.id should be MongoDB ObjectId, not publicKey
208
- // The API should return the correct id (ObjectId) from the database
209
- // If it doesn't, we need to fix the API, not work around it here
210
- // Validate that id is ObjectId format (24 hex characters)
211
- if (fullUser.id && !/^[0-9a-fA-F]{24}$/.test(fullUser.id)) {
212
- console.warn('[useAuthOperations] User.id is not MongoDB ObjectId format:', {
213
- id: fullUser.id.substring(0, 20),
214
- publicKey: fullUser.publicKey.substring(0, 20),
215
- message: 'API should return MongoDB ObjectId as user.id, not publicKey'
216
- });
217
- // Don't override - let the API fix this issue
207
+ // user.id is now publicKey (canonical identity) - this is correct
208
+ // Ensure publicKey field is set for consistency
209
+ if (!fullUser.publicKey && fullUser.id) {
210
+ fullUser.publicKey = fullUser.id;
218
211
  }
219
212
 
220
213
  // Fetch device sessions
@@ -222,7 +215,8 @@ export const useAuthOperations = ({
222
215
  try {
223
216
  allDeviceSessions = await fetchSessionsWithFallback(oxyServices, sessionResponse.sessionId, {
224
217
  fallbackDeviceId: sessionResponse.deviceId,
225
- fallbackUserId: fullUser.id,
218
+ fallbackUserId: fullUser.id, // Still pass for backward compatibility
219
+ fallbackPublicKey: fullUser.publicKey || fullUser.id, // publicKey is canonical identity
226
220
  logger,
227
221
  });
228
222
  } catch (error) {
@@ -292,6 +286,13 @@ export const useAuthOperations = ({
292
286
  setAuthState({ isLoading: true, error: null });
293
287
 
294
288
  try {
289
+ // SESSION CLEANUP: Clear all sessions before creating new identity
290
+ // New identity means old sessions are no longer valid
291
+ await clearSessionState();
292
+ if (__DEV__ && logger) {
293
+ logger('Cleared all sessions before creating new identity');
294
+ }
295
+
295
296
  // Generate new key pair directly (works offline)
296
297
  const { publicKey, privateKey } = await KeyManager.generateKeyPair();
297
298
  await KeyManager.importKeyPair(privateKey);
@@ -463,6 +464,13 @@ export const useAuthOperations = ({
463
464
  setAuthState({ isLoading: true, error: null });
464
465
 
465
466
  try {
467
+ // SESSION CLEANUP: Clear all sessions before importing new identity
468
+ // Importing a different identity means old sessions are no longer valid
469
+ await clearSessionState();
470
+ if (__DEV__ && logger) {
471
+ logger('Cleared all sessions before importing identity');
472
+ }
473
+
466
474
  // Decrypt private key from backup data
467
475
  const Crypto = await import('expo-crypto');
468
476
 
@@ -4,3 +4,4 @@
4
4
  export { useUsernameValidation, USERNAME_MIN_LENGTH, USERNAME_REGEX, USERNAME_FORMAT_ERROR, USERNAME_DEBOUNCE_MS } from './useUsernameValidation';
5
5
  export type { UsernameValidationResult } from './useUsernameValidation';
6
6
 
7
+
@@ -8,7 +8,7 @@ import { fetchSessionsWithFallback, mapSessionsToClient } from '../../utils/sess
8
8
  * Get all active sessions for the current user
9
9
  */
10
10
  export const useSessions = (userId?: string, options?: { enabled?: boolean }) => {
11
- const { oxyServices, activeSessionId } = useOxy();
11
+ const { oxyServices, activeSessionId, user } = useOxy();
12
12
 
13
13
  return useQuery({
14
14
  queryKey: queryKeys.sessions.list(userId),
@@ -17,12 +17,14 @@ export const useSessions = (userId?: string, options?: { enabled?: boolean }) =>
17
17
  throw new Error('No active session');
18
18
  }
19
19
 
20
+ const publicKey = user?.publicKey || user?.id || ''; // user.id is now publicKey
20
21
  const sessions = await fetchSessionsWithFallback(oxyServices, activeSessionId, {
21
22
  fallbackDeviceId: undefined,
22
23
  fallbackUserId: userId,
24
+ fallbackPublicKey: publicKey,
23
25
  });
24
26
 
25
- return mapSessionsToClient(sessions, activeSessionId);
27
+ return sessions; // Already mapped by fetchSessionsWithFallback
26
28
  },
27
29
  enabled: (options?.enabled !== false) && !!activeSessionId,
28
30
  staleTime: 2 * 60 * 1000, // 2 minutes (sessions change frequently)
@@ -49,12 +51,15 @@ export const useSession = (sessionId: string | null, options?: { enabled?: boole
49
51
  }
50
52
 
51
53
  const now = new Date();
54
+ // user.id is now publicKey (canonical identity)
55
+ const publicKey = validation.user.publicKey || validation.user.id || '';
52
56
  return {
53
57
  sessionId,
54
58
  deviceId: '', // Device ID not available from validation response
55
59
  expiresAt: validation.expiresAt || new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
56
60
  lastActive: validation.lastActivity || now.toISOString(),
57
- userId: validation.user.id?.toString() ?? '',
61
+ publicKey, // Canonical user identity
62
+ userId: validation.user.id?.toString(), // Optional MongoDB ObjectId
58
63
  isCurrent: false,
59
64
  } as ClientSession;
60
65
  },
@@ -255,8 +255,10 @@ export const useSessionManagement = ({
255
255
  await activateSession(sessionId, user);
256
256
 
257
257
  try {
258
+ const publicKey = user.publicKey || user.id || ''; // user.id is now publicKey
258
259
  const deviceSessions = await fetchSessionsWithFallback(oxyServices, sessionId, {
259
- fallbackUserId: user.id,
260
+ fallbackUserId: user.id, // Still pass for backward compatibility
261
+ fallbackPublicKey: publicKey,
260
262
  logger,
261
263
  });
262
264
  updateSessions(deviceSessions, { merge: true });
@@ -330,8 +332,13 @@ export const useSessionManagement = ({
330
332
 
331
333
  const refreshPromise = (async () => {
332
334
  try {
335
+ // Get publicKey from active session or current user
336
+ const activeSession = sessions.find(s => s.sessionId === activeSessionId);
337
+ const publicKey = activeSession?.publicKey || activeUserId || ''; // Fallback to userId if publicKey not available
338
+
333
339
  const deviceSessions = await fetchSessionsWithFallback(oxyServices, activeSessionId, {
334
340
  fallbackUserId: activeUserId,
341
+ fallbackPublicKey: publicKey,
335
342
  logger,
336
343
  });
337
344
  updateSessions(deviceSessions, { merge: true });
@@ -7,14 +7,16 @@ interface DeviceSession {
7
7
  deviceName?: string;
8
8
  expiresAt?: string;
9
9
  lastActive?: string;
10
- user?: { id?: string; _id?: { toString(): string } };
10
+ user?: { id?: string; publicKey?: string; _id?: { toString(): string } };
11
11
  userId?: string;
12
+ publicKey?: string;
12
13
  isCurrent?: boolean;
13
14
  }
14
15
 
15
16
  export interface FetchSessionsWithFallbackOptions {
16
17
  fallbackDeviceId?: string;
17
18
  fallbackUserId?: string;
19
+ fallbackPublicKey?: string; // Canonical user identity
18
20
  logger?: (message: string, error?: unknown) => void;
19
21
  }
20
22
 
@@ -37,27 +39,41 @@ export interface SessionValidationResult {
37
39
  * @param sessions - Raw session array returned from the API
38
40
  * @param fallbackDeviceId - Device identifier to use when missing from payload
39
41
  * @param fallbackUserId - User identifier to use when missing from payload
42
+ * @param fallbackPublicKey - Public key to use when missing from payload (canonical identity)
40
43
  */
41
44
  export const mapSessionsToClient = (
42
45
  sessions: DeviceSession[],
43
46
  fallbackDeviceId?: string,
44
47
  fallbackUserId?: string,
48
+ fallbackPublicKey?: string,
45
49
  ): ClientSession[] => {
46
50
  const now = new Date();
47
51
 
48
- return sessions.map((session) => ({
49
- sessionId: session.sessionId,
50
- deviceId: session.deviceId || fallbackDeviceId || '',
51
- expiresAt: session.expiresAt || new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
52
- lastActive: session.lastActive || now.toISOString(),
53
- userId:
54
- session.user?.id ||
52
+ return sessions.map((session) => {
53
+ // publicKey is the canonical user identity (user.id now equals publicKey)
54
+ // Extract from user.id (which is now publicKey), user.publicKey, or session.publicKey
55
+ const publicKey =
56
+ session.user?.id || // user.id is now publicKey (canonical identifier)
57
+ session.user?.publicKey ||
58
+ session.publicKey ||
59
+ fallbackPublicKey ||
60
+ '';
61
+
62
+ // userId is MongoDB ObjectId (internal backend reference, optional)
63
+ const userId =
55
64
  session.userId ||
56
- (session.user?._id ? session.user._id.toString() : undefined) ||
57
- fallbackUserId ||
58
- '',
59
- isCurrent: Boolean(session.isCurrent),
60
- }));
65
+ (session.user?._id ? session.user._id.toString() : undefined);
66
+
67
+ return {
68
+ sessionId: session.sessionId,
69
+ deviceId: session.deviceId || fallbackDeviceId || '',
70
+ expiresAt: session.expiresAt || new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
71
+ lastActive: session.lastActive || now.toISOString(),
72
+ publicKey, // Canonical user identity - REQUIRED
73
+ userId, // MongoDB ObjectId (optional, for backward compatibility)
74
+ isCurrent: Boolean(session.isCurrent),
75
+ };
76
+ });
61
77
  };
62
78
 
63
79
  /**
@@ -73,19 +89,20 @@ export const fetchSessionsWithFallback = async (
73
89
  {
74
90
  fallbackDeviceId,
75
91
  fallbackUserId,
92
+ fallbackPublicKey,
76
93
  logger,
77
94
  }: FetchSessionsWithFallbackOptions = {},
78
95
  ): Promise<ClientSession[]> => {
79
96
  try {
80
97
  const deviceSessions = await oxyServices.getDeviceSessions(sessionId);
81
- return mapSessionsToClient(deviceSessions, fallbackDeviceId, fallbackUserId);
98
+ return mapSessionsToClient(deviceSessions, fallbackDeviceId, fallbackUserId, fallbackPublicKey);
82
99
  } catch (error) {
83
100
  if (__DEV__ && logger) {
84
101
  logger('Failed to get device sessions, falling back to user sessions', error);
85
102
  }
86
103
 
87
104
  const userSessions = await oxyServices.getSessionsBySessionId(sessionId);
88
- return mapSessionsToClient(userSessions, fallbackDeviceId, fallbackUserId);
105
+ return mapSessionsToClient(userSessions, fallbackDeviceId, fallbackUserId, fallbackPublicKey);
89
106
  }
90
107
  };
91
108
 
@@ -12,12 +12,19 @@ import type { ClientSession } from '../models/session';
12
12
  */
13
13
  export function normalizeSession(session: Partial<ClientSession> & { sessionId: string }): ClientSession {
14
14
  const now = new Date().toISOString();
15
+
16
+ // publicKey is required (canonical user identity)
17
+ if (!session.publicKey) {
18
+ throw new Error(`Session ${session.sessionId} is missing required publicKey field`);
19
+ }
20
+
15
21
  return {
16
22
  sessionId: session.sessionId,
17
23
  deviceId: session.deviceId || '',
18
24
  expiresAt: session.expiresAt || now,
19
25
  lastActive: session.lastActive || now,
20
- userId: session.userId || '',
26
+ publicKey: session.publicKey, // Canonical user identity - required
27
+ userId: session.userId, // Optional MongoDB ObjectId for backward compatibility
21
28
  };
22
29
  }
23
30