package-source.test.ts

8.3 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  "fixtures/golden/sync-entry-envelope-v1.json",21  "crypto-core/Cargo.toml",22  "crypto-core/Cargo.lock",23  "crypto-core/src/lib.rs",24  "crypto-core/src/jni_bridge.rs",25  "crypto-core/.cargo/config.toml",26  "scripts/build-noise-core.mjs",27  "scripts/check-noise-core.mjs",28  "manifest.json",29  "manifest.firefox.json",30  "package.json",31  "bun.lock",32  "tsconfig.json",33  "vite.config.ts",34];3536const EXCLUDE_PATTERNS = [37  /node_modules\//,38  /dist-[^/]+\//,39  /\.zip$/,40  /crypto-core\/target\//,41  /crypto-core\/generated\//,42  /\.DS_Store$/,43  /__MACOSX\//,44];4546function listArchive(archivePath: string): string[] {47  return execFileSync("unzip", ["-Z1", archivePath], { encoding: "utf8" })48    .split("\n")49    .filter(Boolean);50}5152describe("PB-26-212: source package — required files", () => {53  it("collects all security-critical files including crypto-core", () => {54    const files = collectSourceFiles();55    for (const required of REQUIRED_FILES) {56      expect(files).toContain(required);57    }58  });5960  it("includes crypto-core/Cargo.toml, Cargo.lock, and src/lib.rs (the core regression)", () => {61    const files = collectSourceFiles();62    expect(files).toContain("crypto-core/Cargo.toml");63    expect(files).toContain("crypto-core/Cargo.lock");64    expect(files).toContain("crypto-core/src/lib.rs");65  });6667  it("excludes forbidden directories and files", () => {68    const files = collectSourceFiles();69    for (const file of files) {70      for (const pattern of EXCLUDE_PATTERNS) {71        expect(pattern.test(file)).toBe(false);72      }73    }74  });7576  it("collects files sorted and without duplicates", () => {77    const files = collectSourceFiles();78    const sorted = [...files].sort();79    expect(files).toEqual(sorted);80    expect(new Set(files).size).toBe(files.length);81  });82});8384describe("PB-26-212: SOURCE-MANIFEST.json", () => {85  it("has correct structure with version, noise WASM SHA-256, toolchain, and file hashes", () => {86    const files = collectSourceFiles();87    const manifest = buildSourceManifest(files, "0.0.0-test");8889    expect(manifest.peachVersion).toBe("0.0.0-test");90    expect(manifest.noiseWasmSha256).toMatch(/^[0-9a-f]{64}$/);91    expect(manifest.toolchain).toBeDefined();92    expect(manifest.toolchain.bun).toBeDefined();93    expect(manifest.toolchain.node).toBeDefined();94    expect(manifest.toolchain.rust).toBeDefined();9596    expect(manifest.files).toHaveLength(files.length);97    for (const entry of manifest.files) {98      expect(entry.path).toBeDefined();99      expect(entry.sha256).toMatch(/^[0-9a-f]{64}$/);100    }101  });102103  it("noiseWasmSha256 matches the actual shipped WASM", () => {104    const wasmPath = join(105      ROOT,106      "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",107    );108    const expected = createHash("sha256")109      .update(readFileSync(wasmPath))110      .digest("hex");111    const files = collectSourceFiles();112    const manifest = buildSourceManifest(files, "0.0.0-test");113    expect(manifest.noiseWasmSha256).toBe(expected);114  });115116  it("file hashes match actual file contents", () => {117    const files = collectSourceFiles();118    const manifest = buildSourceManifest(files, "0.0.0-test");119    for (const entry of manifest.files) {120      const actual = createHash("sha256")121        .update(readFileSync(join(ROOT, entry.path)))122        .digest("hex");123      expect(entry.sha256).toBe(actual);124    }125  });126});127128describe("PB-26-212: source archive contents", () => {129  it("creates an archive containing every required file exactly once", () => {130    const archivePath = packageSourceArchive("0.0.0-test");131    expect(existsSync(archivePath)).toBe(true);132133    const contents = listArchive(archivePath);134    for (const required of REQUIRED_FILES) {135      const matches = contents.filter((c) => c === required);136      expect(matches).toHaveLength(1);137    }138139    expect(contents).toContain("SOURCE-MANIFEST.json");140  });141142  it("excludes forbidden paths from the archive", () => {143    const archivePath = packageSourceArchive("0.0.0-test");144    const contents = listArchive(archivePath);145    for (const file of contents) {146      for (const pattern of EXCLUDE_PATTERNS) {147        expect(pattern.test(file)).toBe(false);148      }149    }150  });151152  it("archive SOURCE-MANIFEST.json contains the noise WASM SHA-256 and version", () => {153    const archivePath = packageSourceArchive("0.0.0-test");154    const manifestJson = execFileSync(155      "unzip",156      ["-p", archivePath, "SOURCE-MANIFEST.json"],157      { encoding: "utf8" },158    );159    const manifest: SourceManifest = JSON.parse(manifestJson);160161    expect(manifest.peachVersion).toBe("0.0.0-test");162    expect(manifest.noiseWasmSha256).toMatch(/^[0-9a-f]{64}$/);163    expect(Array.isArray(manifest.files)).toBe(true);164    expect(manifest.files.length).toBeGreaterThan(0);165  });166167  it("archive contains the same crypto-core source as the repo (rebuild provenance)", () => {168    const archivePath = packageSourceArchive("0.0.0-test");169170    const archiveLibRs = execFileSync(171      "unzip",172      ["-p", archivePath, "crypto-core/src/lib.rs"],173      { encoding: "utf8" },174    );175    const repoLibRs = readFileSync(176      join(ROOT, "crypto-core/src/lib.rs"),177      "utf8",178    );179    expect(archiveLibRs).toBe(repoLibRs);180181    const archiveBuildScript = execFileSync(182      "unzip",183      ["-p", archivePath, "scripts/build-noise-core.mjs"],184      { encoding: "utf8" },185    );186    const repoBuildScript = readFileSync(187      join(ROOT, "scripts/build-noise-core.mjs"),188      "utf8",189    );190    expect(archiveBuildScript).toBe(repoBuildScript);191192    const manifestJson = execFileSync(193      "unzip",194      ["-p", archivePath, "SOURCE-MANIFEST.json"],195      { encoding: "utf8" },196    );197    const manifest: SourceManifest = JSON.parse(manifestJson);198    const wasmPath = join(199      ROOT,200      "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",201    );202    const expectedWasmHash = createHash("sha256")203      .update(readFileSync(wasmPath))204      .digest("hex");205    expect(manifest.noiseWasmSha256).toBe(expectedWasmHash);206  });207});208209describe("PB-26-212: reproducibility", () => {210  it("produces byte-identical archives from the same baseline", () => {211    const archive1 = packageSourceArchive("0.0.0-repro");212    const hash1 = createHash("sha256")213      .update(readFileSync(archive1))214      .digest("hex");215216    const archive2 = packageSourceArchive("0.0.0-repro");217    const hash2 = createHash("sha256")218      .update(readFileSync(archive2))219      .digest("hex");220221    expect(hash1).toBe(hash2);222  });223});224225describe("PB-26-212: all entry points use the shared packager", () => {226  it("package.json package:source calls scripts/package-source.ts", () => {227    const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));228    expect(pkg.scripts["package:source"]).toContain("package-source.ts");229  });230231  it("package.json package:source does not contain a duplicate file list", () => {232    const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));233    const script = pkg.scripts["package:source"];234    expect(script).not.toContain("manifest.json manifest.firefox.json");235    expect(script).not.toMatch(/^zip\s/);236  });237238  it("release.ts imports and calls packageSourceArchive", () => {239    const source = readFileSync(join(ROOT, "scripts/release.ts"), "utf8");240    expect(source).toContain("packageSourceArchive");241  });242243  it("release-firefox.ts imports and calls packageSourceArchive", () => {244    const source = readFileSync(245      join(ROOT, "scripts/release-firefox.ts"),246      "utf8",247    );248    expect(source).toContain("packageSourceArchive");249  });250251  it("release-lib.ts does not contain a duplicate zip file list", () => {252    const source = readFileSync(join(ROOT, "scripts/release-lib.ts"), "utf8");253    expect(source).not.toContain("packageFirefoxSourceZip");254  });255});