@oxyhq/services 5.17.3 → 5.17.5
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 +14 -42
- 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 +28 -11
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionSocket.js +20 -0
- package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +14 -42
- 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 +28 -11
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/hooks/useSessionSocket.js +20 -0
- package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContextBase.d.ts.map +1 -1
- package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ui/context/OxyContext.tsx +13 -37
- package/src/ui/context/OxyContextBase.tsx +68 -68
- package/src/ui/context/hooks/useAuthOperations.ts +35 -19
- package/src/ui/hooks/useSessionSocket.ts +17 -0
|
@@ -9,77 +9,77 @@ import type { RouteName } from '../navigation/routes';
|
|
|
9
9
|
import type { BackupData } from '../../crypto';
|
|
10
10
|
|
|
11
11
|
export interface OxyContextState {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
user: User | null;
|
|
13
|
+
sessions: ClientSession[];
|
|
14
|
+
activeSessionId: string | null;
|
|
15
|
+
currentDeviceId: string | null;
|
|
16
|
+
isAuthenticated: boolean;
|
|
17
|
+
isLoading: boolean;
|
|
18
|
+
isTokenReady: boolean;
|
|
19
|
+
isStorageReady: boolean;
|
|
20
|
+
error: string | null;
|
|
21
|
+
currentLanguage: string;
|
|
22
|
+
currentLanguageMetadata: ReturnType<typeof useLanguageManagement>['metadata'];
|
|
23
|
+
currentLanguageName: string;
|
|
24
|
+
currentNativeLanguageName: string;
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
// Identity sync state (reactive, from Zustand store)
|
|
43
|
+
identitySyncState: {
|
|
44
|
+
isSynced: boolean;
|
|
45
|
+
isSyncing: boolean;
|
|
46
|
+
};
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
48
|
+
// Session management
|
|
49
|
+
logout: (targetSessionId?: string) => Promise<void>;
|
|
50
|
+
logoutAll: () => Promise<void>;
|
|
51
|
+
switchSession: (sessionId: string) => Promise<void>;
|
|
52
|
+
removeSession: (sessionId: string) => Promise<void>;
|
|
53
|
+
refreshSessions: () => Promise<void>;
|
|
54
|
+
setLanguage: (languageId: string) => Promise<void>;
|
|
55
|
+
getDeviceSessions: () => Promise<
|
|
56
|
+
Array<{
|
|
57
|
+
sessionId: string;
|
|
58
|
+
deviceId: string;
|
|
59
|
+
deviceName?: string;
|
|
60
|
+
lastActive?: string;
|
|
61
|
+
expiresAt?: string;
|
|
62
|
+
}>
|
|
63
|
+
>;
|
|
64
|
+
logoutAllDeviceSessions: () => Promise<void>;
|
|
65
|
+
updateDeviceName: (deviceName: string) => Promise<void>;
|
|
66
|
+
clearSessionState: () => Promise<void>;
|
|
67
|
+
clearAllAccountData: () => Promise<void>;
|
|
68
|
+
oxyServices: OxyServices;
|
|
69
|
+
useFollow?: UseFollowHook;
|
|
70
|
+
showBottomSheet?: (screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> }) => void;
|
|
71
|
+
openAvatarPicker: () => void;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export const OxyContext = createContext<OxyContextState | null>(null);
|
|
75
75
|
|
|
76
76
|
export interface OxyContextProviderProps {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
children: ReactNode;
|
|
78
|
+
oxyServices?: OxyServices;
|
|
79
|
+
baseURL?: string;
|
|
80
|
+
storageKeyPrefix?: string;
|
|
81
|
+
onAuthStateChange?: (user: User | null) => void;
|
|
82
|
+
onError?: (error: ApiError) => void;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
@@ -87,9 +87,9 @@ export interface OxyContextProviderProps {
|
|
|
87
87
|
* Must be used within an OxyContextProvider.
|
|
88
88
|
*/
|
|
89
89
|
export const useOxy = (): OxyContextState => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
const context = useContext(OxyContext);
|
|
91
|
+
if (!context) {
|
|
92
|
+
throw new Error('useOxy must be used within an OxyContextProvider');
|
|
93
|
+
}
|
|
94
|
+
return context;
|
|
95
95
|
};
|
|
@@ -96,18 +96,18 @@ export const useAuthOperations = ({
|
|
|
96
96
|
const USER_ID_STORAGE_KEY = 'oxy_user_id';
|
|
97
97
|
|
|
98
98
|
// Online-only sign-in: require backend availability
|
|
99
|
-
// First,
|
|
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
|
|
100
101
|
let userId: string | null = null;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
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
111
|
}
|
|
112
112
|
|
|
113
113
|
const challengeResponse = await oxyServices.requestChallenge(userId);
|
|
@@ -126,14 +126,30 @@ export const useAuthOperations = ({
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
// Verify and create session using userId
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
}
|
|
137
153
|
|
|
138
154
|
// Store tokens immediately (no extra round-trip)
|
|
139
155
|
oxyServices.setTokens(sessionResponse.accessToken, sessionResponse.refreshToken);
|
|
@@ -63,6 +63,21 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// IMPORTANT: If userId is set but we have no token, defer socket creation
|
|
67
|
+
// This prevents socket reconnection race condition during auth flow
|
|
68
|
+
// (e.g., when userId changes but tokens aren't set yet in transfer flow)
|
|
69
|
+
const freshToken = getAccessTokenRef.current();
|
|
70
|
+
if (!freshToken) {
|
|
71
|
+
logger.debug('Deferring socket creation - no access token available yet', { component: 'useSessionSocket', userId });
|
|
72
|
+
// Disconnect existing socket if it exists but we have no token
|
|
73
|
+
if (socketRef.current) {
|
|
74
|
+
socketRef.current.disconnect();
|
|
75
|
+
socketRef.current = null;
|
|
76
|
+
joinedRoomRef.current = null;
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
66
81
|
// Initialize socket with token refresh
|
|
67
82
|
const initializeSocket = async () => {
|
|
68
83
|
try {
|
|
@@ -96,6 +111,8 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
96
111
|
};
|
|
97
112
|
} else {
|
|
98
113
|
logger.debug('No access token available for socket authentication', { component: 'useSessionSocket', userId });
|
|
114
|
+
// Defer socket creation if token is still missing
|
|
115
|
+
return;
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
socketRef.current = io(baseURL, socketOptions);
|