package-source.test.ts

8.2 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  type SourceManifest,13} from "./package-source";1415const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");1617const REQUIRED_FILES = [18  "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",19  "crypto-core/Cargo.toml",20  "crypto-core/Cargo.lock",21  "crypto-core/src/lib.rs",22  "crypto-core/src/jni_bridge.rs",23  "crypto-core/.cargo/config.toml",24  "scripts/build-noise-core.mjs",25  "scripts/check-noise-core.mjs",26  "manifest.json",27  "manifest.firefox.json",28  "package.json",29  "bun.lock",30  "tsconfig.json",31  "vite.config.ts",32];3334const EXCLUDE_PATTERNS = [35  /node_modules\//,36  /dist-[^/]+\//,37  /\.zip$/,38  /crypto-core\/target\//,39  /crypto-core\/generated\//,40  /\.DS_Store$/,41  /__MACOSX\//,42];4344function listArchive(archivePath: string): string[] {45  return execFileSync("unzip", ["-Z1", archivePath], { encoding: "utf8" })46    .split("\n")47    .filter(Boolean);48}4950describe("PB-26-212: source package — required files", () => {51  it("collects all security-critical files including crypto-core", () => {52    const files = collectSourceFiles();53    for (const required of REQUIRED_FILES) {54      expect(files).toContain(required);55    }56  });5758  it("includes crypto-core/Cargo.toml, Cargo.lock, and src/lib.rs (the core regression)", () => {59    const files = collectSourceFiles();60    expect(files).toContain("crypto-core/Cargo.toml");61    expect(files).toContain("crypto-core/Cargo.lock");62    expect(files).toContain("crypto-core/src/lib.rs");63  });6465  it("excludes forbidden directories and files", () => {66    const files = collectSourceFiles();67    for (const file of files) {68      for (const pattern of EXCLUDE_PATTERNS) {69        expect(pattern.test(file)).toBe(false);70      }71    }72  });7374  it("collects files sorted and without duplicates", () => {75    const files = collectSourceFiles();76    const sorted = [...files].sort();77    expect(files).toEqual(sorted);78    expect(new Set(files).size).toBe(files.length);79  });80});8182describe("PB-26-212: SOURCE-MANIFEST.json", () => {83  it("has correct structure with version, noise WASM SHA-256, toolchain, and file hashes", () => {84    const files = collectSourceFiles();85    const manifest = buildSourceManifest(files, "0.0.0-test");8687    expect(manifest.peachVersion).toBe("0.0.0-test");88    expect(manifest.noiseWasmSha256).toMatch(/^[0-9a-f]{64}$/);89    expect(manifest.toolchain).toBeDefined();90    expect(manifest.toolchain.bun).toBeDefined();91    expect(manifest.toolchain.node).toBeDefined();92    expect(manifest.toolchain.rust).toBeDefined();9394    expect(manifest.files).toHaveLength(files.length);95    for (const entry of manifest.files) {96      expect(entry.path).toBeDefined();97      expect(entry.sha256).toMatch(/^[0-9a-f]{64}$/);98    }99  });100101  it("noiseWasmSha256 matches the actual shipped WASM", () => {102    const wasmPath = join(103      ROOT,104      "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",105    );106    const expected = createHash("sha256")107      .update(readFileSync(wasmPath))108      .digest("hex");109    const files = collectSourceFiles();110    const manifest = buildSourceManifest(files, "0.0.0-test");111    expect(manifest.noiseWasmSha256).toBe(expected);112  });113114  it("file hashes match actual file contents", () => {115    const files = collectSourceFiles();116    const manifest = buildSourceManifest(files, "0.0.0-test");117    for (const entry of manifest.files) {118      const actual = createHash("sha256")119        .update(readFileSync(join(ROOT, entry.path)))120        .digest("hex");121      expect(entry.sha256).toBe(actual);122    }123  });124});125126describe("PB-26-212: source archive contents", () => {127  it("creates an archive containing every required file exactly once", () => {128    const archivePath = packageSourceArchive("0.0.0-test");129    expect(existsSync(archivePath)).toBe(true);130131    const contents = listArchive(archivePath);132    for (const required of REQUIRED_FILES) {133      const matches = contents.filter((c) => c === required);134      expect(matches).toHaveLength(1);135    }136137    expect(contents).toContain("SOURCE-MANIFEST.json");138  });139140  it("excludes forbidden paths from the archive", () => {141    const archivePath = packageSourceArchive("0.0.0-test");142    const contents = listArchive(archivePath);143    for (const file of contents) {144      for (const pattern of EXCLUDE_PATTERNS) {145        expect(pattern.test(file)).toBe(false);146      }147    }148  });149150  it("archive SOURCE-MANIFEST.json contains the noise WASM SHA-256 and version", () => {151    const archivePath = packageSourceArchive("0.0.0-test");152    const manifestJson = execFileSync(153      "unzip",154      ["-p", archivePath, "SOURCE-MANIFEST.json"],155      { encoding: "utf8" },156    );157    const manifest: SourceManifest = JSON.parse(manifestJson);158159    expect(manifest.peachVersion).toBe("0.0.0-test");160    expect(manifest.noiseWasmSha256).toMatch(/^[0-9a-f]{64}$/);161    expect(Array.isArray(manifest.files)).toBe(true);162    expect(manifest.files.length).toBeGreaterThan(0);163  });164165  it("archive contains the same crypto-core source as the repo (rebuild provenance)", () => {166    const archivePath = packageSourceArchive("0.0.0-test");167168    const archiveLibRs = execFileSync(169      "unzip",170      ["-p", archivePath, "crypto-core/src/lib.rs"],171      { encoding: "utf8" },172    );173    const repoLibRs = readFileSync(174      join(ROOT, "crypto-core/src/lib.rs"),175      "utf8",176    );177    expect(archiveLibRs).toBe(repoLibRs);178179    const archiveBuildScript = execFileSync(180      "unzip",181      ["-p", archivePath, "scripts/build-noise-core.mjs"],182      { encoding: "utf8" },183    );184    const repoBuildScript = readFileSync(185      join(ROOT, "scripts/build-noise-core.mjs"),186      "utf8",187    );188    expect(archiveBuildScript).toBe(repoBuildScript);189190    const manifestJson = execFileSync(191      "unzip",192      ["-p", archivePath, "SOURCE-MANIFEST.json"],193      { encoding: "utf8" },194    );195    const manifest: SourceManifest = JSON.parse(manifestJson);196    const wasmPath = join(197      ROOT,198      "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",199    );200    const expectedWasmHash = createHash("sha256")201      .update(readFileSync(wasmPath))202      .digest("hex");203    expect(manifest.noiseWasmSha256).toBe(expectedWasmHash);204  });205});206207describe("PB-26-212: reproducibility", () => {208  it("produces byte-identical archives from the same baseline", () => {209    const archive1 = packageSourceArchive("0.0.0-repro");210    const hash1 = createHash("sha256")211      .update(readFileSync(archive1))212      .digest("hex");213214    const archive2 = packageSourceArchive("0.0.0-repro");215    const hash2 = createHash("sha256")216      .update(readFileSync(archive2))217      .digest("hex");218219    expect(hash1).toBe(hash2);220  });221});222223describe("PB-26-212: all entry points use the shared packager", () => {224  it("package.json package:source calls scripts/package-source.ts", () => {225    const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));226    expect(pkg.scripts["package:source"]).toContain("package-source.ts");227  });228229  it("package.json package:source does not contain a duplicate file list", () => {230    const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));231    const script = pkg.scripts["package:source"];232    expect(script).not.toContain("manifest.json manifest.firefox.json");233    expect(script).not.toMatch(/^zip\s/);234  });235236  it("release.ts imports and calls packageSourceArchive", () => {237    const source = readFileSync(join(ROOT, "scripts/release.ts"), "utf8");238    expect(source).toContain("packageSourceArchive");239  });240241  it("release-firefox.ts imports and calls packageSourceArchive", () => {242    const source = readFileSync(243      join(ROOT, "scripts/release-firefox.ts"),244      "utf8",245    );246    expect(source).toContain("packageSourceArchive");247  });248249  it("release-lib.ts does not contain a duplicate zip file list", () => {250    const source = readFileSync(join(ROOT, "scripts/release-lib.ts"), "utf8");251    expect(source).not.toContain("packageFirefoxSourceZip");252  });253});