@oxyhq/services 5.16.18 → 5.16.20

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 (71) hide show
  1. package/README.md +7 -8
  2. package/lib/commonjs/crypto/index.js +0 -7
  3. package/lib/commonjs/crypto/index.js.map +1 -1
  4. package/lib/commonjs/crypto/keyManager.js +2 -2
  5. package/lib/commonjs/crypto/polyfill.js +4 -4
  6. package/lib/commonjs/crypto/types.js +2 -0
  7. package/lib/commonjs/crypto/types.js.map +1 -0
  8. package/lib/commonjs/i18n/locales/en-US.json +1 -1
  9. package/lib/commonjs/index.js +0 -7
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/ui/context/OxyContext.js +54 -11
  12. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  13. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +56 -15
  14. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  15. package/lib/commonjs/ui/hooks/useSessionSocket.js +52 -2
  16. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
  17. package/lib/module/crypto/index.js +2 -4
  18. package/lib/module/crypto/index.js.map +1 -1
  19. package/lib/module/crypto/keyManager.js +2 -2
  20. package/lib/module/crypto/polyfill.js +4 -4
  21. package/lib/module/crypto/types.js +2 -0
  22. package/lib/module/crypto/types.js.map +1 -0
  23. package/lib/module/i18n/locales/en-US.json +1 -1
  24. package/lib/module/index.js +1 -1
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/ui/context/OxyContext.js +49 -6
  27. package/lib/module/ui/context/OxyContext.js.map +1 -1
  28. package/lib/module/ui/context/hooks/useAuthOperations.js +56 -16
  29. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  30. package/lib/module/ui/hooks/useSessionSocket.js +52 -2
  31. package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
  32. package/lib/typescript/crypto/index.d.ts +2 -2
  33. package/lib/typescript/crypto/index.d.ts.map +1 -1
  34. package/lib/typescript/crypto/keyManager.d.ts +2 -2
  35. package/lib/typescript/crypto/polyfill.d.ts +2 -2
  36. package/lib/typescript/crypto/types.d.ts +18 -0
  37. package/lib/typescript/crypto/types.d.ts.map +1 -0
  38. package/lib/typescript/index.d.ts +2 -2
  39. package/lib/typescript/index.d.ts.map +1 -1
  40. package/lib/typescript/ui/context/OxyContext.d.ts +2 -2
  41. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  42. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts +3 -3
  43. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  44. package/lib/typescript/ui/hooks/useSessionSocket.d.ts +7 -1
  45. package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
  46. package/package.json +1 -2
  47. package/src/crypto/index.ts +3 -6
  48. package/src/crypto/keyManager.ts +2 -2
  49. package/src/crypto/polyfill.ts +4 -4
  50. package/src/crypto/types.ts +19 -0
  51. package/src/i18n/locales/en-US.json +1 -1
  52. package/src/index.ts +2 -4
  53. package/src/ui/context/OxyContext.tsx +63 -7
  54. package/src/ui/context/hooks/useAuthOperations.ts +67 -17
  55. package/src/ui/hooks/useSessionSocket.ts +62 -2
  56. package/lib/commonjs/crypto/recoveryPhrase.js +0 -152
  57. package/lib/commonjs/crypto/recoveryPhrase.js.map +0 -1
  58. package/lib/commonjs/ui/hooks/useIdentityMutations.js +0 -111
  59. package/lib/commonjs/ui/hooks/useIdentityMutations.js.map +0 -1
  60. package/lib/module/crypto/recoveryPhrase.js +0 -147
  61. package/lib/module/crypto/recoveryPhrase.js.map +0 -1
  62. package/lib/module/ui/hooks/useIdentityMutations.js +0 -105
  63. package/lib/module/ui/hooks/useIdentityMutations.js.map +0 -1
  64. package/lib/typescript/crypto/recoveryPhrase.d.ts +0 -59
  65. package/lib/typescript/crypto/recoveryPhrase.d.ts.map +0 -1
  66. package/lib/typescript/types/bip39.d.ts +0 -32
  67. package/lib/typescript/ui/hooks/useIdentityMutations.d.ts +0 -29
  68. package/lib/typescript/ui/hooks/useIdentityMutations.d.ts.map +0 -1
  69. package/src/crypto/recoveryPhrase.ts +0 -166
  70. package/src/types/bip39.d.ts +0 -32
  71. package/src/ui/hooks/useIdentityMutations.ts +0 -115
@@ -5,12 +5,12 @@
5
5
  * before any crypto operations are performed.
6
6
  *
7
7
  * Polyfills included:
8
- * - Buffer: Required by bip39 and other crypto libraries
9
- * - crypto.getRandomValues: Required by bip39 for secure random number generation
8
+ * - Buffer: Required by crypto libraries
9
+ * - crypto.getRandomValues: Required for secure random number generation
10
10
  */
11
11
 
12
12
  // Import Buffer polyfill for React Native compatibility
13
- // Libraries like bip39 depend on Buffer which isn't available in React Native
13
+ // Some crypto libraries depend on Buffer which isn't available in React Native
14
14
  import { Buffer } from 'buffer';
15
15
 
16
16
  // Get the global object in a cross-platform way
@@ -30,7 +30,7 @@ if (!globalObject.Buffer) {
30
30
  }
31
31
 
32
32
  // Polyfill crypto.getRandomValues for React Native
33
- // This is required by bip39 and other crypto libraries
33
+ // This is required by crypto libraries for secure random number generation
34
34
  type CryptoLike = {
35
35
  getRandomValues: <T extends ArrayBufferView>(array: T) => T;
36
36
  };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Cryptographic types for identity management
3
+ */
4
+
5
+ /**
6
+ * Encrypted backup data structure
7
+ * Used for identity backup files and QR code transfers
8
+ */
9
+ export interface BackupData {
10
+ /** Base64-encoded encrypted private key */
11
+ encrypted: string;
12
+ /** Hex-encoded salt used for key derivation */
13
+ salt: string;
14
+ /** Hex-encoded initialization vector */
15
+ iv: string;
16
+ /** Public key associated with the encrypted private key */
17
+ publicKey: string;
18
+ }
19
+
@@ -237,7 +237,7 @@
237
237
  "enterCode": "Enter the 6‑digit code from your authenticator app.",
238
238
  "newPassword": "Set New Password",
239
239
  "resetSuccess": "Your password has been reset! You can now sign in.",
240
- "noEmail": "We no longer send recovery emails. Please use your recovery phrase to restore your identity. Contact support if you need assistance.",
240
+ "noEmail": "We no longer send recovery emails. Please use your backup file to restore your identity. Contact support if you need assistance.",
241
241
  "password": {
242
242
  "minLength": "Password must be at least 8 characters long",
243
243
  "mismatch": "Passwords do not match",
package/src/index.ts CHANGED
@@ -13,8 +13,7 @@ import './crypto/polyfill';
13
13
  // Crypto/Identity exports (must be before core to ensure polyfills are available)
14
14
  export {
15
15
  KeyManager,
16
- SignatureService,
17
- RecoveryPhraseService
16
+ SignatureService
18
17
  } from './crypto';
19
18
 
20
19
  // Core exports
@@ -23,8 +22,7 @@ export { OXY_CLOUD_URL, oxyClient } from './core';
23
22
  export type {
24
23
  KeyPair,
25
24
  SignedMessage,
26
- AuthChallenge,
27
- RecoveryPhraseResult
25
+ AuthChallenge
28
26
  } from './crypto';
29
27
 
30
28
  // React context
@@ -9,7 +9,7 @@ import {
9
9
  useState,
10
10
  type ReactNode,
11
11
  } from 'react';
12
- import { Platform } from 'react-native';
12
+ import { Platform, Alert } from 'react-native';
13
13
  import { OxyServices } from '../../core';
14
14
  import type { User, ApiError } from '../../models/interfaces';
15
15
  import type { ClientSession } from '../../models/session';
@@ -29,7 +29,7 @@ import type { RouteName } from '../navigation/routes';
29
29
  import { showBottomSheet as globalShowBottomSheet } from '../navigation/bottomSheetManager';
30
30
  import { useQueryClient } from '@tanstack/react-query';
31
31
  import { clearQueryCache } from '../hooks/queryClient';
32
- import { KeyManager } from '../../crypto/keyManager';
32
+ import { KeyManager, type BackupData } from '../../crypto';
33
33
  import { translate } from '../../i18n';
34
34
  import { updateAvatarVisibility, updateProfileWithAvatar } from '../utils/avatarUtils';
35
35
  import { useAccountStore } from '../stores/accountStore';
@@ -50,8 +50,8 @@ export interface OxyContextState {
50
50
  currentNativeLanguageName: string;
51
51
 
52
52
  // Identity management (public key authentication - offline-first)
53
- createIdentity: () => Promise<{ recoveryPhrase: string[]; synced: boolean }>;
54
- importIdentity: (phrase: string) => Promise<{ synced: boolean }>;
53
+ createIdentity: () => Promise<{ synced: boolean }>;
54
+ importIdentity: (backupData: BackupData, password: string) => Promise<{ synced: boolean }>;
55
55
  signIn: (deviceName?: string) => Promise<User>;
56
56
  hasIdentity: () => Promise<boolean>;
57
57
  getPublicKey: () => Promise<string | null>;
@@ -216,7 +216,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
216
216
  if (__DEV__) {
217
217
  logger(restored
218
218
  ? 'Identity restored from backup successfully'
219
- : 'Identity integrity check failed - user may need to restore from recovery phrase'
219
+ : 'Identity integrity check failed - user may need to restore from backup file'
220
220
  );
221
221
  }
222
222
  } else {
@@ -234,7 +234,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
234
234
  if (__DEV__) {
235
235
  logger('Error during identity integrity check', error);
236
236
  }
237
- // Don't block app startup - user can recover with recovery phrase
237
+ // Don't block app startup - user can recover with backup file
238
238
  }
239
239
  };
240
240
 
@@ -287,7 +287,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
287
287
 
288
288
  const {
289
289
  createIdentity,
290
- importIdentity,
290
+ importIdentity: importIdentityBase,
291
291
  signIn,
292
292
  logout,
293
293
  logoutAll,
@@ -320,6 +320,24 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
320
320
  // syncIdentity - TanStack Query handles offline mutations automatically
321
321
  const syncIdentity = useCallback(() => syncIdentityBase(), [syncIdentityBase]);
322
322
 
323
+ // Wrapper for importIdentity to handle legacy calls gracefully
324
+ const importIdentity = useCallback(
325
+ async (backupData: BackupData | string, password?: string): Promise<{ synced: boolean }> => {
326
+ // Handle legacy calls with single string argument (old recovery phrase signature)
327
+ if (typeof backupData === 'string') {
328
+ throw new Error('Recovery phrase import is no longer supported. Please use backup file import or QR code transfer instead.');
329
+ }
330
+
331
+ // Validate that password is provided
332
+ if (!password || typeof password !== 'string') {
333
+ throw new Error('Password is required for backup file import.');
334
+ }
335
+
336
+ return importIdentityBase(backupData, password);
337
+ },
338
+ [importIdentityBase]
339
+ );
340
+
323
341
  // Clear all account data when identity is lost (for accounts app)
324
342
  // In accounts app, identity = account, so losing identity means losing everything
325
343
  const clearAllAccountData = useCallback(async (): Promise<void> => {
@@ -613,6 +631,43 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
613
631
  logout().catch((remoteError) => logger('Failed to process remote sign out', remoteError));
614
632
  }, [logger, logout]);
615
633
 
634
+ const handleIdentityTransferComplete = useCallback(
635
+ async (data: { transferId: string; sourceDeviceId: string; publicKey: string; completedAt: string }) => {
636
+ // Show confirmation dialog asking if user wants to delete identity from this device
637
+ return new Promise<void>((resolve) => {
638
+ Alert.alert(
639
+ 'Identity Transfer Complete',
640
+ 'Your identity has been successfully transferred to another device. Would you like to remove it from this device?',
641
+ [
642
+ {
643
+ text: 'Keep on This Device',
644
+ style: 'cancel',
645
+ onPress: () => resolve(),
646
+ },
647
+ {
648
+ text: 'Remove from This Device',
649
+ style: 'destructive',
650
+ onPress: async () => {
651
+ try {
652
+ // Delete identity with user confirmation
653
+ await deleteIdentityAndClearAccount(false, false, true);
654
+ toast.success('Identity removed from this device');
655
+ } catch (error: any) {
656
+ logger('Failed to delete identity after transfer', error);
657
+ toast.error(error?.message || 'Failed to remove identity from this device');
658
+ } finally {
659
+ resolve();
660
+ }
661
+ },
662
+ },
663
+ ],
664
+ { cancelable: true, onDismiss: () => resolve() }
665
+ );
666
+ });
667
+ },
668
+ [deleteIdentityAndClearAccount, logger]
669
+ );
670
+
616
671
  useSessionSocket({
617
672
  userId,
618
673
  activeSessionId,
@@ -623,6 +678,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
623
678
  baseURL: oxyServices.getBaseURL(),
624
679
  onRemoteSignOut: handleRemoteSignOut,
625
680
  onSessionRemoved: handleSessionRemoved,
681
+ onIdentityTransferComplete: handleIdentityTransferComplete,
626
682
  });
627
683
 
628
684
  const switchSessionForContext = useCallback(
@@ -7,7 +7,7 @@ import { fetchSessionsWithFallback, mapSessionsToClient } from '../../utils/sess
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, RecoveryPhraseService } from '../../../crypto';
10
+ import { KeyManager, SignatureService, type BackupData } from '../../../crypto';
11
11
 
12
12
  export interface UseAuthOperationsOptions {
13
13
  oxyServices: OxyServices;
@@ -34,9 +34,9 @@ export interface UseAuthOperationsOptions {
34
34
 
35
35
  export interface UseAuthOperationsResult {
36
36
  /** Create a new identity locally (offline-first) and optionally sync with server */
37
- createIdentity: () => Promise<{ recoveryPhrase: string[]; synced: boolean }>;
38
- /** Import an existing identity from recovery phrase */
39
- importIdentity: (phrase: string) => Promise<{ synced: boolean }>;
37
+ createIdentity: () => Promise<{ synced: boolean }>;
38
+ /** Import an existing identity from backup file data */
39
+ importIdentity: (backupData: BackupData, password: string) => Promise<{ synced: boolean }>;
40
40
  /** Sign in with existing identity on device */
41
41
  signIn: (deviceName?: string) => Promise<User>;
42
42
  /** Logout from current session */
@@ -274,18 +274,19 @@ export const useAuthOperations = ({
274
274
  );
275
275
 
276
276
  /**
277
- * Create a new identity with recovery phrase (offline-first)
277
+ * Create a new identity (offline-first)
278
278
  * Identity is purely cryptographic - no username or email required
279
279
  */
280
280
  const createIdentity = useCallback(
281
- async (): Promise<{ recoveryPhrase: string[]; synced: boolean }> => {
281
+ async (): Promise<{ synced: boolean }> => {
282
282
  if (!storage) throw new Error('Storage not initialized');
283
283
 
284
284
  setAuthState({ isLoading: true, error: null });
285
285
 
286
286
  try {
287
- // Generate new identity with recovery phrase (works offline)
288
- const { phrase, words, publicKey } = await RecoveryPhraseService.generateIdentityWithRecovery();
287
+ // Generate new key pair directly (works offline)
288
+ const { publicKey, privateKey } = await KeyManager.generateKeyPair();
289
+ await KeyManager.importKeyPair(privateKey);
289
290
 
290
291
  // Mark as not synced
291
292
  await storage.setItem('oxy_identity_synced', 'false');
@@ -301,7 +302,6 @@ export const useAuthOperations = ({
301
302
  setIdentitySynced(true);
302
303
 
303
304
  return {
304
- recoveryPhrase: words,
305
305
  synced: true,
306
306
  };
307
307
  } catch (syncError) {
@@ -311,13 +311,12 @@ export const useAuthOperations = ({
311
311
  }
312
312
 
313
313
  return {
314
- recoveryPhrase: words,
315
314
  synced: false,
316
315
  };
317
316
  }
318
317
  } catch (error) {
319
318
  // CRITICAL: Never delete identity on error - it may have been successfully created
320
- // Only log the error and let the user recover using their recovery phrase
319
+ // Only log the error and let the user recover using their backup file
321
320
  // Identity deletion should ONLY happen when explicitly requested by the user
322
321
  if (__DEV__ && logger) {
323
322
  logger('Error during identity creation (identity may still exist):', error);
@@ -330,7 +329,7 @@ export const useAuthOperations = ({
330
329
  await storage.setItem('oxy_identity_synced', 'false').catch(() => {});
331
330
  setIdentitySynced(false);
332
331
  if (__DEV__ && logger) {
333
- logger('Identity was created but sync failed - user can sync later using recovery phrase');
332
+ logger('Identity was created but sync failed - user can sync later using backup file');
334
333
  }
335
334
  } else {
336
335
  // No identity exists - this was a generation failure, safe to clean up sync flag
@@ -434,17 +433,68 @@ export const useAuthOperations = ({
434
433
  );
435
434
 
436
435
  /**
437
- * Import identity from recovery phrase (offline-first)
436
+ * Import identity from backup file data (offline-first)
438
437
  */
439
438
  const importIdentity = useCallback(
440
- async (phrase: string): Promise<{ synced: boolean }> => {
439
+ async (backupData: BackupData, password: string): Promise<{ synced: boolean }> => {
441
440
  if (!storage) throw new Error('Storage not initialized');
442
441
 
442
+ // Validate arguments - ensure backupData is an object, not a string (old signature)
443
+ if (!backupData || typeof backupData !== 'object' || Array.isArray(backupData)) {
444
+ throw new Error('Invalid backup data. Please use the backup file import feature.');
445
+ }
446
+
447
+ if (!backupData.encrypted || !backupData.salt || !backupData.iv || !backupData.publicKey) {
448
+ throw new Error('Invalid backup data structure. Missing required fields.');
449
+ }
450
+
451
+ if (!password || typeof password !== 'string') {
452
+ throw new Error('Password is required for backup file import.');
453
+ }
454
+
443
455
  setAuthState({ isLoading: true, error: null });
444
456
 
445
457
  try {
446
- // Restore identity from phrase (works offline)
447
- const publicKey = await RecoveryPhraseService.restoreFromPhrase(phrase);
458
+ // Decrypt private key from backup data
459
+ const Crypto = await import('expo-crypto');
460
+
461
+ // Convert hex strings to Uint8Array
462
+ const saltBytes = new Uint8Array(
463
+ backupData.salt.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []
464
+ );
465
+ const ivBytes = new Uint8Array(
466
+ backupData.iv.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []
467
+ );
468
+
469
+ // Derive key from password (same algorithm as EncryptedBackupGenerator)
470
+ const saltHex = Array.from(saltBytes).map(b => b.toString(16).padStart(2, '0')).join('');
471
+ let key = password + saltHex;
472
+ for (let i = 0; i < 10000; i++) {
473
+ key = await Crypto.digestStringAsync(
474
+ Crypto.CryptoDigestAlgorithm.SHA256,
475
+ key
476
+ );
477
+ }
478
+ const keyBytes = new Uint8Array(32);
479
+ for (let i = 0; i < 64 && i < key.length; i += 2) {
480
+ keyBytes[i / 2] = parseInt(key.substring(i, i + 2), 16);
481
+ }
482
+
483
+ // Decrypt private key (XOR decryption - same as encryption)
484
+ const encryptedBytes = Buffer.from(backupData.encrypted, 'base64');
485
+ const decryptedBytes = new Uint8Array(encryptedBytes.length);
486
+ for (let i = 0; i < encryptedBytes.length; i++) {
487
+ decryptedBytes[i] = encryptedBytes[i] ^ keyBytes[i % keyBytes.length] ^ ivBytes[i % ivBytes.length];
488
+ }
489
+ const privateKey = new TextDecoder().decode(decryptedBytes);
490
+
491
+ // Import the key pair
492
+ const publicKey = await KeyManager.importKeyPair(privateKey);
493
+
494
+ // Verify public key matches
495
+ if (publicKey !== backupData.publicKey) {
496
+ throw new Error('Backup file is corrupted or password is incorrect');
497
+ }
448
498
 
449
499
  // Mark as not synced
450
500
  await storage.setItem('oxy_identity_synced', 'false');
@@ -478,7 +528,7 @@ export const useAuthOperations = ({
478
528
  }
479
529
  } catch (error) {
480
530
  const message = handleAuthError(error, {
481
- defaultMessage: 'Failed to import identity',
531
+ defaultMessage: 'Failed to import identity. Please check your password and backup file.',
482
532
  code: REGISTER_ERROR_CODE,
483
533
  onError,
484
534
  setAuthError: (msg: string) => setAuthState({ error: msg }),
@@ -13,9 +13,10 @@ interface UseSessionSocketProps {
13
13
  baseURL: string;
14
14
  onRemoteSignOut?: () => void;
15
15
  onSessionRemoved?: (sessionId: string) => void;
16
+ onIdentityTransferComplete?: (data: { transferId: string; sourceDeviceId: string; publicKey: string; completedAt: string }) => void;
16
17
  }
17
18
 
18
- export function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSessions, logout, clearSessionState, baseURL, onRemoteSignOut, onSessionRemoved }: UseSessionSocketProps) {
19
+ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSessions, logout, clearSessionState, baseURL, onRemoteSignOut, onSessionRemoved, onIdentityTransferComplete }: UseSessionSocketProps) {
19
20
  const socketRef = useRef<any>(null);
20
21
  const joinedRoomRef = useRef<string | null>(null);
21
22
 
@@ -25,6 +26,7 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
25
26
  const clearSessionStateRef = useRef(clearSessionState);
26
27
  const onRemoteSignOutRef = useRef(onRemoteSignOut);
27
28
  const onSessionRemovedRef = useRef(onSessionRemoved);
29
+ const onIdentityTransferCompleteRef = useRef(onIdentityTransferComplete);
28
30
  const activeSessionIdRef = useRef(activeSessionId);
29
31
  const currentDeviceIdRef = useRef(currentDeviceId);
30
32
 
@@ -35,9 +37,10 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
35
37
  clearSessionStateRef.current = clearSessionState;
36
38
  onRemoteSignOutRef.current = onRemoteSignOut;
37
39
  onSessionRemovedRef.current = onSessionRemoved;
40
+ onIdentityTransferCompleteRef.current = onIdentityTransferComplete;
38
41
  activeSessionIdRef.current = activeSessionId;
39
42
  currentDeviceIdRef.current = currentDeviceId;
40
- }, [refreshSessions, logout, clearSessionState, onRemoteSignOut, onSessionRemoved, activeSessionId, currentDeviceId]);
43
+ }, [refreshSessions, logout, clearSessionState, onRemoteSignOut, onSessionRemoved, onIdentityTransferComplete, activeSessionId, currentDeviceId]);
41
44
 
42
45
  useEffect(() => {
43
46
  if (!userId || !baseURL) {
@@ -192,6 +195,63 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
192
195
  }
193
196
  });
194
197
  }
198
+ } else if (data.type === 'identity_transfer_complete') {
199
+ // Handle identity transfer completion notification
200
+ const transferData = data as {
201
+ type: 'identity_transfer_complete';
202
+ transferId: string;
203
+ sourceDeviceId: string;
204
+ publicKey: string;
205
+ completedAt: string;
206
+ };
207
+
208
+ if (__DEV__) {
209
+ logger.debug('Received identity_transfer_complete event', {
210
+ component: 'useSessionSocket',
211
+ transferId: transferData.transferId,
212
+ sourceDeviceId: transferData.sourceDeviceId,
213
+ currentDeviceId,
214
+ activeSessionId: activeSessionIdRef.current,
215
+ });
216
+ }
217
+
218
+ // Check if this device is the source device
219
+ // Match by deviceId if available, otherwise show prompt if we have an active session
220
+ // (user can decide if they want to delete)
221
+ const isSourceDevice = transferData.sourceDeviceId && (
222
+ transferData.sourceDeviceId === currentDeviceId ||
223
+ // Fallback: if we don't have currentDeviceId but have an active session,
224
+ // show the prompt anyway (user can decide)
225
+ (currentDeviceId === null && activeSessionIdRef.current !== null)
226
+ );
227
+
228
+ if (isSourceDevice) {
229
+ if (__DEV__) {
230
+ logger.debug('Matched source device, showing deletion prompt', {
231
+ component: 'useSessionSocket',
232
+ transferId: transferData.transferId,
233
+ sourceDeviceId: transferData.sourceDeviceId,
234
+ currentDeviceId,
235
+ matchedBy: transferData.sourceDeviceId === currentDeviceId ? 'deviceId' : 'session',
236
+ });
237
+ }
238
+ // This is the source device - notify callback to show deletion prompt
239
+ if (onIdentityTransferCompleteRef.current) {
240
+ onIdentityTransferCompleteRef.current({
241
+ transferId: transferData.transferId,
242
+ sourceDeviceId: transferData.sourceDeviceId,
243
+ publicKey: transferData.publicKey,
244
+ completedAt: transferData.completedAt,
245
+ });
246
+ }
247
+ } else if (__DEV__) {
248
+ logger.debug('Not the source device, ignoring transfer completion', {
249
+ component: 'useSessionSocket',
250
+ sourceDeviceId: transferData.sourceDeviceId,
251
+ currentDeviceId,
252
+ hasActiveSession: activeSessionIdRef.current !== null,
253
+ });
254
+ }
195
255
  } else {
196
256
  // For other event types (e.g., session_created), refresh sessions (with error handling)
197
257
  refreshSessionsRef.current().catch((error) => {
@@ -1,152 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.default = exports.RecoveryPhraseService = void 0;
7
- var bip39 = _interopRequireWildcard(require("bip39"));
8
- var _keyManager = require("./keyManager");
9
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
10
- /**
11
- * Recovery Phrase Service - BIP39 Mnemonic Generation
12
- *
13
- * Handles generation and restoration of recovery phrases (mnemonic seeds)
14
- * for backing up and restoring user identities.
15
- *
16
- * Note: This module requires the polyfill to be loaded first (done via crypto/index.ts)
17
- */
18
-
19
- /**
20
- * Convert Uint8Array or array-like to hexadecimal string
21
- * Works in both Node.js and React Native without depending on Buffer
22
- */
23
- function toHex(data) {
24
- // Convert to array of numbers if needed
25
- const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
26
- return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
27
- }
28
- class RecoveryPhraseService {
29
- /**
30
- * Generate a new identity with a recovery phrase
31
- * Returns the mnemonic phrase (should only be shown once to the user)
32
- */
33
- static async generateIdentityWithRecovery() {
34
- // Generate 128-bit entropy for 12-word mnemonic
35
- const mnemonic = bip39.generateMnemonic(128);
36
-
37
- // Derive private key from mnemonic
38
- // Using the seed directly as the private key (simplified approach)
39
- const seed = await bip39.mnemonicToSeed(mnemonic);
40
-
41
- // Use first 32 bytes of seed as private key
42
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
43
- const privateKeyHex = toHex(seedSlice);
44
-
45
- // Import the derived key pair
46
- const publicKey = await _keyManager.KeyManager.importKeyPair(privateKeyHex);
47
- return {
48
- phrase: mnemonic,
49
- words: mnemonic.split(' '),
50
- publicKey
51
- };
52
- }
53
-
54
- /**
55
- * Generate a 24-word recovery phrase for higher security
56
- */
57
- static async generateIdentityWithRecovery24() {
58
- // Generate 256-bit entropy for 24-word mnemonic
59
- const mnemonic = bip39.generateMnemonic(256);
60
- const seed = await bip39.mnemonicToSeed(mnemonic);
61
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
62
- const privateKeyHex = toHex(seedSlice);
63
- const publicKey = await _keyManager.KeyManager.importKeyPair(privateKeyHex);
64
- return {
65
- phrase: mnemonic,
66
- words: mnemonic.split(' '),
67
- publicKey
68
- };
69
- }
70
-
71
- /**
72
- * Restore an identity from a recovery phrase
73
- */
74
- static async restoreFromPhrase(phrase) {
75
- // Normalize and validate the phrase
76
- const normalizedPhrase = phrase.trim().toLowerCase();
77
- if (!bip39.validateMnemonic(normalizedPhrase)) {
78
- throw new Error('Invalid recovery phrase. Please check the words and try again.');
79
- }
80
-
81
- // Derive the same private key from the mnemonic
82
- const seed = await bip39.mnemonicToSeed(normalizedPhrase);
83
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
84
- const privateKeyHex = toHex(seedSlice);
85
-
86
- // Import and store the key pair
87
- const publicKey = await _keyManager.KeyManager.importKeyPair(privateKeyHex);
88
- return publicKey;
89
- }
90
-
91
- /**
92
- * Validate a recovery phrase without importing it
93
- */
94
- static validatePhrase(phrase) {
95
- const normalizedPhrase = phrase.trim().toLowerCase();
96
- return bip39.validateMnemonic(normalizedPhrase);
97
- }
98
-
99
- /**
100
- * Get the word list for autocomplete/validation
101
- */
102
- static getWordList() {
103
- return bip39.wordlists.english;
104
- }
105
-
106
- /**
107
- * Check if a word is valid in the BIP39 word list
108
- */
109
- static isValidWord(word) {
110
- return bip39.wordlists.english.includes(word.toLowerCase());
111
- }
112
-
113
- /**
114
- * Get suggestions for a partial word
115
- */
116
- static getSuggestions(partial, limit = 5) {
117
- const lowerPartial = partial.toLowerCase();
118
- return bip39.wordlists.english.filter(word => word.startsWith(lowerPartial)).slice(0, limit);
119
- }
120
-
121
- /**
122
- * Derive the public key from a phrase without storing
123
- * Useful for verification before importing
124
- */
125
- static async derivePublicKeyFromPhrase(phrase) {
126
- const normalizedPhrase = phrase.trim().toLowerCase();
127
- if (!bip39.validateMnemonic(normalizedPhrase)) {
128
- throw new Error('Invalid recovery phrase');
129
- }
130
- const seed = await bip39.mnemonicToSeed(normalizedPhrase);
131
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
132
- const privateKeyHex = toHex(seedSlice);
133
- return _keyManager.KeyManager.derivePublicKey(privateKeyHex);
134
- }
135
-
136
- /**
137
- * Convert a phrase to its word array
138
- */
139
- static phraseToWords(phrase) {
140
- return phrase.trim().toLowerCase().split(/\s+/);
141
- }
142
-
143
- /**
144
- * Convert a word array to a phrase string
145
- */
146
- static wordsToPhrase(words) {
147
- return words.map(w => w.toLowerCase().trim()).join(' ');
148
- }
149
- }
150
- exports.RecoveryPhraseService = RecoveryPhraseService;
151
- var _default = exports.default = RecoveryPhraseService;
152
- //# sourceMappingURL=recoveryPhrase.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["bip39","_interopRequireWildcard","require","_keyManager","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","toHex","data","bytes","Uint8Array","Array","from","map","b","toString","padStart","join","RecoveryPhraseService","generateIdentityWithRecovery","mnemonic","generateMnemonic","seed","mnemonicToSeed","seedSlice","subarray","slice","privateKeyHex","publicKey","KeyManager","importKeyPair","phrase","words","split","generateIdentityWithRecovery24","restoreFromPhrase","normalizedPhrase","trim","toLowerCase","validateMnemonic","Error","validatePhrase","getWordList","wordlists","english","isValidWord","word","includes","getSuggestions","partial","limit","lowerPartial","filter","startsWith","derivePublicKeyFromPhrase","derivePublicKey","phraseToWords","wordsToPhrase","w","exports","_default"],"sourceRoot":"../../../src","sources":["crypto/recoveryPhrase.ts"],"mappings":"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","ignoreList":[]}