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