Something went wrong. Try again.
A local-first event pipeline for independent agents, built on Jazz.
Something went wrong. Try again.
7.7 kB · 96 lines
TypeScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697import http from "node:http";import { randomUUID } from "node:crypto";import { afterEach, describe, expect, it, vi } from "vitest";import { startAuthenticatedInspectorProxy } from "../src/web/authenticated-proxy.js";import type { InspectorOAuthAuth } from "../src/web/oauth-auth.js";import type { CoChat } from "../src/web/co-chat.js";
const servers: http.Server[] = [];afterEach(async () => { await Promise.all(servers.splice(0).map((server) => new Promise<void>((resolve) => { server.closeAllConnections(); server.close(() => resolve()); }))); });async function setup(enabled = true) { const chat = { list: vi.fn(() => []), create: vi.fn(async () => ({ id: randomUUID() })), snapshot: vi.fn(async () => ({ rows: [] })), send: vi.fn(async () => ({ status: "dispatching" })), stop: vi.fn(async () => ({ status: "stopping" })), rename: vi.fn(async () => ({})) }; const oauth = { authenticate: vi.fn(async (cookie: string | undefined) => cookie === "synthetic-owner=yes" ? { csrfToken: "synthetic-csrf" } : undefined) } as unknown as InspectorOAuthAuth; const server = await startAuthenticatedInspectorProxy({ port: 0, oauth, basicFallbackEnabled: true, username: "synthetic", password: "synthetic-basic-password-at-least-twenty", publicPages: new Map(), ...(enabled ? { coChat: { chat: chat as unknown as CoChat, origin: "https://thought.example", javascript: "/* synthetic */", stylesheet: "body{}" } } : {}) }); servers.push(server); const address = server.address(); if (!address || typeof address === "string") throw new Error("fixture server failed"); const base = `http://127.0.0.1:${address.port}`; const owner = { cookie: "synthetic-owner=yes" }; const mutation = { ...owner, origin: "https://thought.example", "x-co-csrf": "synthetic-csrf", "content-type": "application/json" }; return { base, chat, owner, mutation };}describe("private chat HTTP boundary", () => { it("allows authenticated top-level return navigation but rejects cross-site APIs and frames", async () => { const {base, owner, mutation, chat} = await setup(); // Node fetch replaces Sec-Fetch-Mode with cors; use raw HTTP for navigation. const navigate = (extra: Record<string,string>) => new Promise<{status:number|undefined,text:string}>((resolve,reject) => { http.get(base + "/chat/", {headers:extra}, response => { let text = ""; response.setEncoding("utf8"); response.on("data", chunk => {text += chunk;}); response.on("end", () => resolve({status:response.statusCode,text})); }).on("error",reject); }); const navigation = {"sec-fetch-site":"cross-site", "sec-fetch-mode":"navigate", "sec-fetch-dest":"document"}; expect((await fetch(base + "/chat/", {headers:navigation})).status).toBe(401); const shell = await navigate({...owner,...navigation}); expect(shell.status).toBe(200); expect(shell.text).toContain('/chat/app.js'); expect((await navigate({...owner,...navigation,"sec-fetch-dest":"iframe"})).status).toBe(403); for (const route of ["/chat/api/session", "/chat/api/conversations", "/chat/app.js"]) { expect((await fetch(base + route, {headers:{...owner,...navigation}})).status).toBe(403); } expect((await fetch(base + "/chat/api/conversations", {method:"POST",headers:{...mutation,...navigation},body:JSON.stringify({requestId:randomUUID()})})).status).toBe(403); expect(chat.list).not.toHaveBeenCalled(); expect(chat.create).not.toHaveBeenCalled(); }); it("gates shell, assets, status and unknown objects before registry access", async () => { const { base, chat } = await setup(); for (const route of ["/chat/", "/chat/app.js", "/chat/app.css", "/chat/api/session", "/chat/api/conversations", `/chat/api/conversations/${randomUUID()}`]) { const response = await fetch(base + route); expect(response.status).toBe(401); expect(response.headers.get("cache-control")).toBe("no-store"); expect(response.headers.get("www-authenticate")).toBeNull(); } const response = await fetch(base + "/chat/", { headers: { authorization: `Basic ${Buffer.from("synthetic:synthetic-basic-password-at-least-twenty").toString("base64")}` } }); expect(response.status).toBe(401); expect(chat.list).not.toHaveBeenCalled(); expect(chat.snapshot).not.toHaveBeenCalled(); }); it("enforces exact Origin, session CSRF, JSON and method bounds", async () => { const { base, chat, owner, mutation } = await setup(); const route = `${base}/chat/api/conversations/${randomUUID()}/send`; const body = JSON.stringify({ requestId: randomUUID(), text: "synthetic" }); expect((await fetch(route, { method: "POST", headers: owner, body })).status).toBe(403); expect((await fetch(route, { method: "POST", headers: { ...mutation, origin: "https://evil.example" }, body })).status).toBe(403); expect((await fetch(route, { method: "POST", headers: { ...mutation, "x-co-csrf": "wrong" }, body })).status).toBe(403); expect((await fetch(route, { method: "POST", headers: { ...mutation, "content-type": "text/plain" }, body })).status).toBe(415); expect((await fetch(route, { method: "PUT", headers: mutation, body })).status).toBe(405); expect(chat.send).not.toHaveBeenCalled(); expect((await fetch(route, { method: "POST", headers: mutation, body })).status).toBe(202); expect(chat.send).toHaveBeenCalledTimes(1); expect((await fetch(route, { method: "POST", headers: mutation, body: "x".repeat(33_000) })).status).toBe(413); expect((await fetch(route + "?other=1", { method: "POST", headers: mutation, body })).status).toBe(405); }); it("serves only fixed private assets with restrictive CSP and no raw runtime protocol", async () => { const { base, owner } = await setup(); const shell = await fetch(base + "/chat/", { headers: owner }); expect(shell.status).toBe(200); expect(shell.headers.get("content-security-policy")).toContain("script-src 'self'"); expect(shell.headers.get("content-security-policy")).not.toContain("unsafe-inline"); expect(await shell.text()).toContain("/chat/app.js"); for (const route of ["/chat/.env", "/chat/api/agents", "/chat/api/conversations/default", "/chat/api/tools", "/chat/api/messages"]) expect((await fetch(base + route, { headers: owner })).status).toBe(404); expect((await fetch(base + "/chat/api/session", { headers: { ...owner, "sec-fetch-site": "cross-site" } })).status).toBe(403); const session = await fetch(base + "/chat/api/session", { headers: owner }); expect(await session.json()).toEqual({ csrfToken: "synthetic-csrf", sharedMemory: true, permissionMode: "unrestricted" }); }); it("rejects duplicate security headers", async () => { const { base } = await setup(); const status = await new Promise<number | undefined>((resolve, reject) => { const request = http.request(base + "/chat/api/conversations", { method: "POST", headers: ["host", new URL(base).host, "cookie", "synthetic-owner=yes", "origin", "https://thought.example", "origin", "https://thought.example", "x-co-csrf", "synthetic-csrf", "content-type", "application/json"] }, (response) => { response.resume(); resolve(response.statusCode); }); request.on("error", reject); request.end(JSON.stringify({ requestId: randomUUID() })); }); expect(status).toBe(403); }); it("does not claim availability when activation is absent", async () => { const { base, owner } = await setup(false); const response = await fetch(base + "/chat/api/session", { headers: owner }); expect(response.status).toBe(503); expect(await response.json()).toEqual({ error: "chat-not-configured" }); });});