import { createHash } from "node:crypto"; import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs"; import { join, relative } from "node:path"; import { VERSION_ID } from "./aidlc-channel.ts"; export type ProjectionStamp = { schemaVersion: 1; frameworkVersion: string; distribution: string; harnessDir: string; }; export type RootIntegration = { path: string; policy: "managed-block" | "json-map" | "json-array" | "whole-file"; marker?: string; jsonKey?: string; optional?: boolean; legacySignatures?: { wholeFileHashes?: string[]; jsonEntryHashes?: Record; }; }; export type ProjectionDescriptor = { schemaVersion: 1; distribution: string; productName: string; configNextStep: string; harnessDir: string; managedDirectories: string[]; legacyManagedFileHashes?: Record; rootIntegrations: RootIntegration[]; }; function parseJson(path: string): T { try { return JSON.parse(readFileSync(path, "utf-8")) as T; } catch (error) { throw new Error(`${path}: invalid JSON (${error instanceof Error ? error.message : String(error)})`); } } function safeRelativePath(value: unknown, label: string, topLevel = false): string { if ( typeof value !== "string" || value.length === 0 || value.includes("\0") || value.includes("\\") || value.startsWith("/") || /^[A-Za-z]:/.test(value) || value.split("/").some((segment) => segment === "" || segment === "." || segment === "..") || (topLevel && value.includes("/")) ) { throw new Error(`${label} is not a safe ${topLevel ? "top-level name" : "relative path"}`); } return value; } function validateHashes(value: unknown, label: string): string[] { if ( !Array.isArray(value) || value.length === 0 || value.some((hash) => typeof hash !== "string" || !/^sha256:[a-f0-9]{64}$/.test(hash)) || new Set(value).size !== value.length ) { throw new Error(`${label} must contain unique lowercase SHA-256 signatures`); } return value; } export function assertProjectionPathHasNoSymlinks( root: string, relativePath: string, ): void { let current = root; const segments = relativePath.split("/"); for (const [index, segment] of segments.entries()) { current = join(current, segment); let stat: ReturnType; try { stat = lstatSync(current); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return; throw error; } if (stat.isSymbolicLink()) { throw new Error(`${root}: projected path traverses a symlink: ${relativePath}`); } if (index < segments.length - 1 && !stat.isDirectory()) { throw new Error(`${root}: projected path parent is not a directory: ${relativePath}`); } } } export function validateProjectionDescriptor( root: string, stamp: ProjectionStamp, descriptor: ProjectionDescriptor, options: { allowMissingRootIntegrations?: boolean } = {}, ): void { if (!VERSION_ID.test(stamp.frameworkVersion)) { throw new Error(`${root}: projection stamp has an invalid framework version`); } if ( !/^[a-z0-9][a-z0-9-]*$/.test(stamp.distribution) || typeof descriptor.productName !== "string" || descriptor.productName.trim().length === 0 || typeof descriptor.configNextStep !== "string" || descriptor.configNextStep.trim().length === 0 ) { throw new Error(`${root}: projection identity is invalid`); } safeRelativePath(stamp.harnessDir, "harnessDir", true); if (!Array.isArray(descriptor.managedDirectories) || !Array.isArray(descriptor.rootIntegrations)) { throw new Error(`${root}: projection descriptor lists are invalid`); } const declared = new Set(); const declare = (safe: string): void => { if (declared.has(safe)) throw new Error(`${root}: duplicate projected path ${safe}`); const overlap = [...declared].find((prior) => safe.startsWith(`${prior}/`) || prior.startsWith(`${safe}/`) ); if (overlap) { throw new Error(`${root}: overlapping projected paths ${overlap} and ${safe}`); } declared.add(safe); }; for (const directory of descriptor.managedDirectories) { const safe = safeRelativePath(directory, "managed directory", true); declare(safe); assertProjectionPathHasNoSymlinks(root, safe); const path = join(root, safe); if (!existsSync(path) || !lstatSync(path).isDirectory()) { throw new Error(`${root}: managed directory is missing or invalid: ${safe}`); } } if (descriptor.legacyManagedFileHashes !== undefined) { const signatures = descriptor.legacyManagedFileHashes; if ( !signatures || typeof signatures !== "object" || Array.isArray(signatures) || Object.keys(signatures).length === 0 ) { throw new Error(`${root}: legacy managed-file signatures are invalid`); } for (const [file, hashes] of Object.entries(signatures)) { const safe = safeRelativePath(file, "legacy managed file"); if ( !descriptor.managedDirectories.some((directory) => safe === directory || safe.startsWith(`${directory}/`) ) ) { throw new Error(`${root}: legacy managed file is outside managed directories: ${safe}`); } assertProjectionPathHasNoSymlinks(root, safe); const path = join(root, safe); if (!existsSync(path) || !lstatSync(path).isFile()) { throw new Error(`${root}: legacy managed file is missing or invalid: ${safe}`); } validateHashes(hashes, `${root}: ${safe} legacy managed-file signatures`); } } for (const integration of descriptor.rootIntegrations) { if (!integration || typeof integration !== "object") { throw new Error(`${root}: root integration is invalid`); } const safe = safeRelativePath(integration.path, "root integration path"); declare(safe); assertProjectionPathHasNoSymlinks(root, safe); const path = join(root, safe); if ( !existsSync(path) && (integration.optional || options.allowMissingRootIntegrations) ) { continue; } if (!existsSync(path) || !lstatSync(path).isFile()) { throw new Error(`${root}: root integration is missing or invalid: ${safe}`); } if (!["managed-block", "json-map", "json-array", "whole-file"].includes(integration.policy)) { throw new Error(`${root}: ${safe} has an invalid integration policy`); } if ( integration.policy === "managed-block" && (typeof integration.marker !== "string" || !/^[a-z0-9-]+$/.test(integration.marker)) ) { throw new Error(`${root}: ${safe} has an invalid managed-block marker`); } if ( (integration.policy === "json-map" || integration.policy === "json-array") && (typeof integration.jsonKey !== "string" || integration.jsonKey.length === 0) ) { throw new Error(`${root}: ${safe} has an invalid JSON integration key`); } const legacy = integration.legacySignatures; if (legacy !== undefined) { if (!legacy || typeof legacy !== "object" || Array.isArray(legacy)) { throw new Error(`${root}: ${safe} has invalid legacy signatures`); } const keys = Object.keys(legacy); if ( keys.length === 0 || keys.some((key) => key !== "wholeFileHashes" && key !== "jsonEntryHashes") ) { throw new Error(`${root}: ${safe} has invalid legacy signature fields`); } if (legacy.wholeFileHashes !== undefined) { if (integration.policy !== "managed-block" && integration.policy !== "whole-file") { throw new Error(`${root}: ${safe} cannot use legacy whole-file signatures`); } validateHashes(legacy.wholeFileHashes, `${root}: ${safe} legacy whole-file signatures`); } if (legacy.jsonEntryHashes !== undefined) { if ( integration.policy !== "json-map" || !legacy.jsonEntryHashes || typeof legacy.jsonEntryHashes !== "object" || Array.isArray(legacy.jsonEntryHashes) || Object.keys(legacy.jsonEntryHashes).length === 0 ) { throw new Error(`${root}: ${safe} has invalid legacy JSON-entry signatures`); } for (const [entry, hashes] of Object.entries(legacy.jsonEntryHashes)) { if (entry.length === 0) { throw new Error(`${root}: ${safe} has an empty legacy JSON entry name`); } validateHashes(hashes, `${root}: ${safe} legacy JSON entry ${entry}`); } } } } } export function projectionFiles(root: string): { stamp: ProjectionStamp; descriptor: ProjectionDescriptor; } { const candidates = readdirSync(root) .filter((name) => existsSync(join(root, name, "tools", "data", "aidlc-stamp.json"))) .sort(); if (candidates.length !== 1) { throw new Error( `${root}: expected exactly one projected harness directory, found ${candidates.length}`, ); } const harnessDir = candidates[0]; const data = join(root, harnessDir, "tools", "data"); const stamp = parseJson(join(data, "aidlc-stamp.json")); const descriptor = parseJson(join(data, "aidlc-projection.json")); if ( stamp.schemaVersion !== 1 || descriptor.schemaVersion !== 1 || stamp.harnessDir !== harnessDir || descriptor.harnessDir !== harnessDir || stamp.distribution !== descriptor.distribution ) { throw new Error(`${root}: projection stamp and descriptor do not describe one distribution`); } validateProjectionDescriptor(root, stamp, descriptor); const allowedTopLevel = new Set([ ...descriptor.managedDirectories, ...descriptor.rootIntegrations.map((item) => item.path.split(/[\\/]/)[0]), ]); const unexpected = readdirSync(root).filter((entry) => !allowedTopLevel.has(entry)); if (unexpected.length > 0) { throw new Error(`${root}: unclassified projection entries: ${unexpected.sort().join(", ")}`); } return { stamp, descriptor }; } export function sha256Bytes(value: string | Buffer): string { return `sha256:${createHash("sha256").update(value).digest("hex")}`; } export function sha256File(path: string): string { return sha256Bytes(readFileSync(path)); } export function walkFiles(root: string): string[] { const files: string[] = []; const visit = (dir: string): void => { for (const entry of readdirSync(dir).sort()) { const path = join(dir, entry); const stat = lstatSync(path); if (stat.isDirectory()) visit(path); else if (stat.isFile()) files.push(relative(root, path)); else throw new Error(`${path}: links and special files are not valid projection content`); } }; visit(root); return files; }