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.
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 1 | /** |
| 2 | * Security updates (`RunnerService.startBump`): what the security service | |
| 3 | * asks for, checked, and the sandbox it becomes, which runs the runner's | |
| 4 | * `bump` mode (crates/runner bump.rs). Pure, so it is tested on its own. | |
| 5 | */ | |
| 6 | import type { BumpArgs, RepoPath, User } from "@g1t/contracts"; | |
| 7 | ||
| 8 | /** g1t's own identity: `g1t_contracts::system::{ID, USERNAME}`. */ | |
| 9 | export const SYSTEM_ID = "g1t"; | |
| 10 | export const SYSTEM_USERNAME = "g1t"; | |
| 11 | ||
| 12 | /** The ecosystems the runner can update, by OSV's names. */ | |
| 13 | export const BUMP_ECOSYSTEMS: readonly string[] = ["npm", "crates.io", "Go", "PyPI"]; | |
| 14 | ||
| 15 | /** Long enough to clone, resolve and push; then the token stops working. */ | |
| 16 | export const BUMP_TOKEN_TTL_SECONDS = 30 * 60; | |
| 17 | ||
| 18 | /** A security update's time cap, and what is reserved for it. */ | |
| 19 | export const BUMP_MINUTES = 20; | |
| 20 | ||
| 21 | /** The most lockfiles one update names. */ | |
| 22 | const MAX_LOCKFILES = 50; | |
| 23 | ||
| 24 | /** | |
| 25 | * g1t itself, working in `workspace`: the actor of the work it does on its | |
| 26 | * own, such as a security update or an agent it puts on one. Mirrors | |
| 27 | * `g1t_contracts::User::system`. Identity makes its run credentials act | |
| 28 | * for the workspace. | |
| 29 | */ | |
| 30 | export function systemActor(workspace: string): User { | |
| 31 | return { | |
| 32 | id: SYSTEM_ID, | |
| 33 | username: SYSTEM_USERNAME, | |
| 34 | kind: "system", | |
| 35 | verified: true, | |
| 36 | workspaces: [{ slug: workspace.toLowerCase(), role: "member" }], | |
| 37 | }; | |
| 38 | } | |
| 39 | ||
| 40 | /** Whether `user` is g1t itself. */ | |
| 41 | export function isSystem(user: User | null | undefined): boolean { | |
| 42 | return user?.kind === "system"; | |
| 43 | } | |
| 44 | ||
| 45 | function isText(value: unknown): value is string { | |
| 46 | return typeof value === "string" && value.trim().length > 0; | |
| 47 | } | |
| 48 | ||
| 49 | /** A name or version the runner passes to a tool as one argument. */ | |
| 50 | function isArgument(text: string): boolean { | |
| 51 | return text.length <= 214 && !text.startsWith("-") && /^[A-Za-z0-9@/._+~-]+$/.test(text); | |
| 52 | } | |
| 53 | ||
| 54 | /** A lockfile's path from the repository's root, inside it. */ | |
| 55 | function isLockfilePath(path: unknown): boolean { | |
| 56 | if (!isText(path) || path.length > 512 || path.startsWith("/") || path.includes("\\")) return false; | |
| 57 | return path.split("/").every((part) => part !== "" && part !== ".."); | |
| 58 | } | |
| 59 | ||
| 60 | /** | |
| 61 | * What is wrong with a request for a security update, or null when it can | |
| 62 | * start: a repository, an ecosystem the runner updates, a package and | |
| 63 | * version it can pass to a tool, lockfiles inside the repository, and a | |
| 64 | * branch under `prefix` (`UPDATE_BRANCH_PREFIX`). | |
| 65 | */ | |
| 66 | export function bumpProblem(input: unknown, prefix: string): string | null { | |
| 67 | if (!input || typeof input !== "object") return "A security update needs its arguments."; | |
| 68 | const args = input as Partial<BumpArgs>; | |
| 69 | if (!args.repo || !isText(args.repo.namespace) || !isText(args.repo.name)) return "A security update needs its repository."; | |
| 70 | if (!isText(args.ecosystem) || !BUMP_ECOSYSTEMS.includes(args.ecosystem)) { | |
| 71 | return `g1t cannot update ${String(args.ecosystem)} dependencies; it updates ${BUMP_ECOSYSTEMS.join(", ")}.`; | |
| 72 | } | |
| 73 | if (!isText(args.package) || !isArgument(args.package.trim())) return "A security update needs the package's name."; | |
| 74 | if (!isText(args.version) || !isArgument(args.version.trim())) return "A security update needs the version to raise it to."; | |
| 75 | if (!Array.isArray(args.lockfiles) || args.lockfiles.length === 0 || args.lockfiles.length > MAX_LOCKFILES) { | |
| 76 | return `A security update names between 1 and ${MAX_LOCKFILES} lockfiles.`; | |
| 77 | } | |
| 78 | const outside = args.lockfiles.find((path) => !isLockfilePath(path)); | |
| 79 | if (outside !== undefined) return `${String(outside)} is not a path inside the repository.`; | |
| 80 | if ( | |
| 81 | !isText(args.branch) || | |
| 82 | !args.branch.startsWith(prefix) || | |
| 83 | args.branch.length <= prefix.length || | |
| 84 | args.branch.length > 200 || | |
| 85 | args.branch.includes("..") || | |
| 86 | /[\s~^:?*[\\]/.test(args.branch) | |
| 87 | ) { | |
| 88 | return `A security update's branch starts with ${prefix}.`; | |
| 89 | } | |
| 90 | return null; | |
| 91 | } | |
| 92 | ||
| 93 | /** The sandbox for one update: the same branch is the same sandbox. */ | |
| 94 | export function bumpSandboxName(args: BumpArgs): string { | |
| 95 | return `bump:${args.repo.namespace}/${args.repo.name}:${args.branch}`.toLowerCase(); | |
| 96 | } | |
| 97 | ||
| 98 | /** The repository's clone URL, as every sandbox is given it. */ | |
| 99 | export function remoteOf(repo: RepoPath): string { | |
| 100 | return `https://g1t.sh/${repo.namespace}/${repo.name}.git`; | |
| 101 | } | |
| 102 | ||
| 103 | /** | |
| 104 | * The sandbox's variables: what `bump` mode reads. `token` is a run | |
| 105 | * credential that reads the repository and pushes only `args.branch`. | |
| 106 | */ | |
| 107 | export function bumpEnv(args: BumpArgs, baseBranch: string, token: string): Record<string, string> { | |
| 108 | const pkg = args.package.trim(); | |
| 109 | const version = args.version.trim(); | |
| 110 | return { | |
| 111 | MODE: "bump", | |
| 112 | // The credential acts for the workspace; git sends any name with it. | |
| 113 | G1T_USER: args.repo.namespace.toLowerCase(), | |
| 114 | G1T_TOKEN: token, | |
| 115 | GIT_REMOTE: remoteOf(args.repo), | |
| 116 | GIT_BRANCH_BASE: baseBranch, | |
| 117 | GIT_BRANCH: args.branch, | |
| 118 | BUMP_ECOSYSTEM: args.ecosystem, | |
| 119 | BUMP_PACKAGE: pkg, | |
| 120 | BUMP_VERSION: version, | |
| 121 | BUMP_LOCKFILES: JSON.stringify(args.lockfiles), | |
| 122 | COMMIT_MESSAGE: isText(args.message) ? args.message : `Update ${pkg} to ${version}`, | |
| 123 | }; | |
| 124 | } |