import { authClient } from '../auth-client'; import { fetchApi } from './client'; import type { User } from './types'; // Thin wrappers over the Better Auth client, preserving the legacy authApi // call surface used by the auth pages. type ClientError = { message?: string; code?: string; status: number } | null; function throwIfError(error: ClientError, fallback: string): void { if (error) { const err = new Error(error.message || fallback); (err as any).code = error.code; (err as any).status = error.status; throw err; } } export const authApi = { // Magic link login. Enumeration-safe UX parity: unknown emails resolve with // the same generic message (magic links never create accounts server-side); // only rate limiting surfaces as an error. requestMagicLink: async (email: string, callbackURL: string = '/dashboard') => { const { error } = await authClient.signIn.magicLink({ email, callbackURL }); if (error && error.status === 429) { throwIfError(error, 'Too many requests. Please try again later.'); } return { message: 'If an account exists with this email, a login link has been sent.' }; }, verifyMagicLink: async (token: string) => { const { data, error } = await authClient.magicLink.verify({ query: { token } }); throwIfError(error, 'Invalid or expired token'); return data; }, // Password reset (Better Auth is enumeration-safe here by default) requestPasswordReset: async (email: string) => { const { error } = await authClient.requestPasswordReset({ email, redirectTo: '/auth/reset-password', }); throwIfError(error, 'Failed to request password reset'); return { message: 'If an account exists with this email, a password reset link has been sent.' }; }, confirmPasswordReset: async (token: string, password: string) => { const { error } = await authClient.resetPassword({ newPassword: password, token }); throwIfError(error, 'Invalid or expired token'); return { message: 'Password reset successfully. Please log in with your new password.' }; }, // Account claiming: a magic link that lands on the claim page, where the // session-holding user sets a password via /api/auth-ext/claim-account. requestClaimAccount: (email: string) => authApi.requestMagicLink(email, '/auth/claim-account'), confirmClaimAccount: (password: string) => fetchApi<{ user: User; message: string }>('/api/auth-ext/claim-account', { method: 'POST', body: JSON.stringify({ password }), }), claimEligibility: (email: string) => fetchApi<{ canClaim: boolean }>( `/api/auth-ext/claim-eligibility?email=${encodeURIComponent(email)}` ), // Google Identity Services credential (ID token) sign-in googleAuth: async (credential: string) => { const { data, error } = await authClient.signIn.social({ provider: 'google', idToken: { token: credential }, }); throwIfError(error, 'Google login failed'); return data; }, // Change password; other sessions are revoked so a stolen session can't // outlive the change (this device stays signed in). changePassword: async (currentPassword: string, newPassword: string) => { const { error } = await authClient.changePassword({ currentPassword, newPassword, revokeOtherSessions: true, }); throwIfError(error, 'Failed to change password'); return { message: 'Password changed successfully' }; }, // Get current user me: async (): Promise<{ user: User | null }> => { const { data, error } = await authClient.getSession(); throwIfError(error, 'Failed to load session'); return { user: (data?.user as unknown as User) ?? null }; }, };