// Fired when any API call returns 401 (session expired/absent). App registers it // to drop to the login screen. let onUnauthorized: (() => void) | null = null; export function setUnauthorizedHandler(fn: (() => void) | null): void { onUnauthorized = fn; } interface ApiOptions extends RequestInit { // When true, a 401 body is returned to the caller instead of throwing + // firing the logout hook. Used by /login to surface { success, error }. allowAuthErrorBody?: boolean; } export async function apiFetch(url: string, options?: ApiOptions): Promise { const { allowAuthErrorBody, ...init } = options ?? {}; const res = await fetch(url, init); if (!res.ok) { if (res.status === 401) { if (!allowAuthErrorBody) { onUnauthorized?.(); throw new Error('unauthenticated'); } // else fall through and return the { success, error } body } else if (res.status === 400 || res.status === 409) { // Validation / conflict responses carry a { error, message } body the caller reads. } else { const text = await res.text().catch(() => res.statusText); throw new Error(text || `HTTP ${res.status}`); } } return res.json() as Promise; } export function apiGet(url: string): Promise { return apiFetch(url); } export function apiPost(url: string, body: unknown, options?: ApiOptions): Promise { return apiFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), ...options, }); }