Skip to content
62 linesCodeBlameRaw
1/**
2 * The outbound handler's side of brokered secrets (broker.ts): `egress`
3 * (guard.ts) asks `brokered` once for each request to a host the sandbox
4 * may reach, before forwarding it itself.
5 */
6import type { OutboundHandlerContext } from "@cloudflare/containers";
7
8import type { ServiceBinding } from "@g1t/contracts";
9
10import { type BrokeredJob, brokerFor, fill, scrub, scrubHeaders, scrubbable, standInsIn } from "./broker.ts";
11import { sandboxNamespace } from "./egress.ts";
12import { forward } from "./forward.ts";
13
14/**
15 * What the outbound handler is given for brokering: `open` for a job whose
16 * network is open but which may be given brokered secrets (every host is
17 * reached, as it would be), and for a workflow job, the job, whose
18 * stand-ins it fills in.
19 */
20export type BrokeredParams = { open?: boolean; job?: BrokeredJob };
21
22/**
23 * Sends a workflow job's request with its brokered secrets filled in: a
24 * stand-in becomes the real value on a request to one of its secret's
25 * hosts, and the answer has the value swapped back should it echo it.
26 * Null when there is nothing to fill in, and `egress` forwards the
27 * request as it is. A filled-in request is not followed through
28 * redirects here, so the value never goes wherever a redirect points: the
29 * job follows it itself, and that request comes through here again. A
30 * stand-in sent to a host it is not for goes as it is, and the job's log
31 * says so once.
32 */
33export async function brokered(
34 request: Request,
35 env: { SANDBOX: DurableObjectNamespace; ACTIONS?: ServiceBinding },
36 ctx: OutboundHandlerContext<BrokeredParams>,
37): Promise<Response | null> {
38 const job = ctx.params?.job;
39 if (!job || !env.ACTIONS) return null;
40 const wanted = standInsIn(request.url, request.headers);
41 if (wanted.length === 0) return null;
42 const secrets = await brokerFor(env.ACTIONS, job, wanted);
43 const { url, headers, filled, misdirected } = fill(request.url, request.headers, secrets);
44 for (const secret of misdirected) {
45 try {
46 const namespace = sandboxNamespace(env, ctx.className);
47 const sandbox = namespace.get(namespace.idFromString(ctx.containerId)) as unknown as {
48 noteMisdirected(name: string, host: string): Promise<void>;
49 };
50 await sandbox.noteMisdirected(secret.name, new URL(request.url).host);
51 } catch (error) {
52 console.log("misdirected secret not reported", secret.name, String(error));
53 }
54 }
55 if (filled.length === 0) return null;
56 const response = await forward(new Request(url, { method: request.method, headers, body: request.body, redirect: "manual" }));
57 const answered = scrubHeaders(response.headers, filled);
58 const init = { status: response.status, statusText: response.statusText, headers: answered };
59 if (!scrubbable(response.headers)) return new Response(response.body, init);
60 answered.delete("content-length");
61 return new Response(scrub(await response.text(), filled), init);
62}