import assert from "node:assert/strict"; import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; import test from "node:test"; import ts from "typescript"; const root = path.resolve(import.meta.dirname, "../../.."); const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-link-safety-unit-")); const sourcePath = path.join(root, "web/protocol/archive-link-safety.ts"); const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true, }); assert.deepEqual(transpiled.diagnostics, []); fs.writeFileSync(path.join(temporary, "archive-link-safety.mjs"), transpiled.outputText); const safety = await import(pathToFileURL(path.join(temporary, "archive-link-safety.mjs"))); const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04D/manifest.json"), "utf8")); const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); const valid = { schemaVersion: 1, temporaryRootId: "staging:archive-1", entries: [ { path: "payload/data.bin", type: "FILE", target: null }, { path: "payload/data-alias.bin", type: "SYMLINK", target: "./data.bin" }, { path: "payload/data-hard.bin", type: "HARDLINK", target: "payload/data.bin" }, { path: "payload", type: "DIRECTORY", target: null }, { path: "alias-dir", type: "SYMLINK", target: "payload" }, ], }; test("M12-04D binds the archive link gate and evidence artifacts", () => { assert.equal(manifest.task, "M12-04D"); assert.equal(manifest.parentTask, "M12-04C"); assert.equal(manifest.nextTask, "M12-04E"); for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); }); test("M12-04D resolves symlinks relative to their parent and hardlinks from the archive root", () => { const result = safety.resolveArchiveLinkEntries(valid); assert.equal(result.status, "READY"); assert.equal(result.entries.find((entry) => entry.path === "payload/data-alias.bin").resolvedPath, "payload/data.bin"); assert.equal(result.entries.find((entry) => entry.path === "payload/data-hard.bin").resolvedPath, "payload/data.bin"); assert.equal(result.entries.find((entry) => entry.path === "alias-dir").resolvedPath, "payload"); assert.ok(result.entries.every((entry) => entry.withinTemporaryRoot === true)); }); test("M12-04D rejects absolute, drive, URI and traversal targets before writing", () => { for (const target of ["/outside", "\\\\server\\share", "C:/outside", "https://evil.example/a", "../../outside", "payload/../../outside"]) { assert.throws(() => safety.resolveArchiveLinkEntries({ ...valid, entries: [{ path: "payload/link", type: "SYMLINK", target }, { path: "payload", type: "DIRECTORY", target: null }], }), { code: "IO_ARCHIVE_UNSAFE" }); } }); test("M12-04D rejects missing targets, cycles and hardlinks to directories", () => { assert.throws(() => safety.resolveArchiveLinkEntries({ ...valid, entries: [{ path: "link", type: "SYMLINK", target: "missing.bin" }], }), { code: "IO_ARCHIVE_UNSAFE" }); assert.throws(() => safety.resolveArchiveLinkEntries({ ...valid, entries: [ { path: "a", type: "SYMLINK", target: "b" }, { path: "b", type: "HARDLINK", target: "a" }, ], }), { code: "IO_ARCHIVE_UNSAFE" }); assert.throws(() => safety.resolveArchiveLinkEntries({ ...valid, entries: [ { path: "dir", type: "DIRECTORY", target: null }, { path: "dir-hard", type: "HARDLINK", target: "dir" }, ], }), { code: "IO_ARCHIVE_UNSAFE" }); }); test("M12-04D rejects duplicate members and undeclared fields", () => { assert.throws(() => safety.parseArchiveLinkRequest({ ...valid, entries: [{ ...valid.entries[0], target: null }, { ...valid.entries[0], target: null }] }), { code: "IO_ARCHIVE_UNSAFE" }); assert.throws(() => safety.parseArchiveLinkRequest({ ...valid, future: true }), { code: "IO_ARCHIVE_UNSAFE" }); assert.throws(() => safety.parseArchiveLinkRequest({ ...valid, entries: [{ ...valid.entries[0], mode: 0o644 }] }), { code: "IO_ARCHIVE_UNSAFE" }); }); test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));