Something went wrong. Try again.
A local-first event pipeline for independent agents, built on Jazz.
Something went wrong. Try again.
7.5 kB · 113 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114import type { IncomingMessage, ServerResponse } from "node:http";import { timingSafeEqual } from "node:crypto";import { z } from "zod";import { ChatError, type CoChat } from "./co-chat.js";import type { InspectorOAuthAuth } from "./oauth-auth.js";
export interface CoChatWeb { chat: CoChat; origin: string; javascript: string; stylesheet: string;}const headers = { "cache-control": "no-store", "content-security-policy": "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'none'; font-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'", "referrer-policy": "no-referrer", "x-content-type-options": "nosniff", "x-frame-options": "DENY", "permissions-policy": "camera=(), microphone=(), geolocation=(), payment=(), usb=()",};const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" media="(prefers-color-scheme:light)" content="#ffffff"><meta name="theme-color" media="(prefers-color-scheme:dark)" content="#0a0a0a"><title>Co · Stream</title><link rel="stylesheet" href="/chat/app.css"></head><body><div id="root"></div><noscript>JavaScript is required for private chat. <a href="/inspector/">Open Stream</a></noscript><script type="module" src="/chat/app.js"></script></body></html>`;function send(response: ServerResponse, status: number, body: unknown, type = "application/json; charset=utf-8"): void { response.writeHead(status, { ...headers, "content-type": type }); response.end(type.startsWith("application/json") ? JSON.stringify(body) : body);}function unique(request: IncomingMessage, name: string): string | undefined { let count = 0; for (let i = 0; i < request.rawHeaders.length; i += 2) if (request.rawHeaders[i]?.toLowerCase() === name) count++; const value = request.headers[name]; return count === 1 && typeof value === "string" ? value : undefined;}function equal(left: string, right: string): boolean { const a = Buffer.from(left); const b = Buffer.from(right); return a.length === b.length && timingSafeEqual(a, b);}async function body(request: IncomingMessage): Promise<unknown> { if (unique(request, "content-type") !== "application/json") throw new ChatError(415, "json-required"); let bytes = 0; const chunks: Buffer[] = []; for await (const chunk of request) { const buffer = Buffer.from(chunk as Uint8Array); bytes += buffer.length; if (bytes > 32_768) throw new ChatError(413, "request-too-large"); chunks.push(buffer); } try { return JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch { throw new ChatError(400, "invalid-request"); }}
/** OAuth is checked here independently; Basic and inspector capabilities cannot enter. */export function createCoChatHandler(web: CoChatWeb | undefined, oauth: InspectorOAuthAuth | undefined) { let readWindow = 0; let reads = 0; let writes = 0; return async (request: IncomingMessage, response: ServerResponse, url: URL): Promise<void> => { try { const session = await oauth?.authenticate(request.headers.cookie); if (!session) { request.resume(); send(response, 401, { error: "sign-in-required" }); return; } if (!web) { request.resume(); send(response, 503, { error: "chat-not-configured" }); return; } if (Date.now() - readWindow > 60_000) { readWindow = Date.now(); reads = 0; writes = 0; } const method = request.method; if (method !== "GET" && method !== "HEAD" && method !== "POST") throw new ChatError(405, "method-not-allowed"); if (++reads > 240) throw new ChatError(429, "rate-limited"); // OAuth redirects and links can be cross-site top-level navigations. // Admit only the inert authenticated shell, never API reads or writes. const shellNavigation = method === "GET" && (url.pathname === "/chat/" || url.pathname === "/chat") && !url.search && unique(request, "sec-fetch-mode") === "navigate" && unique(request, "sec-fetch-dest") === "document"; if (request.headers["sec-fetch-site"] === "cross-site" && !shellNavigation) throw new ChatError(403, "same-origin-required"); if (method === "POST") { if (++writes > 30) throw new ChatError(429, "rate-limited"); if (unique(request, "origin") !== web.origin || !equal(unique(request, "x-co-csrf") ?? "", session.csrfToken)) throw new ChatError(403, "request-not-verified"); } const path = url.pathname; const asset = path === "/chat/" || path === "/chat" || path === "/chat/app.js" || path === "/chat/app.css"; if (asset) { if (method === "POST" || url.search) throw new ChatError(405, "method-not-allowed"); request.resume(); const type = path.endsWith(".js") ? "text/javascript; charset=utf-8" : path.endsWith(".css") ? "text/css; charset=utf-8" : "text/html; charset=utf-8"; send(response, 200, method === "HEAD" ? "" : path.endsWith(".js") ? web.javascript : path.endsWith(".css") ? web.stylesheet : html, type); return; } if (method === "HEAD") throw new ChatError(405, "method-not-allowed"); if (path === "/chat/api/session" && method === "GET" && !url.search) { request.resume(); send(response, 200, { csrfToken: session.csrfToken, sharedMemory: true, permissionMode: "unrestricted" }); return; } if (path === "/chat/api/conversations" && !url.search) { if (method === "GET") { request.resume(); send(response, 200, { conversations: web.chat.list() }); return; } const input = z.object({ requestId: z.string().uuid() }).strict().parse(await body(request)); send(response, 201, await web.chat.create(input.requestId)); return; } const match = /^\/chat\/api\/conversations\/([a-f0-9-]{36})(?:\/(send|stop|rename))?$/.exec(path); if (!match) throw new ChatError(404, "not-found"); const id = z.string().uuid().parse(match[1]); const action = match[2]; if (!action && method === "GET") { const fields = [...url.searchParams.keys()]; if (fields.length > 1 || fields.some((key) => key !== "before")) throw new ChatError(400, "invalid-request"); const before = url.searchParams.get("before") ?? undefined; if (before && !/^[A-Za-z0-9_-]{1,100}$/.test(before)) throw new ChatError(400, "invalid-cursor"); request.resume(); send(response, 200, await web.chat.snapshot(id, before)); return; } if (method !== "POST" || url.search) throw new ChatError(405, "method-not-allowed"); const input = await body(request); if (action === "send") { send(response, 202, await web.chat.send(id, input)); return; } if (action === "stop") { z.object({}).strict().parse(input); send(response, 202, await web.chat.stop(id)); return; } if (action === "rename") { const data = z.object({ title: z.string().trim().min(1).max(100) }).strict().parse(input); send(response, 200, await web.chat.rename(id, data.title)); return; } throw new ChatError(404, "not-found"); } catch (error) { request.resume(); if (response.headersSent) { response.destroy(); return; } send(response, error instanceof ChatError ? error.status : error instanceof z.ZodError ? 400 : 503, { error: error instanceof ChatError ? error.code : error instanceof z.ZodError ? "invalid-request" : "chat-unavailable" }); } };}