/** * Auth, CORS, and static-route helpers for the local Bun listener (serve.ts). * * Local has ONE credential: the locally-minted bearer token (see auth.ts), and * that is the entire security boundary. There is deliberately no Host allowlist * (DNS-rebinding defense): a cross-origin page can't read the token — it lives * in the real origin's storage and is sent as an explicit header, never an * ambient cookie — so it can't forge an authenticated request no matter which * host it connects through. Dropping the Host gate is what lets a local * instance be reached over a tailnet (or any hostname) with no extra flag. * These helpers express the bearer gate, the CORS origin allowlist, and the * single unauthenticated OAuth-callback carve-out. */ import { timingSafeEqual } from "node:crypto"; /** * Loopback hostnames granted credentialed CORS access by default (any port). * This is the only place hostnames are matched — for CORS, not for a Host gate. */ export const DEFAULT_ALLOWED_HOSTS: ReadonlyArray = [ "localhost", "127.0.0.1", "[::1]", "::1", ]; export const normalizeCredential = (value: string | undefined): string | null => { const normalized = value?.trim(); return normalized && normalized.length > 0 ? normalized : null; }; export const safeEqual = (actual: string, expected: string): boolean => { const actualBytes = Buffer.from(actual); const expectedBytes = Buffer.from(expected); return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes); }; const hostnameFromOrigin = (origin: string): string | null => { // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: parsing an untrusted Origin header that may be malformed try { return new URL(origin).hostname; } catch { return null; } }; /** * Whether a cross-origin request's `Origin` is allowed CORS access. Only the * loopback host allowlist (any port) plus operator-added hosts qualify — never * a reflected arbitrary origin. This is what keeps `Allow-Credentials: true` * from handing an authenticated cross-origin channel to any web page. */ export const isAllowedOrigin = (origin: string, allowed: ReadonlySet): boolean => { const hostname = hostnameFromOrigin(origin); if (hostname === null) return false; return allowed.has(hostname) || allowed.has(`[${hostname}]`); }; export const hasBearerToken = (request: Request, token: string): boolean => { const authorization = request.headers.get("authorization"); const bearer = authorization?.match(/^Bearer\s+(.+)$/i)?.[1]?.trim(); return bearer !== undefined && safeEqual(bearer, token); }; /** The request gate: a valid `Authorization: Bearer ` header. */ export const makeIsAuthorized = (token: string) => (request: Request): boolean => hasBearerToken(request, token); export const hasFileExtension = (pathname: string): boolean => { const lastSegment = pathname.split("/").at(-1) ?? ""; return lastSegment.includes("."); }; /** * OAuth provider callbacks land here from the user's external browser, which * has no way to send our bearer header. The `state` parameter is the * cryptographic gate — each in-flight session is server-issued and validated by * the shared `completeOAuth` before any work happens. Bypassing the bearer on * this ONE path is safe. * * Note: the result-polling path (`/api/oauth/await/`) is NOT exempt — * it is polled by our own renderer, which carries the bearer (the desktop * webview injects it; the web SPA sends it from its stored token). */ export const isUnauthenticatedOAuthCallbackPath = (pathname: string): boolean => /^\/api\/oauth\/callback(\/|$)/.test(pathname); export const isUnauthenticatedOAuthClientMetadataPath = (pathname: string): boolean => pathname === "/api/oauth/client-id-metadata.json" || /^\/api\/oauth\/client-id-metadata\/[^/]+\.json$/.test(pathname); export const isUnauthenticatedOAuthPath = (pathname: string): boolean => isUnauthenticatedOAuthCallbackPath(pathname) || isUnauthenticatedOAuthClientMetadataPath(pathname);