Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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