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  "fixtures/golden/peach-payload-v4.json",20  "crypto-core/Cargo.toml",21  "crypto-core/Cargo.lock",22  "crypto-core/src/lib.rs",23  "crypto-core/src/jni_bridge.rs",24  "crypto-core/.cargo/config.toml",25  "scripts/build-noise-core.mjs",26  "scripts/check-noise-core.mjs",27  "manifest.json",28  "manifest.firefox.json",29  "package.json",30  "bun.lock",31  "tsconfig.json",32  "vite.config.ts",33];3435const EXCLUDE_PATTERNS = [36  /node_modules\//,37  /dist-[^/]+\//,38  /\.zip$/,39  /crypto-core\/target\//,40  /crypto-core\/generated\//,41  /\.DS_Store$/,42  /__MACOSX\//,43];4445function listArchive(archivePath: string): string[] {46  return execFileSync("unzip", ["-Z1", archivePath], { encoding: "utf8" })47    .split("\n")48    .filter(Boolean);49}5051describe("PB-26-212: source package — required files", () => {52  it("collects all security-critical files including crypto-core", () => {53    const files = collectSourceFiles();54    for (const required of REQUIRED_FILES) {55      expect(files).toContain(required);56    }57  });5859  it("includes crypto-core/Cargo.toml, Cargo.lock, and src/lib.rs (the core regression)", () => {60    const files = collectSourceFiles();61    expect(files).toContain("crypto-core/Cargo.toml");62    expect(files).toContain("crypto-core/Cargo.lock");63    expect(files).toContain("crypto-core/src/lib.rs");64  });6566  it("excludes forbidden directories and files", () => {67    const files = collectSourceFiles();68    for (const file of files) {69      for (const pattern of EXCLUDE_PATTERNS) {70        expect(pattern.test(file)).toBe(false);71      }72    }73  });7475  it("collects files sorted and without duplicates", () => {76    const files = collectSourceFiles();77    const sorted = [...files].sort();78    expect(files).toEqual(sorted);79    expect(new Set(files).size).toBe(files.length);80  });81});8283describe("PB-26-212: SOURCE-MANIFEST.json", () => {84  it("has correct structure with version, noise WASM SHA-256, toolchain, and file hashes", () => {85    const files = collectSourceFiles();86    const manifest = buildSourceManifest(files, "0.0.0-test");8788    expect(manifest.peachVersion).toBe("0.0.0-test");89    expect(manifest.noiseWasmSha256).toMatch(/^[0-9a-f]{64}$/);90    expect(manifest.toolchain).toBeDefined();91    expect(manifest.toolchain.bun).toBeDefined();92    expect(manifest.toolchain.node).toBeDefined();93    expect(manifest.toolchain.rust).toBeDefined();9495    expect(manifest.files).toHaveLength(files.length);96    for (const entry of manifest.files) {97      expect(entry.path).toBeDefined();98      expect(entry.sha256).toMatch(/^[0-9a-f]{64}$/);99    }100  });101102  it("noiseWasmSha256 matches the actual shipped WASM", () => {103    const wasmPath = join(104      ROOT,105      "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",106    );107    const expected = createHash("sha256")108      .update(readFileSync(wasmPath))109      .digest("hex");110    const files = collectSourceFiles();111    const manifest = buildSourceManifest(files, "0.0.0-test");112    expect(manifest.noiseWasmSha256).toBe(expected);113  });114115  it("file hashes match actual file contents", () => {116    const files = collectSourceFiles();117    const manifest = buildSourceManifest(files, "0.0.0-test");118    for (const entry of manifest.files) {119      const actual = createHash("sha256")120        .update(readFileSync(join(ROOT, entry.path)))121        .digest("hex");122      expect(entry.sha256).toBe(actual);123    }124  });125});126127describe("PB-26-212: source archive contents", () => {128  it("creates an archive containing every required file exactly once", () => {129    const archivePath = packageSourceArchive("0.0.0-test");130    expect(existsSync(archivePath)).toBe(true);131132    const contents = listArchive(archivePath);133    for (const required of REQUIRED_FILES) {134      const matches = contents.filter((c) => c === required);135      expect(matches).toHaveLength(1);136    }137138    expect(contents).toContain("SOURCE-MANIFEST.json");139  });140141  it("excludes forbidden paths from the archive", () => {142    const archivePath = packageSourceArchive("0.0.0-test");143    const contents = listArchive(archivePath);144    for (const file of contents) {145      for (const pattern of EXCLUDE_PATTERNS) {146        expect(pattern.test(file)).toBe(false);147      }148    }149  });150151  it("archive SOURCE-MANIFEST.json contains the noise WASM SHA-256 and version", () => {152    const archivePath = packageSourceArchive("0.0.0-test");153    const manifestJson = execFileSync(154      "unzip",155      ["-p", archivePath, "SOURCE-MANIFEST.json"],156      { encoding: "utf8" },157    );158    const manifest: SourceManifest = JSON.parse(manifestJson);159160    expect(manifest.peachVersion).toBe("0.0.0-test");161    expect(manifest.noiseWasmSha256).toMatch(/^[0-9a-f]{64}$/);162    expect(Array.isArray(manifest.files)).toBe(true);163    expect(manifest.files.length).toBeGreaterThan(0);164  });165166  it("archive contains the same crypto-core source as the repo (rebuild provenance)", () => {167    const archivePath = packageSourceArchive("0.0.0-test");168169    const archiveLibRs = execFileSync(170      "unzip",171      ["-p", archivePath, "crypto-core/src/lib.rs"],172      { encoding: "utf8" },173    );174    const repoLibRs = readFileSync(175      join(ROOT, "crypto-core/src/lib.rs"),176      "utf8",177    );178    expect(archiveLibRs).toBe(repoLibRs);179180    const archiveBuildScript = execFileSync(181      "unzip",182      ["-p", archivePath, "scripts/build-noise-core.mjs"],183      { encoding: "utf8" },184    );185    const repoBuildScript = readFileSync(186      join(ROOT, "scripts/build-noise-core.mjs"),187      "utf8",188    );189    expect(archiveBuildScript).toBe(repoBuildScript);190191    const manifestJson = execFileSync(192      "unzip",193      ["-p", archivePath, "SOURCE-MANIFEST.json"],194      { encoding: "utf8" },195    );196    const manifest: SourceManifest = JSON.parse(manifestJson);197    const wasmPath = join(198      ROOT,199      "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",200    );201    const expectedWasmHash = createHash("sha256")202      .update(readFileSync(wasmPath))203      .digest("hex");204    expect(manifest.noiseWasmSha256).toBe(expectedWasmHash);205  });206});207208describe("PB-26-212: reproducibility", () => {209  it("produces byte-identical archives from the same baseline", () => {210    const archive1 = packageSourceArchive("0.0.0-repro");211    const hash1 = createHash("sha256")212      .update(readFileSync(archive1))213      .digest("hex");214215    const archive2 = packageSourceArchive("0.0.0-repro");216    const hash2 = createHash("sha256")217      .update(readFileSync(archive2))218      .digest("hex");219220    expect(hash1).toBe(hash2);221  });222});223224describe("PB-26-212: all entry points use the shared packager", () => {225  it("package.json package:source calls scripts/package-source.ts", () => {226    const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));227    expect(pkg.scripts["package:source"]).toContain("package-source.ts");228  });229230  it("package.json package:source does not contain a duplicate file list", () => {231    const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));232    const script = pkg.scripts["package:source"];233    expect(script).not.toContain("manifest.json manifest.firefox.json");234    expect(script).not.toMatch(/^zip\s/);235  });236237  it("release.ts imports and calls packageSourceArchive", () => {238    const source = readFileSync(join(ROOT, "scripts/release.ts"), "utf8");239    expect(source).toContain("packageSourceArchive");240  });241242  it("release-firefox.ts imports and calls packageSourceArchive", () => {243    const source = readFileSync(244      join(ROOT, "scripts/release-firefox.ts"),245      "utf8",246    );247    expect(source).toContain("packageSourceArchive");248  });249250  it("release-lib.ts does not contain a duplicate zip file list", () => {251    const source = readFileSync(join(ROOT, "scripts/release-lib.ts"), "utf8");252    expect(source).not.toContain("packageFirefoxSourceZip");253  });254});