verifyhash 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/cli/agent-hook.js +431 -0
- package/docs/ADOPT.md +15 -5
- package/docs/AGENT-HOOK.md +111 -0
- package/docs/ANCHORING.md +43 -22
- package/docs/PUBLISH-VERIFY-VH.md +45 -0
- package/examples/README.md +185 -0
- package/examples/policy.lenient.json +5 -0
- package/examples/policy.strict.json +6 -0
- package/examples/run.js +366 -0
- package/examples/sample-dataset/README.txt +10 -0
- package/examples/sample-dataset/corpus/cc-by-poem.txt +8 -0
- package/examples/sample-dataset/corpus/mit-notes.txt +4 -0
- package/examples/sample-dataset/data/unlabeled.txt +5 -0
- package/examples/sample-dataset/vendored/gpl-snippet.txt +5 -0
- package/examples/sample-dataset.hints.json +7 -0
- package/examples/sample-parcel/data/manifest-of-contents.txt +7 -0
- package/examples/sample-parcel/data/records.csv +4 -0
- package/examples/sample-parcel/delivery-note.txt +9 -0
- package/package.json +26 -3
- package/verifier/README.md +584 -0
- package/verifier/action/README.md +87 -0
- package/verifier/action/action.yml +146 -0
- package/verifier/build-standalone-html.js +1287 -0
- package/verifier/build-standalone.js +989 -0
- package/verifier/ci/journal.generic.sh +96 -0
- package/verifier/ci/journal.github-actions.yml +99 -0
- package/verifier/ci/reproduce-vh.generic.sh +59 -0
- package/verifier/ci/reproduce-vh.github-actions.yml +49 -0
- package/verifier/ci/verify-service.generic.sh +96 -0
- package/verifier/ci/verify-service.github-actions.yml +88 -0
- package/verifier/ci/verify-vh.generic.sh +75 -0
- package/verifier/ci/verify-vh.github-actions.yml +56 -0
- package/verifier/dist/BUILD-PROVENANCE.json +210 -0
- package/verifier/dist/seal-vh-standalone.js +876 -0
- package/verifier/dist/seal-vh-standalone.js.sha256 +1 -0
- package/verifier/dist/verify-vh-standalone.html +3373 -0
- package/verifier/dist/verify-vh-standalone.html.sha256 +1 -0
- package/verifier/dist/verify-vh-standalone.js +5123 -0
- package/verifier/dist/verify-vh-standalone.js.sha256 +1 -0
- package/verifier/lib/canonical.js +141 -0
- package/verifier/lib/keccak.js +30 -0
- package/verifier/lib/keccak256-vendored.js +206 -0
- package/verifier/lib/merkle.js +145 -0
- package/verifier/lib/revocation-core.js +606 -0
- package/verifier/lib/revocation.js +200 -0
- package/verifier/lib/seal-cli.js +374 -0
- package/verifier/lib/seal-evidence.js +237 -0
- package/verifier/lib/secp256k1-recover.js +249 -0
- package/verifier/package.json +39 -0
- package/verifier/verify-vh.js +3376 -0
- package/docs/ADOPTION.json +0 -11
- package/docs/AUDIT.md +0 -55
- package/docs/DECIDE.md +0 -47
- package/docs/DECISIONS-PENDING.md +0 -27
- package/docs/DEPLOY-PUBLIC-SITE.md +0 -301
- package/docs/ENGINE-LEDGER.json +0 -12
- package/docs/LOOP-AUDIT-2026-07-03.json +0 -580
- package/docs/LOOP-HARDENING-PLAN.md +0 -44
- package/docs/METRICS.jsonl +0 -31
- package/docs/MORNING.md +0 -204
- package/docs/STRATEGY-ARCHIVE.md +0 -5055
- package/docs/SUPERVISOR-RUNBOOK.md +0 -52
- package/docs/USAGE-BUDGET.json +0 -121
|
@@ -0,0 +1,1287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// verifier/build-standalone-html.js — the DETERMINISTIC, OFFLINE, zero-third-party-dependency bundler
|
|
5
|
+
// that inlines the T-66.1 PURE verify engine (the marked block of verifier/verify-vh.js) + the verifier
|
|
6
|
+
// libs into ONE self-contained, fully OFFLINE HTML page:
|
|
7
|
+
//
|
|
8
|
+
// verifier/dist/verify-vh-standalone.html (the single-file OFFLINE verify page, FREE tier)
|
|
9
|
+
// verifier/dist/verify-vh-standalone.html.sha256 (the published `sha256sum -c` sidecar)
|
|
10
|
+
// verifier/dist/BUILD-PROVENANCE.json (the shared manifest — this build ADDS its target)
|
|
11
|
+
//
|
|
12
|
+
// It mirrors verifier/build-standalone.js's + trustledger/build-standalone.js's PROVEN technique:
|
|
13
|
+
// * an EXPLICIT, FIXED module list (never a filesystem walk), inlined VERBATIM;
|
|
14
|
+
// * ONLY require() specifiers are rewritten, to a memoizing __require(id) CommonJS shim;
|
|
15
|
+
// * NO timestamp, NO randomness — the emitted bytes are a pure function of the committed sources, so
|
|
16
|
+
// two builds are BYTE-IDENTICAL and `--check` re-compiles everything from source and compares against
|
|
17
|
+
// the committed dist files (a stale bundle is a named MISMATCH, red in CI).
|
|
18
|
+
//
|
|
19
|
+
// WHY THIS EXISTS (T-66.2 / EPIC-66)
|
|
20
|
+
// The cold-prospect 60-second challenge was Node-gated ("you need node >= 18 on your PATH"). This file
|
|
21
|
+
// removes that gate: the human sends ONE link/file; the prospect opens it IN A BROWSER, clicks the
|
|
22
|
+
// built-in sample packet, watches ACCEPT, changes ONE byte of a sample file IN THE PAGE, and watches
|
|
23
|
+
// REJECT name that file — then drags their OWN sealed packet in. NO Node, no install, no network, no
|
|
24
|
+
// trust in us. The privacy/no-network claim is not prose, it is checkable: the emitted file contains NO
|
|
25
|
+
// network API token at all (no fetch(, no XMLHttpRequest, no WebSocket, no EventSource, no sendBeacon,
|
|
26
|
+
// no dynamic import( — pinned by test/verifier.standalone-html.test.js), so the browser devtools
|
|
27
|
+
// Network tab stays empty.
|
|
28
|
+
//
|
|
29
|
+
// WHAT THE EMITTED FILE CONTAINS
|
|
30
|
+
// (a) a DOM-FREE engine <script> between recognizable markers (__VERIFY_VH_ENGINE_BEGIN__ /
|
|
31
|
+
// __VERIFY_VH_ENGINE_END__): the __modules registry inlining keccak256-vendored, merkle, canonical,
|
|
32
|
+
// secp256k1-recover, revocation-core VERBATIM, plus (i) a tiny pure-JS `Buffer` subset shim (the
|
|
33
|
+
// ONE Node global those libs use; the shim implements exactly the surface they call), (ii) the
|
|
34
|
+
// T-66.1 PURE ENGINE SLICE of verify-vh.js — the exact bytes between its BEGIN/END engine markers,
|
|
35
|
+
// wrapped in a build-generated module preamble that binds merkle/canonical/recoverPersonalSignAddress
|
|
36
|
+
// /revocation through __require — and (iii) the embedded demo fixture + a 60-second-challenge
|
|
37
|
+
// runner. No document/window reference — a Node test extracts this block, evaluates it in a BARE
|
|
38
|
+
// `vm` context, and asserts its verdict objects are BYTE-IDENTICAL to the in-tree
|
|
39
|
+
// verifyArtifactFromBytes across the whole verdict matrix.
|
|
40
|
+
// (b) the page UI (drag-and-drop / file picker / folder picker, optional vendor pin, optional
|
|
41
|
+
// revocations drop, the editable built-in sample) — plain DOM script, OUTSIDE the engine markers.
|
|
42
|
+
// (c) the HONEST BOUNDARY, verbatim and visible: ACCEPT is tamper-evidence that these exact bytes match
|
|
43
|
+
// the seal — NOT a trusted timestamp and NOT proof of WHEN; for CI/production gating use the node
|
|
44
|
+
// standalone (verify-vh-standalone.js).
|
|
45
|
+
//
|
|
46
|
+
// OFFLINE + READ-ONLY: this builder reads the committed source files and writes ONLY under
|
|
47
|
+
// verifier/dist/. It opens NO socket and makes NO network call. It never require()s verify-vh.js (or
|
|
48
|
+
// anything that pulls js-sha3) — the demo fixture is extracted TEXTUALLY from the committed source and
|
|
49
|
+
// evaluated in a bare `vm` context, so a copied verifier/ tree with no node_modules can still `--check`.
|
|
50
|
+
|
|
51
|
+
const fs = require("fs");
|
|
52
|
+
const path = require("path");
|
|
53
|
+
const crypto = require("crypto");
|
|
54
|
+
const vm = require("vm");
|
|
55
|
+
|
|
56
|
+
const VERIFIER_DIR = __dirname;
|
|
57
|
+
const DIST_DIR = path.join(VERIFIER_DIR, "dist");
|
|
58
|
+
const OUT_PATH = path.join(DIST_DIR, "verify-vh-standalone.html");
|
|
59
|
+
const SHA256_PATH = OUT_PATH + ".sha256";
|
|
60
|
+
const SHA256_BASENAME = path.basename(OUT_PATH);
|
|
61
|
+
const PROVENANCE_PATH = path.join(DIST_DIR, "BUILD-PROVENANCE.json");
|
|
62
|
+
// The SAME version-free schema tag the shared manifest uses.
|
|
63
|
+
const PROVENANCE_SCHEMA = "verifyhash/build-provenance@1";
|
|
64
|
+
// The html target's key in the shared BUILD-PROVENANCE.json `targets` map.
|
|
65
|
+
const HTML_TARGET_NAME = "verify-html";
|
|
66
|
+
|
|
67
|
+
// The T-66.1 engine markers in verifier/verify-vh.js — the extraction seam this build slices.
|
|
68
|
+
const VV_ENGINE_BEGIN =
|
|
69
|
+
"// ============================ BEGIN VERIFY-VH PURE ENGINE (T-66.1) ============================";
|
|
70
|
+
const VV_ENGINE_END =
|
|
71
|
+
"// ============================= END VERIFY-VH PURE ENGINE (T-66.1) =============================";
|
|
72
|
+
|
|
73
|
+
// The recognizable engine-block markers a Node test extracts + vm-evaluates the emitted block by.
|
|
74
|
+
const ENGINE_BEGIN_MARKER = "// __VERIFY_VH_ENGINE_BEGIN__";
|
|
75
|
+
const ENGINE_END_MARKER = "// __VERIFY_VH_ENGINE_END__";
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Deterministic file reading + hashing (same discipline as the sibling builders).
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
// Read a source file deterministically, normalizing line endings to "\n" (so a checkout with CRLF cannot
|
|
82
|
+
// change the emitted bytes) and stripping a leading shebang line.
|
|
83
|
+
function readSource(rel) {
|
|
84
|
+
let s = fs.readFileSync(path.join(VERIFIER_DIR, rel), "utf8");
|
|
85
|
+
s = s.replace(/\r\n/g, "\n");
|
|
86
|
+
s = s.replace(/^#![^\n]*\n/, "");
|
|
87
|
+
return s;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// sha256 hex of a utf8 string (the canonical hash unit for the bundle and its inlined sources).
|
|
91
|
+
function sha256HexOf(text) {
|
|
92
|
+
return crypto.createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The exact textual contents of the `.sha256` sidecar: one canonical line in the standard
|
|
96
|
+
// `sha256sum`/`shasum -a 256` format (`<hex>␠␠<basename>\n`).
|
|
97
|
+
function sha256SidecarFor(bundleText, basename) {
|
|
98
|
+
return `${sha256HexOf(bundleText)} ${basename}\n`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// require() rewriting — identical technique to the sibling builders, but STRICTER: the engine block runs
|
|
103
|
+
// in a BROWSER <script>, so NO require may survive verbatim (there is no Node core in a page). Every
|
|
104
|
+
// specifier must be in the module's rewrite map; anything else is a hard build error.
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
function requireSpecifiers(src) {
|
|
108
|
+
return [...src.matchAll(/require\(\s*["']([^"']+)["']\s*\)/g)].map((m) => m[1]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function rewriteRequires(src, rewrite, idForError) {
|
|
112
|
+
for (const spec of requireSpecifiers(src)) {
|
|
113
|
+
if (!Object.prototype.hasOwnProperty.call(rewrite, spec)) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`build-standalone-html: module "${idForError}" has an un-inlined require(${JSON.stringify(spec)}). ` +
|
|
116
|
+
"The browser bundle can require NOTHING — add it to the module's rewrite map (and inline its target)."
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return src.replace(/require\(\s*["']([^"']+)["']\s*\)/g, (_full, spec) => {
|
|
121
|
+
return `__require(${JSON.stringify(rewrite[spec])})`;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// SYNTHETIC module bodies. Everything else is inlined verbatim (rewrites aside).
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
// (1) The pure-JS `Buffer` subset the inlined verifier libs (merkle / canonical / secp256k1-recover /
|
|
130
|
+
// the keccak shim) reference as a bare global. It implements EXACTLY the surface they use — from(array |
|
|
131
|
+
// utf8-string | hex-string | Uint8Array), alloc, concat, compare, isBuffer, toString("hex"), and the
|
|
132
|
+
// species-preserving slice/subarray a Uint8Array subclass inherits — and NOTHING else (any other
|
|
133
|
+
// encoding throws by name). UTF-8 encoding matches Node's (lone surrogates -> U+FFFD); hex decoding
|
|
134
|
+
// matches Node's (stop at the first non-hex pair; a trailing odd nibble is dropped).
|
|
135
|
+
const BUFFER_SHIM_BODY = [
|
|
136
|
+
'"use strict";',
|
|
137
|
+
"// Minimal PURE-JS Buffer subset for the browser/vm (see verifier/build-standalone-html.js). NOT a",
|
|
138
|
+
"// general Node Buffer — exactly the calls the inlined verifier libs make, nothing more.",
|
|
139
|
+
"var HEX_CHARS = \"0123456789abcdef\";",
|
|
140
|
+
"function hexNibble(ch) {",
|
|
141
|
+
" var c = ch.charCodeAt(0);",
|
|
142
|
+
" if (c >= 48 && c <= 57) return c - 48; // 0-9",
|
|
143
|
+
" if (c >= 97 && c <= 102) return c - 87; // a-f",
|
|
144
|
+
" if (c >= 65 && c <= 70) return c - 55; // A-F",
|
|
145
|
+
" return -1;",
|
|
146
|
+
"}",
|
|
147
|
+
"function utf8Encode(str) {",
|
|
148
|
+
" var out = [];",
|
|
149
|
+
" for (var i = 0; i < str.length; i++) {",
|
|
150
|
+
" var c = str.codePointAt(i);",
|
|
151
|
+
" if (c > 0xffff) i++; // consumed a full surrogate pair",
|
|
152
|
+
" if (c >= 0xd800 && c <= 0xdfff) c = 0xfffd; // lone surrogate -> replacement char (Node semantics)",
|
|
153
|
+
" if (c < 0x80) out.push(c);",
|
|
154
|
+
" else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 63));",
|
|
155
|
+
" else if (c < 0x10000) out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));",
|
|
156
|
+
" else out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 63), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));",
|
|
157
|
+
" }",
|
|
158
|
+
" return out;",
|
|
159
|
+
"}",
|
|
160
|
+
"function hexDecode(str) {",
|
|
161
|
+
" var out = [];",
|
|
162
|
+
" for (var i = 0; i + 1 < str.length; i += 2) {",
|
|
163
|
+
" var hi = hexNibble(str[i]);",
|
|
164
|
+
" var lo = hexNibble(str[i + 1]);",
|
|
165
|
+
" if (hi < 0 || lo < 0) break; // Node stops at the first invalid pair",
|
|
166
|
+
" out.push((hi << 4) | lo);",
|
|
167
|
+
" }",
|
|
168
|
+
" return out;",
|
|
169
|
+
"}",
|
|
170
|
+
"class VhBuffer extends Uint8Array {",
|
|
171
|
+
" toString(enc) {",
|
|
172
|
+
' if (enc !== "hex") {',
|
|
173
|
+
" throw new Error(\"vh-buffer supports only .toString('hex'), got: \" + String(enc));",
|
|
174
|
+
" }",
|
|
175
|
+
' var s = "";',
|
|
176
|
+
" for (var i = 0; i < this.length; i++) {",
|
|
177
|
+
" s += HEX_CHARS[this[i] >> 4] + HEX_CHARS[this[i] & 15];",
|
|
178
|
+
" }",
|
|
179
|
+
" return s;",
|
|
180
|
+
" }",
|
|
181
|
+
" static from(input, enc) {",
|
|
182
|
+
' if (typeof input === "string") {',
|
|
183
|
+
' if (enc === "hex") return new VhBuffer(hexDecode(input));',
|
|
184
|
+
' if (enc === undefined || enc === "utf8" || enc === "utf-8") return new VhBuffer(utf8Encode(input));',
|
|
185
|
+
' throw new Error("vh-buffer supports only utf8/hex string encodings, got: " + String(enc));',
|
|
186
|
+
" }",
|
|
187
|
+
" if (input instanceof Uint8Array || Array.isArray(input)) return new VhBuffer(input);",
|
|
188
|
+
' throw new TypeError("vh-buffer: Buffer.from requires a string, array, or Uint8Array");',
|
|
189
|
+
" }",
|
|
190
|
+
" static alloc(n) {",
|
|
191
|
+
" return new VhBuffer(n); // zero-filled, like Node's Buffer.alloc",
|
|
192
|
+
" }",
|
|
193
|
+
" static concat(list) {",
|
|
194
|
+
" var total = 0;",
|
|
195
|
+
" for (var i = 0; i < list.length; i++) total += list[i].length;",
|
|
196
|
+
" var out = new VhBuffer(total);",
|
|
197
|
+
" var off = 0;",
|
|
198
|
+
" for (var j = 0; j < list.length; j++) {",
|
|
199
|
+
" out.set(list[j], off);",
|
|
200
|
+
" off += list[j].length;",
|
|
201
|
+
" }",
|
|
202
|
+
" return out;",
|
|
203
|
+
" }",
|
|
204
|
+
" static compare(a, b) {",
|
|
205
|
+
" var n = Math.min(a.length, b.length);",
|
|
206
|
+
" for (var i = 0; i < n; i++) {",
|
|
207
|
+
" if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;",
|
|
208
|
+
" }",
|
|
209
|
+
" return a.length === b.length ? 0 : a.length < b.length ? -1 : 1;",
|
|
210
|
+
" }",
|
|
211
|
+
" static isBuffer(x) {",
|
|
212
|
+
" return x instanceof VhBuffer;",
|
|
213
|
+
" }",
|
|
214
|
+
"}",
|
|
215
|
+
"module.exports = { Buffer: VhBuffer };",
|
|
216
|
+
].join("\n");
|
|
217
|
+
|
|
218
|
+
// (2) The keccak provider the inlined libs require as "./keccak" — body SWAPPED (exactly like the JS
|
|
219
|
+
// bundles' shim) to be backed by the vendored pure-JS keccak256, returning the bundle's pure-JS Buffer so
|
|
220
|
+
// downstream `.slice(...).toString("hex")` / `Buffer.concat([...])` callers behave exactly as in Node.
|
|
221
|
+
const KECCAK_SHIM_BODY = [
|
|
222
|
+
'"use strict";',
|
|
223
|
+
"// Inlined keccak provider for the standalone HTML page: the SAME `keccak256(bytes) -> Buffer` surface",
|
|
224
|
+
"// as verifier/lib/keccak.js, but backed by the PURE-JS vendored implementation",
|
|
225
|
+
"// (verifier/lib/keccak256-vendored.js) and returning the bundle's pure-JS Buffer (vh-buffer).",
|
|
226
|
+
'var vendored = __require("keccak256-vendored");',
|
|
227
|
+
"function keccak256(bytes) {",
|
|
228
|
+
" if (!(bytes instanceof Uint8Array)) {",
|
|
229
|
+
' throw new TypeError("keccak256 requires a Buffer/Uint8Array of input bytes");',
|
|
230
|
+
" }",
|
|
231
|
+
" return Buffer.from(vendored.keccak256(bytes));",
|
|
232
|
+
"}",
|
|
233
|
+
"module.exports = { keccak256: keccak256 };",
|
|
234
|
+
].join("\n");
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// The T-66.1 ENGINE SLICE: the exact bytes of verifier/verify-vh.js between its BEGIN/END engine markers
|
|
238
|
+
// (the block test/verifier.browser-core.test.js proves is pure of fs/os/path/process and require()-free),
|
|
239
|
+
// wrapped in a build-generated preamble that binds its four module-scope names through __require, plus a
|
|
240
|
+
// build-generated exports postamble. NOTHING inside the slice is transformed.
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
function extractEngineSlice() {
|
|
244
|
+
const src = readSource("verify-vh.js");
|
|
245
|
+
const begin = src.indexOf(VV_ENGINE_BEGIN);
|
|
246
|
+
const end = src.indexOf(VV_ENGINE_END);
|
|
247
|
+
if (begin === -1 || end === -1 || end <= begin) {
|
|
248
|
+
throw new Error("build-standalone-html: verify-vh.js engine markers not found (or out of order)");
|
|
249
|
+
}
|
|
250
|
+
if (src.indexOf(VV_ENGINE_BEGIN, begin + 1) !== -1 || src.indexOf(VV_ENGINE_END, end + 1) !== -1) {
|
|
251
|
+
throw new Error("build-standalone-html: verify-vh.js engine markers must be unique");
|
|
252
|
+
}
|
|
253
|
+
return src.slice(begin + VV_ENGINE_BEGIN.length, end);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function engineSliceBody() {
|
|
257
|
+
const slice = extractEngineSlice();
|
|
258
|
+
const preamble = [
|
|
259
|
+
'"use strict";',
|
|
260
|
+
"// BUILD-GENERATED PREAMBLE (verifier/build-standalone-html.js): the four module-scope bindings the",
|
|
261
|
+
"// T-66.1 engine slice references, resolved through the bundle's own __require graph. `revocation`",
|
|
262
|
+
"// binds the PURE decision core directly (verifier/lib/revocation-core.js) — the engine slice only",
|
|
263
|
+
"// ever touches the pure surface (proven by test/verifier.browser-core.test.js), never the fs reader.",
|
|
264
|
+
'var merkle = __require("merkle");',
|
|
265
|
+
'var canonical = __require("canonical");',
|
|
266
|
+
'var recoverPersonalSignAddress = __require("secp256k1-recover").recoverPersonalSignAddress;',
|
|
267
|
+
'var revocation = __require("revocation-core");',
|
|
268
|
+
"// ---- the verbatim T-66.1 engine slice of verifier/verify-vh.js follows. ----",
|
|
269
|
+
].join("\n");
|
|
270
|
+
const postamble = [
|
|
271
|
+
"// BUILD-GENERATED EXPORTS (verifier/build-standalone-html.js): the engine surface the page drives.",
|
|
272
|
+
"module.exports = {",
|
|
273
|
+
" EXIT: EXIT,",
|
|
274
|
+
" KINDS: KINDS,",
|
|
275
|
+
" TRUST_NOTE: TRUST_NOTE,",
|
|
276
|
+
" UsageError: UsageError,",
|
|
277
|
+
" IOError: IOError,",
|
|
278
|
+
" MAX_RELPATH_CHARS: MAX_RELPATH_CHARS,",
|
|
279
|
+
" verifyArtifactFromBytes: verifyArtifactFromBytes,",
|
|
280
|
+
"};",
|
|
281
|
+
].join("\n");
|
|
282
|
+
return preamble + slice + postamble;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// The embedded DEMO FIXTURE — the verifier's OWN shipped, genuinely-signed demo packet (DEMO_SIGNER /
|
|
287
|
+
// DEMO_FILES / DEMO_CONTAINER / DEMO_PACKET_NAME in verify-vh.js), inlined VERBATIM (not re-authored).
|
|
288
|
+
// Extracted TEXTUALLY from the committed source and evaluated in a bare `vm` context (no requires, so a
|
|
289
|
+
// copied tree with no node_modules can still build/`--check`). Pure function of source -> deterministic.
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
function extractDemoFixture() {
|
|
293
|
+
const src = readSource("verify-vh.js");
|
|
294
|
+
const start = src.indexOf("const DEMO_SIGNER =");
|
|
295
|
+
const nameAt = src.indexOf("const DEMO_PACKET_NAME =");
|
|
296
|
+
// T-68.3: the demo constants now END at the agent-demo tamper pair (the last demo const). The slice
|
|
297
|
+
// still holds ONLY const declarations (+ comments) — self-contained, no requires, no I/O.
|
|
298
|
+
const agentEndAt = src.indexOf("const DEMO_AGENT_TAMPER_TO =");
|
|
299
|
+
if (start === -1 || nameAt === -1 || agentEndAt === -1 || nameAt <= start || agentEndAt <= nameAt) {
|
|
300
|
+
throw new Error("build-standalone-html: verify-vh.js demo fixture anchors not found (or out of order)");
|
|
301
|
+
}
|
|
302
|
+
const declEnd = src.indexOf(";", agentEndAt);
|
|
303
|
+
if (declEnd === -1) {
|
|
304
|
+
throw new Error("build-standalone-html: verify-vh.js DEMO_AGENT_TAMPER_TO declaration is unterminated");
|
|
305
|
+
}
|
|
306
|
+
const decl = src.slice(start, declEnd + 1);
|
|
307
|
+
const out = vm.runInNewContext(
|
|
308
|
+
decl +
|
|
309
|
+
"\n;({ signer: DEMO_SIGNER, files: DEMO_FILES, container: DEMO_CONTAINER, packetName: DEMO_PACKET_NAME," +
|
|
310
|
+
" agentPacketName: DEMO_AGENT_PACKET_NAME, agentPacketText: DEMO_AGENT_PACKET_TEXT," +
|
|
311
|
+
" agentTamperSeq: DEMO_AGENT_TAMPER_SEQ, agentTamperFrom: DEMO_AGENT_TAMPER_FROM," +
|
|
312
|
+
" agentTamperTo: DEMO_AGENT_TAMPER_TO });",
|
|
313
|
+
{},
|
|
314
|
+
{ filename: "verify-vh-demo-fixture.js" }
|
|
315
|
+
);
|
|
316
|
+
// Shape sanity so a refactor of the fixture is a HARD build error, never a silently-broken sample.
|
|
317
|
+
if (
|
|
318
|
+
!out ||
|
|
319
|
+
typeof out.signer !== "string" ||
|
|
320
|
+
!/^0x[0-9a-f]{40}$/.test(out.signer) ||
|
|
321
|
+
!out.files ||
|
|
322
|
+
typeof out.files["model-card.md"] !== "string" ||
|
|
323
|
+
!out.container ||
|
|
324
|
+
out.container.kind !== "vh.evidence-seal-signed" ||
|
|
325
|
+
typeof out.packetName !== "string"
|
|
326
|
+
) {
|
|
327
|
+
throw new Error("build-standalone-html: extracted demo fixture has an unexpected shape");
|
|
328
|
+
}
|
|
329
|
+
// Agent-demo sanity (T-68.3): a genuine agent-session packet with ONE redacted event, whose tamper
|
|
330
|
+
// FROM-substring occurs EXACTLY once (so the page's "tamper one byte" edit is well-defined).
|
|
331
|
+
let agentPacket;
|
|
332
|
+
try {
|
|
333
|
+
agentPacket = JSON.parse(out.agentPacketText);
|
|
334
|
+
} catch (_) {
|
|
335
|
+
agentPacket = null;
|
|
336
|
+
}
|
|
337
|
+
if (
|
|
338
|
+
typeof out.agentPacketName !== "string" ||
|
|
339
|
+
!agentPacket ||
|
|
340
|
+
agentPacket.kind !== "vh.agent-session-packet" ||
|
|
341
|
+
!Number.isInteger(out.agentTamperSeq) ||
|
|
342
|
+
typeof out.agentTamperFrom !== "string" ||
|
|
343
|
+
typeof out.agentTamperTo !== "string" ||
|
|
344
|
+
out.agentPacketText.split(out.agentTamperFrom).length !== 2
|
|
345
|
+
) {
|
|
346
|
+
throw new Error("build-standalone-html: extracted agent demo fixture has an unexpected shape");
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// The file the built-in challenge tampers (one byte, in the page).
|
|
352
|
+
const TAMPER_FILE = "model-card.md";
|
|
353
|
+
|
|
354
|
+
// (3) The embedded demo-fixture module body.
|
|
355
|
+
function challengeFixtureBody() {
|
|
356
|
+
const demo = extractDemoFixture();
|
|
357
|
+
return [
|
|
358
|
+
'"use strict";',
|
|
359
|
+
"// The verifier's SHIPPED demo packet (verify-vh.js DEMO_SIGNER/DEMO_FILES/DEMO_CONTAINER), inlined",
|
|
360
|
+
"// VERBATIM at build time — a REAL vh.evidence-seal-signed container signed by the fixed TEST-ONLY",
|
|
361
|
+
"// key (hardhat account #1; never a real key / real funds). CONTAINER_TEXT is the exact JSON the",
|
|
362
|
+
"// in-tree demo verifies, so the page's sample verdict is byte-identical to `verify-vh demo`'s.",
|
|
363
|
+
`var SIGNER = ${JSON.stringify(demo.signer)};`,
|
|
364
|
+
`var PACKET_NAME = ${JSON.stringify(demo.packetName)};`,
|
|
365
|
+
`var CONTAINER_TEXT = ${JSON.stringify(JSON.stringify(demo.container))};`,
|
|
366
|
+
`var FILES = ${JSON.stringify(demo.files)};`,
|
|
367
|
+
`var TAMPER_FILE = ${JSON.stringify(TAMPER_FILE)};`,
|
|
368
|
+
"// The AGENT-SESSION demo packet (T-68.3): the verifier's shipped DEMO_AGENT_* constants, inlined",
|
|
369
|
+
"// VERBATIM — a genuine `vh.agent-session-packet` with one tool_call payload REDACTED behind its",
|
|
370
|
+
"// hash commitment (it STILL verifies). The TAMPER_FROM/TO pair is a one-byte payload edit that",
|
|
371
|
+
"// occurs exactly once in the packet text, so the page's agent challenge is deterministic.",
|
|
372
|
+
`var AGENT_PACKET_NAME = ${JSON.stringify(demo.agentPacketName)};`,
|
|
373
|
+
`var AGENT_PACKET_TEXT = ${JSON.stringify(demo.agentPacketText)};`,
|
|
374
|
+
`var AGENT_TAMPER_SEQ = ${JSON.stringify(demo.agentTamperSeq)};`,
|
|
375
|
+
`var AGENT_TAMPER_FROM = ${JSON.stringify(demo.agentTamperFrom)};`,
|
|
376
|
+
`var AGENT_TAMPER_TO = ${JSON.stringify(demo.agentTamperTo)};`,
|
|
377
|
+
"module.exports = {",
|
|
378
|
+
" SIGNER: SIGNER,",
|
|
379
|
+
" PACKET_NAME: PACKET_NAME,",
|
|
380
|
+
" CONTAINER_TEXT: CONTAINER_TEXT,",
|
|
381
|
+
" FILES: FILES,",
|
|
382
|
+
" TAMPER_FILE: TAMPER_FILE,",
|
|
383
|
+
" AGENT_PACKET_NAME: AGENT_PACKET_NAME,",
|
|
384
|
+
" AGENT_PACKET_TEXT: AGENT_PACKET_TEXT,",
|
|
385
|
+
" AGENT_TAMPER_SEQ: AGENT_TAMPER_SEQ,",
|
|
386
|
+
" AGENT_TAMPER_FROM: AGENT_TAMPER_FROM,",
|
|
387
|
+
" AGENT_TAMPER_TO: AGENT_TAMPER_TO,",
|
|
388
|
+
"};",
|
|
389
|
+
].join("\n");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// (4) The 60-second-challenge runner: verify the genuine embedded packet (signer pinned), then a
|
|
393
|
+
// one-byte-tampered copy, through the SAME verifyArtifactFromBytes the page uses for real packets — no
|
|
394
|
+
// bespoke verify path, so the sample verdicts are exactly what a real packet would get.
|
|
395
|
+
const CHALLENGE_BODY = [
|
|
396
|
+
'"use strict";',
|
|
397
|
+
"// The built-in 60-SECOND CHALLENGE over the embedded demo fixture: genuine -> ACCEPT (signer pinned),",
|
|
398
|
+
"// one tampered byte -> REJECT naming the file. Drives the REAL engine — no special-case verify path.",
|
|
399
|
+
'var engine = __require("verify-vh-engine");',
|
|
400
|
+
'var fixture = __require("challenge-fixture");',
|
|
401
|
+
"function toFilesMap(contents) {",
|
|
402
|
+
" var m = {};",
|
|
403
|
+
" var keys = Object.keys(contents);",
|
|
404
|
+
" for (var i = 0; i < keys.length; i++) {",
|
|
405
|
+
' m[keys[i]] = Buffer.from(contents[keys[i]], "utf8");',
|
|
406
|
+
" }",
|
|
407
|
+
" return m;",
|
|
408
|
+
"}",
|
|
409
|
+
"function verifyContents(contents) {",
|
|
410
|
+
" return engine.verifyArtifactFromBytes({",
|
|
411
|
+
" artifactText: fixture.CONTAINER_TEXT,",
|
|
412
|
+
" files: toFilesMap(contents),",
|
|
413
|
+
" vendor: fixture.SIGNER,",
|
|
414
|
+
" artifactName: fixture.PACKET_NAME,",
|
|
415
|
+
" });",
|
|
416
|
+
"}",
|
|
417
|
+
"function runChallenge() {",
|
|
418
|
+
" var genuine = verifyContents(fixture.FILES);",
|
|
419
|
+
" var tamperedFiles = {};",
|
|
420
|
+
" var keys = Object.keys(fixture.FILES);",
|
|
421
|
+
" for (var i = 0; i < keys.length; i++) tamperedFiles[keys[i]] = fixture.FILES[keys[i]];",
|
|
422
|
+
' tamperedFiles[fixture.TAMPER_FILE] = fixture.FILES[fixture.TAMPER_FILE] + "X";',
|
|
423
|
+
" var tampered = verifyContents(tamperedFiles);",
|
|
424
|
+
" return {",
|
|
425
|
+
" genuine: genuine,",
|
|
426
|
+
" tampered: tampered,",
|
|
427
|
+
" signer: fixture.SIGNER,",
|
|
428
|
+
" tamperedFile: fixture.TAMPER_FILE,",
|
|
429
|
+
" packetName: fixture.PACKET_NAME,",
|
|
430
|
+
" };",
|
|
431
|
+
"}",
|
|
432
|
+
"// The AGENT-SESSION challenge (T-68.3): the SAME engine verifies the embedded *.vhagent.json",
|
|
433
|
+
"// packet — SELF-CONTAINED, so the files map is empty. genuine -> ACCEPT (one payload redacted,",
|
|
434
|
+
"// still verifies); a one-byte payload tamper -> REJECT naming the offending event seq.",
|
|
435
|
+
"function verifyAgentText(packetText) {",
|
|
436
|
+
" return engine.verifyArtifactFromBytes({",
|
|
437
|
+
" artifactText: packetText,",
|
|
438
|
+
" files: {},",
|
|
439
|
+
" artifactName: fixture.AGENT_PACKET_NAME,",
|
|
440
|
+
" });",
|
|
441
|
+
"}",
|
|
442
|
+
"function runAgentChallenge() {",
|
|
443
|
+
" var genuine = verifyAgentText(fixture.AGENT_PACKET_TEXT);",
|
|
444
|
+
" var tamperedText = fixture.AGENT_PACKET_TEXT.replace(fixture.AGENT_TAMPER_FROM, fixture.AGENT_TAMPER_TO);",
|
|
445
|
+
" var tampered = verifyAgentText(tamperedText);",
|
|
446
|
+
" return {",
|
|
447
|
+
" genuine: genuine,",
|
|
448
|
+
" tampered: tampered,",
|
|
449
|
+
" tamperSeq: fixture.AGENT_TAMPER_SEQ,",
|
|
450
|
+
" packetName: fixture.AGENT_PACKET_NAME,",
|
|
451
|
+
" };",
|
|
452
|
+
"}",
|
|
453
|
+
"module.exports = {",
|
|
454
|
+
" runChallenge: runChallenge,",
|
|
455
|
+
" verifyContents: verifyContents,",
|
|
456
|
+
" runAgentChallenge: runAgentChallenge,",
|
|
457
|
+
" verifyAgentText: verifyAgentText,",
|
|
458
|
+
" fixture: fixture,",
|
|
459
|
+
"};",
|
|
460
|
+
].join("\n");
|
|
461
|
+
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
// The EXPLICIT, FIXED engine module list. Order is deterministic by construction (hand-listed).
|
|
464
|
+
// { id, file, rewrite } inlines the file VERBATIM (rewrites aside); { id, body } is a synthetic module
|
|
465
|
+
// whose logic lives in THIS builder; { id, file, slice: true } is the marked verify-vh.js engine slice.
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
|
|
468
|
+
const HTML_MODULES = [
|
|
469
|
+
// The pure-JS Buffer subset every inlined lib references as a bare global (bound lexically below).
|
|
470
|
+
{ id: "vh-buffer", body: BUFFER_SHIM_BODY, note: "pure-JS browser Buffer subset shim — defined in build-standalone-html.js, not a source file" },
|
|
471
|
+
// The pure-JS keccak256 — inlined VERBATIM (it requires nothing).
|
|
472
|
+
{ id: "keccak256-vendored", file: "lib/keccak256-vendored.js", rewrite: {} },
|
|
473
|
+
// The keccak provider the libs require as "./keccak" — body SWAPPED for the vendored-backed shim.
|
|
474
|
+
{ id: "keccak", body: KECCAK_SHIM_BODY, note: "swapped body (keccak provider shim over the vendored pure-JS keccak256, returning vh-buffer Buffers) — defined in build-standalone-html.js, not a source file" },
|
|
475
|
+
// The independent merkle / canonical / secp256k1 / pure-revocation libs, inlined verbatim.
|
|
476
|
+
{ id: "merkle", file: "lib/merkle.js", rewrite: { "./keccak": "keccak" } },
|
|
477
|
+
{ id: "canonical", file: "lib/canonical.js", rewrite: {} },
|
|
478
|
+
{ id: "secp256k1-recover", file: "lib/secp256k1-recover.js", rewrite: { "./keccak": "keccak" } },
|
|
479
|
+
{ id: "revocation-core", file: "lib/revocation-core.js", rewrite: { "./secp256k1-recover": "secp256k1-recover" } },
|
|
480
|
+
// The T-66.1 pure engine slice of verify-vh.js — the entry surface the page drives.
|
|
481
|
+
{ id: "verify-vh-engine", file: "verify-vh.js", slice: true, entry: true, note: "the marked T-66.1 pure-engine slice of verifier/verify-vh.js (the bytes between its BEGIN/END engine markers), wrapped in a build-generated __require preamble + exports postamble; sourceSha256 pins the WHOLE verify-vh.js source file" },
|
|
482
|
+
// The embedded demo fixture + the challenge runner (both build-generated; the fixture DATA is extracted
|
|
483
|
+
// verbatim from verify-vh.js, which the engine-slice record above pins by sha256).
|
|
484
|
+
{ id: "challenge-fixture", body: challengeFixtureBody, note: "the verifier's shipped demo packet (DEMO_SIGNER/DEMO_FILES/DEMO_CONTAINER/DEMO_PACKET_NAME), extracted verbatim at build time from verifier/verify-vh.js (pinned by the verify-vh-engine record's sourceSha256) — defined in build-standalone-html.js" },
|
|
485
|
+
{ id: "challenge", body: CHALLENGE_BODY, note: "the built-in 60-second-challenge runner over the embedded demo fixture — defined in build-standalone-html.js, not a source file" },
|
|
486
|
+
];
|
|
487
|
+
|
|
488
|
+
// Resolve a module's inlined body text (synthetic body, engine slice, or verbatim source with rewrites).
|
|
489
|
+
function bodyOf(m) {
|
|
490
|
+
if (m.slice) return engineSliceBody();
|
|
491
|
+
if (m.body != null) return typeof m.body === "function" ? m.body() : m.body;
|
|
492
|
+
return rewriteRequires(readSource(m.file), m.rewrite, m.id);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// The tiny CommonJS shim the engine block embeds (byte-identical to the sibling builders').
|
|
496
|
+
const COMMONJS_SHIM = [
|
|
497
|
+
"// ---- minimal CommonJS module shim (so the inlined modules keep their require() structure) --------",
|
|
498
|
+
"var __modules = Object.create(null);",
|
|
499
|
+
"var __cache = Object.create(null);",
|
|
500
|
+
"function __require(id) {",
|
|
501
|
+
" if (id in __cache) return __cache[id].exports;",
|
|
502
|
+
" var factory = __modules[id];",
|
|
503
|
+
" if (!factory) throw new Error('standalone bundle: unknown module: ' + id);",
|
|
504
|
+
" var module = { exports: {} };",
|
|
505
|
+
" __cache[id] = module;",
|
|
506
|
+
" factory(module, module.exports, __require);",
|
|
507
|
+
" return module.exports;",
|
|
508
|
+
"}",
|
|
509
|
+
];
|
|
510
|
+
|
|
511
|
+
// Build the DOM-free engine <script> block. Everything between the markers is plain computation — no
|
|
512
|
+
// DOM, no network, no clock on the paths the page drives — so `vm.runInNewContext` in a BARE context
|
|
513
|
+
// proves it needs nothing a browser page would provide (not even Node's Buffer: the shim supplies it).
|
|
514
|
+
// The block defines ONE global, VerifyVhStandalone: { engine, challenge }.
|
|
515
|
+
function engineScriptText() {
|
|
516
|
+
const parts = [];
|
|
517
|
+
parts.push("<script>");
|
|
518
|
+
parts.push(ENGINE_BEGIN_MARKER);
|
|
519
|
+
parts.push('"use strict";');
|
|
520
|
+
parts.push("var VerifyVhStandalone = (function () {");
|
|
521
|
+
parts.push(COMMONJS_SHIM.join("\n"));
|
|
522
|
+
parts.push("");
|
|
523
|
+
|
|
524
|
+
let entryId = null;
|
|
525
|
+
for (const m of HTML_MODULES) {
|
|
526
|
+
if (m.entry) entryId = m.id;
|
|
527
|
+
parts.push(`// ===== module: ${m.id}${m.file ? ` (from verifier/${m.file})` : " (build-generated)"} =====`);
|
|
528
|
+
parts.push(`__modules[${JSON.stringify(m.id)}] = function (module, exports, __require) {`);
|
|
529
|
+
parts.push(bodyOf(m));
|
|
530
|
+
parts.push("};");
|
|
531
|
+
parts.push("");
|
|
532
|
+
if (m.id === "vh-buffer") {
|
|
533
|
+
// The lexical `Buffer` every inlined module body resolves as a free identifier — the pure-JS shim,
|
|
534
|
+
// NOT Node's (a browser has none). Declared once, inside the IIFE, before any factory runs.
|
|
535
|
+
parts.push("// The `Buffer` global the inlined verifier libs reference, satisfied by the pure-JS shim above.");
|
|
536
|
+
parts.push('var Buffer = __require("vh-buffer").Buffer;');
|
|
537
|
+
parts.push("");
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (!entryId) throw new Error("build-standalone-html: no entry module declared");
|
|
541
|
+
|
|
542
|
+
parts.push("return {");
|
|
543
|
+
parts.push(` engine: __require(${JSON.stringify(entryId)}),`);
|
|
544
|
+
parts.push(' challenge: __require("challenge"),');
|
|
545
|
+
parts.push("};");
|
|
546
|
+
parts.push("})();");
|
|
547
|
+
parts.push(ENGINE_END_MARKER);
|
|
548
|
+
parts.push("</scr" + "ipt>");
|
|
549
|
+
return parts.join("\n");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ---------------------------------------------------------------------------
|
|
553
|
+
// The PAGE — markup + styles + the DOM glue script (OUTSIDE the engine markers). All fixed strings, so
|
|
554
|
+
// the emitted bytes stay a pure function of the committed sources. Every user/artifact-controlled value
|
|
555
|
+
// is rendered via textContent (never innerHTML), so a hostile relPath cannot inject markup.
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
|
|
558
|
+
const GENERATED_BANNER = [
|
|
559
|
+
"<!--",
|
|
560
|
+
" verify-vh-standalone.html — the SINGLE-FILE, FULLY OFFLINE verifyhash verify page.",
|
|
561
|
+
"",
|
|
562
|
+
" SPDX-License-Identifier: Apache-2.0",
|
|
563
|
+
" Copyright 2026 verifyhash.com - https://verifyhash.com",
|
|
564
|
+
"",
|
|
565
|
+
" GENERATED by verifier/build-standalone-html.js from the in-tree verifier - DO NOT EDIT BY HAND.",
|
|
566
|
+
" Re-generate with: node verifier/build-standalone-html.js (deterministic; `--check` attests the",
|
|
567
|
+
" committed file reproduces byte-for-byte from source, see verifier/dist/BUILD-PROVENANCE.json).",
|
|
568
|
+
"",
|
|
569
|
+
" HOW TO USE IT (no Node, no install, no account, no server): save this ONE file, open it in a",
|
|
570
|
+
" browser, click the built-in sample packet (ACCEPT), change one byte of a sample file in the page",
|
|
571
|
+
" (REJECT, naming the file) - then drag a real sealed packet + its files in. Everything runs inside",
|
|
572
|
+
" the page: the file contains NO network API, so the packet bytes never leave this machine (verify",
|
|
573
|
+
" in your browser devtools Network tab).",
|
|
574
|
+
"",
|
|
575
|
+
" HONEST BOUNDARY: ACCEPT is tamper-evidence that these exact bytes match the seal - and, for a",
|
|
576
|
+
" signed seal, WHO vouched (signer recovery + optional vendor pin). It is NOT a trusted timestamp",
|
|
577
|
+
" and NOT proof of WHEN. For CI/production gating use the node standalone (verify-vh-standalone.js).",
|
|
578
|
+
"-->",
|
|
579
|
+
].join("\n");
|
|
580
|
+
|
|
581
|
+
const PAGE_STYLE = [
|
|
582
|
+
"<style>",
|
|
583
|
+
":root { color-scheme: light; }",
|
|
584
|
+
"* { box-sizing: border-box; }",
|
|
585
|
+
'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;',
|
|
586
|
+
" margin: 0; background: #f6f7f9; color: #1a1f24; line-height: 1.45; }",
|
|
587
|
+
"main { max-width: 880px; margin: 0 auto; padding: 24px 16px 64px; }",
|
|
588
|
+
"h1 { font-size: 1.5rem; margin: 0.2em 0; }",
|
|
589
|
+
"h2 { font-size: 1.15rem; margin: 1.6em 0 0.4em; }",
|
|
590
|
+
"p { margin: 0.5em 0; }",
|
|
591
|
+
".note { color: #444d56; font-size: 0.92rem; }",
|
|
592
|
+
".boundary { background: #fff8e6; border: 1px solid #e0c869; border-radius: 6px; padding: 10px 12px; font-size: 0.92rem; }",
|
|
593
|
+
"section { background: #ffffff; border: 1px solid #d9dee3; border-radius: 8px; padding: 16px; margin-top: 16px; }",
|
|
594
|
+
"button { font: inherit; padding: 7px 14px; border-radius: 6px; border: 1px solid #9aa4ad; background: #eef1f4; cursor: pointer; }",
|
|
595
|
+
"button.primary { background: #1f6feb; border-color: #1f6feb; color: #fff; }",
|
|
596
|
+
"textarea, input[type=text] { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;",
|
|
597
|
+
" font-size: 0.85rem; width: 100%; border: 1px solid #c4ccd4; border-radius: 6px; padding: 8px; }",
|
|
598
|
+
".drop { border: 2px dashed #9aa4ad; border-radius: 8px; padding: 18px; text-align: center; color: #444d56; background: #fafbfc; }",
|
|
599
|
+
".drop.armed { border-color: #1f6feb; background: #eef4ff; }",
|
|
600
|
+
".mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.82rem; overflow-wrap: anywhere; }",
|
|
601
|
+
".verdict { border-radius: 8px; padding: 12px 14px; margin-top: 12px; border: 1px solid; }",
|
|
602
|
+
".verdict.accept { background: #e9f7ec; border-color: #34a853; }",
|
|
603
|
+
".verdict.reject { background: #fdecec; border-color: #d93025; }",
|
|
604
|
+
".verdict-title { font-weight: 700; margin-bottom: 6px; }",
|
|
605
|
+
".kv { margin: 2px 0; }",
|
|
606
|
+
".kv b { display: inline-block; min-width: 12em; font-weight: 600; }",
|
|
607
|
+
"ul.files { margin: 6px 0; padding-left: 1.2em; }",
|
|
608
|
+
".row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 8px 0; }",
|
|
609
|
+
"footer { margin-top: 28px; color: #444d56; font-size: 0.85rem; }",
|
|
610
|
+
"</style>",
|
|
611
|
+
].join("\n");
|
|
612
|
+
|
|
613
|
+
// The honest boundary, verbatim on the page (T-66.2 acceptance d).
|
|
614
|
+
const BOUNDARY_HTML = [
|
|
615
|
+
'<p class="boundary"><strong>Honest boundary:</strong> ACCEPT is tamper-evidence that these exact bytes',
|
|
616
|
+
"match the seal — and, for a signed seal, WHO vouched (signer recovery + optional vendor pin). It is NOT",
|
|
617
|
+
"a trusted timestamp and NOT proof of WHEN without the P-3 trust-root. For CI/production gating use the",
|
|
618
|
+
"node standalone (<code>verify-vh-standalone.js</code>).</p>",
|
|
619
|
+
].join("\n");
|
|
620
|
+
|
|
621
|
+
function pageBodyText() {
|
|
622
|
+
return [
|
|
623
|
+
"<main>",
|
|
624
|
+
"<header>",
|
|
625
|
+
"<h1>verify-vh — offline verifier (in your browser)</h1>",
|
|
626
|
+
'<p class="note">An INDEPENDENT, read-only, fully OFFLINE verifier for verifyhash evidence packets.',
|
|
627
|
+
"It RE-DERIVES the keccak Merkle root from the bytes YOU drop in and recovers the signer with a",
|
|
628
|
+
"pure-JS secp256k1 routine — it never trusts the artifact's own stored hashes. This one file contains",
|
|
629
|
+
"NO network API at all: your packet bytes never leave this machine (check the devtools Network tab).</p>",
|
|
630
|
+
BOUNDARY_HTML,
|
|
631
|
+
"</header>",
|
|
632
|
+
"",
|
|
633
|
+
'<section id="challenge-section">',
|
|
634
|
+
"<h2>1 — The 60-second challenge (built in)</h2>",
|
|
635
|
+
'<p class="note">A real, genuinely-signed sample packet is embedded in this page (signed by a fixed,',
|
|
636
|
+
"published TEST-ONLY key — never a real key). Load it, watch it ACCEPT, then change ONE byte of a",
|
|
637
|
+
"sample file below and watch the verifier REJECT it and name the file you changed.</p>",
|
|
638
|
+
'<div class="row">',
|
|
639
|
+
'<button id="load-sample" class="primary" type="button">Load the sample packet & verify</button>',
|
|
640
|
+
"</div>",
|
|
641
|
+
'<div id="sample-area" style="display:none">',
|
|
642
|
+
'<p class="kv"><b>packet</b><span id="sample-name" class="mono"></span></p>',
|
|
643
|
+
'<p class="kv"><b>pinned signer</b><span id="sample-signer" class="mono"></span></p>',
|
|
644
|
+
'<p class="note">The editable bytes of <code>model-card.md</code> (one sealed file) — change ANY one',
|
|
645
|
+
"character, then re-verify:</p>",
|
|
646
|
+
'<textarea id="sample-editor" rows="4" spellcheck="false"></textarea>',
|
|
647
|
+
'<div class="row">',
|
|
648
|
+
'<button id="sample-verify" class="primary" type="button">Re-verify the sample packet</button>',
|
|
649
|
+
'<button id="sample-tamper" type="button">Tamper one byte for me</button>',
|
|
650
|
+
'<button id="sample-restore" type="button">Restore the original bytes</button>',
|
|
651
|
+
"</div>",
|
|
652
|
+
'<div id="sample-verdict"></div>',
|
|
653
|
+
"</div>",
|
|
654
|
+
"</section>",
|
|
655
|
+
"",
|
|
656
|
+
'<section id="agent-section">',
|
|
657
|
+
"<h2>1b — The agent-session demo (AgentTrace, built in)</h2>",
|
|
658
|
+
'<p class="note">A sample <code>*.vhagent.json</code> AGENT-SESSION packet is embedded in this page:',
|
|
659
|
+
"an ordered prompt/tool_call/tool_result/completion log under one RFC-6962-style Merkle head, with",
|
|
660
|
+
"the tool_call payload REDACTED behind its hash commitment — and it STILL verifies (redaction can",
|
|
661
|
+
"withhold, never silently alter). Load it, watch it ACCEPT, then change ONE byte of a payload below",
|
|
662
|
+
"and watch the verifier REJECT it and name the offending event seq.</p>",
|
|
663
|
+
'<div class="row">',
|
|
664
|
+
'<button id="load-agent-sample" class="primary" type="button">Load the sample agent packet & verify</button>',
|
|
665
|
+
"</div>",
|
|
666
|
+
'<div id="agent-sample-area" style="display:none">',
|
|
667
|
+
'<p class="kv"><b>packet</b><span id="agent-sample-name" class="mono"></span></p>',
|
|
668
|
+
'<p class="note">The editable packet bytes (self-contained — no sibling files) — change ANY one',
|
|
669
|
+
"payload character, then re-verify:</p>",
|
|
670
|
+
'<textarea id="agent-editor" rows="7" spellcheck="false"></textarea>',
|
|
671
|
+
'<div class="row">',
|
|
672
|
+
'<button id="agent-verify" class="primary" type="button">Re-verify the agent packet</button>',
|
|
673
|
+
'<button id="agent-tamper" type="button">Tamper one byte for me</button>',
|
|
674
|
+
'<button id="agent-restore" type="button">Restore the original bytes</button>',
|
|
675
|
+
"</div>",
|
|
676
|
+
'<div id="agent-verdict"></div>',
|
|
677
|
+
"</div>",
|
|
678
|
+
"</section>",
|
|
679
|
+
"",
|
|
680
|
+
'<section id="verify-section">',
|
|
681
|
+
"<h2>2 — Verify a packet YOU were handed</h2>",
|
|
682
|
+
'<p class="note">Drop the sealed artifact (<code>*.vhevidence.json</code> / <code>*.vhseal</code> /',
|
|
683
|
+
"attestation / proof bundle / <code>*.vhagent.json</code> agent-session packet) together with the",
|
|
684
|
+
"files it references (an agent packet is self-contained) — or pick them below (the folder",
|
|
685
|
+
"picker keeps sub-directory paths). Nothing is uploaded; the page reads the bytes locally.</p>",
|
|
686
|
+
'<div id="drop-zone" class="drop">Drag the packet + its files (or a whole folder) here</div>',
|
|
687
|
+
'<div class="row">',
|
|
688
|
+
'<label>Files: <input id="file-input" type="file" multiple></label>',
|
|
689
|
+
'<label>Folder: <input id="dir-input" type="file" webkitdirectory multiple></label>',
|
|
690
|
+
'<button id="clear-files" type="button">Clear</button>',
|
|
691
|
+
"</div>",
|
|
692
|
+
'<div id="held-files" class="mono"></div>',
|
|
693
|
+
'<div class="row">',
|
|
694
|
+
'<label style="flex:1">Artifact: <select id="artifact-select" style="max-width:100%"></select></label>',
|
|
695
|
+
"</div>",
|
|
696
|
+
'<div class="row">',
|
|
697
|
+
'<label style="flex:1">Vendor pin (optional 0x address — REJECT wrong_issuer if the signer differs):',
|
|
698
|
+
'<input id="vendor-input" type="text" placeholder="0x…" spellcheck="false"></label>',
|
|
699
|
+
"</div>",
|
|
700
|
+
'<div class="row">',
|
|
701
|
+
'<label>Revocations file (optional): <input id="revocations-input" type="file"></label>',
|
|
702
|
+
'<span id="revocations-name" class="mono"></span>',
|
|
703
|
+
"</div>",
|
|
704
|
+
'<div class="row"><button id="run-verify" class="primary" type="button">Verify offline, in this page</button></div>',
|
|
705
|
+
'<div id="verify-verdict"></div>',
|
|
706
|
+
"</section>",
|
|
707
|
+
"",
|
|
708
|
+
"<footer>",
|
|
709
|
+
'<p id="trust-note" class="note"></p>',
|
|
710
|
+
"<p>Who verifies the verifier? This file is reproducible from readable source: rebuild it with",
|
|
711
|
+
"<code>node verifier/build-standalone-html.js --check</code> (offline, Node-core-only) and compare the",
|
|
712
|
+
"published <code>verify-vh-standalone.html.sha256</code>. Per-source pins live in",
|
|
713
|
+
"<code>verifier/dist/BUILD-PROVENANCE.json</code>.</p>",
|
|
714
|
+
"</footer>",
|
|
715
|
+
"</main>",
|
|
716
|
+
].join("\n");
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// The DOM glue script (browser-only; OUTSIDE the vm-extractable engine block). Uses FileReader +
|
|
720
|
+
// TextEncoder/TextDecoder — reading, never any network API.
|
|
721
|
+
function uiScriptText() {
|
|
722
|
+
return [
|
|
723
|
+
"<script>",
|
|
724
|
+
'"use strict";',
|
|
725
|
+
"(function () {",
|
|
726
|
+
" var E = VerifyVhStandalone.engine;",
|
|
727
|
+
" var C = VerifyVhStandalone.challenge;",
|
|
728
|
+
" var enc = new TextEncoder();",
|
|
729
|
+
" var dec = new TextDecoder();",
|
|
730
|
+
"",
|
|
731
|
+
" function $(id) { return document.getElementById(id); }",
|
|
732
|
+
" function el(tag, cls, text) {",
|
|
733
|
+
" var n = document.createElement(tag);",
|
|
734
|
+
" if (cls) n.className = cls;",
|
|
735
|
+
" if (text !== undefined) n.textContent = text;",
|
|
736
|
+
" return n;",
|
|
737
|
+
" }",
|
|
738
|
+
" function kv(label, value) {",
|
|
739
|
+
' var p = el("div", "kv");',
|
|
740
|
+
' p.appendChild(el("b", null, label));',
|
|
741
|
+
' var v = el("span", "mono", value);',
|
|
742
|
+
" p.appendChild(v);",
|
|
743
|
+
" return p;",
|
|
744
|
+
" }",
|
|
745
|
+
"",
|
|
746
|
+
' $("trust-note").textContent = E.TRUST_NOTE;',
|
|
747
|
+
"",
|
|
748
|
+
" // ---------- shared verdict rendering (textContent only — hostile relPaths cannot inject) ----------",
|
|
749
|
+
" function renderVerdict(container, out) {",
|
|
750
|
+
' container.textContent = "";',
|
|
751
|
+
" if (out.error) {",
|
|
752
|
+
' var eb = el("div", "verdict reject");',
|
|
753
|
+
' eb.appendChild(el("div", "verdict-title", "CANNOT VERIFY (" + out.error.name + ", exit " + out.error.code + ")"));',
|
|
754
|
+
' eb.appendChild(el("div", "mono", out.error.message));',
|
|
755
|
+
" container.appendChild(eb);",
|
|
756
|
+
" return;",
|
|
757
|
+
" }",
|
|
758
|
+
" var r = out.result;",
|
|
759
|
+
' var box = el("div", "verdict " + (r.accepted ? "accept" : "reject"));',
|
|
760
|
+
" var title = r.accepted",
|
|
761
|
+
' ? "ACCEPT — the artifact verifies."',
|
|
762
|
+
' : (r.verdict === "REVOKED" ? "REVOKED" : "REJECTED") + " (" + r.reason + ")";',
|
|
763
|
+
' box.appendChild(el("div", "verdict-title", title));',
|
|
764
|
+
' box.appendChild(kv("artifact", String(r.artifact)));',
|
|
765
|
+
' box.appendChild(kv("kind", r.kind + (r.payloadKind !== r.kind ? " (embeds " + r.payloadKind + ")" : "")));',
|
|
766
|
+
' box.appendChild(kv("signed", r.signed ? "yes" : "no"));',
|
|
767
|
+
" if (r.signed) {",
|
|
768
|
+
' box.appendChild(kv("recovered signer", r.recoveredSigner || "(unrecoverable)"));',
|
|
769
|
+
' if (r.pinnedVendor != null) {',
|
|
770
|
+
' box.appendChild(kv("pinned vendor", r.pinnedVendor));',
|
|
771
|
+
' box.appendChild(kv("signer matches vendor", r.signerMatchesVendor ? "yes" : "NO"));',
|
|
772
|
+
" }",
|
|
773
|
+
" }",
|
|
774
|
+
' if (r.sealedRoot != null) box.appendChild(kv("sealed root", r.sealedRoot));',
|
|
775
|
+
' if (r.recomputedRoot != null) box.appendChild(kv("recomputed root", r.recomputedRoot));',
|
|
776
|
+
' if (r.rootMatches != null) box.appendChild(kv("root matches", r.rootMatches ? "yes" : "NO"));',
|
|
777
|
+
" if (r.agent) {",
|
|
778
|
+
' box.appendChild(kv("declared head", "{ size: " + r.agent.head.size + ", root: " + r.agent.head.root + " }"));',
|
|
779
|
+
" if (r.agent.counts) {",
|
|
780
|
+
' box.appendChild(kv("events", r.agent.counts.events + " (" + r.agent.counts.full + " full, " + r.agent.counts.redacted + " redacted)"));',
|
|
781
|
+
' box.appendChild(kv("withheld seqs", r.agent.withheld.length === 0 ? "(none — every payload disclosed)" : r.agent.withheld.join(", ")));',
|
|
782
|
+
" }",
|
|
783
|
+
" if (!r.accepted && r.agent.seq != null) {",
|
|
784
|
+
' box.appendChild(kv("offending event seq", String(r.agent.seq) + (r.agent.reason ? " (" + r.agent.reason + ")" : "")));',
|
|
785
|
+
" }",
|
|
786
|
+
" }",
|
|
787
|
+
' box.appendChild(kv("files", r.counts.matched + " matched, " + r.counts.changed + " changed, " +',
|
|
788
|
+
' r.counts.missing + " missing, " + (r.counts.escaped || 0) + " rejected"));',
|
|
789
|
+
" if (!r.accepted) {",
|
|
790
|
+
' var ul = el("ul", "files");',
|
|
791
|
+
" r.changed.forEach(function (c) {",
|
|
792
|
+
' ul.appendChild(el("li", "mono", "CHANGED " + c.relPath + ": sealed " + c.expectedContentHash + " != held " + c.actualContentHash));',
|
|
793
|
+
" });",
|
|
794
|
+
" r.missing.forEach(function (m) {",
|
|
795
|
+
' ul.appendChild(el("li", "mono", "MISSING " + m.relPath + ": referenced but not among the dropped files"));',
|
|
796
|
+
" });",
|
|
797
|
+
" (r.escaped || []).forEach(function (x) {",
|
|
798
|
+
' ul.appendChild(el("li", "mono", "REJECTED " + x.relPath + ": path escapes the packet (refused to read; no hash computed)"));',
|
|
799
|
+
" });",
|
|
800
|
+
" if (ul.childNodes.length) box.appendChild(ul);",
|
|
801
|
+
" if (r.trustAsOf && r.trustAsOf.governing) {",
|
|
802
|
+
' box.appendChild(el("div", "mono", "key_revoked_as_of: signing key " + r.trustAsOf.governing.vendorAddress +',
|
|
803
|
+
' " was REVOKED as of " + r.trustAsOf.governing.revokedAt + " (reason: " + r.trustAsOf.governing.reason + ")"));',
|
|
804
|
+
" }",
|
|
805
|
+
" }",
|
|
806
|
+
" container.appendChild(box);",
|
|
807
|
+
" }",
|
|
808
|
+
"",
|
|
809
|
+
" // ---------- section 1: the built-in 60-second challenge ----------",
|
|
810
|
+
" function runSampleVerify() {",
|
|
811
|
+
" var contents = {};",
|
|
812
|
+
" Object.keys(C.fixture.FILES).forEach(function (k) { contents[k] = C.fixture.FILES[k]; });",
|
|
813
|
+
' contents[C.fixture.TAMPER_FILE] = $("sample-editor").value;',
|
|
814
|
+
" var out = C.verifyContents(contents);",
|
|
815
|
+
' renderVerdict($("sample-verdict"), out);',
|
|
816
|
+
" }",
|
|
817
|
+
' $("load-sample").onclick = function () {',
|
|
818
|
+
' $("sample-area").style.display = "";',
|
|
819
|
+
' $("sample-name").textContent = C.fixture.PACKET_NAME;',
|
|
820
|
+
' $("sample-signer").textContent = C.fixture.SIGNER + " (fixed TEST-ONLY key — hardhat account #1)";',
|
|
821
|
+
' $("sample-editor").value = C.fixture.FILES[C.fixture.TAMPER_FILE];',
|
|
822
|
+
" runSampleVerify();",
|
|
823
|
+
" };",
|
|
824
|
+
' $("sample-verify").onclick = runSampleVerify;',
|
|
825
|
+
' $("sample-tamper").onclick = function () {',
|
|
826
|
+
' $("sample-editor").value = $("sample-editor").value + "X";',
|
|
827
|
+
" runSampleVerify();",
|
|
828
|
+
" };",
|
|
829
|
+
' $("sample-restore").onclick = function () {',
|
|
830
|
+
' $("sample-editor").value = C.fixture.FILES[C.fixture.TAMPER_FILE];',
|
|
831
|
+
" runSampleVerify();",
|
|
832
|
+
" };",
|
|
833
|
+
"",
|
|
834
|
+
" // ---------- section 1b: the built-in agent-session demo (T-68.3) ----------",
|
|
835
|
+
" function runAgentSampleVerify() {",
|
|
836
|
+
' renderVerdict($("agent-verdict"), C.verifyAgentText($("agent-editor").value));',
|
|
837
|
+
" }",
|
|
838
|
+
' $("load-agent-sample").onclick = function () {',
|
|
839
|
+
' $("agent-sample-area").style.display = "";',
|
|
840
|
+
' $("agent-sample-name").textContent = C.fixture.AGENT_PACKET_NAME;',
|
|
841
|
+
' $("agent-editor").value = C.fixture.AGENT_PACKET_TEXT;',
|
|
842
|
+
" runAgentSampleVerify();",
|
|
843
|
+
" };",
|
|
844
|
+
' $("agent-verify").onclick = runAgentSampleVerify;',
|
|
845
|
+
' $("agent-tamper").onclick = function () {',
|
|
846
|
+
" // One deterministic byte flip inside a payload (a substring that occurs exactly once).",
|
|
847
|
+
' $("agent-editor").value = $("agent-editor").value.replace(C.fixture.AGENT_TAMPER_FROM, C.fixture.AGENT_TAMPER_TO);',
|
|
848
|
+
" runAgentSampleVerify();",
|
|
849
|
+
" };",
|
|
850
|
+
' $("agent-restore").onclick = function () {',
|
|
851
|
+
' $("agent-editor").value = C.fixture.AGENT_PACKET_TEXT;',
|
|
852
|
+
" runAgentSampleVerify();",
|
|
853
|
+
" };",
|
|
854
|
+
"",
|
|
855
|
+
" // ---------- section 2: verify a real packet ----------",
|
|
856
|
+
" var held = {}; // relPath -> Uint8Array",
|
|
857
|
+
" var revocationsText = null;",
|
|
858
|
+
"",
|
|
859
|
+
" function refreshHeld() {",
|
|
860
|
+
' var list = $("held-files");',
|
|
861
|
+
' list.textContent = "";',
|
|
862
|
+
" var keys = Object.keys(held).sort();",
|
|
863
|
+
' if (keys.length === 0) { list.textContent = "(no files held yet)"; return refreshArtifactSelect(); }',
|
|
864
|
+
" keys.forEach(function (k) {",
|
|
865
|
+
' list.appendChild(el("div", null, k + " (" + held[k].length + " bytes)"));',
|
|
866
|
+
" });",
|
|
867
|
+
" refreshArtifactSelect();",
|
|
868
|
+
" }",
|
|
869
|
+
" function artifactCandidates() {",
|
|
870
|
+
" var kinds = Object.keys(E.KINDS).map(function (k) { return E.KINDS[k]; });",
|
|
871
|
+
" return Object.keys(held).sort().filter(function (k) {",
|
|
872
|
+
" if (held[k].length > 8 * 1024 * 1024) return false;",
|
|
873
|
+
" try {",
|
|
874
|
+
" var obj = JSON.parse(dec.decode(held[k]));",
|
|
875
|
+
' return !!obj && typeof obj === "object" && kinds.indexOf(obj.kind) !== -1;',
|
|
876
|
+
" } catch (e) { return false; }",
|
|
877
|
+
" });",
|
|
878
|
+
" }",
|
|
879
|
+
" function refreshArtifactSelect() {",
|
|
880
|
+
' var sel = $("artifact-select");',
|
|
881
|
+
' sel.textContent = "";',
|
|
882
|
+
" var cands = artifactCandidates();",
|
|
883
|
+
" if (cands.length === 0) {",
|
|
884
|
+
' var opt = el("option", null, "(drop a *.vhevidence.json / *.vhseal / attestation / proof / *.vhagent.json first)");',
|
|
885
|
+
' opt.value = "";',
|
|
886
|
+
" sel.appendChild(opt);",
|
|
887
|
+
" return;",
|
|
888
|
+
" }",
|
|
889
|
+
" cands.forEach(function (k) {",
|
|
890
|
+
' var opt = el("option", null, k);',
|
|
891
|
+
" opt.value = k;",
|
|
892
|
+
" sel.appendChild(opt);",
|
|
893
|
+
" });",
|
|
894
|
+
" }",
|
|
895
|
+
" function addHeld(relPath, bytes) {",
|
|
896
|
+
' var key = String(relPath).replace(/\\\\/g, "/").replace(/^\\.\\//, "");',
|
|
897
|
+
" held[key] = bytes;",
|
|
898
|
+
" }",
|
|
899
|
+
" function readFileInto(relPath, file, done) {",
|
|
900
|
+
" var r = new FileReader();",
|
|
901
|
+
" r.onload = function () { addHeld(relPath, new Uint8Array(r.result)); done(); };",
|
|
902
|
+
' r.onerror = function () { done("cannot read " + relPath); };',
|
|
903
|
+
" r.readAsArrayBuffer(file);",
|
|
904
|
+
" }",
|
|
905
|
+
" // Strip the top-level picked/dropped folder name so keys are relative to the packet's own dir,",
|
|
906
|
+
" // exactly like the CLI's `--dir <folder>` resolution.",
|
|
907
|
+
" function innerPath(p) {",
|
|
908
|
+
' var parts = String(p).split("/").filter(function (s) { return s.length > 0; });',
|
|
909
|
+
" return parts.length > 1 ? parts.slice(1).join(\"/\") : parts.join(\"/\");",
|
|
910
|
+
" }",
|
|
911
|
+
" function afterBatch(pending) {",
|
|
912
|
+
" if (pending.n === 0) refreshHeld();",
|
|
913
|
+
" }",
|
|
914
|
+
" function ingestFileList(fileList, useRelative) {",
|
|
915
|
+
" var files = Array.prototype.slice.call(fileList);",
|
|
916
|
+
" var pending = { n: files.length };",
|
|
917
|
+
" if (files.length === 0) return;",
|
|
918
|
+
" files.forEach(function (f) {",
|
|
919
|
+
" var rel = useRelative && f.webkitRelativePath ? innerPath(f.webkitRelativePath) : f.name;",
|
|
920
|
+
" readFileInto(rel, f, function () { pending.n--; afterBatch(pending); });",
|
|
921
|
+
" });",
|
|
922
|
+
" }",
|
|
923
|
+
" function walkEntry(entry, prefix, pending) {",
|
|
924
|
+
" if (entry.isFile) {",
|
|
925
|
+
" pending.n++;",
|
|
926
|
+
" entry.file(function (f) {",
|
|
927
|
+
" readFileInto(prefix + entry.name, f, function () { pending.n--; afterBatch(pending); });",
|
|
928
|
+
" }, function () { pending.n--; afterBatch(pending); });",
|
|
929
|
+
" } else if (entry.isDirectory) {",
|
|
930
|
+
" var reader = entry.createReader();",
|
|
931
|
+
" var readMore = function () {",
|
|
932
|
+
" pending.n++;",
|
|
933
|
+
" reader.readEntries(function (entries) {",
|
|
934
|
+
" entries.forEach(function (e) { walkEntry(e, prefix + entry.name + \"/\", pending); });",
|
|
935
|
+
" pending.n--;",
|
|
936
|
+
" if (entries.length > 0) readMore();",
|
|
937
|
+
" else afterBatch(pending);",
|
|
938
|
+
" }, function () { pending.n--; afterBatch(pending); });",
|
|
939
|
+
" };",
|
|
940
|
+
" readMore();",
|
|
941
|
+
" }",
|
|
942
|
+
" }",
|
|
943
|
+
' var drop = $("drop-zone");',
|
|
944
|
+
' drop.addEventListener("dragover", function (ev) { ev.preventDefault(); drop.classList.add("armed"); });',
|
|
945
|
+
' drop.addEventListener("dragleave", function () { drop.classList.remove("armed"); });',
|
|
946
|
+
' drop.addEventListener("drop", function (ev) {',
|
|
947
|
+
" ev.preventDefault();",
|
|
948
|
+
' drop.classList.remove("armed");',
|
|
949
|
+
" var items = ev.dataTransfer && ev.dataTransfer.items;",
|
|
950
|
+
" var usedEntries = false;",
|
|
951
|
+
" if (items) {",
|
|
952
|
+
" var pending = { n: 0 };",
|
|
953
|
+
" for (var i = 0; i < items.length; i++) {",
|
|
954
|
+
" var entry = items[i].webkitGetAsEntry && items[i].webkitGetAsEntry();",
|
|
955
|
+
" if (entry) {",
|
|
956
|
+
" usedEntries = true;",
|
|
957
|
+
" if (entry.isDirectory) {",
|
|
958
|
+
" // Paths inside a dropped folder are taken relative to THAT folder (like --dir).",
|
|
959
|
+
" var reader = entry.createReader();",
|
|
960
|
+
" (function (rd) {",
|
|
961
|
+
" var readMore = function () {",
|
|
962
|
+
" pending.n++;",
|
|
963
|
+
" rd.readEntries(function (entries) {",
|
|
964
|
+
' entries.forEach(function (e) { walkEntry(e, "", pending); });',
|
|
965
|
+
" pending.n--;",
|
|
966
|
+
" if (entries.length > 0) readMore();",
|
|
967
|
+
" else afterBatch(pending);",
|
|
968
|
+
" }, function () { pending.n--; afterBatch(pending); });",
|
|
969
|
+
" };",
|
|
970
|
+
" readMore();",
|
|
971
|
+
" })(reader);",
|
|
972
|
+
" } else {",
|
|
973
|
+
' walkEntry(entry, "", pending);',
|
|
974
|
+
" }",
|
|
975
|
+
" }",
|
|
976
|
+
" }",
|
|
977
|
+
" }",
|
|
978
|
+
" if (!usedEntries && ev.dataTransfer && ev.dataTransfer.files) {",
|
|
979
|
+
" ingestFileList(ev.dataTransfer.files, false);",
|
|
980
|
+
" }",
|
|
981
|
+
" });",
|
|
982
|
+
' $("file-input").addEventListener("change", function () { ingestFileList(this.files, false); });',
|
|
983
|
+
' $("dir-input").addEventListener("change", function () { ingestFileList(this.files, true); });',
|
|
984
|
+
' $("clear-files").onclick = function () {',
|
|
985
|
+
" held = {};",
|
|
986
|
+
" revocationsText = null;",
|
|
987
|
+
' $("revocations-name").textContent = "";',
|
|
988
|
+
' $("revocations-input").value = "";',
|
|
989
|
+
' $("verify-verdict").textContent = "";',
|
|
990
|
+
" refreshHeld();",
|
|
991
|
+
" };",
|
|
992
|
+
' $("revocations-input").addEventListener("change", function () {',
|
|
993
|
+
" var f = this.files && this.files[0];",
|
|
994
|
+
" if (!f) { revocationsText = null; return; }",
|
|
995
|
+
" var r = new FileReader();",
|
|
996
|
+
" r.onload = function () {",
|
|
997
|
+
" revocationsText = String(r.result);",
|
|
998
|
+
' $("revocations-name").textContent = f.name + " (applies the same revoked-key downgrade the CLI does)";',
|
|
999
|
+
" };",
|
|
1000
|
+
" r.readAsText(f);",
|
|
1001
|
+
" });",
|
|
1002
|
+
' $("run-verify").onclick = function () {',
|
|
1003
|
+
' var out = $("verify-verdict");',
|
|
1004
|
+
' var artifactKey = $("artifact-select").value;',
|
|
1005
|
+
" if (!artifactKey || !held[artifactKey]) {",
|
|
1006
|
+
' out.textContent = "";',
|
|
1007
|
+
' var eb = el("div", "verdict reject");',
|
|
1008
|
+
' eb.appendChild(el("div", "verdict-title", "No artifact selected"));',
|
|
1009
|
+
' eb.appendChild(el("div", null, "Drop the sealed artifact JSON together with the files it references, then pick it above."));',
|
|
1010
|
+
" out.appendChild(eb);",
|
|
1011
|
+
" return;",
|
|
1012
|
+
" }",
|
|
1013
|
+
" var files = {};",
|
|
1014
|
+
" Object.keys(held).forEach(function (k) { if (k !== artifactKey) files[k] = held[k]; });",
|
|
1015
|
+
' var vendor = $("vendor-input").value.trim();',
|
|
1016
|
+
" var params = {",
|
|
1017
|
+
" artifactText: dec.decode(held[artifactKey]),",
|
|
1018
|
+
" files: files,",
|
|
1019
|
+
" artifactName: artifactKey,",
|
|
1020
|
+
" };",
|
|
1021
|
+
" if (vendor) params.vendor = vendor;",
|
|
1022
|
+
" if (revocationsText != null) params.revocationsText = revocationsText;",
|
|
1023
|
+
" renderVerdict(out, E.verifyArtifactFromBytes(params));",
|
|
1024
|
+
" };",
|
|
1025
|
+
" refreshHeld();",
|
|
1026
|
+
"})();",
|
|
1027
|
+
"</scr" + "ipt>",
|
|
1028
|
+
].join("\n");
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// ---------------------------------------------------------------------------
|
|
1032
|
+
// Build the FULL standalone HTML text. Pure function of the committed sources -> deterministic.
|
|
1033
|
+
// ---------------------------------------------------------------------------
|
|
1034
|
+
|
|
1035
|
+
function buildHtml() {
|
|
1036
|
+
const parts = [];
|
|
1037
|
+
parts.push("<!doctype html>");
|
|
1038
|
+
parts.push(GENERATED_BANNER);
|
|
1039
|
+
parts.push('<html lang="en">');
|
|
1040
|
+
parts.push("<head>");
|
|
1041
|
+
parts.push('<meta charset="utf-8">');
|
|
1042
|
+
parts.push('<meta name="viewport" content="width=device-width, initial-scale=1">');
|
|
1043
|
+
parts.push("<title>verify-vh — offline verifier (verifyhash)</title>");
|
|
1044
|
+
parts.push(PAGE_STYLE);
|
|
1045
|
+
parts.push("</head>");
|
|
1046
|
+
parts.push("<body>");
|
|
1047
|
+
parts.push(pageBodyText());
|
|
1048
|
+
parts.push(engineScriptText());
|
|
1049
|
+
parts.push(uiScriptText());
|
|
1050
|
+
parts.push("</body>");
|
|
1051
|
+
parts.push("</html>");
|
|
1052
|
+
parts.push(""); // single trailing newline
|
|
1053
|
+
return parts.join("\n");
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// ---------------------------------------------------------------------------
|
|
1057
|
+
// Build provenance — the SAME schema + module-record shape the shared manifest uses, so a reviewer reads
|
|
1058
|
+
// every target the same way. This builder does NOT own BUILD-PROVENANCE.json: verifier/build-standalone.js
|
|
1059
|
+
// composes the WHOLE manifest (verify + seal + this html target) so the committed file has ONE writer
|
|
1060
|
+
// shape; this module only supplies its own target record.
|
|
1061
|
+
// ---------------------------------------------------------------------------
|
|
1062
|
+
|
|
1063
|
+
function moduleProvenance(m) {
|
|
1064
|
+
if (m.slice) {
|
|
1065
|
+
return {
|
|
1066
|
+
id: m.id,
|
|
1067
|
+
synthetic: false,
|
|
1068
|
+
sourceFile: `verifier/${m.file}`,
|
|
1069
|
+
sourceSha256: sha256HexOf(readSource(m.file)),
|
|
1070
|
+
inlinedSha256: sha256HexOf(bodyOf(m)),
|
|
1071
|
+
entry: m.entry === true,
|
|
1072
|
+
note: m.note,
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
if (m.body != null) {
|
|
1076
|
+
return {
|
|
1077
|
+
id: m.id,
|
|
1078
|
+
synthetic: true,
|
|
1079
|
+
sourceFile: null,
|
|
1080
|
+
sourceSha256: null,
|
|
1081
|
+
inlinedSha256: sha256HexOf(bodyOf(m)),
|
|
1082
|
+
note: m.note,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
const src = readSource(m.file);
|
|
1086
|
+
return {
|
|
1087
|
+
id: m.id,
|
|
1088
|
+
synthetic: false,
|
|
1089
|
+
sourceFile: `verifier/${m.file}`,
|
|
1090
|
+
sourceSha256: sha256HexOf(src),
|
|
1091
|
+
inlinedSha256: sha256HexOf(rewriteRequires(src, m.rewrite, m.id)),
|
|
1092
|
+
entry: m.entry === true,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// The html target's record for the shared BUILD-PROVENANCE.json (bundle sha256 + ordered per-module
|
|
1097
|
+
// source sha256s). Pure function of the committed sources -> deterministic.
|
|
1098
|
+
function htmlTargetProvenance() {
|
|
1099
|
+
const bundleText = buildHtml();
|
|
1100
|
+
return {
|
|
1101
|
+
bundle: SHA256_BASENAME,
|
|
1102
|
+
sidecar: path.basename(SHA256_PATH),
|
|
1103
|
+
bundleBytes: Buffer.byteLength(bundleText, "utf8"),
|
|
1104
|
+
bundleSha256: sha256HexOf(bundleText),
|
|
1105
|
+
sidecarLine: sha256SidecarFor(bundleText, SHA256_BASENAME).trim(),
|
|
1106
|
+
modules: HTML_MODULES.map(moduleProvenance),
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Lazily require the sibling builder (the manifest's single owner). Lazy on BOTH sides so neither module
|
|
1111
|
+
// observes the other's exports mid-load, whichever is the process entrypoint.
|
|
1112
|
+
function jsBuilder() {
|
|
1113
|
+
return require("./build-standalone");
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// ---------------------------------------------------------------------------
|
|
1117
|
+
// Writers
|
|
1118
|
+
// ---------------------------------------------------------------------------
|
|
1119
|
+
|
|
1120
|
+
function writeAll() {
|
|
1121
|
+
const html = buildHtml();
|
|
1122
|
+
const sidecar = sha256SidecarFor(html, SHA256_BASENAME);
|
|
1123
|
+
fs.mkdirSync(DIST_DIR, { recursive: true });
|
|
1124
|
+
fs.writeFileSync(OUT_PATH, html);
|
|
1125
|
+
fs.writeFileSync(SHA256_PATH, sidecar);
|
|
1126
|
+
// Re-emit the SHARED manifest through its single owner (which composes verify + seal + this target),
|
|
1127
|
+
// so the committed BUILD-PROVENANCE.json can never fork between the two builders.
|
|
1128
|
+
const provenance = jsBuilder().writeProvenance();
|
|
1129
|
+
return { html, sidecar, provenance };
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// ---------------------------------------------------------------------------
|
|
1133
|
+
// REPRODUCE-AND-ATTEST (`--check`) — same posture as the sibling builders: re-compile the page from the
|
|
1134
|
+
// in-tree source a skeptic can READ, recompute the published checksum + the shared provenance manifest,
|
|
1135
|
+
// and assert the COMMITTED files are byte-for-byte what that source compiles to. Read-only; writes
|
|
1136
|
+
// NOTHING; a stale/tampered dist is a named MISMATCH (exit 1), never a crash.
|
|
1137
|
+
// ---------------------------------------------------------------------------
|
|
1138
|
+
|
|
1139
|
+
function checkHtml() {
|
|
1140
|
+
const rel = (p) => path.relative(VERIFIER_DIR, p);
|
|
1141
|
+
const result = {
|
|
1142
|
+
bundlePath: rel(OUT_PATH),
|
|
1143
|
+
sha256Path: rel(SHA256_PATH),
|
|
1144
|
+
expectedHex: null,
|
|
1145
|
+
bundle: { ok: false, reason: "" },
|
|
1146
|
+
sidecar: { ok: false, reason: "" },
|
|
1147
|
+
sources: { ok: true, reason: "", offenders: [] },
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
// Source-presence FIRST, so a missing inlined source is a named MISMATCH, not a build crash.
|
|
1151
|
+
const sourceFiles = [...new Set(HTML_MODULES.filter((m) => m.file).map((m) => m.file))];
|
|
1152
|
+
for (const f of sourceFiles) {
|
|
1153
|
+
if (!fs.existsSync(path.join(VERIFIER_DIR, f))) {
|
|
1154
|
+
result.sources.ok = false;
|
|
1155
|
+
result.sources.offenders.push({ sourceFile: `verifier/${f}`, reason: "MISSING" });
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
result.sources.reason = result.sources.offenders.length
|
|
1159
|
+
? `inlined source(s) MISSING: ${result.sources.offenders.map((o) => o.sourceFile).join(", ")}`
|
|
1160
|
+
: `all ${sourceFiles.length} inlined source files present`;
|
|
1161
|
+
|
|
1162
|
+
let expectedHtml, expectedBuf, expectedSidecar;
|
|
1163
|
+
try {
|
|
1164
|
+
expectedHtml = buildHtml();
|
|
1165
|
+
expectedBuf = Buffer.from(expectedHtml, "utf8");
|
|
1166
|
+
result.expectedHex = sha256HexOf(expectedHtml);
|
|
1167
|
+
expectedSidecar = sha256SidecarFor(expectedHtml, SHA256_BASENAME);
|
|
1168
|
+
} catch (e) {
|
|
1169
|
+
const why = `cannot recompile ${result.bundlePath} from source: ${e && e.message ? e.message : e}`;
|
|
1170
|
+
result.bundle.reason = why;
|
|
1171
|
+
result.sidecar.reason = why;
|
|
1172
|
+
result.ok = false;
|
|
1173
|
+
return result;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (!fs.existsSync(OUT_PATH)) {
|
|
1177
|
+
result.bundle.reason = `committed bundle ${result.bundlePath} is MISSING`;
|
|
1178
|
+
} else {
|
|
1179
|
+
const committed = fs.readFileSync(OUT_PATH);
|
|
1180
|
+
if (committed.equals(expectedBuf)) {
|
|
1181
|
+
result.bundle.ok = true;
|
|
1182
|
+
result.bundle.reason = `recomputed bytes == committed bytes (sha256 ${result.expectedHex})`;
|
|
1183
|
+
} else {
|
|
1184
|
+
const committedHex = crypto.createHash("sha256").update(committed).digest("hex");
|
|
1185
|
+
result.bundle.reason =
|
|
1186
|
+
`committed bundle does NOT reproduce from source ` +
|
|
1187
|
+
`(committed sha256 ${committedHex} != recomputed ${result.expectedHex})`;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if (!fs.existsSync(SHA256_PATH)) {
|
|
1192
|
+
result.sidecar.reason = `committed sidecar ${result.sha256Path} is MISSING`;
|
|
1193
|
+
} else {
|
|
1194
|
+
const committedSidecar = fs.readFileSync(SHA256_PATH, "utf8");
|
|
1195
|
+
if (committedSidecar === expectedSidecar) {
|
|
1196
|
+
result.sidecar.ok = true;
|
|
1197
|
+
result.sidecar.reason = `published hex == recomputed hex (${result.expectedHex})`;
|
|
1198
|
+
} else {
|
|
1199
|
+
result.sidecar.reason =
|
|
1200
|
+
`committed sidecar does NOT match the recomputed published line ` +
|
|
1201
|
+
`(expected "${expectedSidecar.trim()}", got "${committedSidecar.trim()}")`;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
result.ok = result.bundle.ok && result.sidecar.ok && result.sources.ok;
|
|
1206
|
+
return result;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function runCheck(io) {
|
|
1210
|
+
const out = (io && io.write) || ((s) => process.stdout.write(s));
|
|
1211
|
+
const err = (io && io.writeErr) || ((s) => process.stderr.write(s));
|
|
1212
|
+
|
|
1213
|
+
out("verifyhash standalone HTML REPRODUCE-AND-ATTEST (--check): re-compiling the offline page from\n");
|
|
1214
|
+
out("in-tree source, recomputing its published checksum + the shared build-provenance manifest, and\n");
|
|
1215
|
+
out("comparing all against the committed files. No network; no writes.\n\n");
|
|
1216
|
+
|
|
1217
|
+
let allOk = true;
|
|
1218
|
+
|
|
1219
|
+
const b = checkHtml();
|
|
1220
|
+
out(`[${b.bundle.ok ? "MATCH" : "MISMATCH"}] bundle ${b.bundlePath}: ${b.bundle.reason}\n`);
|
|
1221
|
+
out(`[${b.sidecar.ok ? "MATCH" : "MISMATCH"}] sidecar ${b.sha256Path}: ${b.sidecar.reason}\n`);
|
|
1222
|
+
out(`[${b.sources.ok ? "MATCH" : "MISMATCH"}] sources ${b.bundlePath}: ${b.sources.reason}\n`);
|
|
1223
|
+
if (!b.ok) allOk = false;
|
|
1224
|
+
|
|
1225
|
+
// The SHARED manifest + source->hash chain (spans verify + seal + this html target) — attested through
|
|
1226
|
+
// its single owner so the two builders can never disagree about what the committed manifest should be.
|
|
1227
|
+
const p = jsBuilder().checkProvenance();
|
|
1228
|
+
out(`[${p.manifest.ok ? "MATCH" : "MISMATCH"}] manifest ${p.manifestPath}: ${p.manifest.reason}\n`);
|
|
1229
|
+
out(`[${p.chain.ok ? "MATCH" : "MISMATCH"}] sources->manifest: ${p.chain.reason}\n`);
|
|
1230
|
+
if (!p.ok) allOk = false;
|
|
1231
|
+
|
|
1232
|
+
if (allOk) {
|
|
1233
|
+
out("\nALL MATCH — the committed offline page, its sidecar AND the shared build-provenance manifest\n");
|
|
1234
|
+
out("reproduce byte-for-byte from the in-tree source, and every inlined source file hashes to its\n");
|
|
1235
|
+
out("manifest-pinned sha256.\n");
|
|
1236
|
+
return 0;
|
|
1237
|
+
}
|
|
1238
|
+
err("\nMISMATCH — at least one committed file does NOT reproduce from source (see above). Re-run\n");
|
|
1239
|
+
err("`node verifier/build-standalone-html.js` (no flag) to regenerate, or distrust this checkout.\n");
|
|
1240
|
+
return 1;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Exports are assigned BEFORE the CLI main block below: the no-flag build calls writeAll(), which asks
|
|
1244
|
+
// the sibling builder to re-emit the SHARED manifest, which requires THIS module back (a benign cycle) —
|
|
1245
|
+
// assigning exports first guarantees the sibling always sees the complete surface.
|
|
1246
|
+
module.exports = {
|
|
1247
|
+
buildHtml,
|
|
1248
|
+
engineScriptText,
|
|
1249
|
+
engineSliceBody,
|
|
1250
|
+
extractDemoFixture,
|
|
1251
|
+
htmlTargetProvenance,
|
|
1252
|
+
moduleProvenance,
|
|
1253
|
+
writeAll,
|
|
1254
|
+
checkHtml,
|
|
1255
|
+
runCheck,
|
|
1256
|
+
sha256HexOf,
|
|
1257
|
+
sha256SidecarFor,
|
|
1258
|
+
BUFFER_SHIM_BODY,
|
|
1259
|
+
KECCAK_SHIM_BODY,
|
|
1260
|
+
CHALLENGE_BODY,
|
|
1261
|
+
HTML_MODULES,
|
|
1262
|
+
HTML_TARGET_NAME,
|
|
1263
|
+
ENGINE_BEGIN_MARKER,
|
|
1264
|
+
ENGINE_END_MARKER,
|
|
1265
|
+
VV_ENGINE_BEGIN,
|
|
1266
|
+
VV_ENGINE_END,
|
|
1267
|
+
TAMPER_FILE,
|
|
1268
|
+
OUT_PATH,
|
|
1269
|
+
SHA256_PATH,
|
|
1270
|
+
SHA256_BASENAME,
|
|
1271
|
+
PROVENANCE_PATH,
|
|
1272
|
+
PROVENANCE_SCHEMA,
|
|
1273
|
+
DIST_DIR,
|
|
1274
|
+
VERIFIER_DIR,
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
if (require.main === module) {
|
|
1278
|
+
if (process.argv.slice(2).includes("--check")) {
|
|
1279
|
+
process.exit(runCheck());
|
|
1280
|
+
}
|
|
1281
|
+
const { html, sidecar, provenance } = writeAll();
|
|
1282
|
+
process.stdout.write(`wrote ${path.relative(VERIFIER_DIR, OUT_PATH)} (${Buffer.byteLength(html)} bytes)\n`);
|
|
1283
|
+
process.stdout.write(`wrote ${path.relative(VERIFIER_DIR, SHA256_PATH)} (${sidecar.trim()})\n`);
|
|
1284
|
+
process.stdout.write(
|
|
1285
|
+
`wrote ${path.relative(VERIFIER_DIR, PROVENANCE_PATH)} (${Buffer.byteLength(provenance)} bytes)\n`
|
|
1286
|
+
);
|
|
1287
|
+
}
|