import type { ErrorEnvelope } from "@/api/types"; function pathnameOf(input: RequestInfo | URL): string { const raw = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; try { return new URL(raw, "http://local.invalid").pathname; } catch { return raw.split("?")[0] ?? raw; } } function skipRefresh(input: RequestInfo | URL): boolean { const path = pathnameOf(input); return ( path === "/api/auth/login" || path === "/api/auth/callback" || path === "/api/auth/refresh" || path === "/api/auth/logout" ); } let refreshInFlight: Promise | null = null; async function refreshSession(): Promise { if (!refreshInFlight) { refreshInFlight = fetch("/api/auth/refresh", { method: "POST", credentials: "include", headers: { Accept: "application/json" }, }) .then((response) => response.status === 204) .catch(() => false) .finally(() => { refreshInFlight = null; }); } return refreshInFlight; } export async function apiFetch( input: RequestInfo | URL, init: RequestInit = {}, ): Promise { const headers = new Headers(init.headers); if (!headers.has("Accept")) headers.set("Accept", "application/json"); headers.delete("Authorization"); const requestInit: RequestInit = { ...init, credentials: "include", headers }; const response = await fetch(input, requestInit); if ((response.status !== 401 && response.status !== 403) || skipRefresh(input)) { return response; } const refreshed = await refreshSession(); if (refreshed) return fetch(input, requestInit); return response; } export class ApiError extends Error { readonly status: number; readonly code?: string; constructor(message: string, status: number, code?: string) { super(message); this.name = "ApiError"; this.status = status; this.code = code; } } export async function readApiJson(response: Response): Promise { const text = await response.text(); let body: T & Partial; try { body = JSON.parse(text) as T & Partial; } catch { throw response.ok ? new Error("Invalid JSON from API") : new ApiError(`HTTP ${response.status}`, response.status); } if (!response.ok) { throw new ApiError( body.error?.message || `HTTP ${response.status}`, response.status, body.error?.code, ); } return body; }