@oxyhq/services 5.17.7 → 5.17.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/crypto/keyManager.js +6 -161
- package/lib/commonjs/crypto/keyManager.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +22 -582
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContextBase.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +14 -331
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +8 -112
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +2 -27
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +2 -27
- package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionSocket.js +2 -88
- package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/module/crypto/keyManager.js +6 -161
- package/lib/module/crypto/keyManager.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +20 -581
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/OxyContextBase.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +14 -330
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +8 -112
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/module/ui/hooks/queries/useAccountQueries.js +2 -27
- package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/module/ui/hooks/queries/useServicesQueries.js +2 -27
- package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -1
- package/lib/module/ui/hooks/useSessionSocket.js +2 -88
- package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/typescript/crypto/keyManager.d.ts +3 -20
- package/lib/typescript/crypto/keyManager.d.ts.map +1 -1
- package/lib/typescript/crypto/types.d.ts +4 -0
- package/lib/typescript/crypto/types.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContextBase.d.ts +0 -37
- package/lib/typescript/ui/context/OxyContextBase.d.ts.map +1 -1
- package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts +1 -20
- package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useSessionSocket.d.ts +1 -14
- package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/crypto/keyManager.ts +4 -170
- package/src/crypto/types.ts +4 -0
- package/src/ui/context/OxyContext.tsx +17 -630
- package/src/ui/context/OxyContextBase.tsx +2 -20
- package/src/ui/context/hooks/useAuthOperations.ts +22 -347
- package/src/ui/hooks/mutations/useAccountMutations.ts +12 -110
- package/src/ui/hooks/queries/useAccountQueries.ts +3 -27
- package/src/ui/hooks/queries/useServicesQueries.ts +3 -27
- package/src/ui/hooks/useSessionSocket.ts +2 -106
|
@@ -23,27 +23,9 @@ export interface OxyContextState {
|
|
|
23
23
|
currentLanguageName: string;
|
|
24
24
|
currentNativeLanguageName: string;
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
createIdentity
|
|
28
|
-
importIdentity: (backupData: BackupData, password: string) => Promise<{ synced: boolean }>;
|
|
26
|
+
// Authentication (Services SDK only handles tokens/sessions, NOT identity)
|
|
27
|
+
// Identity management (createIdentity, importIdentity, etc.) belongs in Accounts app
|
|
29
28
|
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
|
-
};
|
|
47
29
|
|
|
48
30
|
// Session management
|
|
49
31
|
logout: (targetSessionId?: string) => Promise<void>;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import type { ApiError, User } from '../../../models/interfaces';
|
|
3
3
|
import type { AuthState } from '../../stores/authStore';
|
|
4
|
-
import type { ClientSession
|
|
4
|
+
import type { ClientSession } from '../../../models/session';
|
|
5
5
|
import { DeviceManager } from '../../../utils/deviceManager';
|
|
6
|
-
import { fetchSessionsWithFallback
|
|
6
|
+
import { fetchSessionsWithFallback } from '../../utils/sessionHelpers';
|
|
7
7
|
import { handleAuthError, isInvalidSessionError } from '../../utils/errorHandlers';
|
|
8
8
|
import type { StorageInterface } from '../../utils/storageHelpers';
|
|
9
9
|
import type { OxyServices } from '../../../core';
|
|
10
|
-
import { KeyManager, SignatureService
|
|
10
|
+
import { KeyManager, SignatureService } from '../../../crypto';
|
|
11
11
|
|
|
12
12
|
export interface UseAuthOperationsOptions {
|
|
13
13
|
oxyServices: OxyServices;
|
|
@@ -26,38 +26,23 @@ export interface UseAuthOperationsOptions {
|
|
|
26
26
|
loginFailure: (message: string) => void;
|
|
27
27
|
logoutStore: () => void;
|
|
28
28
|
setAuthState: (state: Partial<AuthState>) => void;
|
|
29
|
-
// Identity sync store actions
|
|
30
|
-
setIdentitySynced: (synced: boolean) => void;
|
|
31
|
-
setSyncing: (syncing: boolean) => void;
|
|
32
29
|
logger?: (message: string, error?: unknown) => void;
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
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
33
|
/** Sign in with existing identity on device */
|
|
41
34
|
signIn: (deviceName?: string) => Promise<User>;
|
|
42
35
|
/** Logout from current session */
|
|
43
36
|
logout: (targetSessionId?: string) => Promise<void>;
|
|
44
37
|
/** Logout from all sessions */
|
|
45
38
|
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
39
|
}
|
|
55
40
|
|
|
56
41
|
const LOGIN_ERROR_CODE = 'LOGIN_ERROR';
|
|
57
|
-
const REGISTER_ERROR_CODE = 'REGISTER_ERROR';
|
|
58
42
|
const LOGOUT_ERROR_CODE = 'LOGOUT_ERROR';
|
|
59
43
|
const LOGOUT_ALL_ERROR_CODE = 'LOGOUT_ALL_ERROR';
|
|
60
44
|
|
|
45
|
+
|
|
61
46
|
/**
|
|
62
47
|
* Authentication operations using public key cryptography.
|
|
63
48
|
* No passwords required - identity is based on ECDSA key pairs.
|
|
@@ -78,54 +63,33 @@ export const useAuthOperations = ({
|
|
|
78
63
|
loginSuccess,
|
|
79
64
|
loginFailure,
|
|
80
65
|
logoutStore,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
setSyncing,
|
|
84
|
-
logger,
|
|
66
|
+
setAuthState,
|
|
67
|
+
logger,
|
|
85
68
|
}: UseAuthOperationsOptions): UseAuthOperationsResult => {
|
|
86
69
|
|
|
87
70
|
/**
|
|
88
|
-
* Internal function to perform challenge-response sign in
|
|
71
|
+
* Internal function to perform challenge-response sign in
|
|
89
72
|
*/
|
|
90
73
|
const performSignIn = useCallback(
|
|
91
|
-
async (publicKey: string): Promise<User> => {
|
|
74
|
+
async (publicKey: string, deviceName?: string): Promise<User> => {
|
|
92
75
|
const deviceFingerprintObj = DeviceManager.getDeviceFingerprint();
|
|
93
76
|
const deviceFingerprint = JSON.stringify(deviceFingerprintObj);
|
|
94
77
|
const deviceInfo = await DeviceManager.getDeviceInfo();
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
//
|
|
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
|
|
78
|
+
const defaultDeviceName = deviceInfo.deviceName || DeviceManager.getDefaultDeviceName();
|
|
79
|
+
const finalDeviceName = deviceName || defaultDeviceName;
|
|
80
|
+
|
|
81
|
+
// Look up user by public key
|
|
105
82
|
const userLookup = await oxyServices.getUserByPublicKey(publicKey);
|
|
106
|
-
userId = userLookup.id;
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
if (storage && userId) {
|
|
110
|
-
await storage.setItem(USER_ID_STORAGE_KEY, userId).catch(() => {});
|
|
111
|
-
}
|
|
112
|
-
|
|
83
|
+
const userId = userLookup.id;
|
|
84
|
+
|
|
85
|
+
// Request challenge
|
|
113
86
|
const challengeResponse = await oxyServices.requestChallenge(userId);
|
|
114
87
|
const challenge = challengeResponse.challenge;
|
|
115
88
|
|
|
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
89
|
// Sign the challenge
|
|
121
90
|
const { challenge: signature, timestamp } = await SignatureService.signChallenge(challenge);
|
|
122
91
|
|
|
123
|
-
//
|
|
124
|
-
if (!userId) {
|
|
125
|
-
throw new Error('User ID not found');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Verify and create session using userId
|
|
92
|
+
// Verify and create session
|
|
129
93
|
let sessionResponse;
|
|
130
94
|
try {
|
|
131
95
|
sessionResponse = await oxyServices.verifyChallenge(
|
|
@@ -133,11 +97,10 @@ export const useAuthOperations = ({
|
|
|
133
97
|
challenge,
|
|
134
98
|
signature,
|
|
135
99
|
timestamp,
|
|
136
|
-
|
|
100
|
+
finalDeviceName,
|
|
137
101
|
deviceFingerprint,
|
|
138
102
|
);
|
|
139
103
|
} catch (verifyError) {
|
|
140
|
-
// Add detailed logging for 401 errors to help diagnose auth failures
|
|
141
104
|
if (__DEV__) {
|
|
142
105
|
console.error('[useAuthOperations] verifyChallenge failed:', {
|
|
143
106
|
error: verifyError,
|
|
@@ -151,24 +114,11 @@ export const useAuthOperations = ({
|
|
|
151
114
|
throw verifyError;
|
|
152
115
|
}
|
|
153
116
|
|
|
154
|
-
// Store tokens
|
|
117
|
+
// Store tokens
|
|
155
118
|
oxyServices.setTokens(sessionResponse.accessToken, sessionResponse.refreshToken);
|
|
156
119
|
|
|
157
120
|
// Get full user data
|
|
158
121
|
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
122
|
|
|
173
123
|
// Fetch device sessions
|
|
174
124
|
let allDeviceSessions: ClientSession[] = [];
|
|
@@ -214,13 +164,9 @@ export const useAuthOperations = ({
|
|
|
214
164
|
updateSessions(allDeviceSessions, { merge: true });
|
|
215
165
|
|
|
216
166
|
await applyLanguagePreference(fullUser);
|
|
217
|
-
loginSuccess();
|
|
167
|
+
loginSuccess();
|
|
218
168
|
onAuthStateChange?.(fullUser);
|
|
219
|
-
|
|
220
|
-
await storage.setItem('oxy_identity_synced', 'true').catch(() => {});
|
|
221
|
-
}
|
|
222
|
-
setIdentitySynced(true);
|
|
223
|
-
|
|
169
|
+
|
|
224
170
|
return fullUser;
|
|
225
171
|
},
|
|
226
172
|
[
|
|
@@ -231,263 +177,11 @@ export const useAuthOperations = ({
|
|
|
231
177
|
oxyServices,
|
|
232
178
|
saveActiveSessionId,
|
|
233
179
|
setActiveSessionId,
|
|
234
|
-
setIdentitySynced,
|
|
235
180
|
switchSession,
|
|
236
181
|
updateSessions,
|
|
237
|
-
storage,
|
|
238
182
|
],
|
|
239
183
|
);
|
|
240
184
|
|
|
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 }> => {
|
|
247
|
-
if (!storage) throw new Error('Storage not initialized');
|
|
248
|
-
|
|
249
|
-
setAuthState({ isLoading: true, error: null });
|
|
250
|
-
|
|
251
|
-
try {
|
|
252
|
-
// Generate new key pair directly (works offline)
|
|
253
|
-
const { publicKey, privateKey } = await KeyManager.generateKeyPair();
|
|
254
|
-
await KeyManager.importKeyPair(privateKey);
|
|
255
|
-
|
|
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);
|
|
261
|
-
|
|
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');
|
|
281
|
-
}
|
|
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
|
-
}
|
|
361
|
-
|
|
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)) || []
|
|
415
|
-
);
|
|
416
|
-
|
|
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 };
|
|
467
|
-
}
|
|
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 };
|
|
474
|
-
}
|
|
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
185
|
|
|
492
186
|
/**
|
|
493
187
|
* Sign in with existing identity on device
|
|
@@ -511,7 +205,7 @@ export const useAuthOperations = ({
|
|
|
511
205
|
throw new Error('Identity is not registered. Please register your identity in the Accounts app before signing in.');
|
|
512
206
|
}
|
|
513
207
|
|
|
514
|
-
return await performSignIn(publicKey);
|
|
208
|
+
return await performSignIn(publicKey, deviceName);
|
|
515
209
|
} catch (error) {
|
|
516
210
|
const message = handleAuthError(error, {
|
|
517
211
|
defaultMessage: 'Sign in failed',
|
|
@@ -529,6 +223,7 @@ export const useAuthOperations = ({
|
|
|
529
223
|
[storage, setAuthState, performSignIn, loginFailure, onError, logger, oxyServices],
|
|
530
224
|
);
|
|
531
225
|
|
|
226
|
+
|
|
532
227
|
/**
|
|
533
228
|
* Logout from session
|
|
534
229
|
*/
|
|
@@ -608,29 +303,9 @@ export const useAuthOperations = ({
|
|
|
608
303
|
}
|
|
609
304
|
}, [activeSessionId, clearSessionState, logger, onError, oxyServices, setAuthState]);
|
|
610
305
|
|
|
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
306
|
return {
|
|
626
|
-
createIdentity,
|
|
627
|
-
importIdentity,
|
|
628
307
|
signIn,
|
|
629
308
|
logout,
|
|
630
309
|
logoutAll,
|
|
631
|
-
hasIdentity,
|
|
632
|
-
getPublicKey,
|
|
633
|
-
isIdentitySynced: isIdentitySyncedFn,
|
|
634
|
-
syncIdentity,
|
|
635
310
|
};
|
|
636
311
|
};
|