Something went wrong. Try again.
llm chat done the right way™
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116import type { BlobStore } from "./blobs";import { getConfig } from "./settings";import type { Store } from "./store";
/** * A place to run an artifact that is not the app. * * An HTML artifact is model-authored code, so the app frames it sandboxed * WITHOUT `allow-same-origin` — the one combination that must never appear * beside `allow-scripts`, since together they would hand generated markup the * user's own origin. The cost is that such a page has an opaque origin, and an * opaque origin can hold no permission grant: no camera, no microphone, no * storage, ever, however the page asks. * * The way out of that is not a weaker sandbox but a different origin. Serve the * page from a host that owns nothing — no cookies, no API, no app — and * `allow-same-origin` becomes safe to grant, because "same origin" now means * that empty host. The camera can then be delegated to it explicitly, and what * it is delegated FROM is a place with nothing to take. * * So this module is defined as much by what it refuses as by what it serves: * * - it answers only on the configured artifact host, so a proxy that sends * the whole app there by mistake still cannot make it the app's origin; * - it serves only HTML, because that is the one type this exists for; * - it declares `frame-ancestors`, so only the app may frame what it serves. * * A sha256 is the whole address here — unguessable, and the same capability * shape a publication token has. Worth saying plainly: it is a weaker rule than * `/api/blobs`, which needs a session. It has to be, because the point of this * origin is that the app's cookie never reaches it. */export function sandboxRoutes(deps: { store: Store; blobs: BlobStore }) { const { store, blobs } = deps; const isSha256 = (s: string) => /^[0-9a-f]{64}$/.test(s);
return { "/a/:sha256": { GET: async (req: Bun.BunRequest<"/a/:sha256">) => { const origin = getConfig().server.artifactOrigin; // Unset means this door does not exist. The frame keeps its old sandbox // and the app is exactly as it was. if (!origin) return new Response("not found", { status: 404 }); if (!onArtifactHost(req)) return new Response("not found", { status: 404 }); const sha256 = req.params.sha256; if (!isSha256(sha256)) return new Response("not found", { status: 404 }); const meta = store.getBlob(sha256); // HTML only: everything else has a home already, behind the session. if (!meta || !/^text\/html\b/i.test(meta.mime)) { return new Response("not found", { status: 404 }); } const bytes = await blobs.get(sha256); if (!bytes) return new Response("not found", { status: 404 }); const app = getConfig().auth.baseUrl; return new Response(bytes, { headers: { "Content-Type": meta.mime, "X-Content-Type-Options": "nosniff", // Content-addressed, so it can never change under a reader. "Cache-Control": "public, max-age=31536000, immutable", // A page that may run scripts should not also be a page anyone can // frame: the app embeds it, and nobody else does. A deployment that // never said where it lives (auth.baseUrl) gets no such line, since // there is no honest value for it — the page is still walled off // from everything by its origin, it just isn't told who may show it. ...(app ? { "Content-Security-Policy": `frame-ancestors ${app}` } : {}), "Referrer-Policy": "no-referrer", }, }); }, }, };}
/** Whether this request arrived on the artifact host rather than the app's. */export function onArtifactHost(req: Request): boolean { const origin = getConfig().server.artifactOrigin; if (!origin) return false; try { return new URL(req.url).host === new URL(origin).host; } catch { return false; }}
type Handler = (req: never, ...rest: never[]) => Response | Promise<Response>;type Routes = Record<string, Record<string, Handler>>;
/** * The app's API, refusing to answer on the artifact host. * * The proxy in front should send that hostname nothing but `/a/*`, and this is * the same rule stated where a config change cannot edit it out. The isolation * is worth what its weakest statement is worth, and a deployment with auth off * would otherwise be serving an unauthenticated API to the very pages this * arrangement exists to contain. * * Only the API and the public read paths need it. The app's own HTML carries no * authority — served from the artifact host it would be a shell whose every API * call 404s, which is a curiosity rather than a hole. */export function offArtifactHost<T extends Routes>(routes: T): T { const out: Routes = {}; for (const [path, methods] of Object.entries(routes)) { const wrapped: Record<string, Handler> = {}; for (const [method, fn] of Object.entries(methods)) { wrapped[method] = (req, ...rest) => onArtifactHost(req as unknown as Request) ? new Response("not found", { status: 404 }) : fn(req, ...rest); } out[path] = wrapped; } return out as T;}