diff --git a/lib/exporter/verify.integrity.test.ts b/lib/exporter/verify.integrity.test.ts index cbbf717..c7c3a0e 100644 --- a/lib/exporter/verify.integrity.test.ts +++ b/lib/exporter/verify.integrity.test.ts @@ -20,6 +20,13 @@ describe('verifyIntegrity', () => { const res = verifyIntegrity(extras, assets) expect(res.ok).toBe(true) expect(res.errors.length).toBe(0) + // summary + const parsedIdx = JSON.parse(td(idx)) as { entries: Array<{ path: string }> } + expect(res.summary.total).toBe(parsedIdx.entries.length) + expect(res.summary.missing).toBe(0) + expect(res.summary.integrityMismatches).toBe(0) + expect(res.summary.sizeMismatches).toBe(0) + expect(res.summary.bundleHashMismatch).toBe(false) // sanity: index contains both paths const parsed = JSON.parse(td(idx)) as { entries: Array<{ path: string }> } const paths = parsed.entries.map(e => e.path) @@ -42,6 +49,7 @@ describe('verifyIntegrity', () => { const res = verifyIntegrity({ 'assets.integrity.index.json': idx }, assets) expect(res.ok).toBe(false) expect(res.errors.some((e: string) => e.includes(somePath) && e.toLowerCase().includes('mismatch'))).toBe(true) + expect(res.summary.integrityMismatches).toBeGreaterThanOrEqual(1) }) it('detects missing entry or missing asset referenced by index', async () => { @@ -58,6 +66,7 @@ describe('verifyIntegrity', () => { const res1 = verifyIntegrity({ 'assets.integrity.index.json': idx2 }, assets) expect(res1.ok).toBe(false) expect(res1.errors.some((e: string) => e.toLowerCase().includes('bundlehash'))).toBe(true) + expect(res1.summary.bundleHashMismatch).toBe(true) // reference a missing path in index obj.entries.unshift({ path: 'assets/missing.bin', integrity: 'sha256-AAAA' }) @@ -65,6 +74,7 @@ describe('verifyIntegrity', () => { const res2 = verifyIntegrity({ 'assets.integrity.index.json': idx3 }, assets) expect(res2.ok).toBe(false) expect(res2.errors.some((e: string) => e.includes('assets/missing.bin') && e.toLowerCase().includes('missing'))).toBe(true) + expect(res2.summary.missing).toBeGreaterThanOrEqual(1) // keep lints happy expect(removed).toBeTruthy() }) diff --git a/lib/exporter/verify.ts b/lib/exporter/verify.ts index 69e68b1..a769df0 100644 --- a/lib/exporter/verify.ts +++ b/lib/exporter/verify.ts @@ -1,4 +1,14 @@ -export type VerifyResult = { ok: boolean; errors: string[] } +export type VerifyResult = { + ok: boolean; + errors: string[]; + summary: { + total: number; + missing: number; + integrityMismatches: number; + sizeMismatches: number; + bundleHashMismatch: boolean; + }; +} // Minimal synchronous SHA-256 implementation returning base64 string. function sha256Base64(bytes: Uint8Array): string { @@ -81,30 +91,36 @@ export function verifyIntegrity( const errors: string[] = [] const key = 'assets.integrity.index.json' if (!Object.prototype.hasOwnProperty.call(extras, key)) { - return { ok: false, errors: ['Missing assets.integrity.index.json in extras'] } + return { ok: false, errors: ['Missing assets.integrity.index.json in extras'], summary: { total: 0, missing: 0, integrityMismatches: 0, sizeMismatches: 0, bundleHashMismatch: false } } } let parsed: { entries: Array<{ path: string; integrity: string; size?: number }>; bundleHash: string } try { parsed = JSON.parse(new TextDecoder().decode(extras[key])) } catch { - return { ok: false, errors: ['Invalid assets.integrity.index.json JSON'] } + return { ok: false, errors: ['Invalid assets.integrity.index.json JSON'], summary: { total: 0, missing: 0, integrityMismatches: 0, sizeMismatches: 0, bundleHashMismatch: false } } } // verify each entry exists and integrity matches const recomputedLines: string[] = [] + let missing = 0 + let integrityMismatches = 0 + let sizeMismatches = 0 for (const e of parsed.entries) { const bytes = assets[e.path] if (!bytes) { errors.push(`Missing asset for path in integrity index: ${e.path}`) + missing++ continue } // optional size check when present in index if (typeof e.size === 'number' && e.size !== bytes.length) { errors.push(`Size mismatch for ${e.path}: expected ${e.size}, got ${bytes.length}`) + sizeMismatches++ } const b64 = sha256Base64(bytes) const integ = `sha256-${b64}` if (integ !== e.integrity) { errors.push(`Integrity mismatch for ${e.path}`) + integrityMismatches++ } recomputedLines.push(e.path) recomputedLines.push(integ) @@ -112,8 +128,19 @@ export function verifyIntegrity( // recompute bundle hash const joined = new TextEncoder().encode(recomputedLines.join('\n')) const bundle = `sha256-${sha256Base64(joined)}` - if (bundle !== parsed.bundleHash) { + const bundleHashMismatch = bundle !== parsed.bundleHash + if (bundleHashMismatch) { errors.push('BundleHash mismatch') } - return { ok: errors.length === 0, errors } + return { + ok: errors.length === 0, + errors, + summary: { + total: parsed.entries.length, + missing, + integrityMismatches, + sizeMismatches, + bundleHashMismatch, + }, + } }