import { createHash } from "node:crypto"; import { lstat, readFile, realpath } from "node:fs/promises"; import path from "node:path"; import type { BuildManifestArtifact } from "../../src/contracts/release-artifacts.ts"; import { moduleInventoryArtifactSchema } from "../contracts/release-artifacts.ts"; type VerifyBuildManifestOutputsDependencies = Readonly<{ repositoryRoot?: string; readBytes?: (target: string) => Promise; realpathPath?: (target: string) => Promise; assertRegularFile?: (target: string) => Promise; assertDirectory?: (target: string) => Promise; }>; export const CANONICAL_VITE_MANIFEST_PATH = "dist/.vite/manifest.json"; function isSafeRelativePath(value: string): boolean { return ( value.length > 0 && !path.posix.isAbsolute(value) && !path.win32.isAbsolute(value) && !value.includes("\\") && !value.includes("\0") && path.posix.normalize(value) === value && value !== ".." && !value.startsWith("../") ); } function isWithinRoot(root: string, target: string): boolean { const relative = path.relative(root, target); return ( relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)) ); } function belongsToApprovedRoot(value: string, approvedRoot: string): boolean { return value.startsWith(`${approvedRoot}/`); } async function defaultAssertRegularFile(target: string): Promise { const metadata = await lstat(target); if (!metadata.isFile() || metadata.isSymbolicLink()) { throw new TypeError("not a regular file"); } } async function defaultAssertDirectory(target: string): Promise { const metadata = await lstat(target); if (!metadata.isDirectory() || metadata.isSymbolicLink()) { throw new TypeError("not a directory"); } } export async function verifyBuildManifestOutputs( manifest: BuildManifestArtifact, dependencies: VerifyBuildManifestOutputsDependencies = {}, ): Promise { const repositoryRoot = path.resolve(dependencies.repositoryRoot ?? process.cwd()); const readBytes = dependencies.readBytes ?? readFile; const realpathPath = dependencies.realpathPath ?? realpath; const assertRegularFile = dependencies.assertRegularFile ?? defaultAssertRegularFile; const assertDirectory = dependencies.assertDirectory ?? defaultAssertDirectory; const mismatches: string[] = []; const resolvedRoot = await realpathPath(repositoryRoot); const approvedRoots = new Map>(); function resolveApprovedRoot(relativeRoot: string): Promise { const existing = approvedRoots.get(relativeRoot); if (existing) return existing; const pending = (async () => { const absoluteRoot = path.resolve(repositoryRoot, relativeRoot); try { await assertDirectory(absoluteRoot); const resolvedApprovedRoot = await realpathPath(absoluteRoot); return isWithinRoot(resolvedRoot, resolvedApprovedRoot) ? resolvedApprovedRoot : null; } catch { return null; } })(); approvedRoots.set(relativeRoot, pending); return pending; } async function confinedPath( label: string, relativePath: string, kind: "file" | "directory", approvedRoot?: string, ): Promise { if ( !isSafeRelativePath(relativePath) || (approvedRoot !== undefined && !belongsToApprovedRoot(relativePath, approvedRoot)) ) { mismatches.push(`buildManifest:${label}:path`); return null; } const absolutePath = path.resolve(repositoryRoot, relativePath); if (!isWithinRoot(repositoryRoot, absolutePath)) { mismatches.push(`buildManifest:${label}:path`); return null; } try { if (kind === "file") await assertRegularFile(absolutePath); else await assertDirectory(absolutePath); const resolvedPath = await realpathPath(absolutePath); const resolvedApprovedRoot = approvedRoot ? await resolveApprovedRoot(approvedRoot) : resolvedRoot; if ( resolvedApprovedRoot === null || !isWithinRoot(resolvedRoot, resolvedPath) || !isWithinRoot(resolvedApprovedRoot, resolvedPath) ) { mismatches.push(`buildManifest:${label}:path`); return null; } return absolutePath; } catch { mismatches.push(`buildManifest:${label}:missing`); return null; } } if (manifest.outputs.directory !== "dist") { mismatches.push("buildManifest:directory:path"); } else { await confinedPath("directory", manifest.outputs.directory, "directory"); } if (manifest.outputs.viteManifest !== CANONICAL_VITE_MANIFEST_PATH) { mismatches.push("buildManifest:viteManifest:path"); } else { await confinedPath( "viteManifest", manifest.outputs.viteManifest, "file", "dist", ); } await confinedPath( "runtimeConfigSchema", manifest.outputs.runtimeConfigSchema, "file", "dist", ); for (const [chunkId, chunkPath] of Object.entries(manifest.outputs.routeChunks)) { if (!isSafeRelativePath(chunkPath)) { mismatches.push(`buildManifest:routeChunk:${chunkId}:path`); continue; } await confinedPath( `routeChunk:${chunkId}`, path.posix.join(manifest.outputs.directory, chunkPath), "file", "dist", ); } const moduleInventoryPath = await confinedPath( "moduleInventory", manifest.outputs.moduleInventory, "file", "artifacts/quality", ); if (moduleInventoryPath) { try { const bytes = await readBytes(moduleInventoryPath); const digest = createHash("sha256").update(bytes).digest("hex"); if (digest !== manifest.moduleInventoryHash) { mismatches.push("buildManifest:moduleInventoryHash"); } try { moduleInventoryArtifactSchema.parse(JSON.parse(bytes.toString("utf8"))); } catch { mismatches.push("buildManifest:moduleInventory:invalid"); } } catch { mismatches.push("buildManifest:moduleInventory:missing"); } } return mismatches; }