/** * Browser/React Native-safe auth API client for platform-service. * * Replaces hand-rolled auth clients in ChronoMind web, MindLyst web, NomGap, etc. * No Node.js dependencies — uses globalThis.fetch and configurable storage. * * @example * ```ts * import { createAuthClient } from '@bytelyst/auth-client'; * * const auth = createAuthClient({ * baseUrl: 'http://localhost:4003/api', * productId: 'chronomind', * }); * * const result = await auth.login('user@example.com', 'password123'); * console.log(result.user.displayName); * ``` */ import type { AuthClient, AuthClientConfig, AuthResult, AuthUser, TokenStorage } from './types.js'; // ── Default localStorage adapter ───────────────────────────────── const noopStorage: TokenStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {}, }; function getDefaultStorage(): TokenStorage { if ( typeof globalThis.localStorage !== 'undefined' && typeof globalThis.localStorage?.getItem === 'function' ) { return globalThis.localStorage; } return noopStorage; } // ── UUID helper (browser + RN safe) ────────────────────────────── function uuid(): string { if (typeof globalThis.crypto?.randomUUID === 'function') { return globalThis.crypto.randomUUID(); } return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = (Math.random() * 16) | 0; return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); }); } // ── Factory ────────────────────────────────────────────────────── export function createAuthClient(config: AuthClientConfig): AuthClient { const { baseUrl, productId, timeoutMs = 15_000 } = config; const storage = config.storage ?? getDefaultStorage(); const prefix = config.storagePrefix ?? productId; const KEYS = { accessToken: `${prefix}-auth-token`, refreshToken: `${prefix}-refresh-token`, } as const; // ── Token management ──────────────────────────── function getAccessToken(): string | null { return storage.getItem(KEYS.accessToken); } function getRefreshToken(): string | null { return storage.getItem(KEYS.refreshToken); } function setTokens(accessToken: string, refreshToken: string): void { storage.setItem(KEYS.accessToken, accessToken); storage.setItem(KEYS.refreshToken, refreshToken); } function clearTokens(): void { storage.removeItem(KEYS.accessToken); storage.removeItem(KEYS.refreshToken); } function isAuthenticated(): boolean { return getAccessToken() !== null; } // ── HTTP helper ───────────────────────────────── async function request( path: string, method: string, body?: unknown, opts?: { skipAuth?: boolean } ): Promise { const headers: Record = { 'Content-Type': 'application/json', 'x-product-id': productId, 'x-request-id': uuid(), }; if (!opts?.skipAuth) { const token = getAccessToken(); if (token) headers['Authorization'] = `Bearer ${token}`; } const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const res = await globalThis.fetch(`${baseUrl}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined, signal: controller.signal, }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error( (data as Record).message || (data as Record).error || `HTTP ${res.status}` ); } if (res.status === 204) return undefined as T; return res.json() as Promise; } finally { clearTimeout(timer); } } // ── Singleton refresh guard ───────────────────── let _refreshPromise: Promise | null = null; async function refreshAccessToken(): Promise { if (_refreshPromise) return _refreshPromise; _refreshPromise = (async () => { const rt = getRefreshToken(); if (!rt) return false; try { const data = await request<{ accessToken: string; refreshToken: string }>( '/auth/refresh', 'POST', { refreshToken: rt }, { skipAuth: true } ); setTokens(data.accessToken, data.refreshToken); return true; } catch { clearTokens(); return false; } })(); try { return await _refreshPromise; } finally { _refreshPromise = null; } } // ── Auth operations ───────────────────────────── async function login(email: string, password: string): Promise { const result = await request('/auth/login', 'POST', { email, password, productId, }); setTokens(result.accessToken, result.refreshToken); return result; } async function register( email: string, password: string, displayName: string ): Promise { const result = await request('/auth/register', 'POST', { email, password, displayName, productId, }); setTokens(result.accessToken, result.refreshToken); return result; } async function getMe(): Promise { return request('/auth/me', 'GET'); } // ── Password management ───────────────────────── async function forgotPassword(email: string): Promise<{ message: string }> { return request<{ message: string }>('/auth/forgot-password', 'POST', { email, productId, }); } async function resetPassword(token: string, newPassword: string): Promise<{ message: string }> { return request<{ message: string }>('/auth/reset-password', 'POST', { token, newPassword, }); } async function changePassword( currentPassword: string, newPassword: string ): Promise<{ message: string }> { return request<{ message: string }>('/auth/change-password', 'POST', { currentPassword, newPassword, }); } // ── Account management ────────────────────────── async function deleteAccount(password: string): Promise<{ message: string }> { const result = await request<{ message: string }>('/auth/account', 'DELETE', { password, }); clearTokens(); return result; } // ── Email verification ────────────────────────── async function verifyEmail(token: string): Promise<{ message: string }> { return request<{ message: string }>('/auth/verify-email', 'POST', { token }); } async function resendVerification(email: string): Promise<{ message: string }> { return request<{ message: string }>('/auth/resend-verification', 'POST', { email, productId, }); } return { getAccessToken, getRefreshToken, setTokens, clearTokens, isAuthenticated, login, register, getMe, refreshAccessToken, forgotPassword, resetPassword, changePassword, deleteAccount, verifyEmail, resendVerification, }; }