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.

72 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((value): value is string => typeof value === "string" && value.startsWith("g1t_"));
42 return Promise.all(tokens.map(sha256Hex));
43}
44
45type Storage = {
46 put(key: string, value: unknown): Promise<void>;
47 get<T>(key: string): Promise<T | undefined>;
48 delete(key: string): Promise<boolean>;
49};
50
51/**
52 * Remembers a sandbox's credentials, by hash, so they can be revoked when
53 * it stops, and ties them to the run it recorded. Never stops the sandbox
54 * from starting.
55 */
56export async function holdCredentials(identity: ServiceBinding, storage: Storage, envVars: Record<string, string>, runId: string | null): Promise<void> {
57 const hashes = await credentialHashes(envVars);
58 if (hashes.length === 0) return;
59 await storage.put(STORAGE_KEY, hashes);
60 if (!runId) return;
61 await call(identity, "bind_run_credentials", { tokenHashes: hashes, runId }).catch((error: unknown) =>
62 console.log("run credentials not bound", runId, String(error)),
63 );
64}
65
66/** Ends a sandbox's g1t credentials, once. */
67export async function revokeCredentials(identity: ServiceBinding, storage: Storage): Promise<void> {
68 const hashes = await storage.get<string[]>(STORAGE_KEY);
69 if (!hashes?.length) return;
70 await storage.delete(STORAGE_KEY);
71 await call(identity, "revoke_run_credentials", { tokenHashes: hashes }).catch((error: unknown) => console.log("run credentials not revoked", String(error)));
72}