/** * The proxy, as something a host app can mount instead of run. * * `sandboxedjs-egress` is a whole process, which suits a terminal and suits * nothing else. A host app usually already has a server — an Express route, a * Cloudflare Pages Function, a Worker — and mounting the proxy there is better * than standing another one beside it: the container then calls its own * origin, so there is no second port to start, no CORS on the proxy itself, * and one thing to deploy rather than two. * * A static site has no server at all, which is the case this exists for. Those * hosts all take a function of a `Request`, so that is the shape here, with a * Node adapter for hosts that predate it. * * // Cloudflare Pages: functions/egress.ts * import { handleEgressRequest } from "sandboxedjs/egress"; * export const onRequest = ({ request }) => * handleEgressRequest(request, { allow: ["ollama.com"] }); * * Anything that can reach this can make requests through it, carrying whatever * credentials the guest holds. `allow` is the limit on where those go, and on * a public deployment it should be set and the route should be behind whatever * authentication the rest of the app uses. */ interface EgressOptions { /** Hosts this proxy will fetch, subdomains included. Unset means any. */ allow?: string[]; /** The `fetch` used to make the real request. */ fetch?: typeof globalThis.fetch; } /** Answer one proxied request. Safe to mount wherever a `Request` arrives. */ declare function handleEgressRequest(request: Request, options?: EgressOptions): Promise; /** Minimal shapes of Node's request and response, so this needs no `@types/node`. */ interface NodeRequest { method?: string; url?: string; headers: Record; on(event: string, listener: (chunk?: unknown) => void): unknown; } interface NodeResponse { writeHead(status: number, headers: Record): unknown; end(body?: unknown): unknown; } /** * The same handler for Express and `node:http`. * * app.post("/egress", egressNodeHandler({ allow: ["ollama.com"] })); */ declare function egressNodeHandler(options?: EgressOptions): (request: NodeRequest, response: NodeResponse) => Promise; /** * Finding the proxy instead of being told where it is. * * A page cannot read a response from a host that does not send CORS headers, * so a container in a browser needs a proxy to make its outbound requests -- * and until now every project had to say where that proxy was. That is a * setting whose right value is almost always the same, and getting it wrong * looks like the container being broken: the guest reports "Failed to fetch" * from code that works everywhere else. * * So a browser container with no proxy configured looks for one before it * gives up. A host that mounts `handleEgressRequest` at `EGRESS_PATH` on its * own origin, or runs `sandboxedjs-egress` on its default port, is found * without being named. * * A probe is a GET, which the handler answers with a marker. That matters more * than it sounds: a single-page host answers every unknown path with its own * index.html and status 200, so "the request succeeded" cannot mean "the proxy * is here". Only the marker does. * * Discovery never overrides a proxy the host named, and finding nothing is not * an error -- the request is then made directly, which is right on a server * and is what produces the explanatory CORS failure in a browser. */ /** Where a host app mounts the proxy for its own containers to find. */ declare const EGRESS_PATH = "/__sandboxedjs__/egress"; /** What a GET to the proxy answers, so a probe can tell it from a 200 page. */ declare const EGRESS_MARKER: { readonly sandboxedjs: "egress"; readonly protocol: 1; }; export { EGRESS_MARKER, EGRESS_PATH, type EgressOptions, egressNodeHandler, handleEgressRequest };