/** * Reads the XSRF-TOKEN cookie set by Spring Security's CookieCsrfTokenRepository. * Returns null outside the browser or when the cookie is absent. */ export function getCsrfToken(): string | null { if (typeof document === 'undefined') return null; const match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/); return match ? decodeURIComponent(match[1]) : null; } /** * Merges the X-XSRF-TOKEN header into a RequestInit so Spring Security's * CSRF filter accepts the request. Safe to call server-side (no-op when the * cookie is absent). */ export function withCsrf(init?: RequestInit): RequestInit { const token = getCsrfToken(); if (!token) return init ?? {}; const headers = new Headers(init?.headers); headers.set('X-XSRF-TOKEN', token); return { ...init, headers }; } /** * Wraps a fetch implementation so that every state-mutating call (POST, PUT, * PATCH, DELETE) automatically includes the X-XSRF-TOKEN header. GET/HEAD * requests pass through unchanged. * * Used to CSRF-protect client-side hooks that accept an injectable fetchImpl. * In unit tests the injected mock is wrapped but getCsrfToken() returns null * (no browser cookie), so no header is added and existing test expectations * are unaffected. */ export function makeCsrfFetch(inner: typeof fetch): typeof fetch { return (input: RequestInfo | URL, init?: RequestInit): Promise => { const method = (init?.method ?? 'GET').toUpperCase(); if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) { return inner(input, withCsrf(init)); } return inner(input, init); }; } /** * Extracts the fa_session cookie value from a list of Set-Cookie response headers. * * The backend may append attributes like `Path`, `HttpOnly`, `SameSite=Strict`, * `Max-Age`, `Secure`; we only forward the opaque session id — the SvelteKit * cookies API rewrites the attributes itself when re-emitting to the browser. * * Pass the result of `response.headers.getSetCookie()` (modern Node/Undici) or * a single-element array containing `response.headers.get('set-cookie')` for * older runtimes that lack `getSetCookie`. * * Returns `null` if no fa_session cookie is present. */ export function extractFaSessionId(setCookieHeaders: string[]): string | null { for (const header of setCookieHeaders) { const match = header.match(/^fa_session=([^;]+)/); if (match) return match[1]; } return null; }