#!/usr/bin/env node /* Verify bundle integrity using extras/assets JSON maps. Usage: node scripts/verify.js --extras --assets */ const fs = require('fs/promises'); const crypto = require('crypto'); function parseArgs(argv) { const out = { extras: '', assets: '' }; for (let i = 2; i < argv.length; i++) { const a = argv[i]; if (a === '--extras') out.extras = argv[++i]; else if (a === '--assets') out.assets = argv[++i]; } return out; } function fromB64(b64) { return Buffer.from(b64, 'base64'); } function sha256b64(buf) { return crypto.createHash('sha256').update(buf).digest('base64'); } (async () => { try { const { extras, assets } = parseArgs(process.argv); if (!extras || !assets) { console.log('Usage: node scripts/verify.js --extras --assets '); process.exit(2); } const extrasJson = await fs.readFile(extras, 'utf-8'); const assetsJson = await fs.readFile(assets, 'utf-8'); const extrasMap = JSON.parse(extrasJson); const assetsMap = JSON.parse(assetsJson); const result = { ok: true, errors: [], summary: { total: 0, missing: 0, integrityMismatches: 0, sizeMismatches: 0, bundleHashMismatch: false } }; if (!Object.prototype.hasOwnProperty.call(extrasMap, 'assets.integrity.index.json')) { result.ok = false; result.errors.push('Missing assets.integrity.index.json in extras'); console.log(JSON.stringify(result, null, 2)); process.exit(1); } let parsed; try { parsed = JSON.parse(fromB64(extrasMap['assets.integrity.index.json']).toString('utf-8')); } catch { result.ok = false; result.errors.push('Invalid assets.integrity.index.json JSON'); console.log(JSON.stringify(result, null, 2)); process.exit(1); } const recomputedLines = []; let missing = 0, integrityMismatches = 0, sizeMismatches = 0; for (const e of parsed.entries) { const b64 = assetsMap[e.path]; if (!b64) { result.errors.push(`Missing asset for path in integrity index: ${e.path}`); missing++; continue; } const buf = fromB64(b64); if (typeof e.size === 'number' && e.size !== buf.length) { result.errors.push(`Size mismatch for ${e.path}: expected ${e.size}, got ${buf.length}`); sizeMismatches++; } const integ = 'sha256-' + sha256b64(buf); if (integ !== e.integrity) { result.errors.push(`Integrity mismatch for ${e.path}`); integrityMismatches++; } recomputedLines.push(e.path); recomputedLines.push(integ); } const bundle = 'sha256-' + sha256b64(Buffer.from(recomputedLines.join('\n'), 'utf-8')); const bundleHashMismatch = bundle !== parsed.bundleHash; if (bundleHashMismatch) result.errors.push('BundleHash mismatch'); result.summary.total = parsed.entries.length; result.summary.missing = missing; result.summary.integrityMismatches = integrityMismatches; result.summary.sizeMismatches = sizeMismatches; result.summary.bundleHashMismatch = bundleHashMismatch; result.ok = result.errors.length === 0; console.log(JSON.stringify(result, null, 2)); process.exit(result.ok ? 0 : 1); } catch (e) { console.error(e && e.stack || String(e)); process.exit(1); } })();