const BASE = '/api/experience'; async function request(method: string, path: string, body?: unknown): Promise { const res = await fetch(`${BASE}${path}`, { method, headers: body ? { 'Content-Type': 'application/json' } : undefined, body: body ? JSON.stringify(body) : undefined, credentials: 'same-origin', }); return res; } // --- Shared --- export async function identifyUser(verificationId: string): Promise { const res = await request('POST', '/identification', { verificationId }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.message || `Identification failed (${res.status})`); } } export async function submitInteraction(): Promise { const res = await request('POST', '/submit'); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.message || `Submit failed (${res.status})`); } const data = await res.json(); return data.redirectTo; } // --- Sign-in --- export async function initInteraction(): Promise { const res = await request('PUT', '', { interactionEvent: 'SignIn' }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.message || `Failed to initialize sign-in (${res.status})`); } } function detectIdentifierType(input: string): 'email' | 'username' { return input.includes('@') ? 'email' : 'username'; } export async function verifyPassword( identifier: string, password: string ): Promise { const type = detectIdentifierType(identifier); const res = await request('POST', '/verification/password', { identifier: { type, value: identifier }, password, }); if (!res.ok) { const err = await res.json().catch(() => ({})); if (res.status === 422) { throw new Error('Invalid credentials'); } throw new Error(err.message || `Authentication failed (${res.status})`); } const data = await res.json(); return data.verificationId; } export class MfaRequiredError extends Error { constructor() { super('MFA verification required'); this.name = 'MfaRequiredError'; } } export async function signIn(identifier: string, password: string): Promise { await initInteraction(); const verificationId = await verifyPassword(identifier, password); await identifyUser(verificationId); const res = await request('POST', '/submit'); if (!res.ok) { const err = await res.json().catch(() => ({})); if (err.code === 'user.missing_mfa') { throw new MfaRequiredError(); } throw new Error(err.message || `Submit failed (${res.status})`); } const data = await res.json(); return data.redirectTo; } // --- Registration --- export async function initRegistration(): Promise { const res = await request('PUT', '', { interactionEvent: 'Register' }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.message || `Failed to initialize registration (${res.status})`); } } export async function sendVerificationCode(email: string): Promise { const res = await request('POST', '/verification/verification-code', { identifier: { type: 'email', value: email }, interactionEvent: 'Register', }); if (!res.ok) { const err = await res.json().catch(() => ({})); if (res.status === 422) { throw new Error('This email is already registered'); } throw new Error(err.message || `Failed to send verification code (${res.status})`); } const data = await res.json(); return data.verificationId; } export async function verifyCode( email: string, verificationId: string, code: string ): Promise { const res = await request('POST', '/verification/verification-code/verify', { identifier: { type: 'email', value: email }, verificationId, code, }); if (!res.ok) { const err = await res.json().catch(() => ({})); if (res.status === 422) { throw new Error('Invalid or expired verification code'); } throw new Error(err.message || `Verification failed (${res.status})`); } const data = await res.json(); return data.verificationId; } export async function addProfile(type: string, value: string): Promise { const res = await request('POST', '/profile', { type, value }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.message || `Failed to update profile (${res.status})`); } } /** Phase 1: init registration + send verification email. Returns verificationId for phase 2. */ export async function startRegistration(email: string): Promise { await initRegistration(); return sendVerificationCode(email); } /** Phase 2: verify code, set password, create user, submit. Returns redirect URL. */ export async function completeRegistration( email: string, password: string, verificationId: string, code: string ): Promise { const verifiedId = await verifyCode(email, verificationId, code); await addProfile('password', password); await identifyUser(verifiedId); return submitInteraction(); } // --- Forgot Password --- export async function initForgotPassword(): Promise { const res = await request('PUT', '', { interactionEvent: 'ForgotPassword' }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.message || `Failed to initialize password reset (${res.status})`); } } export async function forgotPasswordSendCode(email: string): Promise { const res = await request('POST', '/verification/verification-code', { identifier: { type: 'email', value: email }, interactionEvent: 'ForgotPassword', }); if (!res.ok) { const err = await res.json().catch(() => ({})); if (res.status === 422) { throw new Error('No account found with this email'); } throw new Error(err.message || `Failed to send reset code (${res.status})`); } const data = await res.json(); return data.verificationId; } export async function forgotPasswordVerifyAndReset( email: string, verificationId: string, code: string, newPassword: string, ): Promise { const verifiedId = await verifyCode(email, verificationId, code); await identifyUser(verifiedId); await addProfile('password', newPassword); await submitInteraction(); } export async function startForgotPassword(email: string): Promise { await initForgotPassword(); return forgotPasswordSendCode(email); } // --- MFA Verification --- export async function verifyTotp(code: string): Promise { const res = await request('POST', '/verification/totp/verify', { code }); if (!res.ok) { const err = await res.json().catch(() => ({})); if (res.status === 422) { throw new Error('Invalid code, please try again'); } throw new Error(err.message || `TOTP verification failed (${res.status})`); } const data = await res.json(); return data.verificationId; } export async function verifyBackupCode(code: string): Promise { const res = await request('POST', '/verification/backup-code/verify', { code }); if (!res.ok) { const err = await res.json().catch(() => ({})); if (res.status === 422) { const msg = err.code === 'backup_code_consumed' ? 'This backup code has already been used' : 'Invalid backup code'; throw new Error(msg); } throw new Error(err.message || `Backup code verification failed (${res.status})`); } const data = await res.json(); return data.verificationId; } export async function submitMfa(verificationId: string): Promise { await identifyUser(verificationId); return submitInteraction(); }