Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172/** * P7-5: `call NAME` runs TypeScript with the runner's privileges, and an AI may write it. So a check runs only when a person has listed it: * `scripts/checks/allowlist.json` holds the SHA-256 of every approved file. A new file, a changed file, a symbolic link, or no list at all means the check is refused. * (The list is regenerated with `npm run checks:allow` and reviewed as a diff; CI asks for a label when anything under scripts/checks/ changes.) */import { createHash } from "node:crypto";import { lstatSync, readdirSync, readFileSync } from "node:fs";import { join } from "node:path";
export const ALLOWLIST_FILE = "allowlist.json";const NAME = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
export const hashOf = (text: string | Buffer): string => createHash("sha256").update(text).digest("hex");
const kindOf = (path: string): "file" | "link" | "other" | "missing" => { try { const s = lstatSync(path); return s.isSymbolicLink() ? "link" : s.isFile() ? "file" : "other"; } catch { return "missing"; }};
/** The approved checks (name to SHA-256), or the sentence that says why the list cannot be trusted. */export function readAllowlist(checksDir: string): { ok: true; checks: Record<string, string> } | { ok: false; message: string } { const no = (message: string) => ({ ok: false as const, message }); if (kindOf(checksDir) === "link") return no("scripts/checks is a symbolic link: checks only run from a real folder inside the project"); const listPath = join(checksDir, ALLOWLIST_FILE); const listKind = kindOf(listPath); if (listKind === "link") return no(`scripts/checks/${ALLOWLIST_FILE} is a symbolic link`); if (listKind !== "file") return no(`there is no scripts/checks/${ALLOWLIST_FILE}: no check may run until a person lists them (npm run checks:allow, then review the diff)`); let listed: unknown; try { listed = JSON.parse(readFileSync(listPath, "utf8")); } catch { return no(`scripts/checks/${ALLOWLIST_FILE} is not valid JSON, so it is not valid`); } const checks = (listed as { version?: unknown; checks?: unknown } | null)?.checks; if ((listed as { version?: unknown } | null)?.version !== 1 || typeof checks !== "object" || checks === null || Object.values(checks).some((h) => typeof h !== "string")) { return no(`scripts/checks/${ALLOWLIST_FILE} is not valid (expected {"version":1,"checks":{"name":"sha256"}})`); } return { ok: true, checks: checks as Record<string, string> };}
/** undefined when `call name` may run; otherwise the sentence that says why not. */export function checkAllowed(checksDir: string, name: string): string | undefined { if (!NAME.test(name)) return `${JSON.stringify(name).slice(0, 80)} is not a check name`; const list = readAllowlist(checksDir); if (!list.ok) return list.message; const checks = list.checks; const want = (checks as Record<string, string>)[name]; if (want === undefined) return `check ${name} is not on the allowlist (scripts/checks/${ALLOWLIST_FILE}): a person must review it and list it first`; const file = join(checksDir, `${name}.ts`); const kind = kindOf(file); if (kind === "link") return `scripts/checks/${name}.ts is a symbolic link`; if (kind !== "file") return `scripts/checks/${name}.ts does not exist or is not a regular file`; if (hashOf(readFileSync(file)) !== want) return `scripts/checks/${name}.ts changed since it was listed: a person must review it and list it again`; return undefined;}
/** The text of a fresh allowlist for every plain .ts file in the folder (symbolic links and other things are left out, so they stay refused). */export function buildAllowlist(checksDir: string): string { const checks: Record<string, string> = {}; for (const entry of readdirSync(checksDir).sort()) { if (!entry.endsWith(".ts")) continue; const name = entry.slice(0, -3); if (!NAME.test(name) || kindOf(join(checksDir, entry)) !== "file") continue; checks[name] = hashOf(readFileSync(join(checksDir, entry))); } return `${JSON.stringify({ version: 1, checks }, null, 2)}\n`;}