/** * Client-side auth token management. * * The web server generates a random bearer token at launch and passes it to * the browser via the URL fragment (e.g. `http://127.0.0.1:3000/#token=`). * Fragments are never sent in HTTP requests or logged by servers/proxies, * keeping the token local to the machine. * * On first load this module extracts the token from the fragment, persists * it to localStorage (so it survives page refreshes and is accessible from * all tabs on the same origin), and clears the fragment from the address bar. * All subsequent API calls attach the token via the `Authorization: Bearer` * header. * * localStorage is shared across all tabs on the same origin. Because each * SF instance binds to a unique random port, the origin already scopes * the token to that instance — no additional namespacing is needed. * * For EventSource (SSE), which cannot send custom headers, the token is * appended as a `?_token=` query parameter instead. */ const AUTH_STORAGE_KEY = "sf-auth-token"; let cachedToken: string | null = null; /** * Extract the auth token from the URL fragment, localStorage, or prompt for login. * If not found, redirect to /login page. */ export function getAuthToken(): string | null { if (cachedToken !== null) return cachedToken; if (typeof window === "undefined") return null; // 1. Try the URL fragment (initial page load from sf --web) const hash = window.location.hash; if (hash) { const match = hash.match(/token=([a-fA-F0-9]+)/); if (match) { cachedToken = match[1]; try { localStorage.setItem(AUTH_STORAGE_KEY, cachedToken); } catch {} window.history.replaceState( null, "", window.location.pathname + window.location.search, ); return cachedToken; } } // 2. Fall back to localStorage try { const stored = localStorage.getItem(AUTH_STORAGE_KEY); if (stored) { cachedToken = stored; return cachedToken; } } catch {} // 3. If not found, redirect to login if (window.location.pathname !== "/login") { window.location.href = "/login"; } return null; } /** * Listen for token changes from other tabs via the `storage` event. * When another tab writes a new token to localStorage, this tab picks * it up immediately without requiring a page refresh. */ if (typeof window !== "undefined") { window.addEventListener("storage", (event) => { if (event.key === AUTH_STORAGE_KEY && event.newValue) { cachedToken = event.newValue; } }); } /** * Returns an object with the `Authorization` header for use with `fetch()`. * Merges with any additional headers provided. */ export function authHeaders( extra?: Record, ): Record { const token = getAuthToken(); const headers: Record = { ...extra }; if (token) { headers["Authorization"] = `Bearer ${token}`; } return headers; } /** * Wrapper around `fetch()` that automatically injects the auth token. * * When no token is available (missing `#token=` fragment and no localStorage * entry), returns a synthetic 401 Response instead of making an unauthenticated * request that will fail server-side anyway. This lets callers handle the * missing-token case uniformly rather than silently cascading 401s. */ export async function authFetch( input: RequestInfo | URL, init?: RequestInit, ): Promise { const token = getAuthToken(); if (!token) { return new Response(JSON.stringify({ error: "No auth token available" }), { status: 401, headers: { "Content-Type": "application/json" }, }); } const headers = new Headers(init?.headers); if (!headers.has("Authorization")) { headers.set("Authorization", `Bearer ${token}`); } return fetch(input, { ...init, headers }); } /** * Append the auth token as a `_token` query parameter to a URL string. * Used for EventSource connections which cannot send custom headers. */ export function appendAuthParam(url: string): string { const token = getAuthToken(); if (!token) return url; const separator = url.includes("?") ? "&" : "?"; return `${url}${separator}_token=${token}`; }