95 lines
3.6 KiB
JavaScript
95 lines
3.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
|
|
function fail(message) {
|
|
console.error(message);
|
|
process.exit(1);
|
|
}
|
|
|
|
function walk(directory, prefix = "") {
|
|
const files = [];
|
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
if (!prefix && entry.name === ".git") continue;
|
|
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
const absolutePath = path.join(directory, entry.name);
|
|
if (entry.isSymbolicLink()) fail(`Symbolic link present in public export: ${relativePath}`);
|
|
if (entry.isDirectory()) files.push(...walk(absolutePath, relativePath));
|
|
else if (entry.isFile()) files.push(relativePath);
|
|
else fail(`Non-regular entry present in public export: ${relativePath}`);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
const required = [
|
|
"README.md", "SECURITY.md", "CONTRIBUTING.md", "LICENSE", "VERSION",
|
|
"PUBLIC_SOURCE_EXPORT.md", "PUBLIC_SOURCE_MANIFEST.json", "docker-compose.yml",
|
|
"backend/pyproject.toml", "frontend/package.json", "node-agent/pyproject.toml",
|
|
"runtime-worker/pyproject.toml"
|
|
];
|
|
const missing = required.filter((entry) => !fs.existsSync(path.join(process.cwd(), entry)));
|
|
if (missing.length > 0) {
|
|
fail(`Missing public source files: ${missing.join(", ")}`);
|
|
}
|
|
|
|
for (const privatePath of [".claude", ".agents", ".codex", "artifacts", "docs/quality"]) {
|
|
if (fs.existsSync(path.join(process.cwd(), privatePath))) {
|
|
fail(`Private-only path present in public export: ${privatePath}`);
|
|
}
|
|
}
|
|
|
|
const manifest = JSON.parse(fs.readFileSync("PUBLIC_SOURCE_MANIFEST.json", "utf8"));
|
|
if (
|
|
manifest.schemaVersion !== 1 ||
|
|
typeof manifest.sourceRevision !== "string" ||
|
|
!/^[0-9a-f]{40}$/u.test(manifest.sourceRevision) ||
|
|
!Array.isArray(manifest.files) ||
|
|
manifest.files.length === 0
|
|
) {
|
|
fail("PUBLIC_SOURCE_MANIFEST.json has an invalid schema or source revision.");
|
|
}
|
|
|
|
const declared = new Set();
|
|
for (const entry of manifest.files) {
|
|
if (
|
|
!entry ||
|
|
typeof entry.path !== "string" ||
|
|
entry.path !== path.posix.normalize(entry.path) ||
|
|
path.posix.isAbsolute(entry.path) ||
|
|
entry.path.startsWith("../") ||
|
|
entry.path.includes("\\") ||
|
|
!Number.isSafeInteger(entry.bytes) ||
|
|
entry.bytes < 0 ||
|
|
typeof entry.sha256 !== "string" ||
|
|
!/^[0-9a-f]{64}$/u.test(entry.sha256)
|
|
) {
|
|
fail("PUBLIC_SOURCE_MANIFEST.json contains an invalid file entry.");
|
|
}
|
|
const collisionKey = entry.path.toLocaleLowerCase("en-US");
|
|
if (declared.has(collisionKey)) fail(`Duplicate manifest path: ${entry.path}`);
|
|
declared.add(collisionKey);
|
|
|
|
const absolutePath = path.join(process.cwd(), ...entry.path.split("/"));
|
|
if (!fs.existsSync(absolutePath)) fail(`Manifest file is missing: ${entry.path}`);
|
|
const stat = fs.lstatSync(absolutePath);
|
|
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
fail(`Manifest path is not a regular file: ${entry.path}`);
|
|
}
|
|
const bytes = fs.readFileSync(absolutePath);
|
|
const digest = crypto.createHash("sha256").update(bytes).digest("hex");
|
|
if (bytes.length !== entry.bytes) fail(`Manifest size mismatch: ${entry.path}`);
|
|
if (digest !== entry.sha256) fail(`Manifest hash mismatch: ${entry.path}`);
|
|
}
|
|
|
|
const generated = new Set(["public_source_export.md", "public_source_manifest.json"]);
|
|
const unexpected = walk(process.cwd()).filter(
|
|
(entry) => !declared.has(entry.toLocaleLowerCase("en-US")) &&
|
|
!generated.has(entry.toLocaleLowerCase("en-US"))
|
|
);
|
|
if (unexpected.length > 0) fail(`Unexpected public source files: ${unexpected.join(", ")}`);
|
|
|
|
console.log(`Public source integrity passed (${manifest.files.length} allowlisted files).`);
|