@oxyhq/services 5.16.29 → 5.16.31

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 (145) 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/index.js +64 -0
  6. package/lib/commonjs/index.js.map +1 -1
  7. package/lib/commonjs/models/interfaces.js +9 -8
  8. package/lib/commonjs/models/interfaces.js.map +1 -1
  9. package/lib/commonjs/ui/context/OxyContext.js +40 -4
  10. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  11. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +25 -14
  12. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  13. package/lib/commonjs/ui/hooks/auth/index.js +37 -0
  14. package/lib/commonjs/ui/hooks/auth/index.js.map +1 -0
  15. package/lib/commonjs/ui/hooks/auth/useUsernameValidation.js +171 -0
  16. package/lib/commonjs/ui/hooks/auth/useUsernameValidation.js.map +1 -0
  17. package/lib/commonjs/ui/hooks/index.js +20 -0
  18. package/lib/commonjs/ui/hooks/index.js.map +1 -1
  19. package/lib/commonjs/ui/hooks/mutations/index.js +12 -0
  20. package/lib/commonjs/ui/hooks/mutations/index.js.map +1 -1
  21. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +45 -1
  22. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  23. package/lib/commonjs/ui/hooks/queries/index.js +12 -0
  24. package/lib/commonjs/ui/hooks/queries/index.js.map +1 -1
  25. package/lib/commonjs/ui/hooks/queries/queryKeys.js +3 -1
  26. package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
  27. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +43 -1
  28. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
  29. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +12 -4
  30. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -1
  31. package/lib/commonjs/ui/hooks/useSessionManagement.js +8 -0
  32. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  33. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +76 -97
  34. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -1
  35. package/lib/commonjs/ui/utils/sessionHelpers.js +26 -11
  36. package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
  37. package/lib/commonjs/utils/sessionUtils.js +8 -1
  38. package/lib/commonjs/utils/sessionUtils.js.map +1 -1
  39. package/lib/module/core/services/SessionService.js +2 -1
  40. package/lib/module/core/services/SessionService.js.map +1 -1
  41. package/lib/module/core/services/TokenService.js +17 -9
  42. package/lib/module/core/services/TokenService.js.map +1 -1
  43. package/lib/module/index.js +3 -3
  44. package/lib/module/index.js.map +1 -1
  45. package/lib/module/models/interfaces.js +9 -8
  46. package/lib/module/models/interfaces.js.map +1 -1
  47. package/lib/module/ui/context/OxyContext.js +40 -4
  48. package/lib/module/ui/context/OxyContext.js.map +1 -1
  49. package/lib/module/ui/context/hooks/useAuthOperations.js +25 -14
  50. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  51. package/lib/module/ui/hooks/auth/index.js +7 -0
  52. package/lib/module/ui/hooks/auth/index.js.map +1 -0
  53. package/lib/module/ui/hooks/auth/useUsernameValidation.js +167 -0
  54. package/lib/module/ui/hooks/auth/useUsernameValidation.js.map +1 -0
  55. package/lib/module/ui/hooks/index.js +1 -0
  56. package/lib/module/ui/hooks/index.js.map +1 -1
  57. package/lib/module/ui/hooks/mutations/index.js +1 -1
  58. package/lib/module/ui/hooks/mutations/index.js.map +1 -1
  59. package/lib/module/ui/hooks/mutations/useAccountMutations.js +42 -0
  60. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  61. package/lib/module/ui/hooks/queries/index.js +1 -1
  62. package/lib/module/ui/hooks/queries/index.js.map +1 -1
  63. package/lib/module/ui/hooks/queries/queryKeys.js +3 -1
  64. package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
  65. package/lib/module/ui/hooks/queries/useAccountQueries.js +40 -0
  66. package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
  67. package/lib/module/ui/hooks/queries/useServicesQueries.js +13 -5
  68. package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -1
  69. package/lib/module/ui/hooks/useSessionManagement.js +8 -0
  70. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  71. package/lib/module/ui/screens/PrivacySettingsScreen.js +77 -98
  72. package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -1
  73. package/lib/module/ui/utils/sessionHelpers.js +26 -11
  74. package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
  75. package/lib/module/utils/sessionUtils.js +8 -1
  76. package/lib/module/utils/sessionUtils.js.map +1 -1
  77. package/lib/typescript/core/services/SessionService.d.ts +4 -2
  78. package/lib/typescript/core/services/SessionService.d.ts.map +1 -1
  79. package/lib/typescript/core/services/TokenService.d.ts +8 -3
  80. package/lib/typescript/core/services/TokenService.d.ts.map +1 -1
  81. package/lib/typescript/index.d.ts +4 -2
  82. package/lib/typescript/index.d.ts.map +1 -1
  83. package/lib/typescript/models/interfaces.d.ts +9 -8
  84. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  85. package/lib/typescript/models/session.d.ts +4 -2
  86. package/lib/typescript/models/session.d.ts.map +1 -1
  87. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  88. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  89. package/lib/typescript/ui/hooks/auth/index.d.ts +6 -0
  90. package/lib/typescript/ui/hooks/auth/index.d.ts.map +1 -0
  91. package/lib/typescript/ui/hooks/auth/useUsernameValidation.d.ts +32 -0
  92. package/lib/typescript/ui/hooks/auth/useUsernameValidation.d.ts.map +1 -0
  93. package/lib/typescript/ui/hooks/index.d.ts +1 -0
  94. package/lib/typescript/ui/hooks/index.d.ts.map +1 -1
  95. package/lib/typescript/ui/hooks/mutations/index.d.ts +1 -1
  96. package/lib/typescript/ui/hooks/mutations/index.d.ts.map +1 -1
  97. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts +12 -0
  98. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  99. package/lib/typescript/ui/hooks/queries/index.d.ts +1 -1
  100. package/lib/typescript/ui/hooks/queries/index.d.ts.map +1 -1
  101. package/lib/typescript/ui/hooks/queries/queryKeys.d.ts +2 -0
  102. package/lib/typescript/ui/hooks/queries/queryKeys.d.ts.map +1 -1
  103. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts +12 -0
  104. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  105. package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
  106. package/lib/typescript/ui/hooks/useSessionManagement.d.ts.map +1 -1
  107. package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts.map +1 -1
  108. package/lib/typescript/ui/utils/sessionHelpers.d.ts +6 -2
  109. package/lib/typescript/ui/utils/sessionHelpers.d.ts.map +1 -1
  110. package/lib/typescript/utils/sessionUtils.d.ts.map +1 -1
  111. package/package.json +1 -1
  112. package/src/core/services/SessionService.ts +4 -2
  113. package/src/core/services/TokenService.ts +18 -10
  114. package/src/index.ts +6 -0
  115. package/src/models/interfaces.ts +11 -10
  116. package/src/models/session.ts +5 -3
  117. package/src/ui/context/OxyContext.tsx +56 -20
  118. package/src/ui/context/hooks/useAuthOperations.ts +23 -15
  119. package/src/ui/hooks/auth/index.ts +7 -0
  120. package/src/ui/hooks/auth/useUsernameValidation.ts +177 -0
  121. package/src/ui/hooks/index.ts +2 -1
  122. package/src/ui/hooks/mutations/index.ts +2 -0
  123. package/src/ui/hooks/mutations/useAccountMutations.ts +36 -0
  124. package/src/ui/hooks/queries/index.ts +2 -0
  125. package/src/ui/hooks/queries/queryKeys.ts +2 -0
  126. package/src/ui/hooks/queries/useAccountQueries.ts +34 -0
  127. package/src/ui/hooks/queries/useServicesQueries.ts +8 -3
  128. package/src/ui/hooks/useSessionManagement.ts +8 -1
  129. package/src/ui/screens/PrivacySettingsScreen.tsx +67 -101
  130. package/src/ui/utils/sessionHelpers.ts +32 -15
  131. package/src/utils/sessionUtils.ts +8 -1
  132. package/lib/commonjs/ui/context/hooks/useSessionManagement.js +0 -281
  133. package/lib/commonjs/ui/context/hooks/useSessionManagement.js.map +0 -1
  134. package/lib/commonjs/ui/context/hooks/useStorage.js +0 -79
  135. package/lib/commonjs/ui/context/hooks/useStorage.js.map +0 -1
  136. package/lib/module/ui/context/hooks/useSessionManagement.js +0 -276
  137. package/lib/module/ui/context/hooks/useSessionManagement.js.map +0 -1
  138. package/lib/module/ui/context/hooks/useStorage.js +0 -74
  139. package/lib/module/ui/context/hooks/useStorage.js.map +0 -1
  140. package/lib/typescript/ui/context/hooks/useSessionManagement.d.ts +0 -41
  141. package/lib/typescript/ui/context/hooks/useSessionManagement.d.ts.map +0 -1
  142. package/lib/typescript/ui/context/hooks/useStorage.d.ts +0 -22
  143. package/lib/typescript/ui/context/hooks/useStorage.d.ts.map +0 -1
  144. package/src/ui/context/hooks/useSessionManagement.ts +0 -401
  145. package/src/ui/context/hooks/useStorage.ts +0 -104
@@ -8,17 +8,17 @@
8
8
  * - Single storage location (no duplication)
9
9
  * - Automatic token refresh when expiring soon
10
10
  * - Type-safe token payload handling
11
- * - userId is always MongoDB ObjectId, never publicKey
11
+ * - publicKey is the canonical user identity
12
12
  */
13
13
 
14
14
  import { jwtDecode } from 'jwt-decode';
15
15
 
16
16
  /**
17
17
  * AccessTokenPayload - Matches the token payload structure from API
18
- * userId is always MongoDB ObjectId (24 hex characters), never publicKey
18
+ * publicKey is the canonical user identity (ECDSA secp256k1 public key)
19
19
  */
20
20
  interface AccessTokenPayload {
21
- userId: string; // MongoDB ObjectId - PRIMARY IDENTIFIER
21
+ publicKey: string; // User's public key - CANONICAL USER IDENTITY
22
22
  sessionId: string; // Session UUID
23
23
  deviceId: string; // Device identifier
24
24
  type: 'access';
@@ -116,21 +116,29 @@ class TokenService {
116
116
  }
117
117
 
118
118
  /**
119
- * Get userId from current access token
120
- * Returns MongoDB ObjectId (never publicKey)
119
+ * Get publicKey from current access token
120
+ * Returns the user's public key (canonical user identity)
121
121
  */
122
- getUserIdFromToken(): string | null {
122
+ getPublicKeyFromToken(): string | null {
123
123
  const token = this.tokenStore.accessToken;
124
124
  if (!token) return null;
125
125
 
126
126
  try {
127
127
  const decoded = jwtDecode<AccessTokenPayload>(token);
128
- return decoded.userId || null;
128
+ return decoded.publicKey || null;
129
129
  } catch {
130
130
  return null;
131
131
  }
132
132
  }
133
133
 
134
+ /**
135
+ * @deprecated Use getPublicKeyFromToken() instead. This method is kept for backward compatibility.
136
+ * Get userId from current access token (returns publicKey, not MongoDB ObjectId)
137
+ */
138
+ getUserIdFromToken(): string | null {
139
+ return this.getPublicKeyFromToken();
140
+ }
141
+
134
142
  /**
135
143
  * Refresh access token if expiring soon
136
144
  * Returns promise that resolves when token is refreshed (or already valid)
@@ -193,10 +201,10 @@ class TokenService {
193
201
  throw new Error('No access token in refresh response');
194
202
  }
195
203
 
196
- // Validate new token has correct userId format (ObjectId)
204
+ // Validate new token has publicKey (canonical user identity)
197
205
  const newDecoded = jwtDecode<AccessTokenPayload>(newToken);
198
- if (newDecoded.userId && !/^[0-9a-fA-F]{24}$/.test(newDecoded.userId)) {
199
- throw new Error(`Invalid userId format in refreshed token: ${newDecoded.userId.substring(0, 20)}...`);
206
+ if (!newDecoded.publicKey) {
207
+ throw new Error('Token missing publicKey after refresh');
200
208
  }
201
209
 
202
210
  this.setTokens(newToken);
package/src/index.ts CHANGED
@@ -137,6 +137,8 @@ export {
137
137
  export { useSessionSocket } from './ui/hooks/useSessionSocket';
138
138
  export { useAssets, setOxyAssetInstance } from './ui/hooks/useAssets';
139
139
  export { useFileDownloadUrl, setOxyFileUrlInstance } from './ui/hooks/useFileDownloadUrl';
140
+ export { useUsernameValidation, USERNAME_MIN_LENGTH, USERNAME_REGEX, USERNAME_FORMAT_ERROR, USERNAME_DEBOUNCE_MS } from './ui/hooks/auth';
141
+ export type { UsernameValidationResult } from './ui/hooks/auth';
140
142
 
141
143
  // UI hooks - Query hooks (TanStack Query)
142
144
  export {
@@ -148,6 +150,8 @@ export {
148
150
  useUserByUsername,
149
151
  useUsersBySessions,
150
152
  usePrivacySettings,
153
+ useBlockedUsers,
154
+ useRestrictedUsers,
151
155
  // Service queries
152
156
  useSessions,
153
157
  useSession,
@@ -167,6 +171,8 @@ export {
167
171
  useUpdateAccountSettings,
168
172
  useUpdatePrivacySettings,
169
173
  useUploadFile,
174
+ useUnblockUser,
175
+ useUnrestrictUser,
170
176
  // Service mutations
171
177
  useSwitchSession,
172
178
  useLogoutSession,
@@ -25,19 +25,20 @@ export interface OxyConfig {
25
25
  * User Model
26
26
  *
27
27
  * IMPORTANT:
28
- * - id: MongoDB ObjectId (24 hex characters) - PRIMARY IDENTIFIER for all internal operations
29
- * - publicKey: Cryptographic public key (130 hex characters) - LOOKUP KEY for authentication and identity operations
28
+ * - id: Public key (ECDSA secp256k1 public key, 130 hex characters) - CANONICAL USER IDENTITY
29
+ * - publicKey: Same as id (kept for backward compatibility and explicit clarity)
30
30
  *
31
- * Never use publicKey as an ID. Always use id (ObjectId) for:
32
- * - Database queries
33
- * - Session userId
34
- * - Token userId
35
- * - Socket room names
36
- * - API route parameters (unless explicitly doing publicKey lookup)
31
+ * publicKey is the canonical user identity across the ecosystem because it's:
32
+ * - Globally unique
33
+ * - Local-first (stored in Accounts app)
34
+ * - Avoids device-bound artifacts
35
+ *
36
+ * MongoDB _id remains internal to backend only and is not exposed in the User interface.
37
+ * Backend may use MongoDB ObjectId internally for database operations, but client always uses publicKey.
37
38
  */
38
39
  export interface User {
39
- id: string; // MongoDB ObjectId - PRIMARY IDENTIFIER (always 24 hex chars)
40
- publicKey: string; // Cryptographic public key - LOOKUP KEY (130 hex chars for secp256k1)
40
+ id: string; // Public key - CANONICAL USER IDENTITY (130 hex chars for secp256k1)
41
+ publicKey: string; // Same as id (kept for backward compatibility)
41
42
  username: string;
42
43
  email?: string;
43
44
  // Avatar file id (asset id)
@@ -2,15 +2,17 @@
2
2
  * Client Session Model
3
3
  *
4
4
  * IMPORTANT:
5
- * - userId: MongoDB ObjectId (24 hex characters), never publicKey
6
- * - Used for session management and user identification
5
+ * - publicKey: Canonical user identity (ECDSA secp256k1 public key) - PRIMARY IDENTIFIER
6
+ * - userId: MongoDB ObjectId (kept for backward compatibility, but publicKey is canonical)
7
+ * - Sessions are bound to the local identity (publicKey) stored in Accounts app
7
8
  */
8
9
  export interface ClientSession {
9
10
  sessionId: string;
10
11
  deviceId: string;
11
12
  expiresAt: string;
12
13
  lastActive: string;
13
- userId?: string; // MongoDB ObjectId - PRIMARY IDENTIFIER (never publicKey)
14
+ publicKey: string; // Canonical user identity - PRIMARY IDENTIFIER
15
+ userId?: string; // MongoDB ObjectId (internal backend reference, optional for compatibility)
14
16
  isCurrent?: boolean;
15
17
  }
16
18
 
@@ -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
 
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Auth-related hooks
3
+ */
4
+ export { useUsernameValidation, USERNAME_MIN_LENGTH, USERNAME_REGEX, USERNAME_FORMAT_ERROR, USERNAME_DEBOUNCE_MS } from './useUsernameValidation';
5
+ export type { UsernameValidationResult } from './useUsernameValidation';
6
+
7
+
@@ -0,0 +1,177 @@
1
+ import { useMemo } from 'react';
2
+ import { useQuery } from '@tanstack/react-query';
3
+ import type { OxyServices } from '../../../core';
4
+ import { handleHttpError, ErrorCodes } from '../../../utils/errorUtils';
5
+ import { useDebounce } from '../../../utils/hookUtils';
6
+
7
+ /**
8
+ * Username validation constants
9
+ */
10
+ export const USERNAME_MIN_LENGTH = 4;
11
+ export const USERNAME_REGEX = /^[a-z0-9]+$/i;
12
+ export const USERNAME_FORMAT_ERROR = 'You can use a-z, 0-9. Minimum length is 4 characters.';
13
+ export const USERNAME_DEBOUNCE_MS = 500;
14
+
15
+ /**
16
+ * Username validation result interface
17
+ */
18
+ export interface UsernameValidationResult {
19
+ isValid: boolean;
20
+ isAvailable: boolean | null; // null = not checked yet
21
+ error: string | null;
22
+ isChecking: boolean;
23
+ }
24
+
25
+ /**
26
+ * Validate username format using services validation utilities
27
+ */
28
+ function validateUsernameFormat(username: string): boolean {
29
+ // Use stricter validation: lowercase alphanumeric only, min 4 chars
30
+ return username.length >= USERNAME_MIN_LENGTH && USERNAME_REGEX.test(username);
31
+ }
32
+
33
+ /**
34
+ * Check if an error is a network or timeout error
35
+ */
36
+ function isNetworkOrTimeoutError(error: unknown): boolean {
37
+ const apiError = handleHttpError(error);
38
+ return (
39
+ apiError.code === ErrorCodes.NETWORK_ERROR ||
40
+ apiError.code === ErrorCodes.TIMEOUT ||
41
+ apiError.code === ErrorCodes.CONNECTION_FAILED
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Extract error message from an unknown error shape
47
+ */
48
+ function extractAuthErrorMessage(error: unknown, fallbackMessage = 'An error occurred'): string {
49
+ const apiError = handleHttpError(error);
50
+ return apiError.message || fallbackMessage;
51
+ }
52
+
53
+ /**
54
+ * Hook for username validation with debouncing and availability checking
55
+ *
56
+ * Uses TanStack Query for efficient API calls with:
57
+ * - Automatic request cancellation when username changes
58
+ * - Built-in caching (same username checked multiple times = cached result)
59
+ * - Request deduplication (multiple components checking same username = single request)
60
+ * - Proper error handling
61
+ *
62
+ * @param username - The username to validate
63
+ * @param oxyServices - OxyServices instance for API calls
64
+ * @returns Username validation state and result
65
+ */
66
+ export function useUsernameValidation(
67
+ username: string,
68
+ oxyServices: OxyServices | null
69
+ ): UsernameValidationResult {
70
+ // Debounce the username input to avoid excessive API calls
71
+ const debouncedUsername = useDebounce(username.trim().toLowerCase(), USERNAME_DEBOUNCE_MS);
72
+
73
+ // Validate format synchronously (no API call needed)
74
+ const isValid = useMemo(() => validateUsernameFormat(username), [username]);
75
+
76
+ // Determine if we should check availability
77
+ const shouldCheckAvailability = useMemo(() => {
78
+ if (!debouncedUsername || debouncedUsername.length < USERNAME_MIN_LENGTH) {
79
+ return false;
80
+ }
81
+ return validateUsernameFormat(debouncedUsername);
82
+ }, [debouncedUsername]);
83
+
84
+ // Use TanStack Query for the API call
85
+ // This provides automatic caching, request cancellation, and deduplication
86
+ const {
87
+ data: availabilityResult,
88
+ isLoading: isChecking,
89
+ error: queryError,
90
+ isFetching,
91
+ } = useQuery({
92
+ queryKey: ['username', 'availability', debouncedUsername],
93
+ queryFn: async () => {
94
+ if (!oxyServices) {
95
+ throw new Error('OxyServices not available');
96
+ }
97
+ return await oxyServices.checkUsernameAvailability(debouncedUsername);
98
+ },
99
+ enabled: shouldCheckAvailability && !!oxyServices,
100
+ staleTime: 5 * 60 * 1000, // Cache for 5 minutes (usernames don't change often)
101
+ gcTime: 30 * 60 * 1000, // Keep in cache for 30 minutes
102
+ retry: (failureCount, error) => {
103
+ // Don't retry on network/timeout errors (user might be offline)
104
+ if (isNetworkOrTimeoutError(error)) {
105
+ return false;
106
+ }
107
+ // Retry up to 2 times for other errors
108
+ return failureCount < 2;
109
+ },
110
+ retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 3000),
111
+ });
112
+
113
+ // Compute the result based on validation and query state
114
+ return useMemo(() => {
115
+ // If username is too short or invalid format, return early validation
116
+ if (!username || username.length < USERNAME_MIN_LENGTH) {
117
+ return {
118
+ isValid: false,
119
+ isAvailable: null,
120
+ error: null,
121
+ isChecking: false,
122
+ };
123
+ }
124
+
125
+ if (!isValid) {
126
+ return {
127
+ isValid: false,
128
+ isAvailable: false,
129
+ error: USERNAME_FORMAT_ERROR,
130
+ isChecking: false,
131
+ };
132
+ }
133
+
134
+ // If we're not checking yet (debounce period), show checking state only if user is typing
135
+ const isCurrentlyChecking = isChecking || isFetching;
136
+
137
+ // Handle network/timeout errors gracefully
138
+ if (queryError && isNetworkOrTimeoutError(queryError)) {
139
+ // Allow proceeding if offline/network issue (optimistic)
140
+ return {
141
+ isValid: true,
142
+ isAvailable: true, // Optimistic: allow proceeding
143
+ error: null,
144
+ isChecking: false,
145
+ };
146
+ }
147
+
148
+ // Handle other errors
149
+ if (queryError) {
150
+ return {
151
+ isValid: true,
152
+ isAvailable: false,
153
+ error: extractAuthErrorMessage(queryError, 'Failed to check username availability'),
154
+ isChecking: false,
155
+ };
156
+ }
157
+
158
+ // If we have a result, use it
159
+ if (availabilityResult) {
160
+ return {
161
+ isValid: true,
162
+ isAvailable: availabilityResult.available,
163
+ error: availabilityResult.available ? null : (availabilityResult.message || 'Username is already taken'),
164
+ isChecking: false,
165
+ };
166
+ }
167
+
168
+ // Still checking (or waiting for debounce)
169
+ return {
170
+ isValid: true,
171
+ isAvailable: null,
172
+ error: null,
173
+ isChecking: isCurrentlyChecking,
174
+ };
175
+ }, [username, isValid, availabilityResult, isChecking, isFetching, queryError]);
176
+ }
177
+
@@ -1,4 +1,5 @@
1
1
  export { useFollow, useFollowerCounts } from './useFollow';
2
2
  export { useFileDownloadUrl, setOxyFileUrlInstance } from './useFileDownloadUrl';
3
3
  export { useThemeStyles } from './useThemeStyles';
4
- export { useThemeColors } from './useThemeColors';
4
+ export { useThemeColors } from './useThemeColors';
5
+ export * from './auth';
@@ -12,6 +12,8 @@ export {
12
12
  useUpdateAccountSettings,
13
13
  useUpdatePrivacySettings,
14
14
  useUploadFile,
15
+ useUnblockUser,
16
+ useUnrestrictUser,
15
17
  } from './useAccountMutations';
16
18
 
17
19
  // Service mutation hooks (sessions, devices)