@oxyhq/services 5.16.25 → 5.16.26
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/ui/context/OxyContext.js +246 -15
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +246 -15
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts +15 -2
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ui/context/OxyContext.tsx +277 -17
|
@@ -59,9 +59,12 @@ export interface OxyContextState {
|
|
|
59
59
|
isIdentitySynced: () => Promise<boolean>;
|
|
60
60
|
syncIdentity: () => Promise<User>;
|
|
61
61
|
deleteIdentityAndClearAccount: (skipBackup?: boolean, force?: boolean, userConfirmed?: boolean) => Promise<void>;
|
|
62
|
-
storeTransferCode: (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => void
|
|
63
|
-
getTransferCode: (transferId: string) => { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number } | null;
|
|
64
|
-
clearTransferCode: (transferId: string) => void
|
|
62
|
+
storeTransferCode: (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => Promise<void>;
|
|
63
|
+
getTransferCode: (transferId: string) => { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number; state: 'pending' | 'completed' | 'failed' } | null;
|
|
64
|
+
clearTransferCode: (transferId: string) => Promise<void>;
|
|
65
|
+
getAllPendingTransfers: () => Array<{ transferId: string; data: { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number; state: 'pending' | 'completed' | 'failed' } }>;
|
|
66
|
+
getActiveTransferId: () => string | null;
|
|
67
|
+
updateTransferState: (transferId: string, state: 'pending' | 'completed' | 'failed') => Promise<void>;
|
|
65
68
|
|
|
66
69
|
// Identity sync state (reactive, from Zustand store)
|
|
67
70
|
identitySyncState: {
|
|
@@ -384,6 +387,21 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
384
387
|
oxyServices.clearCache();
|
|
385
388
|
}, [queryClient, storage, clearSessionState, logger, oxyServices]);
|
|
386
389
|
|
|
390
|
+
// Transfer code management functions (must be defined before deleteIdentityAndClearAccount)
|
|
391
|
+
const getAllPendingTransfers = useCallback(() => {
|
|
392
|
+
const pending: Array<{ transferId: string; data: TransferCodeData }> = [];
|
|
393
|
+
transferCodesRef.current.forEach((data, transferId) => {
|
|
394
|
+
if (data.state === 'pending') {
|
|
395
|
+
pending.push({ transferId, data });
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
return pending;
|
|
399
|
+
}, []);
|
|
400
|
+
|
|
401
|
+
const getActiveTransferId = useCallback(() => {
|
|
402
|
+
return activeTransferIdRef.current;
|
|
403
|
+
}, []);
|
|
404
|
+
|
|
387
405
|
// Delete identity and clear all account data
|
|
388
406
|
// In accounts app, deleting identity means losing the account completely
|
|
389
407
|
const deleteIdentityAndClearAccount = useCallback(async (
|
|
@@ -391,12 +409,30 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
391
409
|
force: boolean = false,
|
|
392
410
|
userConfirmed: boolean = false
|
|
393
411
|
): Promise<void> => {
|
|
412
|
+
// CRITICAL: Check for active transfers before deletion (unless force is true)
|
|
413
|
+
// This prevents accidental identity loss during transfer
|
|
414
|
+
if (!force) {
|
|
415
|
+
const pendingTransfers = getAllPendingTransfers();
|
|
416
|
+
if (pendingTransfers.length > 0) {
|
|
417
|
+
const activeTransferId = getActiveTransferId();
|
|
418
|
+
const hasActiveTransfer = activeTransferId && pendingTransfers.some(t => t.transferId === activeTransferId);
|
|
419
|
+
|
|
420
|
+
if (hasActiveTransfer) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
'Cannot delete identity: An active identity transfer is in progress. ' +
|
|
423
|
+
'Please wait for the transfer to complete or cancel it first. ' +
|
|
424
|
+
'If you proceed, you may lose access to your identity permanently.'
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
394
430
|
// First, clear all account data
|
|
395
431
|
await clearAllAccountData();
|
|
396
432
|
|
|
397
433
|
// Then delete the identity keys
|
|
398
434
|
await KeyManager.deleteIdentity(skipBackup, force, userConfirmed);
|
|
399
|
-
}, [clearAllAccountData]);
|
|
435
|
+
}, [clearAllAccountData, getAllPendingTransfers, getActiveTransferId]);
|
|
400
436
|
|
|
401
437
|
// Network reconnect sync - TanStack Query automatically retries mutations on reconnect
|
|
402
438
|
// We only need to sync identity if it's not synced
|
|
@@ -634,51 +670,207 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
634
670
|
// The JWT token's userId field contains the MongoDB ObjectId
|
|
635
671
|
const userId = oxyServices.getCurrentUserId() || user?.id;
|
|
636
672
|
|
|
637
|
-
//
|
|
638
|
-
|
|
639
|
-
|
|
673
|
+
// Transfer code storage interface
|
|
674
|
+
interface TransferCodeData {
|
|
675
|
+
code: string;
|
|
676
|
+
sourceDeviceId: string | null;
|
|
677
|
+
publicKey: string;
|
|
678
|
+
timestamp: number;
|
|
679
|
+
state: 'pending' | 'completed' | 'failed';
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Store transfer codes in memory for verification (also persisted to storage)
|
|
683
|
+
// Map: transferId -> TransferCodeData
|
|
684
|
+
const transferCodesRef = useRef<Map<string, TransferCodeData>>(new Map());
|
|
685
|
+
const activeTransferIdRef = useRef<string | null>(null);
|
|
686
|
+
const TRANSFER_CODES_STORAGE_KEY = `${storageKeyPrefix}_transfer_codes`;
|
|
687
|
+
const ACTIVE_TRANSFER_STORAGE_KEY = `${storageKeyPrefix}_active_transfer_id`;
|
|
688
|
+
|
|
689
|
+
// Load transfer codes from storage on startup
|
|
690
|
+
useEffect(() => {
|
|
691
|
+
if (!storage || !isStorageReady) return;
|
|
692
|
+
|
|
693
|
+
const loadTransferCodes = async () => {
|
|
694
|
+
try {
|
|
695
|
+
// Load transfer codes
|
|
696
|
+
const storedCodes = await storage.getItem(TRANSFER_CODES_STORAGE_KEY);
|
|
697
|
+
if (storedCodes) {
|
|
698
|
+
const parsed = JSON.parse(storedCodes);
|
|
699
|
+
const now = Date.now();
|
|
700
|
+
const fifteenMinutes = 15 * 60 * 1000;
|
|
701
|
+
|
|
702
|
+
// Only restore non-expired pending transfers
|
|
703
|
+
Object.entries(parsed).forEach(([transferId, data]: [string, any]) => {
|
|
704
|
+
if (data.state === 'pending' && (now - data.timestamp) < fifteenMinutes) {
|
|
705
|
+
transferCodesRef.current.set(transferId, data);
|
|
706
|
+
if (__DEV__) {
|
|
707
|
+
logger('Restored pending transfer from storage', { transferId });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Load active transfer ID
|
|
714
|
+
const activeTransferId = await storage.getItem(ACTIVE_TRANSFER_STORAGE_KEY);
|
|
715
|
+
if (activeTransferId) {
|
|
716
|
+
// Verify it's still valid
|
|
717
|
+
const transferData = transferCodesRef.current.get(activeTransferId);
|
|
718
|
+
if (transferData && transferData.state === 'pending') {
|
|
719
|
+
activeTransferIdRef.current = activeTransferId;
|
|
720
|
+
if (__DEV__) {
|
|
721
|
+
logger('Restored active transfer ID from storage', { transferId: activeTransferId });
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
// Clear invalid active transfer
|
|
725
|
+
await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
} catch (error) {
|
|
729
|
+
if (__DEV__) {
|
|
730
|
+
logger('Failed to load transfer codes from storage', error);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
loadTransferCodes();
|
|
736
|
+
}, [storage, isStorageReady, logger, storageKeyPrefix]);
|
|
737
|
+
|
|
738
|
+
// Persist transfer codes to storage whenever they change
|
|
739
|
+
const persistTransferCodes = useCallback(async () => {
|
|
740
|
+
if (!storage) return;
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
const codesToStore: Record<string, TransferCodeData> = {};
|
|
744
|
+
transferCodesRef.current.forEach((value, key) => {
|
|
745
|
+
codesToStore[key] = value;
|
|
746
|
+
});
|
|
747
|
+
await storage.setItem(TRANSFER_CODES_STORAGE_KEY, JSON.stringify(codesToStore));
|
|
748
|
+
} catch (error) {
|
|
749
|
+
if (__DEV__) {
|
|
750
|
+
logger('Failed to persist transfer codes', error);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}, [storage, logger]);
|
|
640
754
|
|
|
641
755
|
// Cleanup old transfer codes (older than 15 minutes)
|
|
642
756
|
useEffect(() => {
|
|
643
|
-
const cleanup = setInterval(() => {
|
|
757
|
+
const cleanup = setInterval(async () => {
|
|
644
758
|
const now = Date.now();
|
|
645
759
|
const fifteenMinutes = 15 * 60 * 1000;
|
|
760
|
+
let needsPersist = false;
|
|
761
|
+
|
|
646
762
|
transferCodesRef.current.forEach((value, key) => {
|
|
647
763
|
if (now - value.timestamp > fifteenMinutes) {
|
|
648
764
|
transferCodesRef.current.delete(key);
|
|
765
|
+
needsPersist = true;
|
|
649
766
|
if (__DEV__) {
|
|
650
767
|
logger('Cleaned up expired transfer code', { transferId: key });
|
|
651
768
|
}
|
|
652
769
|
}
|
|
653
770
|
});
|
|
771
|
+
|
|
772
|
+
// Clear active transfer if it was deleted
|
|
773
|
+
if (activeTransferIdRef.current && !transferCodesRef.current.has(activeTransferIdRef.current)) {
|
|
774
|
+
activeTransferIdRef.current = null;
|
|
775
|
+
if (storage) {
|
|
776
|
+
try {
|
|
777
|
+
await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
|
|
778
|
+
} catch (error) {
|
|
779
|
+
// Ignore storage errors
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (needsPersist) {
|
|
785
|
+
await persistTransferCodes();
|
|
786
|
+
}
|
|
654
787
|
}, 60000); // Check every minute
|
|
655
788
|
|
|
656
789
|
return () => clearInterval(cleanup);
|
|
657
|
-
}, [logger]);
|
|
790
|
+
}, [logger, persistTransferCodes, storage]);
|
|
658
791
|
|
|
659
792
|
// Transfer code management functions
|
|
660
|
-
const storeTransferCode = useCallback((transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => {
|
|
661
|
-
|
|
793
|
+
const storeTransferCode = useCallback(async (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => {
|
|
794
|
+
const transferData: TransferCodeData = {
|
|
662
795
|
code,
|
|
663
796
|
sourceDeviceId,
|
|
664
797
|
publicKey,
|
|
665
798
|
timestamp: Date.now(),
|
|
666
|
-
|
|
799
|
+
state: 'pending',
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
transferCodesRef.current.set(transferId, transferData);
|
|
803
|
+
activeTransferIdRef.current = transferId;
|
|
804
|
+
|
|
805
|
+
// Persist to storage
|
|
806
|
+
await persistTransferCodes();
|
|
807
|
+
if (storage) {
|
|
808
|
+
try {
|
|
809
|
+
await storage.setItem(ACTIVE_TRANSFER_STORAGE_KEY, transferId);
|
|
810
|
+
} catch (error) {
|
|
811
|
+
if (__DEV__) {
|
|
812
|
+
logger('Failed to persist active transfer ID', error);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
667
817
|
if (__DEV__) {
|
|
668
818
|
logger('Stored transfer code', { transferId, sourceDeviceId, publicKey: publicKey.substring(0, 16) + '...' });
|
|
669
819
|
}
|
|
670
|
-
}, [logger]);
|
|
820
|
+
}, [logger, persistTransferCodes, storage]);
|
|
671
821
|
|
|
672
822
|
const getTransferCode = useCallback((transferId: string) => {
|
|
673
823
|
return transferCodesRef.current.get(transferId) || null;
|
|
674
824
|
}, []);
|
|
675
825
|
|
|
676
|
-
const
|
|
826
|
+
const updateTransferState = useCallback(async (transferId: string, state: 'pending' | 'completed' | 'failed') => {
|
|
827
|
+
const transferData = transferCodesRef.current.get(transferId);
|
|
828
|
+
if (transferData) {
|
|
829
|
+
transferData.state = state;
|
|
830
|
+
transferCodesRef.current.set(transferId, transferData);
|
|
831
|
+
|
|
832
|
+
// Clear active transfer if completed or failed
|
|
833
|
+
if (state === 'completed' || state === 'failed') {
|
|
834
|
+
if (activeTransferIdRef.current === transferId) {
|
|
835
|
+
activeTransferIdRef.current = null;
|
|
836
|
+
if (storage) {
|
|
837
|
+
try {
|
|
838
|
+
await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
|
|
839
|
+
} catch (error) {
|
|
840
|
+
// Ignore storage errors
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
await persistTransferCodes();
|
|
847
|
+
|
|
848
|
+
if (__DEV__) {
|
|
849
|
+
logger('Updated transfer state', { transferId, state });
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}, [logger, persistTransferCodes, storage]);
|
|
853
|
+
|
|
854
|
+
const clearTransferCode = useCallback(async (transferId: string) => {
|
|
677
855
|
transferCodesRef.current.delete(transferId);
|
|
856
|
+
|
|
857
|
+
if (activeTransferIdRef.current === transferId) {
|
|
858
|
+
activeTransferIdRef.current = null;
|
|
859
|
+
if (storage) {
|
|
860
|
+
try {
|
|
861
|
+
await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
|
|
862
|
+
} catch (error) {
|
|
863
|
+
// Ignore storage errors
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
await persistTransferCodes();
|
|
869
|
+
|
|
678
870
|
if (__DEV__) {
|
|
679
871
|
logger('Cleared transfer code', { transferId });
|
|
680
872
|
}
|
|
681
|
-
}, [logger]);
|
|
873
|
+
}, [logger, persistTransferCodes, storage]);
|
|
682
874
|
|
|
683
875
|
const refreshSessionsWithUser = useCallback(
|
|
684
876
|
() => refreshSessions(userId),
|
|
@@ -789,6 +981,44 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
789
981
|
return;
|
|
790
982
|
}
|
|
791
983
|
|
|
984
|
+
// CRITICAL: Verify target device has identity before deleting on source device
|
|
985
|
+
// This ensures we never lose the identity
|
|
986
|
+
try {
|
|
987
|
+
logger('Verifying target device has identity before deletion', {
|
|
988
|
+
transferId: data.transferId,
|
|
989
|
+
publicKey: data.publicKey.substring(0, 16) + '...',
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
// Verify target device has active session with matching public key
|
|
993
|
+
const verifyResponse = await oxyServices.makeRequest<{ verified: boolean; hasActiveSession: boolean }>(
|
|
994
|
+
'GET',
|
|
995
|
+
`/api/identity/verify-transfer?publicKey=${encodeURIComponent(data.publicKey)}`,
|
|
996
|
+
undefined,
|
|
997
|
+
{ cache: false }
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
if (!verifyResponse.verified || !verifyResponse.hasActiveSession) {
|
|
1001
|
+
logger('Target device verification failed - identity will not be deleted', {
|
|
1002
|
+
transferId: data.transferId,
|
|
1003
|
+
verified: verifyResponse.verified,
|
|
1004
|
+
hasActiveSession: verifyResponse.hasActiveSession,
|
|
1005
|
+
});
|
|
1006
|
+
await updateTransferState(data.transferId, 'failed');
|
|
1007
|
+
toast.error('Transfer verification failed: Target device does not have active session. Identity will not be deleted.');
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
logger('Target device verification passed', {
|
|
1012
|
+
transferId: data.transferId,
|
|
1013
|
+
});
|
|
1014
|
+
} catch (verifyError: any) {
|
|
1015
|
+
// If verification fails, don't delete identity - it's safer to keep it
|
|
1016
|
+
logger('Failed to verify target device - identity will not be deleted', verifyError);
|
|
1017
|
+
await updateTransferState(data.transferId, 'failed');
|
|
1018
|
+
toast.error('Transfer verification failed: Could not verify target device. Identity will not be deleted.');
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
792
1022
|
logger('All transfer verifications passed, deleting identity from source device', {
|
|
793
1023
|
transferId: data.transferId,
|
|
794
1024
|
sourceDeviceId: data.sourceDeviceId,
|
|
@@ -796,8 +1026,31 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
796
1026
|
});
|
|
797
1027
|
|
|
798
1028
|
try {
|
|
1029
|
+
// Verify identity still exists before deletion (safety check)
|
|
1030
|
+
const identityStillExists = await KeyManager.hasIdentity();
|
|
1031
|
+
if (!identityStillExists) {
|
|
1032
|
+
logger('Identity already deleted - skipping deletion', {
|
|
1033
|
+
transferId: data.transferId,
|
|
1034
|
+
});
|
|
1035
|
+
await updateTransferState(data.transferId, 'completed');
|
|
1036
|
+
await clearTransferCode(data.transferId);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
799
1040
|
await deleteIdentityAndClearAccount(false, false, true);
|
|
800
|
-
|
|
1041
|
+
|
|
1042
|
+
// Verify identity was actually deleted
|
|
1043
|
+
const identityDeleted = !(await KeyManager.hasIdentity());
|
|
1044
|
+
if (!identityDeleted) {
|
|
1045
|
+
logger('Identity deletion failed - identity still exists', {
|
|
1046
|
+
transferId: data.transferId,
|
|
1047
|
+
});
|
|
1048
|
+
await updateTransferState(data.transferId, 'failed');
|
|
1049
|
+
throw new Error('Identity deletion failed - identity still exists');
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
await updateTransferState(data.transferId, 'completed');
|
|
1053
|
+
await clearTransferCode(data.transferId);
|
|
801
1054
|
|
|
802
1055
|
logger('Identity successfully deleted and transfer code cleared', {
|
|
803
1056
|
transferId: data.transferId,
|
|
@@ -806,6 +1059,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
806
1059
|
toast.success('Identity successfully transferred and removed from this device');
|
|
807
1060
|
} catch (deleteError: any) {
|
|
808
1061
|
logger('Error during identity deletion', deleteError);
|
|
1062
|
+
await updateTransferState(data.transferId, 'failed');
|
|
809
1063
|
throw deleteError;
|
|
810
1064
|
}
|
|
811
1065
|
} catch (error: any) {
|
|
@@ -813,7 +1067,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
813
1067
|
toast.error(error?.message || 'Failed to remove identity from this device. Please try again manually from Security Settings.');
|
|
814
1068
|
}
|
|
815
1069
|
},
|
|
816
|
-
[deleteIdentityAndClearAccount, logger, getTransferCode, clearTransferCode, currentDeviceId, activeSessionId],
|
|
1070
|
+
[deleteIdentityAndClearAccount, logger, getTransferCode, clearTransferCode, updateTransferState, currentDeviceId, activeSessionId, oxyServices],
|
|
817
1071
|
);
|
|
818
1072
|
|
|
819
1073
|
useSessionSocket({
|
|
@@ -907,6 +1161,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
907
1161
|
storeTransferCode,
|
|
908
1162
|
getTransferCode,
|
|
909
1163
|
clearTransferCode,
|
|
1164
|
+
getAllPendingTransfers,
|
|
1165
|
+
getActiveTransferId,
|
|
1166
|
+
updateTransferState,
|
|
910
1167
|
identitySyncState: {
|
|
911
1168
|
isSynced: isIdentitySyncedStore ?? true,
|
|
912
1169
|
isSyncing: isSyncing ?? false,
|
|
@@ -940,6 +1197,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
940
1197
|
storeTransferCode,
|
|
941
1198
|
getTransferCode,
|
|
942
1199
|
clearTransferCode,
|
|
1200
|
+
getAllPendingTransfers,
|
|
1201
|
+
getActiveTransferId,
|
|
1202
|
+
updateTransferState,
|
|
943
1203
|
isIdentitySyncedStore,
|
|
944
1204
|
isSyncing,
|
|
945
1205
|
currentLanguage,
|