Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_delivery.mjs

7.8 KiB, 1 run

created by r2519314175:353, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// verify_delivery.mjs — the in-page delivery check, in a real browser.
2//
3// verify/lib.mjs + verify/check.mjs are unit-tested (verify/verify.test.mjs),
4// and the browser shares their fingerprint algorithm (asserted identical there).
5// This drives the actual www/verify.html page to prove the browser wiring: a
6// clean served build reports a green verdict, and a tampered served file is
7// caught and named.
8//
9// The public transparency log lives on GitHub (a different origin, the whole
10// point). Here it is routed to the LOCAL verify/transparency.jsonl, so the
11// "sealed in the public log" check can pass offline against the same chain.
12//
13// WHAT IS BEING MEASURED, AND WHAT IS NOT. `www/manifest.json` and the chain are
14// written by `node dev/stamp-build.mjs "note" && node verify/manifest.mjs`, which runs
15// when a release is SEALED — so between seals the working tree legitimately does
16// not match the manifest that is committed beside it. From 2026-08-12 that made
17// this file fail on every commit that was not itself a seal (4e13bbc, 40e6ed4,
18// 5a0bcbf: "3 ok, 2 failed" each), and the page was right every time: the served
19// build really did not match the published source, and it said so.
20//
21// That is the release state, and `dev/repro-check.sh` is what gates it. THIS
22// file exists to prove the BROWSER WIRING — that the page fetches a manifest,
23// re-derives the bundle hash, finds it in a chain on a foreign origin, rehashes
24// every served byte, and draws the right verdict. So when the tree is between
25// seals, the manifest and the chain entry for it are computed HERE, from the
26// served tree, with `verify/lib.mjs` — the same functions `verify/manifest.mjs`
27// seals with, not a copy of them — and routed into the page. Every check the
28// page makes still runs over the real served bytes; only the requirement that
29// the repository be sitting on a seal is lifted. A sealed tree is left entirely
30// alone, and the run says which of the two it was.
31//
32// node dev/verify_delivery.mjs
33// node dev/verify_delivery.mjs --break=stale # the pre-2026-08-13 state
34// node dev/verify_delivery.mjs --break=serve # a clean build that is not
35// node dev/verify_delivery.mjs --break=notamper # a tamper that never happens
36//
37// The `--break` modes are the red proof: each one must turn a named check red,
38// so a green run cannot be a check that had stopped being able to fail.
39//
40// Needs dev/serve.mjs (DAIMOND_PORT, default 8777).
41import { open, APP } from './harness.mjs';
42import { readFile } from 'node:fs/promises';
43import { fileURLToPath } from 'node:url';
44import { hashTree, bundleHash, parseLog, nextEntry } from '../verify/lib.mjs';
45
46const LOG = fileURLToPath(new URL('../verify/transparency.jsonl', import.meta.url));
47const WWW = fileURLToPath(new URL('../www', import.meta.url));
48const logText = await readFile(LOG, 'utf8');
49
50/// Which fault to inject, so every check below can be shown going red.
51const BREAK = (process.argv.find(a => a.startsWith('--break=')) || '').slice(8);
52
53const ok = [], bad = [];
54const check = (name, pass, detail) => { (pass ? ok : bad).push(name); console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); };
55
56// ── The manifest the page will be asked to verify against ───────────
57//
58// Read the tree the dev server is serving and seal it exactly as a release
59// would. If that comes out identical to the committed manifest, this tree IS a
60// sealed one and nothing is substituted.
61const files = await hashTree(WWW);
62const bundle = bundleHash(files);
63const onDisk = await readFile(`${WWW}/manifest.json`, 'utf8').then(JSON.parse).catch(() => null);
64const sealed = !!onDisk && onDisk.bundle === bundle;
65const build = (onDisk && onDisk.build) || bundle.slice(0, 12);
66const manifest = { algo: 'sha-256', build, bundle, files };
67
68// The chain the page fetches from the foreign origin. When the bundle is not a
69// released one, its entry is appended here — through `nextEntry`, so the hash
70// chain the page re-walks is a real one and check 2 still means what it says.
71const entries = parseLog(logText);
72const chainText = sealed || BREAK === 'stale'
73 ? logText
74 : logText + (logText.endsWith('\n') ? '' : '\n')
75 + JSON.stringify(nextEntry(entries, {
76 ts: new Date().toISOString(), build, bundle, note: 'verify_delivery, not a release',
77 })) + '\n';
78
79console.log(sealed
80 ? ` note this tree IS sealed (bundle ${bundle.slice(0, 12)}…); the committed manifest is used as it stands`
81 : ` note this tree is between seals; the manifest and one chain entry are computed here `
82 + `over ${Object.keys(files).length} served files (bundle ${bundle.slice(0, 12)}…)`);
83if (BREAK) console.log(` note --break=${BREAK}: a fault is being injected on purpose`);
84
85const s = await open({ name: 'delivery', signIn: false, connect: false });
86const { page } = s;
87
88// The public log, served from its real (foreign) origin — routed to the local chain.
89await page.route('https://raw.githubusercontent.com/**', r => r.fulfill({
90 status: 200, contentType: 'text/plain', headers: { 'access-control-allow-origin': '*' }, body: chainText,
91}));
92// The manifest for the tree as served, unless this tree is already a sealed one
93// or the run is deliberately reproducing the old failure.
94if (!sealed && BREAK !== 'stale') {
95 await page.route('**/manifest.json', r => r.fulfill({
96 status: 200, contentType: 'application/json', body: JSON.stringify(manifest),
97 }));
98}
99
100const waitVerdict = async () => {
101 await page.waitForFunction(() => {
102 const d = document.getElementById('dot');
103 return d && (d.classList.contains('ok') || d.classList.contains('no') || d.classList.contains('warn'));
104 }, { timeout: 60000 });
105 return page.evaluate(() => {
106 const d = document.getElementById('dot');
107 return {
108 klass: d.className,
109 headline: document.getElementById('headline').textContent,
110 checks: [...document.querySelectorAll('#checks li')].map(li => li.textContent),
111 };
112 });
113};
114
115/// A check the page drew, and whether it drew it GREEN.
116///
117/// The page marks each line ✓ / ✗ / ?, and reading only the NAME was how this
118/// file reported "the per-file check passed" while the page was showing that
119/// same line with a ✗ and twenty-four differing files against it. A check that
120/// only asks whether a line exists is a check that cannot fail.
121const passed = (checks, re) => checks.some(c => re.test(c) && c.trim().startsWith('✓'));
122const line = (checks, re) => checks.find(c => re.test(c)) || '(no such check)';
123
124// ── 1. A clean served build verifies green ──────────────────────────
125if (BREAK === 'serve') {
126 await page.route('**/js/render.js', r => r.fulfill({
127 status: 200, contentType: 'text/javascript', body: '/* BREAK=serve */\n' }));
128}
129await page.goto(`${APP}/verify.html`, { waitUntil: 'domcontentloaded' });
130let v = await waitVerdict();
131check('a clean served build reports OK', /\bok\b/.test(v.klass), v.headline);
132check('the public-log seal check passed', passed(v.checks, /sealed in the public log/),
133 line(v.checks, /log/));
134check('the per-file check passed', passed(v.checks, /every served file matches/),
135 line(v.checks, /every served file/));
136
137// ── 2. A tampered served file is caught ─────────────────────────────
138if (BREAK !== 'notamper') {
139 await page.route('**/js/render.js', r => r.fulfill({
140 status: 200, contentType: 'text/javascript', body: '/* TAMPERED */\n' }));
141}
142await page.goto(`${APP}/verify.html`, { waitUntil: 'domcontentloaded' });
143v = await waitVerdict();
144check('a tampered served file fails the verdict', /\bno\b/.test(v.klass), v.headline);
145check('the tampered file is named as differing',
146 v.checks.some(c => /every served file matches/.test(c) && /js\/render\.js/.test(c)),
147 line(v.checks, /every served file/));
148
149await s.close();
150console.log('\n' + ok.length + ' ok, ' + bad.length + ' failed');
151process.exit(bad.length ? 1 : 0);