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