export const apiBaseUrl = import.meta.env.VITE_API_URL ?? "http://localhost:3001"; export function extractErrorMessage(payload, status) { if (payload?.msg) return payload.msg; if (payload?.error) return payload.error; if (payload?.message) return payload.message; return `http_error:${status}`; } export class ApiError extends Error { constructor(category, message, extra = {}) { super(message || category); this.category = category; Object.assign(this, extra); } } export function categorizeHttpStatus(status) { if (status === 401) return "invalid_credentials"; if (status === 429) return "locked"; return "server"; } let _accessToken = null; let _refreshFn = null; let _authErrorFn = null; let _refreshPromise = null; export function setAccessToken(token) { _accessToken = token ?? null; } export function setRefreshCallback(fn) { _refreshFn = fn ?? null; } export function setAuthErrorCallback(fn) { _authErrorFn = fn ?? null; } export function buildAuthHeaders(extra = {}) { const headers = { "content-type": "application/json", ...extra }; if (_accessToken) headers["Authorization"] = `Bearer ${_accessToken}`; return headers; } export async function refreshAccessToken() { if (!_refreshFn) return null; if (!_refreshPromise) { _refreshPromise = _refreshFn().finally(() => { _refreshPromise = null; }); } return _refreshPromise; } export function notifyAuthError() { _authErrorFn?.(); } export async function apiFetch(path, { method = "GET", body } = {}) { const headers = { "content-type": "application/json" }; if (_accessToken) headers["Authorization"] = `Bearer ${_accessToken}`; let res; try { res = await fetch(`${apiBaseUrl}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined }); } catch { throw new ApiError("network", "network_error"); } if (res.status === 401 && _refreshFn) { let newToken; try { newToken = await refreshAccessToken(); } catch { _authErrorFn?.(); throw new Error("session_expired"); } if (newToken) { headers["Authorization"] = `Bearer ${newToken}`; try { res = await fetch(`${apiBaseUrl}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined }); } catch { throw new ApiError("network", "network_error"); } } } if (res.status === 401) { _authErrorFn?.(); throw new Error("session_expired"); } if (!res.ok) { const text = await res.text().catch(() => ""); let payload = null; try { payload = JSON.parse(text); } catch {} throw new Error(extractErrorMessage(payload, res.status) || text || `http_error:${res.status}`); } const contentType = res.headers.get("content-type") ?? ""; if (contentType.includes("application/json")) return res.json(); return res.text(); }