1234567import { createHash } from "node:crypto";8import { crc32, deflateRawSync } from "node:zlib";9import {10 existsSync,11 mkdirSync,12 readFileSync,13 readdirSync,14 writeFileSync,15 type Dirent,16} from "node:fs";17import { basename, join, relative, resolve } from "node:path";18import { fileURLToPath } from "node:url";1920const SCRIPT_DIR = resolve(dirname_safe());21const REPO_ROOT = resolve(SCRIPT_DIR, "..");2223function dirname_safe(): string {24 try {25 return fileURLToPath(new URL(".", import.meta.url));26 } catch {27 return __dirname;28 }29}30313233const EXCLUDE_PATTERNS = [34 /node_modules\//,35 /dist-[^/]+\//,36 /\.zip$/,37 /crypto-core\/target\//,38 /crypto-core\/generated\//,39 /\.DS_Store$/,40 /__MACOSX\//,41];4243function shouldExclude(relPath: string): boolean {44 return EXCLUDE_PATTERNS.some((p) => p.test(relPath));45}46474849const INCLUDE_DIRS = ["src", "scripts", "public", "crypto-core"];50515253545556export const REVIEWED_THIRD_PARTY_FILE = "node_modules/pdfjs-dist/build/pdf.worker.min.mjs";5758const INCLUDE_FILES = [59 "fixtures/golden/peach-payload-v4.json",60 "fixtures/golden/sync-entry-envelope-v1.json",61 "manifest.json",62 "manifest.firefox.json",63 "package.json",64 "bun.lock",65 "tsconfig.json",66 "vite.config.ts",67 REVIEWED_THIRD_PARTY_FILE,68];6970function walkDir(dir: string, files: string[]): void {71 let entries: Dirent<string>[];72 try {73 entries = readdirSync(dir, { withFileTypes: true });74 } catch {75 return;76 }77 for (const entry of entries) {78 const fullPath = join(dir, entry.name);79 const relPath = relative(REPO_ROOT, fullPath);80 if (entry.isDirectory()) {81 if (shouldExclude(`${relPath}/`)) continue;82 walkDir(fullPath, files);83 } else if (entry.isFile()) {84 if (shouldExclude(relPath)) continue;85 files.push(relPath);86 }87 }88}8990export function collectSourceFiles(): string[] {91 const files: string[] = [];92 for (const dir of INCLUDE_DIRS) {93 const absDir = join(REPO_ROOT, dir);94 if (existsSync(absDir)) {95 walkDir(absDir, files);96 }97 }98 for (const file of INCLUDE_FILES) {99 if (existsSync(join(REPO_ROOT, file))) {100 files.push(file);101 }102 }103 return [...new Set(files)].sort();104}105106107108export interface SourceManifest {109 peachVersion: string;110 noiseWasmSha256: string;111 toolchain: {112 bun: string;113 node: string;114 rust: string;115 };116 files: Array<{ path: string; sha256: string }>;117}118119function sha256(data: Buffer | string): string {120 return createHash("sha256").update(data).digest("hex");121}122123function getNoiseWasmSha256(): string {124 const wasmPath = join(125 REPO_ROOT,126 "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",127 );128 return sha256(readFileSync(wasmPath));129}130131function readToolchain(): { bun: string; node: string; rust: string } {132 const pkg = JSON.parse(133 readFileSync(join(REPO_ROOT, "package.json"), "utf8"),134 );135 const bunVersion = (pkg.packageManager ?? "").replace("bun@", "");136137 const cargoToml = readFileSync(138 join(REPO_ROOT, "crypto-core/Cargo.toml"),139 "utf8",140 );141 const rustMatch = cargoToml.match(/rust-version\s*=\s*"([^"]+)"/);142 const rustVersion = rustMatch ? rustMatch[1] : "unknown";143144 return {145 bun: bunVersion,146 node: pkg.engines?.node ?? "unknown",147 rust: rustVersion,148 };149}150151export function buildSourceManifest(152 files: string[],153 version: string,154): SourceManifest {155 const entries = files.map((path) => ({156 path,157 sha256: sha256(readFileSync(join(REPO_ROOT, path))),158 }));159 return {160 peachVersion: version,161 noiseWasmSha256: getNoiseWasmSha256(),162 toolchain: readToolchain(),163 files: entries,164 };165}166167168169170const FIXED_DOS_TIME = 0x0000;171const FIXED_DOS_DATE = 0x0021;172173interface ZipEntry {174 path: string;175 data: Buffer;176}177178export function createDeterministicZip(entries: ZipEntry[]): Buffer {179 const sorted = [...entries].sort((a, b) =>180 a.path.localeCompare(b.path),181 );182 const localParts: Buffer[] = [];183 const centralParts: Buffer[] = [];184 let offset = 0;185186 for (const entry of sorted) {187 const filename = Buffer.from(entry.path, "utf8");188 const compressed = deflateRawSync(entry.data, { level: 9 });189 const crc = crc32(entry.data) >>> 0;190 const compSize = compressed.length;191 const uncompSize = entry.data.length;192193 194 const lh = Buffer.alloc(30 + filename.length);195 lh.writeUInt32LE(0x04034b50, 0);196 lh.writeUInt16LE(20, 4);197 lh.writeUInt16LE(0, 6);198 lh.writeUInt16LE(8, 8);199 lh.writeUInt16LE(FIXED_DOS_TIME, 10);200 lh.writeUInt16LE(FIXED_DOS_DATE, 12);201 lh.writeUInt32LE(crc, 14);202 lh.writeUInt32LE(compSize, 18);203 lh.writeUInt32LE(uncompSize, 22);204 lh.writeUInt16LE(filename.length, 26);205 lh.writeUInt16LE(0, 28);206 filename.copy(lh, 30);207208 localParts.push(lh, compressed);209210 211 const cd = Buffer.alloc(46 + filename.length);212 cd.writeUInt32LE(0x02014b50, 0);213 cd.writeUInt16LE(20, 4);214 cd.writeUInt16LE(20, 6);215 cd.writeUInt16LE(0, 8);216 cd.writeUInt16LE(8, 10);217 cd.writeUInt16LE(FIXED_DOS_TIME, 12);218 cd.writeUInt16LE(FIXED_DOS_DATE, 14);219 cd.writeUInt32LE(crc, 16);220 cd.writeUInt32LE(compSize, 20);221 cd.writeUInt32LE(uncompSize, 24);222 cd.writeUInt16LE(filename.length, 28);223 cd.writeUInt16LE(0, 30);224 cd.writeUInt16LE(0, 32);225 cd.writeUInt16LE(0, 34);226 cd.writeUInt16LE(0, 36);227 cd.writeUInt32LE(0, 38);228 cd.writeUInt32LE(offset, 42);229 filename.copy(cd, 46);230231 centralParts.push(cd);232 offset += lh.length + compressed.length;233 }234235 const centralDir = Buffer.concat(centralParts);236 const centralOffset = offset;237 const centralSize = centralDir.length;238239 240 const eocd = Buffer.alloc(22);241 eocd.writeUInt32LE(0x06054b50, 0);242 eocd.writeUInt16LE(0, 4);243 eocd.writeUInt16LE(0, 6);244 eocd.writeUInt16LE(sorted.length, 8);245 eocd.writeUInt16LE(sorted.length, 10);246 eocd.writeUInt32LE(centralSize, 12);247 eocd.writeUInt32LE(centralOffset, 16);248 eocd.writeUInt16LE(0, 20);249250 return Buffer.concat([...localParts, centralDir, eocd]);251}252253254255function readVersion(manifestPath: string): string {256 const manifest = JSON.parse(257 readFileSync(join(REPO_ROOT, manifestPath), "utf8"),258 );259 if (!manifest.version) {260 throw new Error(`Could not read version from ${manifestPath}`);261 }262 return manifest.version;263}264265function ensureArtifactsDir(): string {266 const dir = join(REPO_ROOT, "artifacts");267 if (!existsSync(dir)) mkdirSync(dir);268 return dir;269}270271export function packageSourceArchive(version: string): string {272 const artifactsDir = ensureArtifactsDir();273 const archivePath = join(artifactsDir, `peach-source-${version}.zip`);274275 const files = collectSourceFiles();276 const manifest = buildSourceManifest(files, version);277 const manifestJson = `${JSON.stringify(manifest, null, 2)}\n`;278279 const entries: ZipEntry[] = files.map((path) => ({280 path,281 data: readFileSync(join(REPO_ROOT, path)),282 }));283 entries.push({284 path: "SOURCE-MANIFEST.json",285 data: Buffer.from(manifestJson, "utf8"),286 });287288 const zipBuffer = createDeterministicZip(entries);289 writeFileSync(archivePath, zipBuffer);290291 console.log(292 `Packaged source archive → ${basename(archivePath)} (${files.length} files + SOURCE-MANIFEST.json)`,293 );294 return archivePath;295}296297298299if (import.meta.main) {300 const version = readVersion("manifest.json");301 const archivePath = packageSourceArchive(version);302 console.log(`\nSource archive: ${archivePath}`);303}