feat: isolate privacy scanning and release signing

This commit is contained in:
2026-09-02 10:07:01 +03:00
parent 847e6e4da9
commit 8592581b18
15 changed files with 2128 additions and 1 deletions

View File

@@ -0,0 +1,136 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { lstat, readFile, readdir, realpath } from "node:fs/promises";
import { relative, resolve, sep } from "node:path";
import { pathToFileURL } from "node:url";
import {
canonicalJsonDigest,
canonicalValueBytes,
} from "../../src/security/canonical-digest.ts";
const validClass =
/^(?:git-object|git-ref|lfs|source|build|precompressed|release-archive)$/u;
const byteCompare = (left, right) =>
Buffer.from(left).compare(Buffer.from(right));
function normalizedRelativePath(root, path) {
const value = relative(root, path).split(sep).join("/");
if (
value === "" ||
value.startsWith("../") ||
value.startsWith("/") ||
value.includes("/../") ||
value !== value.normalize("NFC")
) {
throw new Error(
"Closure member path is outside the root or non-canonical.",
);
}
return value;
}
async function walk(root, current, members, memberClass) {
const entries = await readdir(current, { withFileTypes: true });
entries.sort((a, b) => byteCompare(a.name, b.name));
for (const entry of entries) {
if (
entry.name !== entry.name.normalize("NFC") ||
entry.name === "." ||
entry.name === ".."
)
throw new Error("Closure path is non-canonical.");
const path = resolve(current, entry.name);
const info = await lstat(path);
if (info.isSymbolicLink())
throw new Error("Closure cannot contain a symlink.");
if (info.isDirectory()) {
await walk(root, path, members, memberClass);
continue;
}
if (!info.isFile())
throw new Error(
"Closure cannot contain device, socket, FIFO, or other special entries.",
);
if (info.nlink !== 1)
throw new Error("Closure cannot contain hard-link aliases.");
const bytes = await readFile(path);
members.push({
class: memberClass,
path: normalizedRelativePath(root, path),
bytes: bytes.byteLength,
sha256: createHash("sha256").update(bytes).digest("hex"),
});
}
}
export async function enumerateClosure(inputs) {
if (!Array.isArray(inputs) || inputs.length === 0)
throw new Error("At least one explicit closure root is required.");
const members = [];
const roots = new Set();
for (const input of inputs) {
if (
input === null ||
typeof input !== "object" ||
!validClass.test(input.class) ||
typeof input.root !== "string"
) {
throw new Error("Closure root descriptor is invalid.");
}
const root = await realpath(input.root);
if (roots.has(`${input.class}\0${root}`))
throw new Error("Duplicate closure root.");
roots.add(`${input.class}\0${root}`);
const info = await lstat(root);
if (!info.isDirectory() || info.isSymbolicLink())
throw new Error("Closure root must be a real directory.");
await walk(root, root, members, input.class);
}
members.sort((a, b) =>
byteCompare(`${a.class}\0${a.path}`, `${b.class}\0${b.path}`),
);
const identities = new Set();
const folded = new Set();
for (const member of members) {
const identity = `${member.class}\0${member.path}`;
const fold = identity.toLocaleLowerCase("en-US");
if (identities.has(identity) || folded.has(fold))
throw new Error("Closure contains a duplicate or case-fold collision.");
identities.add(identity);
folded.add(fold);
}
const value = { schema: "portfolio-publication-closure/v1", members };
return {
...value,
digest: canonicalJsonDigest("portfolio-publication-closure/v1", value),
};
}
async function main() {
const descriptors = process.argv.slice(2).map((argument) => {
const separator = argument.indexOf("=");
if (separator < 1)
throw new Error("Usage: enumerate-closure.mjs <class>=<directory> [...]");
return {
class: argument.slice(0, separator),
root: argument.slice(separator + 1),
};
});
process.stdout.write(
Buffer.from(canonicalValueBytes(await enumerateClosure(descriptors))),
);
process.stdout.write("\n");
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
main().catch((error) => {
process.stderr.write(
`Closure enumeration failed: ${error instanceof Error ? error.message : "unknown error"}\n`,
);
process.exitCode = 1;
});
}