const API_BASE = '/api/v1'; export interface ApiError extends Error { status?: number; retryAfter?: number; } async function request(method: string, path: string, body?: unknown): Promise { const headers: Record = {}; if (body !== undefined) { headers['Content-Type'] = 'application/json'; } const response = await fetch(API_BASE + path, { method, headers, credentials: 'include', body: body !== undefined ? JSON.stringify(body) : undefined, }); if (!response.ok) { let errorMessage = 'Request failed: ' + response.status; let retryAfter: number | undefined; try { const errorData = await response.json(); if (typeof errorData?.message === 'string') { errorMessage = errorData.message; } else if (typeof errorData?.error === 'string') { errorMessage = errorData.error; } if (response.status === 429 && typeof errorData?.retry_after === 'number') { retryAfter = errorData.retry_after; } } catch { // ignore parse error } const err = new Error(errorMessage) as ApiError; err.status = response.status; if (response.status === 429) { const ra = response.headers.get('Retry-After'); err.retryAfter = retryAfter ?? (ra ? parseInt(ra, 10) : undefined); } throw err; } if (response.status === 204) { return undefined as T; } return response.json() as Promise; } export const api = { get: (path: string) => request('GET', path), post: (path: string, body?: unknown) => request('POST', path, body), put: (path: string, body?: unknown) => request('PUT', path, body), patch: (path: string, body?: unknown) => request('PATCH', path, body), delete: (path: string) => request('DELETE', path), }; export default api;