1234567import { createHash } from "node:crypto";8import { crc32, deflateRawSync } from "node:zlib";9import {10 existsSync,11 mkdirSync,12 readFileSync,13 readdirSync,14 writeFileSync,15} from "node:fs";16import { basename, join, relative, resolve } from "node:path";17import { fileURLToPath } from "node:url";1819const SCRIPT_DIR = resolve(dirname_safe());20const REPO_ROOT = resolve(SCRIPT_DIR, "..");2122function dirname_safe(): string {23 try {24 return fileURLToPath(new URL(".", import.meta.url));25 } catch {26 return __dirname;27 }28}29303132const EXCLUDE_PATTERNS = [33 /node_modules\//,34 /dist-[^/]+\//,35 /\.zip$/,36 /crypto-core\/target\//,37 /crypto-core\/generated\//,38 /\.DS_Store$/,39 /__MACOSX\//,40];4142function shouldExclude(relPath: string): boolean {43 return EXCLUDE_PATTERNS.some((p) => p.test(relPath));44}45464748const INCLUDE_DIRS = ["src", "scripts", "public", "crypto-core"];49const INCLUDE_FILES = [50 "fixtures/golden/peach-payload-v4.json",51 "manifest.json",52 "manifest.firefox.json",53 "package.json",54 "bun.lock",55 "tsconfig.json",56 "vite.config.ts",57];5859function walkDir(dir: string, files: string[]): void {60 let entries: ReturnType<typeof readdirSync>;61 try {62 entries = readdirSync(dir, { withFileTypes: true });63 } catch {64 return;65 }66 for (const entry of entries) {67 const fullPath = join(dir, entry.name);68 const relPath = relative(REPO_ROOT, fullPath);69 if (entry.isDirectory()) {70 if (shouldExclude(`${relPath}/`)) continue;71 walkDir(fullPath, files);72 } else if (entry.isFile()) {73 if (shouldExclude(relPath)) continue;74 files.push(relPath);75 }76 }77}7879export function collectSourceFiles(): string[] {80 const files: string[] = [];81 for (const dir of INCLUDE_DIRS) {82 const absDir = join(REPO_ROOT, dir);83 if (existsSync(absDir)) {84 walkDir(absDir, files);85 }86 }87 for (const file of INCLUDE_FILES) {88 if (existsSync(join(REPO_ROOT, file))) {89 files.push(file);90 }91 }92 return [...new Set(files)].sort();93}94959697export interface SourceManifest {98 peachVersion: string;99 noiseWasmSha256: string;100 toolchain: {101 bun: string;102 node: string;103 rust: string;104 };105 files: Array<{ path: string; sha256: string }>;106}107108function sha256(data: Buffer | string): string {109 return createHash("sha256").update(data).digest("hex");110}111112function getNoiseWasmSha256(): string {113 const wasmPath = join(114 REPO_ROOT,115 "src/shared/noise-core-wasm/peach_noise_core_bg.wasm",116 );117 return sha256(readFileSync(wasmPath));118}119120function readToolchain(): { bun: string; node: string; rust: string } {121 const pkg = JSON.parse(122 readFileSync(join(REPO_ROOT, "package.json"), "utf8"),123 );124 const bunVersion = (pkg.packageManager ?? "").replace("bun@", "");125126 const cargoToml = readFileSync(127 join(REPO_ROOT, "crypto-core/Cargo.toml"),128 "utf8",129 );130 const rustMatch = cargoToml.match(/rust-version\s*=\s*"([^"]+)"/);131 const rustVersion = rustMatch ? rustMatch[1] : "unknown";132133 return {134 bun: bunVersion,135 node: pkg.engines?.node ?? "unknown",136 rust: rustVersion,137 };138}139140export function buildSourceManifest(141 files: string[],142 version: string,143): SourceManifest {144 const entries = files.map((path) => ({145 path,146 sha256: sha256(readFileSync(join(REPO_ROOT, path))),147 }));148 return {149 peachVersion: version,150 noiseWasmSha256: getNoiseWasmSha256(),151 toolchain: readToolchain(),152 files: entries,153 };154}155156157158159const FIXED_DOS_TIME = 0x0000;160const FIXED_DOS_DATE = 0x0021;161162interface ZipEntry {163 path: string;164 data: Buffer;165}166167export function createDeterministicZip(entries: ZipEntry[]): Buffer {168 const sorted = [...entries].sort((a, b) =>169 a.path.localeCompare(b.path),170 );171 const localParts: Buffer[] = [];172 const centralParts: Buffer[] = [];173 let offset = 0;174175 for (const entry of sorted) {176 const filename = Buffer.from(entry.path, "utf8");177 const compressed = deflateRawSync(entry.data, { level: 9 });178 const crc = crc32(entry.data) >>> 0;179 const compSize = compressed.length;180 const uncompSize = entry.data.length;181182 183 const lh = Buffer.alloc(30 + filename.length);184 lh.writeUInt32LE(0x04034b50, 0);185 lh.writeUInt16LE(20, 4);186 lh.writeUInt16LE(0, 6);187 lh.writeUInt16LE(8, 8);188 lh.writeUInt16LE(FIXED_DOS_TIME, 10);189 lh.writeUInt16LE(FIXED_DOS_DATE, 12);190 lh.writeUInt32LE(crc, 14);191 lh.writeUInt32LE(compSize, 18);192 lh.writeUInt32LE(uncompSize, 22);193 lh.writeUInt16LE(filename.length, 26);194 lh.writeUInt16LE(0, 28);195 filename.copy(lh, 30);196197 localParts.push(lh, compressed);198199 200 const cd = Buffer.alloc(46 + filename.length);201 cd.writeUInt32LE(0x02014b50, 0);202 cd.writeUInt16LE(20, 4);203 cd.writeUInt16LE(20, 6);204 cd.writeUInt16LE(0, 8);205 cd.writeUInt16LE(8, 10);206 cd.writeUInt16LE(FIXED_DOS_TIME, 12);207 cd.writeUInt16LE(FIXED_DOS_DATE, 14);208 cd.writeUInt32LE(crc, 16);209 cd.writeUInt32LE(compSize, 20);210 cd.writeUInt32LE(uncompSize, 24);211 cd.writeUInt16LE(filename.length, 28);212 cd.writeUInt16LE(0, 30);213 cd.writeUInt16LE(0, 32);214 cd.writeUInt16LE(0, 34);215 cd.writeUInt16LE(0, 36);216 cd.writeUInt32LE(0, 38);217 cd.writeUInt32LE(offset, 42);218 filename.copy(cd, 46);219220 centralParts.push(cd);221 offset += lh.length + compressed.length;222 }223224 const centralDir = Buffer.concat(centralParts);225 const centralOffset = offset;226 const centralSize = centralDir.length;227228 229 const eocd = Buffer.alloc(22);230 eocd.writeUInt32LE(0x06054b50, 0);231 eocd.writeUInt16LE(0, 4);232 eocd.writeUInt16LE(0, 6);233 eocd.writeUInt16LE(sorted.length, 8);234 eocd.writeUInt16LE(sorted.length, 10);235 eocd.writeUInt32LE(centralSize, 12);236 eocd.writeUInt32LE(centralOffset, 16);237 eocd.writeUInt16LE(0, 20);238239 return Buffer.concat([...localParts, centralDir, eocd]);240}241242243244function readVersion(manifestPath: string): string {245 const manifest = JSON.parse(246 readFileSync(join(REPO_ROOT, manifestPath), "utf8"),247 );248 if (!manifest.version) {249 throw new Error(`Could not read version from ${manifestPath}`);250 }251 return manifest.version;252}253254function ensureArtifactsDir(): string {255 const dir = join(REPO_ROOT, "artifacts");256 if (!existsSync(dir)) mkdirSync(dir);257 return dir;258}259260export function packageSourceArchive(version: string): string {261 const artifactsDir = ensureArtifactsDir();262 const archivePath = join(artifactsDir, `peach-source-${version}.zip`);263264 const files = collectSourceFiles();265 const manifest = buildSourceManifest(files, version);266 const manifestJson = `${JSON.stringify(manifest, null, 2)}\n`;267268 const entries: ZipEntry[] = files.map((path) => ({269 path,270 data: readFileSync(join(REPO_ROOT, path)),271 }));272 entries.push({273 path: "SOURCE-MANIFEST.json",274 data: Buffer.from(manifestJson, "utf8"),275 });276277 const zipBuffer = createDeterministicZip(entries);278 writeFileSync(archivePath, zipBuffer);279280 console.log(281 `Packaged source archive → ${basename(archivePath)} (${files.length} files + SOURCE-MANIFEST.json)`,282 );283 return archivePath;284}285286287288if (import.meta.main) {289 const version = readVersion("manifest.json");290 const archivePath = packageSourceArchive(version);291 console.log(`\nSource archive: ${archivePath}`);292}