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
77 linesCodeBlameRaw
1/**
2 * Run credentials: the tokens a sandbox works with. Each is bound to its
3 * run, its repository and what that kind of run needs, acts on behalf of
4 * the person (or g1t itself) the work is for, and is revoked the moment
5 * the sandbox stops. See `crates/contracts/src/credentials.rs` for what
6 * each kind of run may do.
7 */
8
9import type { CreateRunCredentialInput, ServiceBinding } from "@g1t/contracts";
10
11/** The environment variable a sandbox's g1t token is passed in. */
12export const CREDENTIAL_VARS = ["G1T_TOKEN"] as const;
13
14const STORAGE_KEY = "credentials";
15
16/** Identity's JSON protocol, as the contracts' client speaks it. */
17async function call<T>(identity: ServiceBinding, method: string, args: object): Promise<T> {
18 const response = await identity.fetch(`https://service/rpc/${method}`, {
19 method: "POST",
20 headers: { "content-type": "application/json" },
21 body: JSON.stringify(args),
22 });
23 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
24 return (await response.json()) as T;
25}
26
27/** A run credential's text. */
28export async function runCredential(identity: ServiceBinding, input: CreateRunCredentialInput): Promise<string> {
29 const { token } = await call<{ token: string }>(identity, "create_run_credential", input);
30 return token;
31}
32
33/** SHA-256 in lowercase hex, as identity stores tokens. */
34export async function sha256Hex(text: string): Promise<string> {
35 const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
36 return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
37}
38
39/** The hashes of the g1t tokens among a sandbox's variables. */
40export async function credentialHashes(envVars: Record<string, string>): Promise<string[]> {
41 const tokens = CREDENTIAL_VARS.map((name) => envVars[name]).filter(
42 (value): value is string => typeof value === "string" && value.startsWith("g1t_"),
43 );
44 return Promise.all(tokens.map(sha256Hex));
45}
46
47type Storage = {
48 put(key: string, value: unknown): Promise<void>;
49 get<T>(key: string): Promise<T | undefined>;
50 delete(key: string): Promise<boolean>;
51};
52
53/**
54 * Remembers a sandbox's credentials, by hash, so they can be revoked when
55 * it stops, and ties them to the run it recorded. Never stops the sandbox
56 * from starting.
57 */
58export async function holdCredentials(
59 identity: ServiceBinding,
60 storage: Storage,
61 envVars: Record<string, string>,
62 runId: string | null,
63): Promise<void> {
64 const hashes = await credentialHashes(envVars);
65 if (hashes.length === 0) return;
66 await storage.put(STORAGE_KEY, hashes);
67 if (!runId) return;
68 await call(identity, "bind_run_credentials", { tokenHashes: hashes, runId }).catch((error: unknown) => console.log("run credentials not bound", runId, String(error)));
69}
70
71/** Ends a sandbox's g1t credentials, once. */
72export async function revokeCredentials(identity: ServiceBinding, storage: Storage): Promise<void> {
73 const hashes = await storage.get<string[]>(STORAGE_KEY);
74 if (!hashes?.length) return;
75 await storage.delete(STORAGE_KEY);
76 await call(identity, "revoke_run_credentials", { tokenHashes: hashes }).catch((error: unknown) => console.log("run credentials not revoked", String(error)));
77}