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});