UNPKG

@oxyhq/services

Version:

OxyHQ Expo/React Native SDK — UI components, screens, and native features

310 lines (293 loc) 11.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useAuthOperations = void 0; var _react = require("react"); var _core = require("@oxyhq/core"); var _sessionHelpers = require("../../utils/sessionHelpers.js"); var _errorHandlers = require("../../utils/errorHandlers.js"); var Crypto = _interopRequireWildcard(require("expo-crypto")); 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); } const LOGIN_ERROR_CODE = 'LOGIN_ERROR'; const LOGOUT_ERROR_CODE = 'LOGOUT_ERROR'; const LOGOUT_ALL_ERROR_CODE = 'LOGOUT_ALL_ERROR'; /** * Authentication operations using public key cryptography. * Accepts public key as parameter - identity management is handled by the app layer. */ const useAuthOperations = ({ oxyServices, storage, sessions, activeSessionId, setActiveSessionId, updateSessions, saveActiveSessionId, clearSessionState, switchSession, applyLanguagePreference, onAuthStateChange, onError, loginSuccess, loginFailure, logoutStore, setAuthState, logger }) => { // Ref to avoid recreating callbacks when sessions change const sessionsRef = (0, _react.useRef)(sessions); sessionsRef.current = sessions; /** * Internal function to perform challenge-response sign in (works offline) */ const performSignIn = (0, _react.useCallback)(async publicKey => { const deviceFingerprintObj = _core.DeviceManager.getDeviceFingerprint(); const deviceFingerprint = JSON.stringify(deviceFingerprintObj); const deviceInfo = await _core.DeviceManager.getDeviceInfo(); const deviceName = deviceInfo.deviceName || _core.DeviceManager.getDefaultDeviceName(); let challenge; let isOffline = false; // Try to request challenge from server (online) try { const challengeResponse = await oxyServices.requestChallenge(publicKey); challenge = challengeResponse.challenge; } catch (error) { // Network error - generate challenge locally for offline sign-in const errorMessage = error instanceof Error ? error.message : String(error); const isNetworkError = errorMessage.includes('Network') || errorMessage.includes('network') || errorMessage.includes('Failed to fetch') || errorMessage.includes('fetch failed') || error?.code === 'NETWORK_ERROR' || error?.status === 0; if (isNetworkError) { if (__DEV__ && logger) { logger('Network unavailable, performing offline sign-in'); } // Generate challenge locally challenge = await _core.SignatureService.generateChallenge(); isOffline = true; } else { // Re-throw non-network errors throw error; } } // Note: Biometric authentication check should be handled by the app layer // (e.g., accounts app) before calling signIn. The biometric preference is stored // in local storage as 'oxy_biometric_enabled' and can be checked there. // Sign the challenge const { challenge: signature, timestamp } = await _core.SignatureService.signChallenge(challenge); let fullUser; let sessionResponse; if (isOffline) { // Offline sign-in: create local session and minimal user object if (__DEV__ && logger) { logger('Creating offline session'); } // Generate a local session ID using cryptographically secure randomness const localSessionId = `offline_${Crypto.getRandomUUID()}`; const localDeviceId = `device_${Crypto.getRandomUUID()}`; const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // 7 days // Create minimal user object with publicKey as id fullUser = { id: publicKey, // Use publicKey as id (per migration document) publicKey, username: '', privacySettings: {} }; sessionResponse = { sessionId: localSessionId, deviceId: localDeviceId, expiresAt, user: { id: publicKey, username: '' } }; // Store offline session locally const offlineSession = { sessionId: localSessionId, deviceId: localDeviceId, expiresAt, lastActive: new Date().toISOString(), userId: publicKey, isCurrent: true }; setActiveSessionId(localSessionId); await saveActiveSessionId(localSessionId); updateSessions([offlineSession], { merge: true }); // Mark session as offline for later sync if (storage) { await storage.setItem(`oxy_session_${localSessionId}_offline`, 'true'); } if (__DEV__ && logger) { logger('Offline sign-in successful'); } } else { // Online sign-in: use normal flow // Verify and create session sessionResponse = await oxyServices.verifyChallenge(publicKey, challenge, signature, timestamp, deviceName, deviceFingerprint); // Get token for the session try { await oxyServices.getTokenBySession(sessionResponse.sessionId); } catch (tokenError) { const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError); const status = tokenError?.status; if (status === 404 || errorMessage.includes('404')) { throw new Error(`Session was created but token could not be retrieved. Session ID: ${sessionResponse.sessionId.substring(0, 8)}...`); } throw tokenError; } // Get full user data fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId); // Fetch device sessions let allDeviceSessions = []; try { allDeviceSessions = await (0, _sessionHelpers.fetchSessionsWithFallback)(oxyServices, sessionResponse.sessionId, { fallbackDeviceId: sessionResponse.deviceId, fallbackUserId: fullUser.id, logger }); } catch (error) { if (__DEV__ && logger) { logger('Failed to fetch device sessions after login', error); } } // Check for existing session for same user and switch to it to avoid duplicates const existingSession = allDeviceSessions.find(session => session.userId?.toString() === fullUser.id?.toString() && session.sessionId !== sessionResponse.sessionId); if (existingSession) { // Switch to existing session instead of creating duplicate try { await oxyServices.logoutSession(sessionResponse.sessionId, sessionResponse.sessionId); } catch (logoutError) { // Non-critical - continue to switch session even if logout fails if (__DEV__ && logger) { logger('Failed to logout duplicate session, continuing with switch', logoutError); } } await switchSession(existingSession.sessionId); updateSessions(allDeviceSessions.filter(session => session.sessionId !== sessionResponse.sessionId), { merge: false }); onAuthStateChange?.(fullUser); return fullUser; } setActiveSessionId(sessionResponse.sessionId); await saveActiveSessionId(sessionResponse.sessionId); updateSessions(allDeviceSessions, { merge: true }); } await applyLanguagePreference(fullUser); loginSuccess(fullUser); onAuthStateChange?.(fullUser); return fullUser; }, [applyLanguagePreference, logger, loginSuccess, onAuthStateChange, oxyServices, saveActiveSessionId, setActiveSessionId, switchSession, updateSessions, storage]); /** * Sign in with existing public key */ const signIn = (0, _react.useCallback)(async (publicKey, deviceName) => { setAuthState({ isLoading: true, error: null }); try { return await performSignIn(publicKey); } catch (error) { const message = (0, _errorHandlers.handleAuthError)(error, { defaultMessage: 'Sign in failed', code: LOGIN_ERROR_CODE, onError, setAuthError: msg => setAuthState({ error: msg }), logger }); loginFailure(message); throw error; } finally { setAuthState({ isLoading: false }); } }, [setAuthState, performSignIn, loginFailure, onError, logger]); /** * Logout from session */ const logout = (0, _react.useCallback)(async targetSessionId => { if (!activeSessionId) return; try { const sessionToLogout = targetSessionId || activeSessionId; await oxyServices.logoutSession(activeSessionId, sessionToLogout); const filteredSessions = sessionsRef.current.filter(session => session.sessionId !== sessionToLogout); updateSessions(filteredSessions, { merge: false }); if (sessionToLogout === activeSessionId) { if (filteredSessions.length > 0) { await switchSession(filteredSessions[0].sessionId); } else { await clearSessionState(); return; } } } catch (error) { const isInvalid = (0, _errorHandlers.isInvalidSessionError)(error); if (isInvalid && targetSessionId === activeSessionId) { await clearSessionState(); return; } (0, _errorHandlers.handleAuthError)(error, { defaultMessage: 'Logout failed', code: LOGOUT_ERROR_CODE, onError, setAuthError: msg => setAuthState({ error: msg }), logger, status: isInvalid ? 401 : undefined }); } }, [activeSessionId, clearSessionState, logger, onError, oxyServices, setAuthState, switchSession, updateSessions]); /** * Logout from all sessions */ const logoutAll = (0, _react.useCallback)(async () => { if (!activeSessionId) { const error = new Error('No active session found'); setAuthState({ error: error.message }); onError?.({ message: error.message, code: LOGOUT_ALL_ERROR_CODE, status: 404 }); throw error; } try { await oxyServices.logoutAllSessions(activeSessionId); await clearSessionState(); } catch (error) { (0, _errorHandlers.handleAuthError)(error, { defaultMessage: 'Logout all failed', code: LOGOUT_ALL_ERROR_CODE, onError, setAuthError: msg => setAuthState({ error: msg }), logger }); throw error instanceof Error ? error : new Error('Logout all failed'); } }, [activeSessionId, clearSessionState, logger, onError, oxyServices, setAuthState]); return { signIn, logout, logoutAll }; }; exports.useAuthOperations = useAuthOperations; //# sourceMappingURL=useAuthOperations.js.map