package-source.test.ts

8.5 KB · raw

1import { execFileSync } from "node:child_process";2import { createHash } from "node:crypto";3import { existsSync, readFileSync } from "node:fs";4import { fileURLToPath } from "node:url";5import { dirname, join, resolve } from "node:path";6import { describe, expect, it } from "vitest";78import {9  buildSourceManifest,10  collectSourceFiles,11  packageSourceArchive,12  REVIEWED_THIRD_PARTY_FILE,13  type SourceManifest,14} from "./package-source";1516const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");1718const REQUIRED_FILES = [19  "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",20  "fixtures/golden/peach-payload-v4.json",21  "fixtures/golden/sync-entry-envelope-v1.json",22  "crypto-core/Cargo.toml",23  "crypto-core/Cargo.lock",24  "crypto-core/src/lib.rs",25  "crypto-core/src/jni_bridge.rs",26  "crypto-core/.cargo/config.toml",27  "scripts/build-noise-core.mjs",28  "scripts/check-noise-core.mjs",29  "manifest.json",30  "manifest.firefox.json",31  "package.json",32  "bun.lock",33  "tsconfig.json",34  "vite.config.ts",35];3637const EXCLUDE_PATTERNS = [38  /node_modules\//,39  /dist-[^/]+\//,40  /\.zip$/,41  /crypto-core\/target\//,42  /crypto-core\/generated\//,43  /\.DS_Store$/,44  /__MACOSX\//,45];4647function listArchive(archivePath: string): string[] {48  return execFileSync("unzip", ["-Z1", archivePath], { encoding: "utf8" })49    .split("\n")50    .filter(Boolean);51}5253describe("PB-26-212: source package — required files", () => {54  it("collects all security-critical files including crypto-core", () => {55    const files = collectSourceFiles();56    for (const required of REQUIRED_FILES) {57      expect(files).toContain(required);58    }59  });6061  it("includes crypto-core/Cargo.toml, Cargo.lock, and src/lib.rs (the core regression)", () => {62    const files = collectSourceFiles();63    expect(files).toContain("crypto-core/Cargo.toml");64    expect(files).toContain("crypto-core/Cargo.lock");65    expect(files).toContain("crypto-core/src/lib.rs");66  });6768  it("excludes forbidden directories and files", () => {69    const files = collectSourceFiles();70    for (const file of files) {71      if (file === REVIEWED_THIRD_PARTY_FILE) continue;72      for (const pattern of EXCLUDE_PATTERNS) {73        expect(pattern.test(file)).toBe(false);74      }75    }76  });7778  it("collects files sorted and without duplicates", () => {79    const files = collectSourceFiles();80    const sorted = [...files].sort();81    expect(files).toEqual(sorted);82    expect(new Set(files).size).toBe(files.length);83  });84});8586describe("PB-26-212: SOURCE-MANIFEST.json", () => {87  it("has correct structure with version, noise WASM SHA-256, toolchain, and file hashes", () => {88    const files = collectSourceFiles();89    const manifest = buildSourceManifest(files, "0.0.0-test");9091    expect(manifest.peachVersion).toBe("0.0.0-test");92    expect(manifest.noiseWasmSha256).toMatch(/^[0-9a-f]{64}$/);93    expect(manifest.toolchain).toBeDefined();94    expect(manifest.toolchain.bun).toBeDefined();95    expect(manifest.toolchain.node).toBeDefined();96    expect(manifest.toolchain.rust).toBeDefined();9798    expect(manifest.files).toHaveLength(files.length);99    for (const entry of manifest.files) {100      expect(entry.path).toBeDefined();101      expect(entry.sha256).toMatch(/^[0-9a-f]{64}$/);102    }103  });104105  it("noiseWasmSha256 matches the actual shipped WASM", () => {106    const wasmPath = join(107      ROOT,108      "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",109    );110    const expected = createHash("sha256")111      .update(readFileSync(wasmPath))112      .digest("hex");113    const files = collectSourceFiles();114    const manifest = buildSourceManifest(files, "0.0.0-test");115    expect(manifest.noiseWasmSha256).toBe(expected);116  });117118  it("file hashes match actual file contents", () => {119    const files = collectSourceFiles();120    const manifest = buildSourceManifest(files, "0.0.0-test");121    for (const entry of manifest.files) {122      const actual = createHash("sha256")123        .update(readFileSync(join(ROOT, entry.path)))124        .digest("hex");125      expect(entry.sha256).toBe(actual);126    }127  });128});129130describe("PB-26-212: source archive contents", () => {131  it("creates an archive containing every required file exactly once", () => {132    const archivePath = packageSourceArchive("0.0.0-test");133    expect(existsSync(archivePath)).toBe(true);134135    const contents = listArchive(archivePath);136    for (const required of REQUIRED_FILES) {137      const matches = contents.filter((c) => c === required);138      expect(matches).toHaveLength(1);139    }140141    expect(contents).toContain("SOURCE-MANIFEST.json");142  });143144  it("excludes forbidden paths from the archive", () => {145    const archivePath = packageSourceArchive("0.0.0-test");146    const contents = listArchive(archivePath);147    // PB-26-913: the reviewed pdf.js worker is the single deliberate148    // third-party file, allowed through the node_modules exclusion.149    for (const file of contents) {150      if (file === REVIEWED_THIRD_PARTY_FILE) continue;151      for (const pattern of EXCLUDE_PATTERNS) {152        expect(pattern.test(file)).toBe(false);153      }154    }155  });156157  it("archive SOURCE-MANIFEST.json contains the noise WASM SHA-256 and version", () => {158    const archivePath = packageSourceArchive("0.0.0-test");159    const manifestJson = execFileSync(160      "unzip",161      ["-p", archivePath, "SOURCE-MANIFEST.json"],162      { encoding: "utf8" },163    );164    const manifest: SourceManifest = JSON.parse(manifestJson);165166    expect(manifest.peachVersion).toBe("0.0.0-test");167    expect(manifest.noiseWasmSha256).toMatch(/^[0-9a-f]{64}$/);168    expect(Array.isArray(manifest.files)).toBe(true);169    expect(manifest.files.length).toBeGreaterThan(0);170  });171172  it("archive contains the same crypto-core source as the repo (rebuild provenance)", () => {173    const archivePath = packageSourceArchive("0.0.0-test");174175    const archiveLibRs = execFileSync(176      "unzip",177      ["-p", archivePath, "crypto-core/src/lib.rs"],178      { encoding: "utf8" },179    );180    const repoLibRs = readFileSync(181      join(ROOT, "crypto-core/src/lib.rs"),182      "utf8",183    );184    expect(archiveLibRs).toBe(repoLibRs);185186    const archiveBuildScript = execFileSync(187      "unzip",188      ["-p", archivePath, "scripts/build-noise-core.mjs"],189      { encoding: "utf8" },190    );191    const repoBuildScript = readFileSync(192      join(ROOT, "scripts/build-noise-core.mjs"),193      "utf8",194    );195    expect(archiveBuildScript).toBe(repoBuildScript);196197    const manifestJson = execFileSync(198      "unzip",199      ["-p", archivePath, "SOURCE-MANIFEST.json"],200      { encoding: "utf8" },201    );202    const manifest: SourceManifest = JSON.parse(manifestJson);203    const wasmPath = join(204      ROOT,205      "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",206    );207    const expectedWasmHash = createHash("sha256")208      .update(readFileSync(wasmPath))209      .digest("hex");210    expect(manifest.noiseWasmSha256).toBe(expectedWasmHash);211  });212});213214describe("PB-26-212: reproducibility", () => {215  it("produces byte-identical archives from the same baseline", () => {216    const archive1 = packageSourceArchive("0.0.0-repro");217    const hash1 = createHash("sha256")218      .update(readFileSync(archive1))219      .digest("hex");220221    const archive2 = packageSourceArchive("0.0.0-repro");222    const hash2 = createHash("sha256")223      .update(readFileSync(archive2))224      .digest("hex");225226    expect(hash1).toBe(hash2);227  });228});229230describe("PB-26-212: all entry points use the shared packager", () => {231  it("package.json package:source calls scripts/package-source.ts", () => {232    const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));233    expect(pkg.scripts["package:source"]).toContain("package-source.ts");234  });235236  it("package.json package:source does not contain a duplicate file list", () => {237    const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));238    const script = pkg.scripts["package:source"];239    expect(script).not.toContain("manifest.json manifest.firefox.json");240    expect(script).not.toMatch(/^zip\s/);241  });242243  it("release.ts imports and calls packageSourceArchive", () => {244    const source = readFileSync(join(ROOT, "scripts/release.ts"), "utf8");245    expect(source).toContain("packageSourceArchive");246  });247248  it("release-firefox.ts imports and calls packageSourceArchive", () => {249    const source = readFileSync(250      join(ROOT, "scripts/release-firefox.ts"),251      "utf8",252    );253    expect(source).toContain("packageSourceArchive");254  });255256  it("release-lib.ts does not contain a duplicate zip file list", () => {257    const source = readFileSync(join(ROOT, "scripts/release-lib.ts"), "utf8");258    expect(source).not.toContain("packageFirefoxSourceZip");259  });260});