36 lines
1.7 KiB
TypeScript
36 lines
1.7 KiB
TypeScript
import { lstatSync, realpathSync } from "node:fs";
|
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
|
export interface SecretInputPlan {
|
|
command: "tea";
|
|
args: string[];
|
|
stdinFile?: string;
|
|
}
|
|
|
|
// Plan only: never reads secret bytes, spawns tea, or provisions a secret.
|
|
// This is an inspectable contract, not upload authorization or a race-free open.
|
|
// An operator must recheck metadata and approve the actual upload separately.
|
|
export function planSecretInput(login: string, file: string, mode: "file" | "stdin", repositoryRoot: string): SecretInputPlan {
|
|
try {
|
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(login) || !isAbsolute(file)
|
|
|| (mode !== "file" && mode !== "stdin")) throw new Error();
|
|
const canonical = realpathSync(file);
|
|
const local = relative(realpathSync(repositoryRoot), canonical);
|
|
if (canonical !== resolve(file) || !(local === ".." || local.startsWith(`..${sep}`) || isAbsolute(local))) throw new Error();
|
|
const metadata = lstatSync(file);
|
|
const directory = lstatSync(dirname(file));
|
|
const owner = process.getuid?.();
|
|
if (owner === undefined || !metadata.isFile() || metadata.isSymbolicLink() || metadata.size === 0
|
|
|| metadata.uid !== owner || (metadata.mode & 0o777) !== 0o600
|
|
|| !directory.isDirectory() || directory.uid !== owner || (directory.mode & 0o777) !== 0o700) throw new Error();
|
|
} catch {
|
|
throw new Error("Invalid protected secret input; no provisioning was performed.");
|
|
}
|
|
return {
|
|
command: "tea",
|
|
args: ["actions", "secrets", "create", "--login", login, "--repo", "dmg/purpur-sentinel",
|
|
...(mode === "file" ? ["--file", file] : ["--stdin"]), "RELEASE_TOKEN"],
|
|
...(mode === "stdin" ? { stdinFile: file } : {}),
|
|
};
|
|
}
|