// Central fetch wrapper: the access token is kept in memory only (never // localStorage/sessionStorage, per the auth design), and a 401 triggers a // single silent refresh-and-retry via the httpOnly refresh cookie. let accessToken: string | null = null; export function setAccessToken(token: string | null): void { accessToken = token; } export function getAccessToken(): string | null { return accessToken; } export class ApiError extends Error { status: number; constructor(message: string, status: number) { super(message); this.status = status; } } const NO_RETRY_PATHS = ["/api/auth/admin/refresh", "/api/auth/admin/login"]; async function rawFetch(path: string, init: RequestInit): Promise { const headers = new Headers(init.headers); if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`); if (init.body && !(init.body instanceof FormData) && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } return fetch(path, { ...init, headers, credentials: "include" }); } let refreshInFlight: Promise | null = null; async function refreshAccessToken(): Promise { if (!refreshInFlight) { refreshInFlight = (async () => { try { const res = await fetch("/api/auth/admin/refresh", { method: "POST", credentials: "include", }); if (!res.ok) return false; const data = (await res.json()) as { access_token: string }; setAccessToken(data.access_token); return true; } catch { return false; } finally { refreshInFlight = null; } })(); } return refreshInFlight; } export async function apiFetch(path: string, init: RequestInit = {}): Promise { let res = await rawFetch(path, init); if (res.status === 401 && !NO_RETRY_PATHS.includes(path)) { const refreshed = await refreshAccessToken(); if (refreshed) { res = await rawFetch(path, init); } } if (!res.ok) { let message = `Request failed with status ${res.status}`; try { const body = (await res.json()) as { error?: string }; if (body?.error) message = body.error; } catch { // response had no JSON body } throw new ApiError(message, res.status); } if (res.status === 204) { return undefined as T; } return (await res.json()) as T; } export function apiUpload(path: string, formData: FormData): Promise { return apiFetch(path, { method: "POST", body: formData }); }