coulomb-social/scripts/verify-bubble-visuals.mjs
tegwick 0c714d3c56 Preserve captured Bubble Visuals in private review trees
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
2026-10-31 14:54:06 +01:00

49 lines
2.6 KiB
JavaScript

#!/usr/bin/env node
/** Decode only locally retained Visual files in an offline disposable browser; emit counts only. */
import { readFileSync, lstatSync, realpathSync } from 'node:fs';
import { resolve, join } from 'node:path';
import { createHash } from 'node:crypto';
import { chromium } from 'playwright';
let browser;
try {
const root = resolve(process.argv[2]);
const reportBytes = readFileSync(join(root, 'reconciliation.json'));
const report = JSON.parse(reportBytes);
if (report.status !== 'completed_private_visual_preparation') throw new Error('unfinished_bundle');
const entries = Object.entries(report.file_sha256).filter(([name]) => name.startsWith('captured-visuals/'));
browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ serviceWorkers: 'block', offline: true });
await context.route('**/*', route => route.abort());
const page = await context.newPage();
let decoded = 0;
for (const [name, hash] of entries) {
if (!/^captured-visuals\/[a-f0-9]{64}\.(png|jpg|webp)$/.test(name)) throw new Error('invalid_asset_path');
const path = join(root, name), info = lstatSync(path);
if (!info.isFile() || info.isSymbolicLink() || info.size > 16 * 1024 * 1024 ||
(info.mode & 0o077) || info.uid !== process.getuid() ||
realpathSync(path) !== path) throw new Error('unsafe_asset');
const data = readFileSync(path);
if (createHash('sha256').update(data).digest('hex') !== hash) throw new Error('changed_asset');
const mime = name.endsWith('.jpg') ? 'image/jpeg' : name.endsWith('.webp') ? 'image/webp' : 'image/png';
const ok = await page.evaluate(async ({data, mime}) => {
const bytes = Uint8Array.from(atob(data), x => x.charCodeAt(0));
const url = URL.createObjectURL(new Blob([bytes], {type: mime}));
try {
const img = new Image(); img.src = url;
await Promise.race([img.decode(), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 10000))]);
return img.naturalWidth > 0 && img.naturalHeight > 0;
} finally { URL.revokeObjectURL(url); }
}, {data: data.toString('base64'), mime});
if (!ok) throw new Error('image_decode_failed');
decoded++;
}
console.log(JSON.stringify({schema: 'coulomb.private-visual-decode.v1', status: 'all_captured_visuals_decoded',
decoded_images: decoded, bundle_sha256: createHash('sha256').update(reportBytes).digest('hex'),
browser_network_disabled: true, productive_fidelity_accepted: false}));
} catch {
console.error('Private visual decode verification failed; no acceptance inferred.');
process.exitCode = 1;
} finally {
if (browser) await browser.close();
}