@oxyhq/services
Version:
310 lines (293 loc) • 11.2 kB
JavaScript
;
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