// Fetch + CSRF primitives ported from mnw.js:8,399. `postJson`/`getJson` are new // consolidations that replace the ~20 hand-rolled fetch+CSRF+error blocks across // the legacy feature files. /** * CSRF header for a manual `fetch()`. Read live on every call — the token * rotates mid-session (e.g. the 2FA cycle), so a snapshot would go stale and * 403. Ported from mnw.js:8. */ export function csrfHeaders(): Record { const token = document.querySelector('meta[name="csrf-token"]')?.content; return token ? { 'X-CSRF-Token': token } : {}; } /** * Read the server-supplied error message from a non-2xx fetch `Response`. API * routes return `{"error": "..."}` via the json-error middleware; use this so * users see the real reason instead of a generic "Failed". Ported from * mnw.js:399 (`apiErrorMessage`). */ export async function apiError( response: Response | null | undefined, fallback = 'Request failed', ): Promise { if (!response || typeof response.json !== 'function') return fallback; try { const d = (await response.json()) as { error?: unknown } | null; return d && typeof d.error === 'string' && d.error ? d.error : fallback; } catch { return fallback; } } /** POST JSON with CSRF; resolve the parsed body or reject with the server's * error message. Replaces the hand-rolled fetch+CSRF+error idiom. */ export async function postJson(url: string, body: unknown): Promise { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...csrfHeaders() }, body: JSON.stringify(body), credentials: 'same-origin', }); if (!res.ok) throw new Error(await apiError(res)); return (await res.json()) as T; } /** GET JSON; resolve the parsed body or reject with the server's error message. */ export async function getJson(url: string): Promise { const res = await fetch(url, { credentials: 'same-origin' }); if (!res.ok) throw new Error(await apiError(res)); return (await res.json()) as T; }