import { useAuthStore } from '../stores/auth' import type { TokenResponse } from './types' export class ApiError extends Error { status: number code: string constructor(status: number, code: string, message: string) { super(message) this.status = status this.code = code } } async function parse(res: Response): Promise { if (res.status === 204) return undefined const text = await res.text() return text ? JSON.parse(text) : undefined } async function toError(res: Response): Promise { const body = (await parse(res).catch(() => undefined)) as { error?: string; message?: string } | undefined return new ApiError(res.status, body?.error ?? 'unknown', body?.message ?? res.statusText) } /** Refresh the access token via the HttpOnly cookie. Returns false if it failed. */ export async function refreshAccessToken(): Promise { const auth = useAuthStore() const res = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'same-origin' }) if (!res.ok) { auth.clear() return false } const data = (await parse(res)) as TokenResponse auth.setSession(data) return true } async function request(method: string, path: string, body?: unknown, retry = true): Promise { const auth = useAuthStore() const headers: Record = {} if (body !== undefined) headers['Content-Type'] = 'application/json' if (auth.accessToken) headers['Authorization'] = `Bearer ${auth.accessToken}` const res = await fetch(path, { method, headers, credentials: 'same-origin', body: body === undefined ? undefined : JSON.stringify(body), }) if (res.status === 401 && retry && !path.startsWith('/api/auth/')) { if (await refreshAccessToken()) return request(method, path, body, false) throw new ApiError(401, 'unauthorized', 'session expired') } if (!res.ok) throw await toError(res) return (await parse(res)) as T } export const api = { get: (path: string) => request('GET', path), post: (path: string, body?: unknown) => request('POST', path, body), patch: (path: string, body?: unknown) => request('PATCH', path, body), }