@oxyhq/services 5.17.7 → 5.17.9

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 (119) hide show
  1. package/lib/commonjs/crypto/index.js +0 -23
  2. package/lib/commonjs/crypto/index.js.map +1 -1
  3. package/lib/commonjs/index.js +0 -15
  4. package/lib/commonjs/index.js.map +1 -1
  5. package/lib/commonjs/ui/components/Icon.js.map +1 -1
  6. package/lib/commonjs/ui/components/IconButton/utils.js.map +1 -1
  7. package/lib/commonjs/ui/components/TextField/Adornment/utils.js.map +1 -1
  8. package/lib/commonjs/ui/components/TextField/helpers.js.map +1 -1
  9. package/lib/commonjs/ui/components/TouchableRipple/utils.js.map +1 -1
  10. package/lib/commonjs/ui/components/Typography/AnimatedText.js.map +1 -1
  11. package/lib/commonjs/ui/context/OxyContext.js +37 -589
  12. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  13. package/lib/commonjs/ui/context/OxyContextBase.js.map +1 -1
  14. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +60 -425
  15. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  16. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +8 -112
  17. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  18. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +2 -27
  19. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
  20. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +2 -27
  21. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -1
  22. package/lib/commonjs/ui/hooks/useSessionSocket.js +2 -88
  23. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
  24. package/lib/commonjs/ui/screens/OxyAuthScreen.js +0 -1
  25. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  26. package/lib/commonjs/ui/stores/authStore.js +52 -15
  27. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  28. package/lib/commonjs/ui/utils/avatarUtils.js +2 -32
  29. package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
  30. package/lib/module/crypto/index.js +4 -6
  31. package/lib/module/crypto/index.js.map +1 -1
  32. package/lib/module/index.js +6 -3
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/ui/components/Icon.js.map +1 -1
  35. package/lib/module/ui/components/IconButton/utils.js.map +1 -1
  36. package/lib/module/ui/components/TextField/Adornment/utils.js.map +1 -1
  37. package/lib/module/ui/components/TextField/helpers.js.map +1 -1
  38. package/lib/module/ui/components/TouchableRipple/utils.js.map +1 -1
  39. package/lib/module/ui/components/Typography/AnimatedText.js.map +1 -1
  40. package/lib/module/ui/context/OxyContext.js +35 -588
  41. package/lib/module/ui/context/OxyContext.js.map +1 -1
  42. package/lib/module/ui/context/OxyContextBase.js.map +1 -1
  43. package/lib/module/ui/context/hooks/useAuthOperations.js +60 -424
  44. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  45. package/lib/module/ui/hooks/mutations/useAccountMutations.js +8 -112
  46. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  47. package/lib/module/ui/hooks/queries/useAccountQueries.js +2 -27
  48. package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
  49. package/lib/module/ui/hooks/queries/useServicesQueries.js +2 -27
  50. package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -1
  51. package/lib/module/ui/hooks/useSessionSocket.js +2 -88
  52. package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
  53. package/lib/module/ui/screens/OxyAuthScreen.js +0 -1
  54. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  55. package/lib/module/ui/stores/authStore.js +52 -15
  56. package/lib/module/ui/stores/authStore.js.map +1 -1
  57. package/lib/module/ui/utils/avatarUtils.js +2 -32
  58. package/lib/module/ui/utils/avatarUtils.js.map +1 -1
  59. package/lib/typescript/crypto/index.d.ts +2 -5
  60. package/lib/typescript/crypto/index.d.ts.map +1 -1
  61. package/lib/typescript/crypto/types.d.ts +6 -2
  62. package/lib/typescript/crypto/types.d.ts.map +1 -1
  63. package/lib/typescript/index.d.ts +4 -2
  64. package/lib/typescript/index.d.ts.map +1 -1
  65. package/lib/typescript/ui/components/IconButton/utils.d.ts +1 -1
  66. package/lib/typescript/ui/components/TextField/Adornment/utils.d.ts +1 -1
  67. package/lib/typescript/ui/components/TextField/Adornment/utils.d.ts.map +1 -1
  68. package/lib/typescript/ui/components/TextField/helpers.d.ts +6 -6
  69. package/lib/typescript/ui/components/types.d.ts +0 -4
  70. package/lib/typescript/ui/components/types.d.ts.map +1 -1
  71. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  72. package/lib/typescript/ui/context/OxyContextBase.d.ts +2 -39
  73. package/lib/typescript/ui/context/OxyContextBase.d.ts.map +1 -1
  74. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts +10 -25
  75. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  76. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  77. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  78. package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
  79. package/lib/typescript/ui/hooks/useSessionSocket.d.ts +1 -14
  80. package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
  81. package/lib/typescript/ui/stores/authStore.d.ts +27 -4
  82. package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
  83. package/lib/typescript/ui/utils/avatarUtils.d.ts +0 -2
  84. package/lib/typescript/ui/utils/avatarUtils.d.ts.map +1 -1
  85. package/package.json +2 -2
  86. package/src/crypto/index.ts +3 -11
  87. package/src/crypto/types.ts +6 -2
  88. package/src/index.ts +6 -11
  89. package/src/ui/components/Icon.tsx +1 -1
  90. package/src/ui/components/IconButton/utils.ts +1 -1
  91. package/src/ui/components/TextField/Adornment/utils.ts +2 -2
  92. package/src/ui/components/TextField/helpers.tsx +8 -8
  93. package/src/ui/components/TouchableRipple/utils.ts +2 -2
  94. package/src/ui/components/Typography/AnimatedText.tsx +2 -2
  95. package/src/ui/components/types.tsx +0 -6
  96. package/src/ui/context/OxyContext.tsx +33 -637
  97. package/src/ui/context/OxyContextBase.tsx +5 -23
  98. package/src/ui/context/hooks/useAuthOperations.ts +84 -460
  99. package/src/ui/hooks/mutations/useAccountMutations.ts +12 -110
  100. package/src/ui/hooks/queries/useAccountQueries.ts +3 -27
  101. package/src/ui/hooks/queries/useServicesQueries.ts +3 -27
  102. package/src/ui/hooks/useSessionSocket.ts +2 -106
  103. package/src/ui/screens/OxyAuthScreen.tsx +1 -1
  104. package/src/ui/stores/authStore.ts +57 -18
  105. package/src/ui/utils/avatarUtils.ts +4 -36
  106. package/lib/commonjs/crypto/keyManager.js +0 -511
  107. package/lib/commonjs/crypto/keyManager.js.map +0 -1
  108. package/lib/commonjs/crypto/signatureService.js +0 -269
  109. package/lib/commonjs/crypto/signatureService.js.map +0 -1
  110. package/lib/module/crypto/keyManager.js +0 -508
  111. package/lib/module/crypto/keyManager.js.map +0 -1
  112. package/lib/module/crypto/signatureService.js +0 -266
  113. package/lib/module/crypto/signatureService.js.map +0 -1
  114. package/lib/typescript/crypto/keyManager.d.ts +0 -97
  115. package/lib/typescript/crypto/keyManager.d.ts.map +0 -1
  116. package/lib/typescript/crypto/signatureService.d.ts +0 -77
  117. package/lib/typescript/crypto/signatureService.d.ts.map +0 -1
  118. package/src/crypto/keyManager.ts +0 -545
  119. package/src/crypto/signatureService.ts +0 -301
@@ -2,11 +2,10 @@ import { createContext, useContext } from 'react';
2
2
  import type { ReactNode } from 'react';
3
3
  import type { OxyServices } from '../../core';
4
4
  import type { User, ApiError } from '../../models/interfaces';
5
- import type { ClientSession } from '../../models/session';
5
+ import type { ClientSession, SessionLoginResponse } from '../../models/session';
6
6
  import type { UseFollowHook } from '../hooks/useFollow.types';
7
7
  import type { useLanguageManagement } from '../hooks/useLanguageManagement';
8
8
  import type { RouteName } from '../navigation/routes';
9
- import type { BackupData } from '../../crypto';
10
9
 
11
10
  export interface OxyContextState {
12
11
  user: User | null;
@@ -23,27 +22,10 @@ export interface OxyContextState {
23
22
  currentLanguageName: string;
24
23
  currentNativeLanguageName: string;
25
24
 
26
- // Identity management (public key authentication - offline-first)
27
- createIdentity: () => Promise<{ synced: boolean }>;
28
- importIdentity: (backupData: BackupData, password: string) => Promise<{ synced: boolean }>;
29
- signIn: (deviceName?: string) => Promise<User>;
30
- hasIdentity: () => Promise<boolean>;
31
- getPublicKey: () => Promise<string | null>;
32
- isIdentitySynced: () => Promise<boolean>;
33
- syncIdentity: () => Promise<User>;
34
- deleteIdentityAndClearAccount: (skipBackup?: boolean, force?: boolean, userConfirmed?: boolean) => Promise<void>;
35
- storeTransferCode: (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => Promise<void>;
36
- getTransferCode: (transferId: string) => { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number; state: 'pending' | 'completed' | 'failed' } | null;
37
- clearTransferCode: (transferId: string) => Promise<void>;
38
- getAllPendingTransfers: () => Array<{ transferId: string; data: { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number; state: 'pending' | 'completed' | 'failed' } }>;
39
- getActiveTransferId: () => string | null;
40
- updateTransferState: (transferId: string, state: 'pending' | 'completed' | 'failed') => Promise<void>;
41
-
42
- // Identity sync state (reactive, from Zustand store)
43
- identitySyncState: {
44
- isSynced: boolean;
45
- isSyncing: boolean;
46
- };
25
+ // Authentication (Services SDK only handles tokens/sessions, NOT identity)
26
+ // Identity management (KeyManager, SignatureService, challenge signing) belongs ONLY in Accounts app
27
+ // completeSignIn accepts already-verified session data from Accounts app
28
+ completeSignIn: (sessionResponse: SessionLoginResponse) => Promise<User>;
47
29
 
48
30
  // Session management
49
31
  logout: (targetSessionId?: string) => Promise<void>;
@@ -2,12 +2,10 @@ import { useCallback } from 'react';
2
2
  import type { ApiError, User } from '../../../models/interfaces';
3
3
  import type { AuthState } from '../../stores/authStore';
4
4
  import type { ClientSession, SessionLoginResponse } from '../../../models/session';
5
- import { DeviceManager } from '../../../utils/deviceManager';
6
- import { fetchSessionsWithFallback, mapSessionsToClient } from '../../utils/sessionHelpers';
5
+ import { fetchSessionsWithFallback } from '../../utils/sessionHelpers';
7
6
  import { handleAuthError, isInvalidSessionError } from '../../utils/errorHandlers';
8
7
  import type { StorageInterface } from '../../utils/storageHelpers';
9
8
  import type { OxyServices } from '../../../core';
10
- import { KeyManager, SignatureService, type BackupData } from '../../../crypto';
11
9
 
12
10
  export interface UseAuthOperationsOptions {
13
11
  oxyServices: OxyServices;
@@ -26,41 +24,29 @@ export interface UseAuthOperationsOptions {
26
24
  loginFailure: (message: string) => void;
27
25
  logoutStore: () => void;
28
26
  setAuthState: (state: Partial<AuthState>) => void;
29
- // Identity sync store actions
30
- setIdentitySynced: (synced: boolean) => void;
31
- setSyncing: (syncing: boolean) => void;
32
27
  logger?: (message: string, error?: unknown) => void;
33
28
  }
34
29
 
35
30
  export interface UseAuthOperationsResult {
36
- /** Create a new identity locally (offline-first) and optionally sync with server */
37
- createIdentity: () => Promise<{ synced: boolean }>;
38
- /** Import an existing identity from backup file data */
39
- importIdentity: (backupData: BackupData, password: string) => Promise<{ synced: boolean }>;
40
- /** Sign in with existing identity on device */
41
- signIn: (deviceName?: string) => Promise<User>;
31
+ /** Complete sign-in after external challenge-response (Accounts app signs, Services completes session) */
32
+ completeSignIn: (sessionResponse: SessionLoginResponse) => Promise<User>;
42
33
  /** Logout from current session */
43
34
  logout: (targetSessionId?: string) => Promise<void>;
44
35
  /** Logout from all sessions */
45
36
  logoutAll: () => Promise<void>;
46
- /** Check if device has an identity stored */
47
- hasIdentity: () => Promise<boolean>;
48
- /** Get the public key of the stored identity */
49
- getPublicKey: () => Promise<string | null>;
50
- /** Check if identity is synced with server */
51
- isIdentitySynced: () => Promise<boolean>;
52
- /** Sync local identity with server (when online) */
53
- syncIdentity: () => Promise<User>;
54
37
  }
55
38
 
56
- const LOGIN_ERROR_CODE = 'LOGIN_ERROR';
57
- const REGISTER_ERROR_CODE = 'REGISTER_ERROR';
58
39
  const LOGOUT_ERROR_CODE = 'LOGOUT_ERROR';
59
40
  const LOGOUT_ALL_ERROR_CODE = 'LOGOUT_ALL_ERROR';
60
41
 
42
+
61
43
  /**
62
- * Authentication operations using public key cryptography.
63
- * No passwords required - identity is based on ECDSA key pairs.
44
+ * Authentication operations for Services SDK.
45
+ *
46
+ * ARCHITECTURE:
47
+ * - Services SDK only handles tokens/sessions (NOT identity)
48
+ * - Accounts app handles identity (KeyManager, SignatureService, challenge signing)
49
+ * - completeSignIn() accepts already-verified session from Accounts app
64
50
  */
65
51
  export const useAuthOperations = ({
66
52
  oxyServices,
@@ -78,444 +64,87 @@ export const useAuthOperations = ({
78
64
  loginSuccess,
79
65
  loginFailure,
80
66
  logoutStore,
81
- setAuthState,
82
- setIdentitySynced,
83
- setSyncing,
84
- logger,
67
+ setAuthState,
68
+ logger,
85
69
  }: UseAuthOperationsOptions): UseAuthOperationsResult => {
86
70
 
87
71
  /**
88
- * Internal function to perform challenge-response sign in (works offline)
72
+ * Complete sign-in after external authentication.
73
+ *
74
+ * This is called by Accounts app AFTER it has:
75
+ * 1. Requested challenge from backend
76
+ * 2. Signed challenge locally with private key
77
+ * 3. Verified challenge with backend to get sessionResponse
78
+ *
79
+ * Services SDK only stores tokens and manages session state.
80
+ *
81
+ * @param sessionResponse - Session data from verifyChallenge API call
89
82
  */
90
- const performSignIn = useCallback(
91
- async (publicKey: string): Promise<User> => {
92
- const deviceFingerprintObj = DeviceManager.getDeviceFingerprint();
93
- const deviceFingerprint = JSON.stringify(deviceFingerprintObj);
94
- const deviceInfo = await DeviceManager.getDeviceInfo();
95
- const deviceName = deviceInfo.deviceName || DeviceManager.getDefaultDeviceName();
96
- const USER_ID_STORAGE_KEY = 'oxy_user_id';
97
-
98
- // Online-only sign-in: require backend availability
99
- // First, look up the user by public key to get the correct userId
100
- // This ensures we always use the userId that matches the current identity's public key
101
- let userId: string | null = null;
102
-
103
- // Always verify the userId matches the current public key
104
- // This prevents auth failures when identity has changed
105
- const userLookup = await oxyServices.getUserByPublicKey(publicKey);
106
- userId = userLookup.id;
107
-
108
- // Update stored userId to match current identity
109
- if (storage && userId) {
110
- await storage.setItem(USER_ID_STORAGE_KEY, userId).catch(() => {});
111
- }
112
-
113
- const challengeResponse = await oxyServices.requestChallenge(userId);
114
- const challenge = challengeResponse.challenge;
115
-
116
- // Note: Biometric authentication check should be handled by the app layer
117
- // (e.g., accounts app) before calling signIn. The biometric preference is stored
118
- // in local storage as 'oxy_biometric_enabled' and can be checked there.
119
-
120
- // Sign the challenge
121
- const { challenge: signature, timestamp } = await SignatureService.signChallenge(challenge);
122
-
123
- // Online sign-in: use normal flow
124
- if (!userId) {
125
- throw new Error('User ID not found');
126
- }
127
-
128
- // Verify and create session using userId
129
- let sessionResponse;
130
- try {
131
- sessionResponse = await oxyServices.verifyChallenge(
132
- userId,
133
- challenge,
134
- signature,
135
- timestamp,
136
- deviceName,
137
- deviceFingerprint,
138
- );
139
- } catch (verifyError) {
140
- // Add detailed logging for 401 errors to help diagnose auth failures
141
- if (__DEV__) {
142
- console.error('[useAuthOperations] verifyChallenge failed:', {
143
- error: verifyError,
144
- userId,
145
- challengeLength: challenge?.length,
146
- signatureLength: signature?.length,
147
- timestamp,
148
- timeSinceChallenge: Date.now() - timestamp,
149
- });
150
- }
151
- throw verifyError;
152
- }
153
-
154
- // Store tokens immediately (no extra round-trip)
155
- oxyServices.setTokens(sessionResponse.accessToken, sessionResponse.refreshToken);
156
-
157
- // Get full user data
158
- const fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
159
-
160
- // IMPORTANT: user.id should be MongoDB ObjectId, not publicKey
161
- // The API should return the correct id (ObjectId) from the database
162
- // If it doesn't, we need to fix the API, not work around it here
163
- // Validate that id is ObjectId format (24 hex characters)
164
- if (fullUser.id && !/^[0-9a-fA-F]{24}$/.test(fullUser.id)) {
165
- console.warn('[useAuthOperations] User.id is not MongoDB ObjectId format:', {
166
- id: fullUser.id.substring(0, 20),
167
- publicKey: fullUser.publicKey.substring(0, 20),
168
- message: 'API should return MongoDB ObjectId as user.id, not publicKey'
169
- });
170
- // Don't override - let the API fix this issue
171
- }
172
-
173
- // Fetch device sessions
174
- let allDeviceSessions: ClientSession[] = [];
175
- try {
176
- allDeviceSessions = await fetchSessionsWithFallback(oxyServices, sessionResponse.sessionId, {
177
- fallbackDeviceId: sessionResponse.deviceId,
178
- fallbackUserId: fullUser.id,
179
- logger,
180
- });
181
- } catch (error) {
182
- if (__DEV__) {
183
- console.warn('Failed to fetch device sessions after login:', error);
184
- }
185
- }
186
-
187
- // Check for existing session for same user
188
- const existingSession = allDeviceSessions.find(
189
- (session) =>
190
- session.userId?.toString() === fullUser.id?.toString() &&
191
- session.sessionId !== sessionResponse.sessionId,
192
- );
193
-
194
- if (existingSession) {
195
- // Logout duplicate session
196
- try {
197
- await oxyServices.logoutSession(sessionResponse.sessionId, sessionResponse.sessionId);
198
- } catch (logoutError) {
199
- if (__DEV__) {
200
- console.warn('Failed to logout duplicate session:', logoutError);
201
- }
202
- }
203
- await switchSession(existingSession.sessionId);
204
- updateSessions(
205
- allDeviceSessions.filter((session) => session.sessionId !== sessionResponse.sessionId),
206
- { merge: false },
207
- );
208
- onAuthStateChange?.(fullUser);
209
- return fullUser;
210
- }
211
-
212
- setActiveSessionId(sessionResponse.sessionId);
213
- await saveActiveSessionId(sessionResponse.sessionId);
214
- updateSessions(allDeviceSessions, { merge: true });
215
-
216
- await applyLanguagePreference(fullUser);
217
- loginSuccess(); // Services never caches profile - only tokens
218
- onAuthStateChange?.(fullUser);
219
- if (storage) {
220
- await storage.setItem('oxy_identity_synced', 'true').catch(() => {});
221
- }
222
- setIdentitySynced(true);
223
-
224
- return fullUser;
225
- },
226
- [
227
- applyLanguagePreference,
228
- logger,
229
- loginSuccess,
230
- onAuthStateChange,
231
- oxyServices,
232
- saveActiveSessionId,
233
- setActiveSessionId,
234
- setIdentitySynced,
235
- switchSession,
236
- updateSessions,
237
- storage,
238
- ],
239
- );
240
-
241
- /**
242
- * Create a new identity (offline-first)
243
- * Identity is purely cryptographic - no username or email required
244
- */
245
- const createIdentity = useCallback(
246
- async (): Promise<{ synced: boolean }> => {
83
+ const completeSignIn = useCallback(
84
+ async (sessionResponse: SessionLoginResponse): Promise<User> => {
247
85
  if (!storage) throw new Error('Storage not initialized');
248
86
 
249
87
  setAuthState({ isLoading: true, error: null });
250
88
 
251
89
  try {
252
- // Generate new key pair directly (works offline)
253
- const { publicKey, privateKey } = await KeyManager.generateKeyPair();
254
- await KeyManager.importKeyPair(privateKey);
90
+ // Store tokens (Services' only responsibility)
91
+ oxyServices.setTokens(sessionResponse.accessToken, sessionResponse.refreshToken);
255
92
 
256
- // Mark as not synced
257
- // Note: createIdentity only creates the key locally (offline-first)
258
- // Registration with server (registerIdentity) must be done by the app (Accounts) with profile data
259
- await storage.setItem('oxy_identity_synced', 'false');
260
- setIdentitySynced(false);
93
+ // Get full user data
94
+ const fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
261
95
 
262
- return {
263
- synced: false, // Always false - registration must be done separately by Accounts app
264
- };
265
- } catch (error) {
266
- // CRITICAL: Never delete identity on error - it may have been successfully created
267
- // Only log the error and let the user recover using their backup file
268
- // Identity deletion should ONLY happen when explicitly requested by the user
269
- if (__DEV__ && logger) {
270
- logger('Error during identity creation (identity may still exist):', error);
271
- }
272
-
273
- // Check if identity was actually created (keys exist)
274
- const hasIdentity = await KeyManager.hasIdentity().catch(() => false);
275
- if (hasIdentity) {
276
- // Identity exists - don't delete it! Just mark as not synced
277
- await storage.setItem('oxy_identity_synced', 'false').catch(() => {});
278
- setIdentitySynced(false);
279
- if (__DEV__ && logger) {
280
- logger('Identity was created but sync failed - user can sync later using backup file');
96
+ // Fetch device sessions
97
+ let allDeviceSessions: ClientSession[] = [];
98
+ try {
99
+ allDeviceSessions = await fetchSessionsWithFallback(oxyServices, sessionResponse.sessionId, {
100
+ fallbackDeviceId: sessionResponse.deviceId,
101
+ fallbackUserId: fullUser.id,
102
+ logger,
103
+ });
104
+ } catch (error) {
105
+ if (__DEV__) {
106
+ console.warn('Failed to fetch device sessions after login:', error);
281
107
  }
282
- } else {
283
- // No identity exists - this was a generation failure, safe to clean up sync flag
284
- await storage.removeItem('oxy_identity_synced').catch(() => {});
285
- setIdentitySynced(false);
286
- }
287
-
288
- const message = handleAuthError(error, {
289
- defaultMessage: 'Failed to create identity',
290
- code: REGISTER_ERROR_CODE,
291
- onError,
292
- setAuthError: (msg: string) => setAuthState({ error: msg }),
293
- logger,
294
- });
295
- loginFailure(message);
296
- throw error;
297
- } finally {
298
- setAuthState({ isLoading: false });
299
- }
300
- },
301
- [oxyServices, storage, setAuthState, loginFailure, onError, logger, setIdentitySynced],
302
- );
303
-
304
- /**
305
- * Check if identity is synced with server (reads from storage for persistence)
306
- */
307
- const isIdentitySyncedFn = useCallback(async (): Promise<boolean> => {
308
- if (!storage) return true;
309
- const synced = await storage.getItem('oxy_identity_synced');
310
- const isSynced = synced !== 'false';
311
- setIdentitySynced(isSynced);
312
- return isSynced;
313
- }, [storage, setIdentitySynced]);
314
-
315
- /**
316
- * Sync local identity with server (call when online)
317
- * TanStack Query handles offline mutations automatically
318
- */
319
- const syncIdentity = useCallback(
320
- async (): Promise<User> => {
321
- if (!storage) throw new Error('Storage not initialized');
322
-
323
- setAuthState({ isLoading: true, error: null });
324
- setSyncing(true);
325
-
326
- try {
327
- const publicKey = await KeyManager.getPublicKey();
328
- if (!publicKey) {
329
- throw new Error('No identity found on this device');
330
- }
331
-
332
- // Check if already synced
333
- const alreadySynced = await storage.getItem('oxy_identity_synced');
334
- if (alreadySynced === 'true') {
335
- setIdentitySynced(true);
336
- return await performSignIn(publicKey);
337
- }
338
-
339
- // Check if already registered on server
340
- const { registered } = await oxyServices.checkPublicKeyRegistered(publicKey);
341
-
342
- if (!registered) {
343
- // Identity is not registered - registration must be done by Accounts app with profile data
344
- // syncIdentity only syncs already-registered identities
345
- throw new Error('Identity is not registered. Please register your identity first using the Accounts app.');
346
- }
347
-
348
- // Mark as synced (Zustand store + storage)
349
- await storage.setItem('oxy_identity_synced', 'true');
350
- setIdentitySynced(true);
351
-
352
- // Sign in (Services never caches profile - only tokens)
353
- const user = await performSignIn(publicKey);
354
-
355
- // Check if user has username - required for syncing
356
- if (!user.username) {
357
- const usernameError = new Error('USERNAME_REQUIRED');
358
- (usernameError as any).code = 'USERNAME_REQUIRED';
359
- throw usernameError;
360
108
  }
361
109
 
362
- // TanStack Query will automatically retry any pending mutations
363
-
364
- return user;
365
- } catch (error) {
366
- const message = handleAuthError(error, {
367
- defaultMessage: 'Failed to sync identity',
368
- code: REGISTER_ERROR_CODE,
369
- onError,
370
- setAuthError: (msg: string) => setAuthState({ error: msg }),
371
- logger,
372
- });
373
- loginFailure(message);
374
- throw error;
375
- } finally {
376
- setAuthState({ isLoading: false });
377
- setSyncing(false);
378
- }
379
- },
380
- [oxyServices, storage, setAuthState, performSignIn, loginFailure, onError, logger, setSyncing, setIdentitySynced],
381
- );
382
-
383
- /**
384
- * Import identity from backup file data (offline-first)
385
- */
386
- const importIdentity = useCallback(
387
- async (backupData: BackupData, password: string): Promise<{ synced: boolean }> => {
388
- if (!storage) throw new Error('Storage not initialized');
389
-
390
- // Validate arguments - ensure backupData is an object, not a string (old signature)
391
- if (!backupData || typeof backupData !== 'object' || Array.isArray(backupData)) {
392
- throw new Error('Invalid backup data. Please use the backup file import feature.');
393
- }
394
-
395
- if (!backupData.encrypted || !backupData.salt || !backupData.iv || !backupData.publicKey) {
396
- throw new Error('Invalid backup data structure. Missing required fields.');
397
- }
398
-
399
- if (!password || typeof password !== 'string') {
400
- throw new Error('Password is required for backup file import.');
401
- }
402
-
403
- setAuthState({ isLoading: true, error: null });
404
-
405
- try {
406
- // Decrypt private key from backup data
407
- const Crypto = await import('expo-crypto');
408
-
409
- // Convert hex strings to Uint8Array
410
- const saltBytes = new Uint8Array(
411
- backupData.salt.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []
412
- );
413
- const ivBytes = new Uint8Array(
414
- backupData.iv.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []
110
+ // Check for existing session for same user
111
+ const existingSession = allDeviceSessions.find(
112
+ (session) =>
113
+ session.userId?.toString() === fullUser.id?.toString() &&
114
+ session.sessionId !== sessionResponse.sessionId,
415
115
  );
416
116
 
417
- // Derive key from password (same algorithm as EncryptedBackupGenerator)
418
- const saltHex = Array.from(saltBytes).map(b => b.toString(16).padStart(2, '0')).join('');
419
- let key = password + saltHex;
420
- for (let i = 0; i < 10000; i++) {
421
- key = await Crypto.digestStringAsync(
422
- Crypto.CryptoDigestAlgorithm.SHA256,
423
- key
424
- );
425
- }
426
- const keyBytes = new Uint8Array(32);
427
- for (let i = 0; i < 64 && i < key.length; i += 2) {
428
- keyBytes[i / 2] = parseInt(key.substring(i, i + 2), 16);
429
- }
430
-
431
- // Decrypt private key (XOR decryption - same as encryption)
432
- const encryptedBytes = Buffer.from(backupData.encrypted, 'base64');
433
- const decryptedBytes = new Uint8Array(encryptedBytes.length);
434
- for (let i = 0; i < encryptedBytes.length; i++) {
435
- decryptedBytes[i] = encryptedBytes[i] ^ keyBytes[i % keyBytes.length] ^ ivBytes[i % ivBytes.length];
436
- }
437
- const privateKey = new TextDecoder().decode(decryptedBytes);
438
-
439
- // Import the key pair
440
- const publicKey = await KeyManager.importKeyPair(privateKey);
441
-
442
- // Verify public key matches
443
- if (publicKey !== backupData.publicKey) {
444
- throw new Error('Backup file is corrupted or password is incorrect');
445
- }
446
-
447
- // Mark as not synced
448
- await storage.setItem('oxy_identity_synced', 'false');
449
- setIdentitySynced(false);
450
-
451
- // Try to sync with server
452
- try {
453
- // Check if this identity is already registered
454
- const { registered } = await oxyServices.checkPublicKeyRegistered(publicKey);
455
-
456
- if (registered) {
457
- // Identity exists, mark as synced
458
- await storage.setItem('oxy_identity_synced', 'true');
459
- setIdentitySynced(true);
460
- return { synced: true };
461
- } else {
462
- // Identity is not registered - registration must be done by Accounts app with profile data
463
- // importIdentity only imports already-registered identities
464
- await storage.setItem('oxy_identity_synced', 'false');
465
- setIdentitySynced(false);
466
- return { synced: false };
117
+ if (existingSession) {
118
+ // Logout duplicate session
119
+ try {
120
+ await oxyServices.logoutSession(sessionResponse.sessionId, sessionResponse.sessionId);
121
+ } catch (logoutError) {
122
+ if (__DEV__) {
123
+ console.warn('Failed to logout duplicate session:', logoutError);
124
+ }
467
125
  }
468
- } catch (syncError) {
469
- // Offline - identity restored locally but not synced
470
- if (__DEV__) {
471
- console.log('[Auth] Identity imported locally, will sync when online:', syncError);
472
- }
473
- return { synced: false };
126
+ await switchSession(existingSession.sessionId);
127
+ updateSessions(
128
+ allDeviceSessions.filter((session) => session.sessionId !== sessionResponse.sessionId),
129
+ { merge: false },
130
+ );
131
+ onAuthStateChange?.(fullUser);
132
+ return fullUser;
474
133
  }
475
- } catch (error) {
476
- const message = handleAuthError(error, {
477
- defaultMessage: 'Failed to import identity. Please check your password and backup file.',
478
- code: REGISTER_ERROR_CODE,
479
- onError,
480
- setAuthError: (msg: string) => setAuthState({ error: msg }),
481
- logger,
482
- });
483
- loginFailure(message);
484
- throw error;
485
- } finally {
486
- setAuthState({ isLoading: false });
487
- }
488
- },
489
- [oxyServices, storage, setAuthState, loginFailure, onError, logger, setIdentitySynced],
490
- );
491
-
492
- /**
493
- * Sign in with existing identity on device
494
- */
495
- const signIn = useCallback(
496
- async (deviceName?: string): Promise<User> => {
497
- if (!storage) throw new Error('Storage not initialized');
498
134
 
499
- setAuthState({ isLoading: true, error: null });
135
+ setActiveSessionId(sessionResponse.sessionId);
136
+ await saveActiveSessionId(sessionResponse.sessionId);
137
+ updateSessions(allDeviceSessions, { merge: true });
500
138
 
501
- try {
502
- // Get stored public key
503
- const publicKey = await KeyManager.getPublicKey();
504
- if (!publicKey) {
505
- throw new Error('No identity found on this device. Please create or import an identity.');
506
- }
507
-
508
- // Ensure identity is registered before attempting sign-in
509
- const { registered } = await oxyServices.checkPublicKeyRegistered(publicKey);
510
- if (!registered) {
511
- throw new Error('Identity is not registered. Please register your identity in the Accounts app before signing in.');
512
- }
139
+ await applyLanguagePreference(fullUser);
140
+ loginSuccess();
141
+ onAuthStateChange?.(fullUser);
513
142
 
514
- return await performSignIn(publicKey);
143
+ return fullUser;
515
144
  } catch (error) {
516
145
  const message = handleAuthError(error, {
517
146
  defaultMessage: 'Sign in failed',
518
- code: LOGIN_ERROR_CODE,
147
+ code: 'COMPLETE_SIGNIN_ERROR',
519
148
  onError,
520
149
  setAuthError: (msg: string) => setAuthState({ error: msg }),
521
150
  logger,
@@ -526,9 +155,24 @@ export const useAuthOperations = ({
526
155
  setAuthState({ isLoading: false });
527
156
  }
528
157
  },
529
- [storage, setAuthState, performSignIn, loginFailure, onError, logger, oxyServices],
158
+ [
159
+ storage,
160
+ setAuthState,
161
+ oxyServices,
162
+ logger,
163
+ saveActiveSessionId,
164
+ setActiveSessionId,
165
+ switchSession,
166
+ updateSessions,
167
+ applyLanguagePreference,
168
+ loginSuccess,
169
+ onAuthStateChange,
170
+ onError,
171
+ loginFailure,
172
+ ],
530
173
  );
531
174
 
175
+
532
176
  /**
533
177
  * Logout from session
534
178
  */
@@ -608,29 +252,9 @@ export const useAuthOperations = ({
608
252
  }
609
253
  }, [activeSessionId, clearSessionState, logger, onError, oxyServices, setAuthState]);
610
254
 
611
- /**
612
- * Check if device has an identity stored
613
- */
614
- const hasIdentity = useCallback(async (): Promise<boolean> => {
615
- return KeyManager.hasIdentity();
616
- }, []);
617
-
618
- /**
619
- * Get the public key of the stored identity
620
- */
621
- const getPublicKey = useCallback(async (): Promise<string | null> => {
622
- return KeyManager.getPublicKey();
623
- }, []);
624
-
625
255
  return {
626
- createIdentity,
627
- importIdentity,
628
- signIn,
256
+ completeSignIn,
629
257
  logout,
630
258
  logoutAll,
631
- hasIdentity,
632
- getPublicKey,
633
- isIdentitySynced: isIdentitySyncedFn,
634
- syncIdentity,
635
259
  };
636
260
  };