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
56 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.1/**
2 * How the outbound handler sends a sandbox's request on to an allowed
3 * host. An upstream that resets the connection or fails outright would
4 * otherwise throw out of the handler, and the sandbox would see its own
5 * connection reset, which tools such as npm count as a failure rather
6 * than something to retry. So a request that can be sent again (a GET or
7 * HEAD with no body) is tried twice more after a short wait, and what
8 * still fails comes back as a `502` saying which host and why, which
9 * those tools do retry. It never throws.
10 */
11
12import { normalizeHost } from "./egress.ts";
13
14/** The waits before the second and third tries, in milliseconds. */
15export const RETRY_WAITS_MS: readonly number[] = [250, 1000];
16
17/** Whether a request can be sent again as it was: no body, nothing changed. */
18export function replayable(request: Request): boolean {
19 return (request.method === "GET" || request.method === "HEAD") && request.body === null;
20}
21
22/** What a request that could not be sent gets back. */
23export function upstreamFailed(host: string, error: unknown): Response {
24 const why = error instanceof Error ? error.message : String(error);
25 return new Response(`g1t egress: ${normalizeHost(host)} could not be reached: ${why}\n`, {
26 status: 502,
27 headers: { "content-type": "text/plain; charset=utf-8", "x-g1t-egress": "upstream-failed" },
28 });
29}
30
31const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
32
33/**
34 * Sends `request` with `send` (`fetch`), trying a replayable one again on
35 * a thrown error, and answering `502` when it cannot. Each failure is one
36 * line of the Worker's log.
37 */
38export async function forward(
39 request: Request,
40 send: (request: Request) => Promise<Response> = fetch,
41 sleep: (ms: number) => Promise<void> = wait,
42): Promise<Response> {
43 const host = new URL(request.url).host;
44 const tries = replayable(request) ? RETRY_WAITS_MS.length + 1 : 1;
45 let failure: unknown = null;
46 for (let attempt = 1; attempt <= tries; attempt++) {
47 if (attempt > 1) await sleep(RETRY_WAITS_MS[attempt - 2]);
48 try {
49 return await send(request);
50 } catch (error) {
51 failure = error;
52 console.log("egress upstream failed", normalizeHost(host), request.method, `try ${attempt} of ${tries}`, String(error));
53 }
54 }
55 return upstreamFailed(host, failure);
56}

This file's history is long; its oldest lines are credited to the oldest commit read.