import createClient from "openapi-fetch"; import createQueryClient from "openapi-react-query"; import type { paths } from "./generated/schema"; const HTTP_UNAUTHORIZED = 401; const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:3000"; type AuthHandlers = { generation?: () => number; accessToken: () => string | null; refresh: () => Promise; clear: () => void; }; let auth: AuthHandlers | null = null; let hasAuthenticatedSession = false; const authSessionListeners = new Set<() => void>(); function setAuthenticatedSession(next: boolean) { if (next === hasAuthenticatedSession) return; hasAuthenticatedSession = next; for (const listener of authSessionListeners) listener(); } function configureAuthClient(handlers: AuthHandlers | null) { auth = handlers; setAuthenticatedSession(Boolean(auth?.accessToken())); } function getAuthenticatedSessionSnapshot() { return hasAuthenticatedSession; } function subscribeAuthenticatedSession(listener: () => void) { authSessionListeners.add(listener); return () => authSessionListeners.delete(listener); } async function authenticatedFetch(request: Request): Promise { const isAuthRequest = new URL(request.url).pathname.startsWith( "/api/v1/auth/", ); const session = auth; const generation = session?.generation?.(); const isCurrent = () => generation === auth?.generation?.(); const token = session?.accessToken(); const withToken = (source: Request, value: string) => { const headers = new Headers(source.headers); headers.set("Authorization", `Bearer ${value}`); return new Request(source, { headers }); }; const authorized = token && !isAuthRequest ? withToken(request, token) : request; const retryRequest = authorized.clone(); const response = await fetch(authorized); if (!isAuthRequest && !isCurrent()) throw new DOMException("Session changed", "AbortError"); if ( response.status !== HTTP_UNAUTHORIZED || isAuthRequest || !auth || ( await response .clone() .json() .catch(() => null) )?.code !== "UNAUTHORIZED" ) return response; if (!isCurrent()) throw new DOMException("Session changed", "AbortError"); if (!(await auth.refresh())) { if (isCurrent()) auth.clear(); return response; } if (!isCurrent()) throw new DOMException("Session changed", "AbortError"); const refreshedToken = auth.accessToken(); if (!refreshedToken) return response; const retried = await fetch(withToken(retryRequest, refreshedToken)); if (!isCurrent()) throw new DOMException("Session changed", "AbortError"); return retried; } const apiClient = createClient({ baseUrl: apiBaseUrl, credentials: "include", fetch: authenticatedFetch, }); const apiQueryClient = createQueryClient(apiClient); export { apiClient, apiQueryClient, configureAuthClient, getAuthenticatedSessionSnapshot, subscribeAuthenticatedSession, };