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