Preserve captured Bubble Visuals in private review trees
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
This commit is contained in:
parent
000ca43f0c
commit
0c714d3c56
9 changed files with 777 additions and 1 deletions
49
scripts/verify-bubble-visuals.mjs
Normal file
49
scripts/verify-bubble-visuals.mjs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#!/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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue