/** * Velocity-OS API Client * Thin wrapper around fetch. Injects JWT auth header automatically. * All requests go to /api (proxied by Traefik to core-api:8443). */ import { useAuthStore } from '@/store/authStore'; const BASE_URL = '/api'; export class ApiError extends Error { constructor(public status: number, message: string) { super(message); this.name = 'ApiError'; } } async function apiFetch( path: string, options: RequestInit = {} ): Promise { const token = useAuthStore.getState().token; const response = await fetch(`${BASE_URL}${path}`, { ...options, headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(options.headers ?? {}), }, }); if (response.status === 401) { // Token expired — clear session and let AuthGuard redirect useAuthStore.getState().clearSession(); throw new ApiError(401, 'Session expired'); } if (!response.ok) { const body = await response.text().catch(() => ''); throw new ApiError(response.status, body || `HTTP ${response.status}`); } if (response.status === 204) return undefined as T; return response.json(); } export const api = { get: (path: string) => apiFetch(path), post: (path: string, body: unknown) => apiFetch(path, { method: 'POST', body: JSON.stringify(body) }), patch: (path: string, body: unknown) => apiFetch(path, { method: 'PATCH', body: JSON.stringify(body) }), delete: (path: string) => apiFetch(path, { method: 'DELETE' }), };