@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.
- package/lib/commonjs/crypto/index.js +0 -23
- package/lib/commonjs/crypto/index.js.map +1 -1
- package/lib/commonjs/index.js +0 -15
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/Icon.js.map +1 -1
- package/lib/commonjs/ui/components/IconButton/utils.js.map +1 -1
- package/lib/commonjs/ui/components/TextField/Adornment/utils.js.map +1 -1
- package/lib/commonjs/ui/components/TextField/helpers.js.map +1 -1
- package/lib/commonjs/ui/components/TouchableRipple/utils.js.map +1 -1
- package/lib/commonjs/ui/components/Typography/AnimatedText.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +37 -589
- 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 +60 -425
- 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/commonjs/ui/screens/OxyAuthScreen.js +0 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/commonjs/ui/stores/authStore.js +52 -15
- package/lib/commonjs/ui/stores/authStore.js.map +1 -1
- package/lib/commonjs/ui/utils/avatarUtils.js +2 -32
- package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
- package/lib/module/crypto/index.js +4 -6
- package/lib/module/crypto/index.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/Icon.js.map +1 -1
- package/lib/module/ui/components/IconButton/utils.js.map +1 -1
- package/lib/module/ui/components/TextField/Adornment/utils.js.map +1 -1
- package/lib/module/ui/components/TextField/helpers.js.map +1 -1
- package/lib/module/ui/components/TouchableRipple/utils.js.map +1 -1
- package/lib/module/ui/components/Typography/AnimatedText.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +35 -588
- 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 +60 -424
- 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/module/ui/screens/OxyAuthScreen.js +0 -1
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/ui/stores/authStore.js +52 -15
- package/lib/module/ui/stores/authStore.js.map +1 -1
- package/lib/module/ui/utils/avatarUtils.js +2 -32
- package/lib/module/ui/utils/avatarUtils.js.map +1 -1
- package/lib/typescript/crypto/index.d.ts +2 -5
- package/lib/typescript/crypto/index.d.ts.map +1 -1
- package/lib/typescript/crypto/types.d.ts +6 -2
- package/lib/typescript/crypto/types.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +4 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ui/components/IconButton/utils.d.ts +1 -1
- package/lib/typescript/ui/components/TextField/Adornment/utils.d.ts +1 -1
- package/lib/typescript/ui/components/TextField/Adornment/utils.d.ts.map +1 -1
- package/lib/typescript/ui/components/TextField/helpers.d.ts +6 -6
- package/lib/typescript/ui/components/types.d.ts +0 -4
- package/lib/typescript/ui/components/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 +2 -39
- package/lib/typescript/ui/context/OxyContextBase.d.ts.map +1 -1
- package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts +10 -25
- 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/lib/typescript/ui/stores/authStore.d.ts +27 -4
- package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
- package/lib/typescript/ui/utils/avatarUtils.d.ts +0 -2
- package/lib/typescript/ui/utils/avatarUtils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/crypto/index.ts +3 -11
- package/src/crypto/types.ts +6 -2
- package/src/index.ts +6 -11
- package/src/ui/components/Icon.tsx +1 -1
- package/src/ui/components/IconButton/utils.ts +1 -1
- package/src/ui/components/TextField/Adornment/utils.ts +2 -2
- package/src/ui/components/TextField/helpers.tsx +8 -8
- package/src/ui/components/TouchableRipple/utils.ts +2 -2
- package/src/ui/components/Typography/AnimatedText.tsx +2 -2
- package/src/ui/components/types.tsx +0 -6
- package/src/ui/context/OxyContext.tsx +33 -637
- package/src/ui/context/OxyContextBase.tsx +5 -23
- package/src/ui/context/hooks/useAuthOperations.ts +84 -460
- 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
- package/src/ui/screens/OxyAuthScreen.tsx +1 -1
- package/src/ui/stores/authStore.ts +57 -18
- package/src/ui/utils/avatarUtils.ts +4 -36
- package/lib/commonjs/crypto/keyManager.js +0 -511
- package/lib/commonjs/crypto/keyManager.js.map +0 -1
- package/lib/commonjs/crypto/signatureService.js +0 -269
- package/lib/commonjs/crypto/signatureService.js.map +0 -1
- package/lib/module/crypto/keyManager.js +0 -508
- package/lib/module/crypto/keyManager.js.map +0 -1
- package/lib/module/crypto/signatureService.js +0 -266
- package/lib/module/crypto/signatureService.js.map +0 -1
- package/lib/typescript/crypto/keyManager.d.ts +0 -97
- package/lib/typescript/crypto/keyManager.d.ts.map +0 -1
- package/lib/typescript/crypto/signatureService.d.ts +0 -77
- package/lib/typescript/crypto/signatureService.d.ts.map +0 -1
- package/src/crypto/keyManager.ts +0 -545
- 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
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 {
|
|
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
|
-
/**
|
|
37
|
-
|
|
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
|
|
63
|
-
*
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
setSyncing,
|
|
84
|
-
logger,
|
|
67
|
+
setAuthState,
|
|
68
|
+
logger,
|
|
85
69
|
}: UseAuthOperationsOptions): UseAuthOperationsResult => {
|
|
86
70
|
|
|
87
71
|
/**
|
|
88
|
-
*
|
|
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
|
|
91
|
-
async (
|
|
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
|
-
//
|
|
253
|
-
|
|
254
|
-
await KeyManager.importKeyPair(privateKey);
|
|
90
|
+
// Store tokens (Services' only responsibility)
|
|
91
|
+
oxyServices.setTokens(sessionResponse.accessToken, sessionResponse.refreshToken);
|
|
255
92
|
|
|
256
|
-
//
|
|
257
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
135
|
+
setActiveSessionId(sessionResponse.sessionId);
|
|
136
|
+
await saveActiveSessionId(sessionResponse.sessionId);
|
|
137
|
+
updateSessions(allDeviceSessions, { merge: true });
|
|
500
138
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
|
143
|
+
return fullUser;
|
|
515
144
|
} catch (error) {
|
|
516
145
|
const message = handleAuthError(error, {
|
|
517
146
|
defaultMessage: 'Sign in failed',
|
|
518
|
-
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
|
-
[
|
|
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
|
-
|
|
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
|
};
|