Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
49 lines
2.6 KiB
JavaScript
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();
|
|
}
|