Something went wrong. Try again.
A local-first event pipeline for independent agents, built on Jazz.
Something went wrong. Try again.
30 kB · 734 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735import { createHash, timingSafeEqual } from "node:crypto";import http, { type IncomingHttpHeaders, type IncomingMessage, type ServerResponse,} from "node:http";import { isIP } from "node:net";import { InspectorOAuthAuth, OAuthCallbackQuarantineCapacityError } from "./oauth-auth.js";import { createOAuthRouteRateLimiter, type OAuthRouteRateLimiter } from "./rate-limit.js";import { loadPublicPages, publicPageForRoute, type PublicPage } from "./public-site.js";import { FONT_DEBUG_ASSET_PATH, FONT_DEBUG_SCRIPT } from "./font-debug.js";import { createCoChatHandler, type CoChatWeb } from "./co-chat-http.js";import { REVIEW_CSRF_HEADER, REVIEW_NONCE_HEADER, REVIEW_SIGNATURE_HEADER, REVIEW_TIMESTAMP_HEADER, decodeReviewCapability, signReviewRequest,} from "../review/web-capability.js";import { COURSE_CHAT_CSRF_HEADER, COURSE_CHAT_NONCE_HEADER, COURSE_CHAT_SIGNATURE_HEADER, COURSE_CHAT_TIMESTAMP_HEADER, decodeCourseChatCapability, signCourseChatRequest,} from "../courses/web-capability.js";
export interface AuthenticatedInspectorProxyOptions { host?: string; port?: number; upstreamHost?: string; upstreamPort?: number; username?: string; password?: string; basicFallbackEnabled?: boolean; oauth?: InspectorOAuthAuth; projectRoot?: string; publicPages?: Map<string, PublicPage>; oauthRateLimiter?: OAuthRouteRateLimiter; reviewCapability?: Buffer | undefined; courseChatCapability?: Buffer | undefined; coChat?: CoChatWeb;}
const SECURITY_HEADERS = { "cache-control": "no-store", "content-security-policy": "default-src 'self'; script-src 'none'; style-src 'unsafe-inline'; font-src data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'", "permissions-policy": "camera=(), microphone=(), geolocation=(), payment=(), usb=()", "referrer-policy": "no-referrer", "x-content-type-options": "nosniff", "x-frame-options": "DENY",} as const;
const PUBLIC_PAGE_CONTENT_SECURITY_POLICY = "default-src 'self'; script-src 'self'; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src data: https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'";
const LANDING_CONTENT_SECURITY_POLICY = "default-src 'self'; script-src 'self'; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src data: https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self' https:";
const OAUTH_LOGIN_CONTENT_SECURITY_POLICY = "default-src 'self'; script-src 'none'; style-src 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self' https:";
const INSPECTOR_HTML_CONTENT_SECURITY_POLICY = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'unsafe-inline' https://fonts.googleapis.com; connect-src 'self'; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; worker-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'";
const FORWARDED_REQUEST_HEADERS = new Set([ "accept", "accept-language", "if-modified-since", "if-none-match", "range", "user-agent",]);
export async function startAuthenticatedInspectorProxy( options: AuthenticatedInspectorProxyOptions,): Promise<http.Server> { const host = options.host ?? "127.0.0.1"; if (!isLoopback(host)) { throw new Error("thought stream authenticated proxy may only bind to a loopback address"); } const port = boundedPort(options.port ?? 4319, "proxy port"); const upstreamHost = options.upstreamHost ?? "127.0.0.1"; if (!isLoopback(upstreamHost)) { throw new Error("thought stream authenticated proxy upstream must be loopback"); } const upstreamPort = boundedPort(options.upstreamPort ?? 4317, "upstream port"); const basicFallbackEnabled = options.basicFallbackEnabled === true; if (!basicFallbackEnabled && !options.oauth) { throw new Error("Inspector proxy requires OAuth or enabled Basic fallback"); } let expectedAuthorizationDigest: Buffer | undefined; if (basicFallbackEnabled) { const username = options.username ?? ""; const password = options.password ?? ""; if (!/^[A-Za-z0-9._-]+$/.test(username)) { throw new Error("Inspector proxy username must use only letters, numbers, dot, underscore, or hyphen"); } if (Buffer.byteLength(password, "utf8") < 20) { throw new Error("Inspector proxy password must contain at least 20 UTF-8 bytes"); } expectedAuthorizationDigest = digest( `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`, ); } const publicPages = options.publicPages ?? await loadPublicPages(options.projectRoot ?? process.cwd()); const oauthRateLimiter = options.oauthRateLimiter ?? createOAuthRouteRateLimiter(); const coChatHandler = createCoChatHandler(options.coChat, options.oauth); const server = http.createServer((request, response) => { const url = requestUrl(request); if (url && (url.pathname === "/chat" || url.pathname.startsWith("/chat/"))) { void coChatHandler(request, response, url).catch(() => response.destroy()); return; } void handleRequest({ request, response, upstreamHost, upstreamPort, publicPages, ...(options.oauth ? { oauth: options.oauth } : {}), basicFallbackEnabled, ...(expectedAuthorizationDigest ? { expectedAuthorizationDigest } : {}), oauthRateLimiter, reviewCapability: options.reviewCapability, courseChatCapability: options.courseChatCapability, coChatEnabled: Boolean(options.coChat), }).catch(() => { request.resume(); if (!response.headersSent) { send(response, 500, "Request failed.\n", { "content-type": "text/plain; charset=utf-8" }); } else { response.destroy(); } }); }); server.headersTimeout = 10_000; server.requestTimeout = 30_000; server.keepAliveTimeout = 5_000; server.maxRequestsPerSocket = 100;
await new Promise<void>((resolve, reject) => { server.once("error", reject); server.listen(port, host, () => { server.off("error", reject); resolve(); }); }); return server;}
export function authenticatedProxyOptionsFromEnv( env: NodeJS.ProcessEnv = process.env,): AuthenticatedInspectorProxyOptions { const basicFallbackEnabled = explicitBoolean(env.PROXY_BASIC_FALLBACK_ENABLED, "PROXY_BASIC_FALLBACK_ENABLED"); const options: AuthenticatedInspectorProxyOptions = { host: env.PROXY_HOST ?? "127.0.0.1", port: optionalPort(env.PROXY_PORT, 4319, "PROXY_PORT"), upstreamHost: env.PROXY_UPSTREAM_HOST ?? "127.0.0.1", upstreamPort: optionalPort(env.PROXY_UPSTREAM_PORT, 4317, "PROXY_UPSTREAM_PORT"), basicFallbackEnabled, projectRoot: env.PROXY_PROJECT_ROOT ?? process.cwd(), reviewCapability: decodeReviewCapability(env.THOUGHTSTREAM_REVIEW_CAPABILITY_B64), courseChatCapability: decodeCourseChatCapability(env.THOUGHTSTREAM_COURSE_CHAT_CAPABILITY_B64), }; if (!basicFallbackEnabled) return options;
const encodedPassword = env.PROXY_PASSWORD_B64?.trim(); if (!encodedPassword) throw new Error("PROXY_PASSWORD_B64 is required when Basic fallback is enabled"); const passwordBytes = Buffer.from(encodedPassword, "base64"); if (passwordBytes.length === 0 || passwordBytes.toString("base64") !== normalizeBase64(encodedPassword)) { throw new Error("PROXY_PASSWORD_B64 must contain canonical base64"); } return { ...options, username: env.PROXY_USER ?? "", password: passwordBytes.toString("utf8"), };}
async function handleRequest(options: { request: IncomingMessage; response: ServerResponse; upstreamHost: string; upstreamPort: number; publicPages: Map<string, PublicPage>; oauth?: InspectorOAuthAuth; basicFallbackEnabled: boolean; expectedAuthorizationDigest?: Buffer; oauthRateLimiter: OAuthRouteRateLimiter; reviewCapability?: Buffer | undefined; courseChatCapability?: Buffer | undefined; coChatEnabled: boolean;}): Promise<void> { const url = requestUrl(options.request); if (!url) { options.request.resume(); send(options.response, 400, "Invalid request.\n", { "content-type": "text/plain; charset=utf-8" }); return; } if (url.pathname === FONT_DEBUG_ASSET_PATH) { if (!isReadMethod(options.request.method)) return methodNotAllowed(options.request, options.response, "GET, HEAD"); options.request.resume(); send(options.response, 200, options.request.method === "HEAD" ? "" : FONT_DEBUG_SCRIPT, { "content-type": "text/javascript; charset=utf-8", }); return; } const publicPage = publicPageForRoute(options.publicPages, url.pathname); if (publicPage) { if (!isReadMethod(options.request.method)) return methodNotAllowed(options.request, options.response, "GET, HEAD"); options.request.resume(); send(options.response, 200, options.request.method === "HEAD" ? "" : publicPage.html, { "content-type": "text/html; charset=utf-8", "content-security-policy": publicPage.route === "/" ? LANDING_CONTENT_SECURITY_POLICY : PUBLIC_PAGE_CONTENT_SECURITY_POLICY, }); return; } if (url.pathname === "/oauth/client-metadata.json" || url.pathname === "/oauth/jwks.json") { if (!isReadMethod(options.request.method)) return methodNotAllowed(options.request, options.response, "GET, HEAD"); options.request.resume(); if (!options.oauth) return notFound(options.response); const payload = url.pathname.endsWith("client-metadata.json") ? options.oauth.clientMetadata : options.oauth.jwks; send(options.response, 200, options.request.method === "HEAD" ? "" : `${JSON.stringify(payload)}\n`, { "content-type": "application/json; charset=utf-8", }); return; } if (url.pathname === "/oauth/login") { if (!options.oauth) return notFound(options.response); if (options.request.method === "GET" || options.request.method === "HEAD") { options.request.resume(); const html = loginPage(); send(options.response, 200, options.request.method === "HEAD" ? "" : html, { "content-type": "text/html; charset=utf-8", "content-security-policy": OAUTH_LOGIN_CONTENT_SECURITY_POLICY, }); return; } if (options.request.method !== "POST") return methodNotAllowed(options.request, options.response, "GET, HEAD, POST"); const decision = options.oauthRateLimiter.login(rateLimitClientKey(options.request)); if (!decision.allowed) { options.request.resume(); return rateLimited(options.response, decision.retryAfterSeconds); } options.request.resume(); const pending = abortOnDisconnect(options.request, options.response); try { const result = await options.oauth.begin(pending.signal); if (!pending.signal.aborted) { redirect(options.response, result.redirect.href, [result.setCookie], 303, { "content-security-policy": OAUTH_LOGIN_CONTENT_SECURITY_POLICY, }); } } catch { if (!pending.signal.aborted) { send(options.response, 400, "OAuth login could not start.\n", { "content-type": "text/plain; charset=utf-8" }); } } finally { pending.cleanup(); } return; } if (url.pathname === "/oauth/callback") { if (!options.oauth) return notFound(options.response); if (options.request.method !== "GET") return methodNotAllowed(options.request, options.response, "GET"); const decision = options.oauthRateLimiter.callback(rateLimitClientKey(options.request)); if (!decision.allowed) { options.request.resume(); return rateLimited(options.response, decision.retryAfterSeconds); } options.request.resume(); try { const result = await options.oauth.finish(url.searchParams, options.request.headers.cookie); redirect(options.response, options.coChatEnabled && result.redirect === "/inspector/" ? "/chat/" : result.redirect, result.setCookies); } catch (error) { if (error instanceof OAuthCallbackQuarantineCapacityError) { send(options.response, 503, "OAuth callback capacity reached. Operator recycle required.\n", { "content-type": "text/plain; charset=utf-8", "retry-after": "60", }); } else { send(options.response, 400, "OAuth callback could not be verified.\n", { "content-type": "text/plain; charset=utf-8", "set-cookie": [clearFlowCookie()], }); } } return; } if (url.pathname === "/oauth/logout") { if (!options.oauth) return notFound(options.response); const session = await options.oauth.authenticate(options.request.headers.cookie); if (!session) return authenticationRequired(options.request, options.response, options.oauth !== undefined, options.basicFallbackEnabled); if (options.request.method === "GET" || options.request.method === "HEAD") { options.request.resume(); const html = logoutPage(session.csrfToken); send(options.response, 200, options.request.method === "HEAD" ? "" : html, { "content-type": "text/html; charset=utf-8" }); return; } if (options.request.method !== "POST") return methodNotAllowed(options.request, options.response, "GET, HEAD, POST"); try { const fields = new URLSearchParams(await readFormBody(options.request)); const clear = await options.oauth.logout(options.request.headers.cookie, oneField(fields, "csrfToken")); redirect(options.response, "/", [clear], 303); } catch { send(options.response, 403, "Logout request could not be verified.\n", { "content-type": "text/plain; charset=utf-8" }); } return; } if (url.pathname !== "/inspector" && !url.pathname.startsWith("/inspector/")) { options.request.resume(); return notFound(options.response); }
// Review-capability write routes: Review decisions, agent-proposal decisions, // and the bounded workbench document routes. Each is one exact POST path with // no query; the same OAuth session, CSRF header, and body-bound signature apply. const workbenchWriteRoute = url.search === "" && options.request.method === "POST" && ( url.pathname === "/inspector/api/workbench/documents" || /^\/inspector\/api\/workbench\/documents\/[^/]+\/(?:versions|selections|proposals)$/.test(url.pathname) || /^\/inspector\/api\/workbench\/proposals\/[^/]+\/decisions$/.test(url.pathname) ); const reviewWriteRoute = (url.search === "" && /^\/inspector\/api\/(?:reviews|proposals)\/[^/]+\/decisions$/.test(url.pathname)) || workbenchWriteRoute; const courseChatWriteRoute = url.search === "" && url.pathname === "/inspector/api/courses/post-training/questions"; const basicAuthorized = options.expectedAuthorizationDigest !== undefined && isAuthorized(options.request.headers.authorization, options.expectedAuthorizationDigest); const oauthAuthorized = (reviewWriteRoute || courseChatWriteRoute || !basicAuthorized) && options.oauth ? await options.oauth.authenticate(options.request.headers.cookie) : undefined; if (!basicAuthorized && !oauthAuthorized) { options.request.resume(); return authenticationRequired(options.request, options.response, options.oauth !== undefined, options.basicFallbackEnabled); } if (url.pathname === "/inspector/api/session") { if (!isReadMethod(options.request.method)) return methodNotAllowed(options.request, options.response, "GET, HEAD"); options.request.resume(); const body = oauthAuthorized ? { reviewWriteEnabled: Boolean(options.reviewCapability), courseChatEnabled: Boolean(options.courseChatCapability), ...((options.reviewCapability || options.courseChatCapability) ? { csrfToken: oauthAuthorized.csrfToken } : {}), } : { reviewWriteEnabled: false, courseChatEnabled: false }; send(options.response, 200, options.request.method === "HEAD" ? "" : JSON.stringify(body), { "content-type": "application/json; charset=utf-8", }); return; } if (reviewWriteRoute) { if (options.request.method !== "POST") return methodNotAllowed(options.request, options.response, "POST"); if (!oauthAuthorized || !options.reviewCapability) { options.request.resume(); send(options.response, 403, "Review write is unavailable.\n", { "content-type": "text/plain; charset=utf-8" }); return; } const csrfToken = uniqueHeader(options.request.headers[REVIEW_CSRF_HEADER]); if (!csrfToken || !safeEqual(csrfToken, oauthAuthorized.csrfToken)) { options.request.resume(); send(options.response, 403, "Review request could not be verified.\n", { "content-type": "text/plain; charset=utf-8" }); return; } let body: Buffer; try { body = await readJsonBody(options.request, 98_304); JSON.parse(body.toString("utf8")); } catch { options.request.resume(); send(options.response, 400, "Review request is invalid.\n", { "content-type": "text/plain; charset=utf-8" }); return; } const upstreamPath = `${url.pathname.slice("/inspector".length)}${url.search}`; const signature = signReviewRequest(options.reviewCapability, { method: "POST", path: upstreamPath, body, }); proxyRequest({ request: options.request, response: options.response, upstreamHost: options.upstreamHost, upstreamPort: options.upstreamPort, upstreamPath, body, additionalHeaders: { "content-type": "application/json", "content-length": String(body.length), [REVIEW_TIMESTAMP_HEADER]: signature.timestamp, [REVIEW_NONCE_HEADER]: signature.nonce, [REVIEW_SIGNATURE_HEADER]: signature.signature, }, }); return; } if (courseChatWriteRoute) { if (options.request.method !== "POST") return methodNotAllowed(options.request, options.response, "POST"); if (!oauthAuthorized || !options.courseChatCapability) { options.request.resume(); send(options.response, 403, "Course chat is unavailable.\n", { "content-type": "text/plain; charset=utf-8" }); return; } const csrfToken = uniqueHeader(options.request.headers[COURSE_CHAT_CSRF_HEADER]); if (!csrfToken || !safeEqual(csrfToken, oauthAuthorized.csrfToken)) { options.request.resume(); send(options.response, 403, "Course question could not be verified.\n", { "content-type": "text/plain; charset=utf-8" }); return; } let body: Buffer; try { body = await readJsonBody(options.request, 4_096); JSON.parse(body.toString("utf8")); } catch { options.request.resume(); send(options.response, 400, "Course question is invalid.\n", { "content-type": "text/plain; charset=utf-8" }); return; } const upstreamPath = url.pathname.slice("/inspector".length); const signature = signCourseChatRequest(options.courseChatCapability, { method: "POST", path: upstreamPath, body, }); proxyRequest({ request: options.request, response: options.response, upstreamHost: options.upstreamHost, upstreamPort: options.upstreamPort, upstreamPath, body, additionalHeaders: { "content-type": "application/json", "content-length": String(body.length), [COURSE_CHAT_TIMESTAMP_HEADER]: signature.timestamp, [COURSE_CHAT_NONCE_HEADER]: signature.nonce, [COURSE_CHAT_SIGNATURE_HEADER]: signature.signature, }, }); return; } if (!isReadMethod(options.request.method)) return methodNotAllowed(options.request, options.response, "GET, HEAD"); if (url.pathname === "/inspector") { options.request.resume(); return redirect(options.response, `/inspector/${url.search}`, [], 308); } const upstreamPath = `${url.pathname.slice("/inspector".length) || "/"}${url.search}`; proxyRequest({ request: options.request, response: options.response, upstreamHost: options.upstreamHost, upstreamPort: options.upstreamPort, upstreamPath, });}
function proxyRequest(options: { request: IncomingMessage; response: ServerResponse; upstreamHost: string; upstreamPort: number; upstreamPath: string; body?: Buffer | undefined; additionalHeaders?: Record<string, string> | undefined;}): void { const headers = forwardedHeaders(options.request.headers); headers.host = `${options.upstreamHost}:${options.upstreamPort}`; Object.assign(headers, options.additionalHeaders); const upstream = http.request({ hostname: options.upstreamHost, port: options.upstreamPort, path: options.upstreamPath, method: options.request.method, headers, }, (upstreamResponse) => { const responseHeaders: Record<string, string | string[]> = {}; for (const [name, value] of Object.entries(upstreamResponse.headers)) { if (value === undefined || isHopByHop(name) || name === "set-cookie" || name === "www-authenticate") continue; responseHeaders[name] = value; } Object.assign(responseHeaders, SECURITY_HEADERS); if (isHtmlContentType(upstreamResponse.headers["content-type"])) { responseHeaders["content-security-policy"] = INSPECTOR_HTML_CONTENT_SECURITY_POLICY; } options.response.writeHead(upstreamResponse.statusCode ?? 502, responseHeaders); upstreamResponse.pipe(options.response); }); upstream.on("error", () => { if (!options.response.headersSent) { send(options.response, 502, "Inspector unavailable.\n", { "content-type": "text/plain; charset=utf-8", }); } else { options.response.destroy(); } }); upstream.end(options.body);}
function isHtmlContentType(value: string | string[] | undefined): boolean { const contentType = Array.isArray(value) ? value[0] : value; return contentType?.split(";", 1)[0]?.trim().toLowerCase() === "text/html";}
function forwardedHeaders(source: IncomingHttpHeaders): Record<string, string | string[]> { const headers: Record<string, string | string[]> = {}; for (const [name, value] of Object.entries(source)) { if (!FORWARDED_REQUEST_HEADERS.has(name) || value === undefined) continue; headers[name] = value; } return headers;}
function isAuthorized(value: string | undefined, expectedDigest: Buffer): boolean { return timingSafeEqual(digest(typeof value === "string" ? value : ""), expectedDigest);}
function digest(value: string): Buffer { return createHash("sha256").update(value, "utf8").digest();}
function send( response: ServerResponse, status: number, body: string, headers: Record<string, string | string[]>,): void { response.writeHead(status, { ...SECURITY_HEADERS, ...headers }); response.end(body);}
function redirect( response: ServerResponse, location: string, cookies: string[] = [], status = 302, headers: Record<string, string | string[]> = {},): void { send(response, status, "", { location, ...(cookies.length > 0 ? { "set-cookie": cookies } : {}), ...headers, });}
function authenticationRequired( request: IncomingMessage, response: ServerResponse, oauthEnabled: boolean, basicFallbackEnabled: boolean,): void { const acceptsHtml = request.headers.accept?.includes("text/html") ?? false; if (oauthEnabled && acceptsHtml) return redirect(response, "/oauth/login", [], 303); send(response, 401, "Authentication required.\n", { "content-type": "text/plain; charset=utf-8", ...(basicFallbackEnabled ? { "www-authenticate": 'Basic realm="thought stream", charset="UTF-8"' } : {}), });}
function rateLimited(response: ServerResponse, retryAfterSeconds: number): void { send(response, 429, "Too many requests.\n", { "content-type": "text/plain; charset=utf-8", "retry-after": String(retryAfterSeconds), });}
function abortOnDisconnect(request: IncomingMessage, response: ServerResponse): { signal: AbortSignal; cleanup: () => void;} { const controller = new AbortController(); const abort = () => controller.abort(); const close = () => { if (!response.writableEnded) controller.abort(); }; request.once("aborted", abort); response.once("close", close); return { signal: controller.signal, cleanup: () => { request.off("aborted", abort); response.off("close", close); }, };}
function rateLimitClientKey(request: IncomingMessage): string { const remote = normalizeAddress(request.socket.remoteAddress ?? "unknown"); const forwarded = request.headers["x-real-ip"]; if (isLoopback(remote) && typeof forwarded === "string" && isIP(forwarded.trim()) !== 0) { return forwarded.trim(); } return remote;}
function normalizeAddress(value: string): string { return value.startsWith("::ffff:") ? value.slice("::ffff:".length) : value;}
function methodNotAllowed(request: IncomingMessage, response: ServerResponse, allow: string): void { request.resume(); send(response, 405, "Method not allowed.\n", { allow, "content-type": "text/plain; charset=utf-8", });}
function notFound(response: ServerResponse): void { send(response, 404, "Not found.\n", { "content-type": "text/plain; charset=utf-8" });}
function requestUrl(request: IncomingMessage): URL | undefined { const raw = request.url ?? "/"; if (Buffer.byteLength(raw, "utf8") > 8_192) return undefined; try { return new URL(raw, "http://thoughtstream.invalid"); } catch { return undefined; }}
async function readFormBody(request: IncomingMessage): Promise<string> { if (!(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/x-www-form-urlencoded")) { throw new Error("Expected form body"); } const parts: Buffer[] = []; let bytes = 0; for await (const part of request) { const buffer = Buffer.isBuffer(part) ? part : Buffer.from(part); bytes += buffer.length; if (bytes > 4_096) throw new Error("Form body too large"); parts.push(buffer); } return Buffer.concat(parts).toString("utf8");}
async function readJsonBody(request: IncomingMessage, maxBytes: number): Promise<Buffer> { const contentType = (request.headers["content-type"] ?? "").toLowerCase().split(";", 1)[0]?.trim(); if (contentType !== "application/json") throw new Error("Expected JSON body"); const declared = request.headers["content-length"]; if (typeof declared === "string" && (!/^\d+$/.test(declared) || Number(declared) > maxBytes)) { throw new Error("JSON body too large"); } const parts: Buffer[] = []; let bytes = 0; for await (const part of request) { const buffer = Buffer.isBuffer(part) ? part : Buffer.from(part); bytes += buffer.length; if (bytes > maxBytes) throw new Error("JSON body too large"); parts.push(buffer); } if (bytes === 0) throw new Error("JSON body is empty"); return Buffer.concat(parts);}
function oneField(fields: URLSearchParams, name: string): string | undefined { const values = fields.getAll(name); return values.length === 1 ? values[0] : undefined;}
function uniqueHeader(value: string | string[] | undefined): string | undefined { if (Array.isArray(value)) return value.length === 1 ? value[0] : undefined; if (!value || value.includes(",") || Buffer.byteLength(value, "utf8") > 512) return undefined; return value;}
function safeEqual(left: string, right: string): boolean { return timingSafeEqual(digest(left), digest(right));}
function loginPage(): string { return page("Private inspector", "<p>Authenticate with the configured ATProto identity.</p><form method=\"post\" action=\"/oauth/login\"><button type=\"submit\">Continue with ATProto</button></form>");}
function logoutPage(csrfToken: string): string { return page("Sign out", `<p>Revoke this inspector session.</p><form method="post" action="/oauth/logout"><input type="hidden" name="csrfToken" value="${escapeHtml(csrfToken)}"><button type="submit">Sign out</button></form>`);}
function page(title: string, body: string): string { return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title><style>:root{color-scheme:light dark}body{max-width:44rem;margin:4rem auto;padding:0 1rem;font:16px/1.5 system-ui,sans-serif}button{font:inherit;padding:.55rem .8rem}</style></head><body><h1>${escapeHtml(title)}</h1>${body}<p><a href="/">Public documentation</a></p></body></html>`;}
function escapeHtml(value: string): string { return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");}
function clearFlowCookie(): string { return "__Host-thoughtstream_oauth=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax";}
function isReadMethod(method: string | undefined): boolean { return method === "GET" || method === "HEAD";}
function optionalPort(value: string | undefined, fallback: number, label: string): number { if (value === undefined || value.trim() === "") return fallback; return boundedPort(Number(value), label);}
function boundedPort(value: number, label: string): number { if (!Number.isSafeInteger(value) || value < 0 || value > 65_535) { throw new Error(`${label} must be an integer between 0 and 65535`); } return value;}
function normalizeBase64(value: string): string { return value.replaceAll(/\s+/g, "");}
function explicitBoolean(value: string | undefined, label: string): boolean { if (value === undefined) return false; if (value === "1" || value === "true") return true; if (value === "0" || value === "false") return false; throw new Error(`${label} must be one of 1, true, 0, or false`);}
function isLoopback(host: string): boolean { return host === "127.0.0.1" || host === "::1" || host === "localhost";}
function isHopByHop(name: string): boolean { return name === "connection" || name === "keep-alive" || name === "proxy-authenticate" || name === "proxy-authorization" || name === "te" || name === "trailer" || name === "transfer-encoding" || name === "upgrade";}