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.
Files changed (64) hide show
  1. package/README.md +5 -3
  2. package/cli/agent-hook.js +431 -0
  3. package/docs/ADOPT.md +15 -5
  4. package/docs/AGENT-HOOK.md +111 -0
  5. package/docs/ANCHORING.md +43 -22
  6. package/docs/PUBLISH-VERIFY-VH.md +45 -0
  7. package/examples/README.md +185 -0
  8. package/examples/policy.lenient.json +5 -0
  9. package/examples/policy.strict.json +6 -0
  10. package/examples/run.js +366 -0
  11. package/examples/sample-dataset/README.txt +10 -0
  12. package/examples/sample-dataset/corpus/cc-by-poem.txt +8 -0
  13. package/examples/sample-dataset/corpus/mit-notes.txt +4 -0
  14. package/examples/sample-dataset/data/unlabeled.txt +5 -0
  15. package/examples/sample-dataset/vendored/gpl-snippet.txt +5 -0
  16. package/examples/sample-dataset.hints.json +7 -0
  17. package/examples/sample-parcel/data/manifest-of-contents.txt +7 -0
  18. package/examples/sample-parcel/data/records.csv +4 -0
  19. package/examples/sample-parcel/delivery-note.txt +9 -0
  20. package/package.json +26 -3
  21. package/verifier/README.md +584 -0
  22. package/verifier/action/README.md +87 -0
  23. package/verifier/action/action.yml +146 -0
  24. package/verifier/build-standalone-html.js +1287 -0
  25. package/verifier/build-standalone.js +989 -0
  26. package/verifier/ci/journal.generic.sh +96 -0
  27. package/verifier/ci/journal.github-actions.yml +99 -0
  28. package/verifier/ci/reproduce-vh.generic.sh +59 -0
  29. package/verifier/ci/reproduce-vh.github-actions.yml +49 -0
  30. package/verifier/ci/verify-service.generic.sh +96 -0
  31. package/verifier/ci/verify-service.github-actions.yml +88 -0
  32. package/verifier/ci/verify-vh.generic.sh +75 -0
  33. package/verifier/ci/verify-vh.github-actions.yml +56 -0
  34. package/verifier/dist/BUILD-PROVENANCE.json +210 -0
  35. package/verifier/dist/seal-vh-standalone.js +876 -0
  36. package/verifier/dist/seal-vh-standalone.js.sha256 +1 -0
  37. package/verifier/dist/verify-vh-standalone.html +3373 -0
  38. package/verifier/dist/verify-vh-standalone.html.sha256 +1 -0
  39. package/verifier/dist/verify-vh-standalone.js +5123 -0
  40. package/verifier/dist/verify-vh-standalone.js.sha256 +1 -0
  41. package/verifier/lib/canonical.js +141 -0
  42. package/verifier/lib/keccak.js +30 -0
  43. package/verifier/lib/keccak256-vendored.js +206 -0
  44. package/verifier/lib/merkle.js +145 -0
  45. package/verifier/lib/revocation-core.js +606 -0
  46. package/verifier/lib/revocation.js +200 -0
  47. package/verifier/lib/seal-cli.js +374 -0
  48. package/verifier/lib/seal-evidence.js +237 -0
  49. package/verifier/lib/secp256k1-recover.js +249 -0
  50. package/verifier/package.json +39 -0
  51. package/verifier/verify-vh.js +3376 -0
  52. package/docs/ADOPTION.json +0 -11
  53. package/docs/AUDIT.md +0 -55
  54. package/docs/DECIDE.md +0 -47
  55. package/docs/DECISIONS-PENDING.md +0 -27
  56. package/docs/DEPLOY-PUBLIC-SITE.md +0 -301
  57. package/docs/ENGINE-LEDGER.json +0 -12
  58. package/docs/LOOP-AUDIT-2026-07-03.json +0 -580
  59. package/docs/LOOP-HARDENING-PLAN.md +0 -44
  60. package/docs/METRICS.jsonl +0 -31
  61. package/docs/MORNING.md +0 -204
  62. package/docs/STRATEGY-ARCHIVE.md +0 -5055
  63. package/docs/SUPERVISOR-RUNBOOK.md +0 -52
  64. package/docs/USAGE-BUDGET.json +0 -121
@@ -0,0 +1,989 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // verifier/build-standalone.js — the DETERMINISTIC, OFFLINE, zero-third-party-dependency bundler that
5
+ // inlines the in-tree verifier/sealer (verifier/verify-vh.js / verifier/lib/seal-cli.js + verifier/lib/*)
6
+ // into self-contained single files under verifier/dist/ (T-35.2 verify bundle; T-36.2 SEAL bundle).
7
+ //
8
+ // IT EMITS TWO TARGETS (both deterministic, both zero-third-party):
9
+ // * verify-vh-standalone.js — the free VERIFY half (T-35.2): a counterparty handed ONE sealed packet
10
+ // saves this and runs it. (Bytes UNCHANGED by T-36.2 — the verify target list/order is untouched.)
11
+ // * seal-vh-standalone.js — the free PRODUCE half (T-36.2): a stranger SEALS up to 25 of their OWN
12
+ // files into a `vh.evidence-seal` the verify bundle then accepts. Together the two close the organic
13
+ // adoption loop with ZERO install on EITHER side.
14
+ //
15
+ // WHY THIS EXISTS
16
+ // The free verifier is the FUNNEL: every counterparty who runs it on a partner's seal is a warm lead
17
+ // for the paid seal. But today a third party who received ONE sealed packet must clone this repo (or be
18
+ // handed the verifier/ tree) and `npm install` — which still pulls a runtime dependency (`js-sha3`, via
19
+ // verifier/lib/keccak.js). This bundler removes that last friction: it emits a SINGLE file a skeptic can
20
+ // save with no clone, no `npm install`, no `node_modules`, no `package.json`, and run with `node` —
21
+ // and audit in one sitting. It is the strongest possible form of "don't trust us, check it yourself."
22
+ // T-36.2 applies the SAME bundling discipline to the PRODUCE side so a prospect can both make AND check
23
+ // a free seal with no install anywhere.
24
+ //
25
+ // WHAT IT GUARANTEES (proven by test/verifier.standalone.test.js)
26
+ // * DETERMINISTIC — running it twice yields BYTE-IDENTICAL output. There is no timestamp, no randomness,
27
+ // no filesystem-order dependence: the module set + order are an EXPLICIT, fixed list below, and the
28
+ // emitted bytes are a pure function of the (committed) source files. The test rebuilds in a temp dir
29
+ // and asserts the committed dist file matches byte-for-byte (a stale committed bundle FAILS CI).
30
+ // * ZERO third-party / relative deps — the keccak256 module is swapped for the PURE-JS vendored
31
+ // implementation (verifier/lib/keccak256-vendored.js), so the bundle `require`s NOTHING but Node core
32
+ // (it does not even need Buffer-only Node — it uses Buffer, which is Node core). A grep over the
33
+ // emitted file finds no `require('js-sha3')`, no `require('./lib/...')`, no `../`, no bare 3rd-party
34
+ // name (only `require("fs")` / `require("path")`, which are Node core).
35
+ // * SAME VERDICTS — the inlined verify-vh.js is the EXACT in-tree source (the in-tree file is UNCHANGED);
36
+ // only the keccak provider is swapped for a byte-identical vendored one (cross-checked against js-sha3
37
+ // AND ethers by test/verifier.keccak-vendored.test.js). So the standalone produces the identical
38
+ // verdict text + exit code as the in-tree verifier across the whole artifact/verdict matrix.
39
+ //
40
+ // HOW IT WORKS — a tiny CommonJS shim
41
+ // The bundle embeds a `__modules` registry of factory functions keyed by a canonical module id, plus a
42
+ // memoizing `__require(id)`. Each source file is inlined VERBATIM as a factory body, with ONLY its
43
+ // require() specifiers rewritten to `__require("<canonical id>")`. The keccak module's body is replaced
44
+ // with a vendored-backed shim (returns a Buffer to match keccak.js's contract). NOTHING else is
45
+ // transformed, so the inlined logic is the audited in-tree logic, line for line.
46
+ //
47
+ // OFFLINE + READ-ONLY: this builder reads the committed source files and WRITES one output file under
48
+ // verifier/dist/. It opens NO socket and makes NO network call. The produced file is an ARTIFACT — the
49
+ // loop never executes it against any network.
50
+
51
+ const fs = require("fs");
52
+ const path = require("path");
53
+ const crypto = require("crypto");
54
+
55
+ const VERIFIER_DIR = __dirname;
56
+ const DIST_DIR = path.join(VERIFIER_DIR, "dist");
57
+ const OUT_PATH = path.join(DIST_DIR, "verify-vh-standalone.js");
58
+ const SEAL_OUT_PATH = path.join(DIST_DIR, "seal-vh-standalone.js");
59
+ // The PUBLISHED checksum sidecar (T-35.3): the SHA-256 of the committed bundle, in the standard
60
+ // `sha256sum`/`shasum -a 256` line format (`<hex>␠␠<basename>\n`) so a counterparty can run
61
+ // `sha256sum -c <bundle>.sha256` after a one-line `curl`/save, BEFORE running the file. It is a pure
62
+ // function of the (deterministic) bundle bytes, so it never rots: the build re-emits it and the test
63
+ // asserts it equals sha256(committed bundle).
64
+ const SHA256_PATH = OUT_PATH + ".sha256";
65
+ const SEAL_SHA256_PATH = SEAL_OUT_PATH + ".sha256";
66
+ const SHA256_BASENAME = path.basename(OUT_PATH);
67
+ const SEAL_SHA256_BASENAME = path.basename(SEAL_OUT_PATH);
68
+
69
+ // The PUBLISHED build-provenance manifest (T-54.2): a single committed, deterministic JSON file that maps
70
+ // each published bundle's sha256 back to the ORDERED list of in-tree source files it inlines, EACH pinned by
71
+ // its own sha256. It is the bridge that lets trust root in READING SOURCE, not in trusting our published hex:
72
+ // a skeptic hashes the `lib/*.js` files they just audited, finds those exact hashes in this manifest, and sees
73
+ // they compose (in this exact order) the bundle whose hash is published in the `.sha256` sidecar. `--check`
74
+ // recomputes the WHOLE manifest from source and asserts the committed one matches byte-for-byte, so it can
75
+ // never drift from the bundles it describes. Pure function of the (committed) sources -> deterministic.
76
+ const PROVENANCE_PATH = path.join(DIST_DIR, "BUILD-PROVENANCE.json");
77
+ const PROVENANCE_BASENAME = path.basename(PROVENANCE_PATH);
78
+ // A version-free schema tag so a consumer can pin the shape without a timestamp leaking into the bytes.
79
+ const PROVENANCE_SCHEMA = "verifyhash/build-provenance@1";
80
+
81
+ // The exact textual contents of the `.sha256` sidecar for a given bundle text + basename. One canonical
82
+ // line, the standard two-space `sha256sum` separator, a trailing newline. Pure function -> deterministic.
83
+ function sha256SidecarFor(bundleText, basename) {
84
+ const hex = crypto.createHash("sha256").update(Buffer.from(bundleText, "utf8")).digest("hex");
85
+ return `${hex} ${basename}\n`;
86
+ }
87
+ // Back-compat alias: the verify bundle's sidecar (the original single-target signature T-35.3 tests call).
88
+ function sha256Sidecar(bundleText) {
89
+ return sha256SidecarFor(bundleText, SHA256_BASENAME);
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // The EXPLICIT, FIXED module list. Order is deterministic by construction (it is hand-listed, not derived
94
+ // from a filesystem walk). Each entry:
95
+ // id — the canonical module id used inside the bundle's __require() graph.
96
+ // file — the source file under verifier/ to inline (relative to VERIFIER_DIR).
97
+ // rewrite— map of the EXACT require() specifier as written in that file -> the canonical id it resolves
98
+ // to inside the bundle. Every relative require in a file MUST appear here (the build asserts it).
99
+ // body — (optional) when present, REPLACES the file's body entirely (used to swap the keccak provider
100
+ // for the vendored pure-JS one). When absent, the file's source is inlined with rewrites applied.
101
+ //
102
+ // The entrypoint (verify-vh.js) is inlined LAST and is the module the bundle boots.
103
+ // ---------------------------------------------------------------------------
104
+
105
+ // The vendored keccak provider, inlined as the body of the "keccak" module so that merkle/secp256k1 — which
106
+ // `require("./keccak")` — transparently use the pure-JS implementation with NO js-sha3 dependency. It
107
+ // exposes the SAME surface keccak.js exposes (`keccak256(bytes) -> Buffer`); merkle.js/secp256k1-recover.js
108
+ // rely on a Buffer return (`.slice(...).toString("hex")`, `Buffer.concat([...])`), so we wrap the vendored
109
+ // Uint8Array result in a Buffer. The vendored source is itself inlined as a private module the shim pulls.
110
+ const KECCAK_SHIM_BODY = [
111
+ '"use strict";',
112
+ "// Inlined keccak provider for the standalone bundle: the SAME `keccak256(bytes) -> Buffer` surface as",
113
+ "// verifier/lib/keccak.js, but backed by the PURE-JS, zero-dependency vendored implementation",
114
+ "// (verifier/lib/keccak256-vendored.js) instead of js-sha3 — so the bundle requires nothing external.",
115
+ 'var vendored = __require("keccak256-vendored");',
116
+ "function keccak256(bytes) {",
117
+ " if (!(bytes instanceof Uint8Array) && !Buffer.isBuffer(bytes)) {",
118
+ ' throw new TypeError("keccak256 requires a Buffer/Uint8Array of input bytes");',
119
+ " }",
120
+ " // The vendored routine returns a Uint8Array; wrap it as a Buffer so downstream `.slice(...).toString",
121
+ ' // ("hex")` and `Buffer.concat([...])` callers behave exactly as they do with the js-sha3-backed shim.',
122
+ " return Buffer.from(vendored.keccak256(bytes));",
123
+ "}",
124
+ "module.exports = { keccak256 };",
125
+ ].join("\n");
126
+
127
+ // The shared vendored-keccak modules every target inlines first: the pure-JS keccak256 and the "./keccak"
128
+ // shim swapped to use it. Both bundles share these byte-identical entries.
129
+ const SHARED_KECCAK_MODULES = [
130
+ // The pure-JS keccak256 — inlined VERBATIM from the committed vendored source (it already `require`s
131
+ // nothing, so there is no rewrite to apply). The keccak shim below pulls it via __require.
132
+ { id: "keccak256-vendored", file: "lib/keccak256-vendored.js", rewrite: {} },
133
+ // The keccak provider the rest of the verifier/sealer requires as "./keccak" — body SWAPPED to the shim.
134
+ { id: "keccak", file: "lib/keccak.js", rewrite: {}, body: KECCAK_SHIM_BODY },
135
+ ];
136
+
137
+ // --- TARGET 1: the VERIFY bundle (verify-vh-standalone.js). Module list/order UNCHANGED by T-36.2 so the
138
+ // committed verify bundle stays byte-identical. ---
139
+ const VERIFY_MODULES = [
140
+ ...SHARED_KECCAK_MODULES,
141
+ // The independent merkle / canonical / secp256k1 libs, inlined verbatim with their relative requires
142
+ // rewritten to canonical ids.
143
+ { id: "merkle", file: "lib/merkle.js", rewrite: { "./keccak": "keccak" } },
144
+ { id: "canonical", file: "lib/canonical.js", rewrite: {} },
145
+ { id: "secp256k1-recover", file: "lib/secp256k1-recover.js", rewrite: { "./keccak": "keccak" } },
146
+ // The stack-free recipient-side revocation decision (T-51.4), split by T-66.1 into the PURE core
147
+ // (revocation-core: validation/recovery/as-of decision; require()s only ./secp256k1-recover) and the
148
+ // thin fs-backed reader wrapper (revocation: readRevocationsFromPath/loadAndApply; Node core fs/path
149
+ // kept verbatim, both allowed-core).
150
+ { id: "revocation-core", file: "lib/revocation-core.js", rewrite: { "./secp256k1-recover": "secp256k1-recover" } },
151
+ { id: "revocation", file: "lib/revocation.js", rewrite: { "./revocation-core": "revocation-core" } },
152
+ // The entrypoint, inlined LAST. Its relative requires resolve to the canonical ids above.
153
+ {
154
+ id: "verify-vh",
155
+ file: "verify-vh.js",
156
+ rewrite: {
157
+ "./lib/merkle": "merkle",
158
+ "./lib/canonical": "canonical",
159
+ "./lib/secp256k1-recover": "secp256k1-recover",
160
+ "./lib/revocation": "revocation",
161
+ },
162
+ entry: true,
163
+ },
164
+ ];
165
+
166
+ // Back-compat alias for the original single-target name (some tests reference builder.MODULES).
167
+ const MODULES = VERIFY_MODULES;
168
+
169
+ // --- TARGET 2: the SEAL bundle (seal-vh-standalone.js). The free PRODUCE half (T-36.2). It needs ONLY the
170
+ // keccak shim + the merkle convention + the sealer CLI — NO secp256k1 (signing is the paid surface, so
171
+ // the standalone sealer has no key path at all) and NO canonical (the seal is plain JSON.stringify). ---
172
+ const SEAL_MODULES = [
173
+ ...SHARED_KECCAK_MODULES,
174
+ // The independent merkle convention (the SAME the verifier re-derives), so a seal this builds re-derives
175
+ // to the same root the verify bundle recomputes from the bytes.
176
+ { id: "merkle", file: "lib/merkle.js", rewrite: { "./keccak": "keccak" } },
177
+ // The sealer CLI, inlined LAST. Its only relative require ("./merkle") resolves to the canonical id above.
178
+ {
179
+ id: "seal-cli",
180
+ file: "lib/seal-cli.js",
181
+ rewrite: { "./merkle": "merkle" },
182
+ entry: true,
183
+ },
184
+ ];
185
+
186
+ // The require() specifiers a bundled module is ALLOWED to keep verbatim — Node core modules the standalone
187
+ // genuinely uses. Anything else (a bare third-party name, an unlisted relative path) is a build error: the
188
+ // bundle must never silently carry an external dependency. `crypto` is Node core too (used by the embedded
189
+ // `--self-attest` boot code that hashes the bundle's own bytes) — all install-free.
190
+ // `os` is Node core too (the T-55.2 `demo` quickstart uses `os.tmpdir()` for its throwaway working dir) —
191
+ // all install-free.
192
+ const ALLOWED_CORE = new Set([
193
+ "fs",
194
+ "path",
195
+ "crypto",
196
+ "os",
197
+ "node:fs",
198
+ "node:path",
199
+ "node:crypto",
200
+ "node:os",
201
+ ]);
202
+
203
+ // Collect every require("…") specifier in a source string.
204
+ function requireSpecifiers(src) {
205
+ return [...src.matchAll(/require\(\s*["']([^"']+)["']\s*\)/g)].map((m) => m[1]);
206
+ }
207
+
208
+ // Rewrite a module's source: every require("<spec>") is either (a) rewritten to __require("<canonical>")
209
+ // when <spec> is in the module's rewrite map, or (b) left verbatim when <spec> is an allowed Node-core
210
+ // module. Any other specifier is a hard build error (an un-inlined dependency would break the zero-dep
211
+ // guarantee). Returns the rewritten source.
212
+ function rewriteRequires(src, rewrite, idForError) {
213
+ // Assert up front that every relative require is covered by the rewrite map (so an added dependency in a
214
+ // source file can never slip into the bundle un-inlined).
215
+ for (const spec of requireSpecifiers(src)) {
216
+ const isCore = ALLOWED_CORE.has(spec);
217
+ const isMapped = Object.prototype.hasOwnProperty.call(rewrite, spec);
218
+ if (!isCore && !isMapped) {
219
+ throw new Error(
220
+ `build-standalone: module "${idForError}" has an un-inlined require(${JSON.stringify(spec)}). ` +
221
+ "Add it to the module's rewrite map (and inline its target) or it would break the zero-dependency bundle."
222
+ );
223
+ }
224
+ }
225
+ return src.replace(/require\(\s*["']([^"']+)["']\s*\)/g, (full, spec) => {
226
+ if (Object.prototype.hasOwnProperty.call(rewrite, spec)) {
227
+ return `__require(${JSON.stringify(rewrite[spec])})`;
228
+ }
229
+ return full; // an allowed Node-core require, kept verbatim
230
+ });
231
+ }
232
+
233
+ // Read a source file deterministically, normalizing line endings to "\n" (so a checkout with CRLF cannot
234
+ // change the emitted bytes) and stripping a leading shebang line (the bundle has its own).
235
+ function readSource(rel) {
236
+ let s = fs.readFileSync(path.join(VERIFIER_DIR, rel), "utf8");
237
+ s = s.replace(/\r\n/g, "\n");
238
+ s = s.replace(/^#![^\n]*\n/, "");
239
+ return s;
240
+ }
241
+
242
+ // sha256 hex of a utf8 string (the canonical hash unit for both bundles and their inlined sources).
243
+ function sha256HexOf(text) {
244
+ return crypto.createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
245
+ }
246
+
247
+ // The provenance record for ONE inlined module. There are two honest kinds:
248
+ // * a SOURCE module — inlined verbatim (with require()s rewritten) from a committed `verifier/<file>`. We
249
+ // pin it by `sourceFile` + the sha256 of its NORMALIZED source (the exact bytes the build reads), so a
250
+ // skeptic can `sha256` the file they audited and find that hash here. We also pin `inlinedSha256` — the
251
+ // hash of the post-rewrite text actually placed in the bundle — so the rewrite step is itself attested.
252
+ // * a SYNTHETIC module — the keccak "./keccak" shim, whose body is a constant in THIS builder (not a file).
253
+ // It has no `sourceFile`; we pin `inlinedSha256` of its body and mark `synthetic: true` so the chain is
254
+ // complete and honest (nothing inlined is left unaccounted for).
255
+ function moduleProvenance(m) {
256
+ if (typeof m.body === "string") {
257
+ return {
258
+ id: m.id,
259
+ synthetic: true,
260
+ sourceFile: null,
261
+ sourceSha256: null,
262
+ inlinedSha256: sha256HexOf(m.body),
263
+ note: "swapped body (keccak provider shim) — defined in build-standalone.js, not a source file",
264
+ };
265
+ }
266
+ const src = readSource(m.file);
267
+ const inlined = rewriteRequires(src, m.rewrite, m.id);
268
+ return {
269
+ id: m.id,
270
+ synthetic: false,
271
+ sourceFile: `verifier/${m.file}`,
272
+ sourceSha256: sha256HexOf(src),
273
+ inlinedSha256: sha256HexOf(inlined),
274
+ entry: m.entry === true,
275
+ };
276
+ }
277
+
278
+ // The provenance record for ONE target: the bundle's published basename/sidecar/size/sha256 + the ORDERED,
279
+ // individually-hashed source modules that compose it. This is the SINGLE source of truth shared verbatim by
280
+ // (a) the committed BUILD-PROVENANCE.json manifest and (b) the copy EMBEDDED inside the bundle itself
281
+ // (T-54.2 rework) — so the artifact a counterparty holds carries its OWN provenance, and `--check` proves the
282
+ // embedded copy equals the manifest's. `bundleSha256` is passed in (the build resolves the self-hash chicken-
283
+ // and-egg via a placeholder, see buildTarget) so this stays a pure function of its inputs -> deterministic.
284
+ function targetProvenance(target, bundleText, bundleSha256) {
285
+ return {
286
+ bundle: target.sha256Basename, // the bundle's basename (also the sidecar's pinned name)
287
+ sidecar: path.basename(target.sha256Path),
288
+ bundleBytes: Buffer.byteLength(bundleText, "utf8"),
289
+ bundleSha256,
290
+ sidecarLine: sha256SidecarFor(bundleText, target.sha256Basename).trim(),
291
+ // The ORDERED inlined modules — the exact composition a skeptic re-hashes the source against.
292
+ modules: target.modules.map(moduleProvenance),
293
+ };
294
+ }
295
+
296
+ // Build the FULL build-provenance object (the committed BUILD-PROVENANCE.json's content) from source. It maps
297
+ // each target's published bundle hash -> the ordered modules (each pinned by its own source hash) that compose
298
+ // it, plus the bundle's own size + the sidecar line that publishes its hash. Pure function of source + the
299
+ // (constant) target descriptors -> deterministic. The JSON text is the canonical 2-space pretty-print + a
300
+ // trailing newline so the committed file is stable and human-readable (and `git diff`-able across releases).
301
+ function buildProvenanceObject() {
302
+ const targets = {};
303
+ // Iterate the SAME fixed target order the build/--check use, so the manifest's key order is deterministic.
304
+ for (const key of ["verify", "seal"]) {
305
+ const target = TARGETS[key];
306
+ const bundleText = buildTarget(target);
307
+ targets[target.name] = targetProvenance(target, bundleText, sha256HexOf(bundleText));
308
+ }
309
+ // T-66.2: the OFFLINE HTML page target (verify-vh-standalone.html), built by the sibling builder
310
+ // verifier/build-standalone-html.js and recorded in this SAME manifest so ONE committed file pins every
311
+ // published bundle. Lazily required so neither builder observes the other's exports mid-load (this
312
+ // function only runs at build/check time, never at module load). The record shape is identical
313
+ // (bundle/sidecar/bytes/sha256/sidecarLine + ordered per-module source sha256s), so the chain check
314
+ // below attests the html page's sources — including verify-vh.js itself — exactly like the JS bundles'.
315
+ const htmlBuilder = require("./build-standalone-html");
316
+ targets[htmlBuilder.HTML_TARGET_NAME] = htmlBuilder.htmlTargetProvenance();
317
+ return {
318
+ schema: PROVENANCE_SCHEMA,
319
+ description:
320
+ "Maps each published verifyhash standalone bundle's sha256 to the ordered, individually-hashed in-tree " +
321
+ "source files it inlines. Reproduce + attest the whole chain offline with: node verifier/build-standalone.js --check",
322
+ targets,
323
+ };
324
+ }
325
+
326
+ // The canonical TEXT of BUILD-PROVENANCE.json: stable 2-space JSON + trailing newline. Deterministic.
327
+ function buildProvenanceText() {
328
+ return JSON.stringify(buildProvenanceObject(), null, 2) + "\n";
329
+ }
330
+
331
+ // The fixed, version-free banner (NO timestamp -> deterministic) for each target. Each is a function of
332
+ // nothing — a constant header array — so the emitted bytes stay a pure function of the source files.
333
+ const VERIFY_HEADER = [
334
+ "// verify-vh-standalone.js — the SINGLE-FILE, ZERO-DEPENDENCY, OFFLINE verifyhash verifier.",
335
+ "//",
336
+ "// SPDX-License-Identifier: Apache-2.0",
337
+ "// Copyright 2026 verifyhash.com — https://verifyhash.com",
338
+ "//",
339
+ "// GENERATED by verifier/build-standalone.js from the in-tree verifier — DO NOT EDIT BY HAND.",
340
+ "// Re-generate with: node verifier/build-standalone.js (the build is deterministic; see that file.)",
341
+ "//",
342
+ "// HOW TO USE IT (no clone, no `npm install`, no node_modules, no package.json):",
343
+ "// 1. Save THIS one file somewhere next to the sealed artifact you were handed.",
344
+ "// 2. Run: node verify-vh-standalone.js <artifact> [--vendor <0xaddr>] [--dir <d>] [--json]",
345
+ "// Exit codes: 0 ok / 3 rejected / 2 usage / 1 IO. It is READ-ONLY and opens NO network.",
346
+ "//",
347
+ "// SELF-DESCRIBING (needs NO second file): this bundle carries its OWN build-provenance.",
348
+ "// node verify-vh-standalone.js --self-attest # confirm THIS file's bytes are intact (0 ok / 1 modified)",
349
+ "// node verify-vh-standalone.js --provenance # print the ordered source modules + sha256 it was built from",
350
+ "//",
351
+ "// It RE-DERIVES the keccak Merkle root from the bytes YOU hold and recovers the signer with a",
352
+ "// pure-JS secp256k1 routine — it never trusts the artifact's own stored hashes, and it requires",
353
+ "// NOTHING outside Node core. This is the in-tree verifier inlined verbatim, with keccak256 swapped",
354
+ "// for a byte-identical pure-JS implementation (cross-checked against js-sha3 AND ethers).",
355
+ ];
356
+
357
+ const SEAL_HEADER = [
358
+ "// seal-vh-standalone.js — the SINGLE-FILE, ZERO-DEPENDENCY, OFFLINE verifyhash SEALER (free tier).",
359
+ "//",
360
+ "// SPDX-License-Identifier: Apache-2.0",
361
+ "// Copyright 2026 verifyhash.com — https://verifyhash.com",
362
+ "//",
363
+ "// GENERATED by verifier/build-standalone.js from the in-tree sealer — DO NOT EDIT BY HAND.",
364
+ "// Re-generate with: node verifier/build-standalone.js (the build is deterministic; see that file.)",
365
+ "//",
366
+ "// HOW TO USE IT (no clone, no `npm install`, no node_modules, no package.json, no account):",
367
+ "// 1. Save THIS one file somewhere.",
368
+ "// 2. Run: node seal-vh-standalone.js <folder> -o out.vhevidence.json",
369
+ "// 3. Hand `out.vhevidence.json` (+ your folder) to anyone; they verify it with verify-vh-standalone.js",
370
+ "// — also zero-install. Exit codes: 0 sealed / 1 IO / 2 usage (incl. >25 files) / 3 seal-build error.",
371
+ "//",
372
+ "// SELF-DESCRIBING (needs NO second file): this bundle carries its OWN build-provenance.",
373
+ "// node seal-vh-standalone.js --self-attest # confirm THIS file's bytes are intact (0 ok / 1 modified)",
374
+ "// node seal-vh-standalone.js --provenance # print the ordered source modules + sha256 it was built from",
375
+ "//",
376
+ "// FREE TIER: an UNSIGNED seal of up to 25 files. Sealing MORE files (`evidence_unlimited`) or a SIGNED",
377
+ "// wrap (`evidence_signed`) is the PAID surface via `vh evidence seal` — this file has NO --sign/--license",
378
+ "// /--key flag and uses NO key. It is READ-ONLY apart from the -o file you name, and opens NO network. The",
379
+ "// seal is TAMPER-EVIDENT + OFFLINE-RECOMPUTABLE, NOT a trusted timestamp. keccak256 is the byte-identical",
380
+ "// pure-JS implementation the verifier uses, so a seal this builds is accepted verbatim by the verifier.",
381
+ ];
382
+
383
+ // The tiny CommonJS shim every target embeds: a module registry + a memoizing __require. Byte-identical
384
+ // across targets.
385
+ const COMMONJS_SHIM = [
386
+ "// ---- minimal CommonJS module shim (so the inlined modules keep their require() structure) --------",
387
+ "var __modules = Object.create(null);",
388
+ "var __cache = Object.create(null);",
389
+ "function __require(id) {",
390
+ " if (id in __cache) return __cache[id].exports;",
391
+ " var factory = __modules[id];",
392
+ " if (!factory) throw new Error('standalone bundle: unknown module: ' + id);",
393
+ " var module = { exports: {} };",
394
+ " __cache[id] = module;",
395
+ " factory(module, module.exports, __require);",
396
+ " return module.exports;",
397
+ "}",
398
+ ];
399
+
400
+ // The two build targets — each fully describes ONE deterministic bundle (its module list + banner + the
401
+ // human noun used in the boot comment). Both go through the SAME buildTarget() so the seal bundle inherits
402
+ // every zero-dependency / determinism property the verify bundle has.
403
+ const TARGETS = {
404
+ verify: { name: "verify", modules: VERIFY_MODULES, header: VERIFY_HEADER, cliNoun: "verifier", outPath: OUT_PATH, sha256Path: SHA256_PATH, sha256Basename: SHA256_BASENAME },
405
+ seal: { name: "seal", modules: SEAL_MODULES, header: SEAL_HEADER, cliNoun: "sealer", outPath: SEAL_OUT_PATH, sha256Path: SEAL_SHA256_PATH, sha256Basename: SEAL_SHA256_BASENAME },
406
+ };
407
+
408
+ // The fixed 64-char sentinel the bundle's EMBEDDED self-hash is computed against (and re-blanked to at
409
+ // runtime). The bundle can't embed sha256(itself) directly — that is a fixed point (writing the hash changes
410
+ // the bytes it hashes). Instead the bundle's `selfSha256` is DEFINED as sha256(bundle text with the selfSha256
411
+ // field set to this sentinel). `--self-attest` re-blanks its own selfSha256 line back to the sentinel and
412
+ // re-hashes, so it can confirm its bytes are intact from the SINGLE file alone — no repo, no network, no
413
+ // sidecar. (The published `.sha256` sidecar still pins sha256(final file) so `sha256sum -c` is unchanged.)
414
+ const SELF_SHA256_SENTINEL = "0".repeat(64);
415
+
416
+ // The provenance object EMBEDDED in a bundle so the single shipped file carries — and can self-attest — its
417
+ // own provenance. It lists the SAME ordered source modules the manifest records for this target (so `--check`
418
+ // proves the embedded copy == the manifest), PLUS a `selfSha256`. The selfSha256 value is supplied by the
419
+ // caller: during pass 1 of buildTarget it is the SENTINEL (the hash is not yet known); the real hash is
420
+ // substituted in pass 2. Pure function of its inputs -> deterministic.
421
+ function embeddedProvenanceObject(target, selfSha256) {
422
+ return {
423
+ schema: PROVENANCE_SCHEMA,
424
+ target: target.name,
425
+ note:
426
+ "This bundle's OWN provenance, embedded so the single file is self-describing. Run " +
427
+ "`node " + target.sha256Basename + " --self-attest` to recompute selfSha256 from these very bytes, or " +
428
+ "`--provenance` to print the ordered source modules + hashes it was built from. Cross-check against " +
429
+ "verifier/dist/BUILD-PROVENANCE.json (the same data) with: node verifier/build-standalone.js --check",
430
+ selfSha256, // sha256 of THESE bytes with the selfSha256 field blanked to SELF_SHA256_SENTINEL
431
+ modules: target.modules.map(moduleProvenance),
432
+ };
433
+ }
434
+
435
+ // The boot-time handler embedded in every bundle: intercepts `--provenance` / `--self-attest` BEFORE the
436
+ // inlined CLI sees them (it would reject unknown flags). `--provenance` prints the embedded provenance JSON.
437
+ // `--self-attest` re-reads THIS file, blanks its own selfSha256 back to the sentinel, re-hashes, and prints
438
+ // MATCH/MISMATCH — proving (from the single file, no network) that its bytes are exactly what the build
439
+ // produced. Returns an exit code to use, or null to fall through to the normal CLI. Uses only Node core.
440
+ const PROVENANCE_BOOT = [
441
+ "function __maybeProvenance(argv) {",
442
+ " var wantProv = argv.indexOf('--provenance') !== -1;",
443
+ " var wantAttest = argv.indexOf('--self-attest') !== -1;",
444
+ " if (!wantProv && !wantAttest) return null;",
445
+ " if (wantProv) { process.stdout.write(JSON.stringify(__PROVENANCE, null, 2) + '\\n'); }",
446
+ " if (wantAttest) {",
447
+ " var fs = require('fs');",
448
+ " var crypto = require('crypto');",
449
+ " var selfText;",
450
+ " try { selfText = fs.readFileSync(__filename, 'utf8'); }",
451
+ " catch (e) { process.stderr.write('self-attest: cannot read this file: ' + e.message + '\\n'); return 1; }",
452
+ " // Re-blank our own selfSha256 line back to the sentinel, then hash — reproducing the build's pass-1 hash.",
453
+ " var blanked = selfText.replace(",
454
+ " '\"selfSha256\": \"' + __PROVENANCE.selfSha256 + '\"',",
455
+ " '\"selfSha256\": \"' + __SELF_SHA256_SENTINEL + '\"'",
456
+ " );",
457
+ " var got = crypto.createHash('sha256').update(Buffer.from(blanked, 'utf8')).digest('hex');",
458
+ " if (got === __PROVENANCE.selfSha256) {",
459
+ " process.stdout.write('[MATCH] self-attest: this file is intact (selfSha256 ' + got + ').\\n');",
460
+ " return 0;",
461
+ " }",
462
+ " process.stderr.write('[MISMATCH] self-attest: this file has been MODIFIED ' +",
463
+ " '(embedded selfSha256 ' + __PROVENANCE.selfSha256 + ' != recomputed ' + got + ').\\n');",
464
+ " return 1;",
465
+ " }",
466
+ " return 0;",
467
+ "}",
468
+ ];
469
+
470
+ // Build a target's bundle TEXT deterministically. Pure function of the committed source files + the target
471
+ // descriptor (a constant). Same target -> byte-identical output.
472
+ function buildTarget(target) {
473
+ const parts = [];
474
+
475
+ // --- header: shebang + a fixed, version-free banner (NO timestamp -> deterministic) ---
476
+ parts.push("#!/usr/bin/env node");
477
+ parts.push('"use strict";');
478
+ parts.push("");
479
+ parts.push(target.header.join("\n"));
480
+ parts.push("");
481
+
482
+ // --- the tiny CommonJS shim: a module registry + a memoizing __require. ---
483
+ parts.push(COMMONJS_SHIM.join("\n"));
484
+ parts.push("");
485
+
486
+ let entryId = null;
487
+
488
+ // --- inline each module as a factory in the fixed list order. ---
489
+ for (const m of target.modules) {
490
+ let body;
491
+ if (typeof m.body === "string") {
492
+ // The body is supplied verbatim (the keccak shim). It may itself call __require (e.g. for the
493
+ // vendored keccak) — that is the bundle's own shim, not an external dependency, so it is allowed.
494
+ body = m.body;
495
+ } else {
496
+ const src = readSource(m.file);
497
+ body = rewriteRequires(src, m.rewrite, m.id);
498
+ }
499
+ if (m.entry) entryId = m.id;
500
+
501
+ parts.push(`// ===== module: ${m.id} (from verifier/${m.file}) =====`);
502
+ parts.push(`__modules[${JSON.stringify(m.id)}] = function (module, exports, __require) {`);
503
+ parts.push(body);
504
+ parts.push("};");
505
+ parts.push("");
506
+ }
507
+
508
+ if (!entryId) throw new Error("build-standalone: no entry module declared");
509
+
510
+ // --- EMBEDDED self-attesting provenance (T-54.2 rework): the bundle carries its OWN provenance so a
511
+ // counterparty handed JUST this one file can (a) `--provenance` to see the ordered source modules +
512
+ // hashes it was built from, and (b) `--self-attest` to confirm its own bytes are intact — from the
513
+ // single file, no repo / network / sidecar. The selfSha256 is computed below via the sentinel trick. ---
514
+ parts.push(
515
+ "// ---- embedded build-provenance (this file's own): see `--provenance` / `--self-attest` below. ----"
516
+ );
517
+ parts.push(`var __SELF_SHA256_SENTINEL = ${JSON.stringify(SELF_SHA256_SENTINEL)};`);
518
+ // Placeholder object emitted with the sentinel; the real selfSha256 is substituted after the full text is
519
+ // assembled and hashed (see the substitution at the end of this function).
520
+ parts.push("var __PROVENANCE = __SELF_SHA256_PLACEHOLDER__;");
521
+ parts.push(PROVENANCE_BOOT.join("\n"));
522
+ parts.push("");
523
+
524
+ // --- boot the entrypoint exactly as the inlined CLI does at the bottom of its own file. ---
525
+ // The inlined entry module sets `module.exports = { ..., run }` and has a `require.main === module` CLI
526
+ // shim that does NOT fire inside the bundle's factory (its `module` is the shim's, not Node's), so we
527
+ // drive the CLI explicitly here: load the entry module and run it with process.argv, exiting on its code.
528
+ // The embedded-provenance flags (`--provenance` / `--self-attest`) are handled FIRST so they never reach
529
+ // the inlined CLI (which would reject them as unknown). All other argv passes through unchanged.
530
+ parts.push(`// ---- boot: run the inlined ${target.cliNoun} CLI with this process's argv. ----`);
531
+ parts.push(`var __entry = __require(${JSON.stringify(entryId)});`);
532
+ parts.push("if (require.main === module) {");
533
+ parts.push(" var __code = __maybeProvenance(process.argv.slice(2));");
534
+ parts.push(" if (__code !== null) process.exit(__code);");
535
+ parts.push(" process.exit(__entry.run(process.argv.slice(2)));");
536
+ parts.push("}");
537
+ parts.push("module.exports = __entry;");
538
+ parts.push(""); // single trailing newline
539
+
540
+ // Assemble with a sentinel selfSha256, hash, then substitute the real self-hash in. Two passes resolve the
541
+ // self-reference: pass 1 computes sha256 over the bytes WITH the sentinel; pass 2 writes that hash in. The
542
+ // runtime `--self-attest` re-blanks selfSha256 back to the sentinel before re-hashing, so it reproduces the
543
+ // pass-1 hash exactly. Deterministic: the sentinel + module list are constants.
544
+ const provWithSentinel = embeddedProvenanceObject(target, SELF_SHA256_SENTINEL);
545
+ const textWithSentinel = parts
546
+ .join("\n")
547
+ .replace("__SELF_SHA256_PLACEHOLDER__", JSON.stringify(provWithSentinel, null, 2));
548
+ const selfSha256 = sha256HexOf(textWithSentinel);
549
+ // Substitute the real self-hash for the sentinel selfSha256 (a unique, fixed-length token swap).
550
+ return textWithSentinel.replace(
551
+ `"selfSha256": ${JSON.stringify(SELF_SHA256_SENTINEL)}`,
552
+ `"selfSha256": ${JSON.stringify(selfSha256)}`
553
+ );
554
+ }
555
+
556
+ // Compute the EXPECTED embedded provenance object for a target (with its REAL, two-pass-resolved selfSha256),
557
+ // straight from source. This is the canonical "what the bundle's __PROVENANCE should be" the `--check` path
558
+ // compares the committed bundle's embedded copy against. Pure function of source -> deterministic.
559
+ function expectedEmbeddedProvenance(target) {
560
+ const provWithSentinel = embeddedProvenanceObject(target, SELF_SHA256_SENTINEL);
561
+ const textWithSentinel = buildTarget(target).replace(
562
+ new RegExp(`"selfSha256": "[0-9a-f]{64}"`),
563
+ `"selfSha256": ${JSON.stringify(SELF_SHA256_SENTINEL)}`
564
+ );
565
+ const selfSha256 = sha256HexOf(textWithSentinel);
566
+ return embeddedProvenanceObject(target, selfSha256);
567
+ // (provWithSentinel is the same shape; we recompute selfSha256 honestly here so this helper is standalone.)
568
+ }
569
+
570
+ // Extract the embedded __PROVENANCE object from a built/committed bundle's TEXT, so `--check` can read what the
571
+ // SHIPPED file actually carries (not just what we recompute). The bundle assigns `var __PROVENANCE = { ... };`
572
+ // as a pretty-printed JSON literal; we slice that literal out and JSON.parse it. Returns the object, or null if
573
+ // the bundle has no embedded provenance (e.g. a stale/foreign file) so the caller reports a clean MISMATCH.
574
+ function extractEmbeddedProvenance(bundleText) {
575
+ const marker = "var __PROVENANCE = ";
576
+ const start = bundleText.indexOf(marker);
577
+ if (start === -1) return null;
578
+ // The literal runs from the first "{" after the marker to its matching "}" (brace-balanced; the JSON has no
579
+ // braces inside string values that aren't balanced, but we scan with a tiny string-aware matcher to be safe).
580
+ let i = bundleText.indexOf("{", start);
581
+ if (i === -1) return null;
582
+ let depth = 0;
583
+ let inStr = false;
584
+ let esc = false;
585
+ let end = -1;
586
+ for (; i < bundleText.length; i++) {
587
+ const c = bundleText[i];
588
+ if (inStr) {
589
+ if (esc) esc = false;
590
+ else if (c === "\\") esc = true;
591
+ else if (c === '"') inStr = false;
592
+ continue;
593
+ }
594
+ if (c === '"') inStr = true;
595
+ else if (c === "{") depth++;
596
+ else if (c === "}") {
597
+ depth--;
598
+ if (depth === 0) { end = i + 1; break; }
599
+ }
600
+ }
601
+ if (end === -1) return null;
602
+ try {
603
+ return JSON.parse(bundleText.slice(bundleText.indexOf("{", start), end));
604
+ } catch (_) {
605
+ return null;
606
+ }
607
+ }
608
+
609
+ // Build the VERIFY bundle TEXT (the original single-target API; UNCHANGED bytes). Pure function of source.
610
+ function buildBundle() {
611
+ return buildTarget(TARGETS.verify);
612
+ }
613
+
614
+ // Build the SEAL bundle TEXT. Pure function of source.
615
+ function buildSealBundle() {
616
+ return buildTarget(TARGETS.seal);
617
+ }
618
+
619
+ // Write ONE target's bundle + its `.sha256` sidecar to disk. Creates verifier/dist/ if absent. Returns the
620
+ // emitted text so callers can compare without a re-read.
621
+ function writeTarget(target) {
622
+ const text = buildTarget(target);
623
+ fs.mkdirSync(DIST_DIR, { recursive: true });
624
+ fs.writeFileSync(target.outPath, text);
625
+ // Re-emit the published checksum so the sidecar can never drift from the bundle it pins.
626
+ fs.writeFileSync(target.sha256Path, sha256SidecarFor(text, target.sha256Basename));
627
+ return text;
628
+ }
629
+
630
+ // Write the VERIFY bundle (original single-target API name). Returns its text.
631
+ function writeBundle() {
632
+ return writeTarget(TARGETS.verify);
633
+ }
634
+
635
+ // Write the build-provenance manifest. Spans BOTH targets, so it is written once (not per-target). Returns
636
+ // the emitted text. Creates verifier/dist/ if absent.
637
+ function writeProvenance() {
638
+ const text = buildProvenanceText();
639
+ fs.mkdirSync(DIST_DIR, { recursive: true });
640
+ fs.writeFileSync(PROVENANCE_PATH, text);
641
+ return text;
642
+ }
643
+
644
+ // Write BOTH targets AND the provenance manifest. Returns { verify, seal, provenance } texts.
645
+ function writeAll() {
646
+ const verify = writeTarget(TARGETS.verify);
647
+ const seal = writeTarget(TARGETS.seal);
648
+ const provenance = writeProvenance();
649
+ return { verify, seal, provenance };
650
+ }
651
+
652
+ // ---------------------------------------------------------------------------
653
+ // REPRODUCE-AND-ATTEST (`--check`) — the third-party-runnable answer to "who verifies the verifier?"
654
+ //
655
+ // WHY THIS EXISTS (T-54.1 / T-54.2)
656
+ // The free standalone verifier is the FUNNEL: a cold prospect's security team is asked to RUN
657
+ // `verify-vh-standalone.js` and trust its verdict (P-8 step 3a/3b). The bundle ships beside a `.sha256`
658
+ // sidecar — but that sidecar comes FROM THE SAME PLACE as the bundle, so on its own it proves only that the
659
+ // file survived transport, not that it is the audited in-tree source. `--check` closes that gap WITHOUT a
660
+ // network, WITHOUT trusting us: it RE-COMPILES each bundle from the in-tree source the skeptic can READ,
661
+ // recomputes the published checksum from those bytes, and asserts the COMMITTED bundle + sidecar are
662
+ // byte-for-byte what that source compiles to. A skeptic clones the repo (or is handed the verifier/ tree),
663
+ // reads the deterministic builder + the sources, runs `node verifier/build-standalone.js --check`, and gets
664
+ // a per-target MATCH/MISMATCH verdict — purely from local files, writing NOTHING under the source tree.
665
+ //
666
+ // The COMMITTED build-provenance manifest (verifier/dist/BUILD-PROVENANCE.json) is what raises this from "the
667
+ // bundle reproduces" to "the EXACT source files I audited, each pinned by its own sha256, are what the
668
+ // published bundle was built from." `--check` reproduces the whole manifest from source AND cross-checks every
669
+ // inlined source file against the hash the COMMITTED manifest pins for it — so a one-byte change to ANY single
670
+ // source file (not just the bundle) is named precisely, by its own filename, against the published pin. That
671
+ // is the table-stakes attestation a procurement/security reviewer needs: "the file I read is the file that
672
+ // shipped." A reviewer can also pin/track each source hash across releases straight from this human-readable
673
+ // JSON, with no tooling.
674
+ //
675
+ // It is the strongest honest claim we can make: "don't trust our binary or our checksum — reproduce both, and
676
+ // every source file that composes them, from the source you just read." It opens NO socket and (unlike the
677
+ // default build) writes NOTHING.
678
+
679
+ // Reproduce ONE target in memory and compare against the committed on-disk bundle + sidecar. Pure read-only:
680
+ // it rebuilds the bundle text from source (no write), recomputes the sidecar text, then reads the two
681
+ // committed files and compares. Returns a structured result with a per-file (bundle, sidecar) verdict; the
682
+ // `ok` flag is true only when BOTH the bundle bytes AND the sidecar bytes reproduce exactly.
683
+ function checkTarget(target) {
684
+ const rel = (p) => path.relative(VERIFIER_DIR, p);
685
+
686
+ const result = {
687
+ name: target.name,
688
+ bundlePath: rel(target.outPath),
689
+ sha256Path: rel(target.sha256Path),
690
+ expectedHex: null,
691
+ bundle: { ok: false, reason: "" },
692
+ sidecar: { ok: false, reason: "" },
693
+ sources: { ok: true, reason: "", offenders: [] },
694
+ embedded: { ok: false, reason: "" },
695
+ };
696
+
697
+ // --- the SOURCE PRESENCE check runs FIRST, BEFORE recompiling — so a MISSING inlined source yields a clean
698
+ // MISMATCH verdict naming that file, never an uncaught build crash. Recording the offending SOURCE FILE
699
+ // by name turns "the bundle changed" into "THIS audited file is gone/changed", the actionable verdict a
700
+ // security reviewer wants. (Synthetic modules — the keccak shim — live in THIS builder, not a file, so
701
+ // they cannot be tampered independently and are reported as such, never as a missing source.) ---
702
+ for (const m of target.modules) {
703
+ if (typeof m.body === "string") continue; // synthetic body (keccak shim) — no on-disk source to attest
704
+ const abs = path.join(VERIFIER_DIR, m.file);
705
+ if (!fs.existsSync(abs)) {
706
+ result.sources.ok = false;
707
+ result.sources.offenders.push({ id: m.id, sourceFile: `verifier/${m.file}`, reason: "MISSING" });
708
+ }
709
+ }
710
+ if (result.sources.offenders.length) {
711
+ result.sources.reason =
712
+ `inlined source(s) MISSING: ` + result.sources.offenders.map((o) => o.sourceFile).join(", ");
713
+ } else {
714
+ result.sources.reason = `all ${target.modules.filter((m) => typeof m.body !== "string").length} inlined source files present`;
715
+ }
716
+
717
+ // Recompute from source — exactly the bytes a fresh build would emit, but held in memory. Wrapped so that a
718
+ // build failure (a missing/unreadable inlined source) becomes a clean MISMATCH verdict, not a stack trace:
719
+ // a third-party reproduce tool must always report, never crash.
720
+ let expectedBundle, expectedBundleBuf, expectedHex, expectedSidecar;
721
+ try {
722
+ expectedBundle = buildTarget(target); // utf8 text
723
+ expectedBundleBuf = Buffer.from(expectedBundle, "utf8");
724
+ expectedHex = crypto.createHash("sha256").update(expectedBundleBuf).digest("hex");
725
+ expectedSidecar = sha256SidecarFor(expectedBundle, target.sha256Basename); // utf8 text
726
+ result.expectedHex = expectedHex;
727
+ } catch (e) {
728
+ const why = `cannot recompile ${result.bundlePath} from source: ${e && e.message ? e.message : e}`;
729
+ result.bundle.reason = why;
730
+ result.sidecar.reason = why;
731
+ result.ok = false;
732
+ return result;
733
+ }
734
+
735
+ // --- the BUNDLE: recomputed bytes must equal the committed bytes EXACTLY. ---
736
+ if (!fs.existsSync(target.outPath)) {
737
+ result.bundle.reason = `committed bundle ${result.bundlePath} is MISSING`;
738
+ } else {
739
+ const committedBundle = fs.readFileSync(target.outPath); // raw bytes, as shipped
740
+ if (committedBundle.equals(expectedBundleBuf)) {
741
+ result.bundle.ok = true;
742
+ result.bundle.reason = `recomputed bytes == committed bytes (sha256 ${expectedHex})`;
743
+ } else {
744
+ const committedHex = crypto.createHash("sha256").update(committedBundle).digest("hex");
745
+ result.bundle.reason =
746
+ `committed bundle does NOT reproduce from source ` +
747
+ `(committed sha256 ${committedHex} != recomputed ${expectedHex})`;
748
+ }
749
+ }
750
+
751
+ // --- the SIDECAR: recomputed published-hex line must equal the committed sidecar EXACTLY (and the hex it
752
+ // publishes must be the hex of the recomputed bundle, not whatever a tampered file claims). ---
753
+ if (!fs.existsSync(target.sha256Path)) {
754
+ result.sidecar.reason = `committed sidecar ${result.sha256Path} is MISSING`;
755
+ } else {
756
+ const committedSidecar = fs.readFileSync(target.sha256Path, "utf8");
757
+ if (committedSidecar === expectedSidecar) {
758
+ result.sidecar.ok = true;
759
+ result.sidecar.reason = `published hex == recomputed hex (${expectedHex})`;
760
+ } else {
761
+ result.sidecar.reason =
762
+ `committed sidecar does NOT match the recomputed published line ` +
763
+ `(expected "${expectedSidecar.trim()}", got "${committedSidecar.trim()}")`;
764
+ }
765
+ }
766
+
767
+ // --- the EMBEDDED provenance (T-54.2 rework): the committed bundle carries its OWN __PROVENANCE so a holder
768
+ // of just the single file can `--provenance` / `--self-attest` it. We assert that embedded copy is
769
+ // (a) extractable, (b) IDENTICAL to what the build expects (same ordered modules + selfSha256), so it can
770
+ // never silently drift from the source-of-truth it claims to mirror. (When the bundle bytes already
771
+ // reproduce, this is implied — but naming it as its own verdict makes the customer-facing self-attest
772
+ // surface an explicit, separately-attested guarantee.) ---
773
+ const expectedEmbedded = expectedEmbeddedProvenance(target);
774
+ if (!fs.existsSync(target.outPath)) {
775
+ result.embedded.reason = `committed bundle ${result.bundlePath} is MISSING (no embedded provenance to read)`;
776
+ } else {
777
+ const committedText = fs.readFileSync(target.outPath, "utf8");
778
+ const got = extractEmbeddedProvenance(committedText);
779
+ if (!got) {
780
+ result.embedded.reason = `committed bundle carries NO readable embedded __PROVENANCE`;
781
+ } else if (JSON.stringify(got) !== JSON.stringify(expectedEmbedded)) {
782
+ result.embedded.reason =
783
+ `embedded __PROVENANCE does NOT match what the build expects ` +
784
+ `(embedded selfSha256 ${got.selfSha256 || "?"} vs expected ${expectedEmbedded.selfSha256})`;
785
+ } else {
786
+ result.embedded.ok = true;
787
+ result.embedded.reason = `embedded __PROVENANCE == expected (selfSha256 ${expectedEmbedded.selfSha256})`;
788
+ }
789
+ }
790
+
791
+ result.ok = result.bundle.ok && result.sidecar.ok && result.sources.ok && result.embedded.ok;
792
+ return result;
793
+ }
794
+
795
+ // Reproduce the build-provenance MANIFEST from source and compare against the committed BUILD-PROVENANCE.json,
796
+ // AND cross-check every per-module source hash IN that manifest against the file on disk — so a one-byte change
797
+ // to ANY inlined source is named precisely (by its `sourceFile`), not just surfaced as an opaque bundle drift.
798
+ // Pure read-only. Returns { ok, manifestPath, manifest: {ok, reason}, chain: {ok, reason, offenders} }.
799
+ function checkProvenance() {
800
+ const rel = (p) => path.relative(VERIFIER_DIR, p);
801
+
802
+ const result = {
803
+ manifestPath: rel(PROVENANCE_PATH),
804
+ manifest: { ok: false, reason: "" },
805
+ chain: { ok: true, reason: "", offenders: [] },
806
+ };
807
+
808
+ // Recompute the manifest from source. Wrapped so a missing/unreadable source becomes a clean MISMATCH (the
809
+ // chain check below still names the offending file from the COMMITTED manifest's pins) rather than a crash.
810
+ let expectedText, expectedObj;
811
+ try {
812
+ expectedText = buildProvenanceText();
813
+ expectedObj = buildProvenanceObject();
814
+ } catch (e) {
815
+ result.manifest.reason =
816
+ `cannot recompute ${result.manifestPath} from source: ${e && e.message ? e.message : e}`;
817
+ expectedText = null;
818
+ expectedObj = { targets: {} };
819
+ }
820
+
821
+ // (a) the committed manifest must reproduce byte-for-byte from source (so it can never drift from the
822
+ // bundles it describes — a stale or hand-edited manifest is a MISMATCH).
823
+ let committedObj = null;
824
+ if (!fs.existsSync(PROVENANCE_PATH)) {
825
+ result.manifest.reason = `committed manifest ${result.manifestPath} is MISSING`;
826
+ } else {
827
+ const committed = fs.readFileSync(PROVENANCE_PATH, "utf8");
828
+ try {
829
+ committedObj = JSON.parse(committed);
830
+ } catch (_) {
831
+ committedObj = null; // unparseable committed manifest -> the chain check below falls back to source
832
+ }
833
+ if (expectedText === null) {
834
+ // The recompute already failed (a source is missing/unreadable); result.manifest.reason is set above.
835
+ // Leave manifest.ok=false so the manifest line MISMATCHes — the chain below still names the offender.
836
+ } else if (committed === expectedText) {
837
+ result.manifest.ok = true;
838
+ result.manifest.reason = `recomputed manifest == committed manifest (sha256 ${sha256HexOf(expectedText)})`;
839
+ } else {
840
+ result.manifest.reason =
841
+ `committed manifest does NOT reproduce from source ` +
842
+ `(committed sha256 ${sha256HexOf(committed)} != recomputed ${sha256HexOf(expectedText)})`;
843
+ }
844
+ }
845
+
846
+ // (b) the CHAIN — the load-bearing attestation: every NON-synthetic source file must hash to exactly the
847
+ // sha256 the COMMITTED manifest PINS for it. This is precisely "the file I audited is the file the
848
+ // published bundle was built from" — pinned by an artifact that ships WITH the bundles, not recomputed
849
+ // on the fly. A one-byte change to ANY inlined source is named HERE by its own filename (with the pinned
850
+ // vs. on-disk hash), even though it ALSO perturbs the bundle hash. We pin against the COMMITTED manifest
851
+ // (not the freshly recomputed one) so the check is anchored to what was published; if the committed
852
+ // manifest is missing/unparseable we fall back to the recomputed pins so the chain is still attested.
853
+ const pinSource = committedObj && committedObj.targets ? committedObj : expectedObj;
854
+ // Gather the canonical {sourceFile -> pinned sha256} from whichever manifest we are pinning against, de-duped
855
+ // across the verify+seal targets (which share modules like merkle / keccak-vendored source).
856
+ const pinned = new Map();
857
+ for (const target of Object.values(pinSource.targets)) {
858
+ for (const mod of target.modules || []) {
859
+ if (mod.synthetic || !mod.sourceFile) continue;
860
+ if (!pinned.has(mod.sourceFile)) pinned.set(mod.sourceFile, { id: mod.id, sha256: mod.sourceSha256 });
861
+ }
862
+ }
863
+ for (const [sourceFile, pin] of pinned) {
864
+ const relFile = sourceFile.replace(/^verifier\//, "");
865
+ const abs = path.join(VERIFIER_DIR, relFile);
866
+ const onDisk = fs.existsSync(abs) ? sha256HexOf(readSource(relFile)) : null;
867
+ if (onDisk !== pin.sha256) {
868
+ result.chain.offenders.push({
869
+ id: pin.id,
870
+ sourceFile,
871
+ expected: pin.sha256,
872
+ got: onDisk === null ? "MISSING" : onDisk,
873
+ });
874
+ result.chain.ok = false;
875
+ }
876
+ }
877
+ if (result.chain.offenders.length) {
878
+ result.chain.reason =
879
+ `source(s) do NOT match the manifest's pinned sha256: ` +
880
+ result.chain.offenders.map((o) => `${o.sourceFile} (pinned ${String(o.expected).slice(0, 12)}…, got ${o.got === "MISSING" ? "MISSING" : o.got.slice(0, 12) + "…"})`).join("; ");
881
+ } else {
882
+ result.chain.reason = "every inlined source file hashes to its manifest-pinned sha256";
883
+ }
884
+
885
+ result.ok = result.manifest.ok && result.chain.ok;
886
+ return result;
887
+ }
888
+
889
+ // Drive `--check` across BOTH targets. Writes a per-file MATCH/MISMATCH report to the supplied io (defaults
890
+ // to process stdout/stderr) and returns the process exit code: 0 iff EVERY bundle AND EVERY sidecar
891
+ // reproduced byte-for-byte from source, else 1. Read-only: it writes NOTHING under the source tree.
892
+ function runCheck(io) {
893
+ const out = (io && io.write) || ((s) => process.stdout.write(s));
894
+ const err = (io && io.writeErr) || ((s) => process.stderr.write(s));
895
+
896
+ out("verifyhash standalone REPRODUCE-AND-ATTEST (--check): re-compiling each bundle from in-tree source,\n");
897
+ out("recomputing its published checksum + build-provenance manifest, and comparing all against the committed\n");
898
+ out("files. No network; no writes.\n\n");
899
+
900
+ let allOk = true;
901
+ for (const target of [TARGETS.verify, TARGETS.seal]) {
902
+ const r = checkTarget(target);
903
+ const bundleTag = r.bundle.ok ? "MATCH" : "MISMATCH";
904
+ const sidecarTag = r.sidecar.ok ? "MATCH" : "MISMATCH";
905
+ const sourcesTag = r.sources.ok ? "MATCH" : "MISMATCH";
906
+ const embeddedTag = r.embedded.ok ? "MATCH" : "MISMATCH";
907
+ out(`[${bundleTag}] bundle ${r.bundlePath}: ${r.bundle.reason}\n`);
908
+ out(`[${sidecarTag}] sidecar ${r.sha256Path}: ${r.sidecar.reason}\n`);
909
+ out(`[${sourcesTag}] sources ${r.bundlePath}: ${r.sources.reason}\n`);
910
+ // The bundle's OWN embedded provenance / self-attest record — the customer-facing trust surface that
911
+ // travels WITH the single file (see `node <bundle> --self-attest` / `--provenance`).
912
+ out(`[${embeddedTag}] embedded ${r.bundlePath}: ${r.embedded.reason}\n`);
913
+ if (!r.ok) allOk = false;
914
+ }
915
+
916
+ // The build-provenance manifest + the source->hash chain it pins. This is what lets trust root in READING
917
+ // SOURCE: a corrupted inlined source file is named HERE by its own filename, with the manifest-pinned hash.
918
+ const prov = checkProvenance();
919
+ const manifestTag = prov.manifest.ok ? "MATCH" : "MISMATCH";
920
+ const chainTag = prov.chain.ok ? "MATCH" : "MISMATCH";
921
+ out(`[${manifestTag}] manifest ${prov.manifestPath}: ${prov.manifest.reason}\n`);
922
+ out(`[${chainTag}] sources->manifest: ${prov.chain.reason}\n`);
923
+ if (!prov.ok) allOk = false;
924
+
925
+ if (allOk) {
926
+ out("\nALL MATCH — every committed bundle, sidecar AND the build-provenance manifest reproduces byte-for-byte\n");
927
+ out("from the in-tree source, and every inlined source file hashes to its manifest-pinned sha256.\n");
928
+ return 0;
929
+ }
930
+ err("\nMISMATCH — at least one committed bundle/sidecar does NOT reproduce from source (see above). Re-run\n");
931
+ err("`node verifier/build-standalone.js` (no flag) to regenerate, or distrust this checkout.\n");
932
+ return 1;
933
+ }
934
+
935
+ if (require.main === module) {
936
+ // `--check` is the read-only REPRODUCE-AND-ATTEST mode: it writes NOTHING, only compares. The default
937
+ // (no-flag) invocation is the deterministic build that (re-)emits the four files.
938
+ if (process.argv.slice(2).includes("--check")) {
939
+ process.exit(runCheck());
940
+ }
941
+ for (const target of [TARGETS.verify, TARGETS.seal]) {
942
+ const text = writeTarget(target);
943
+ process.stdout.write(
944
+ `wrote ${path.relative(VERIFIER_DIR, target.outPath)} (${Buffer.byteLength(text)} bytes)\n`
945
+ );
946
+ process.stdout.write(
947
+ `wrote ${path.relative(VERIFIER_DIR, target.sha256Path)} (${sha256SidecarFor(text, target.sha256Basename).trim()})\n`
948
+ );
949
+ }
950
+ // The build-provenance manifest (spans both targets) — written once, after both bundles.
951
+ const provText = writeProvenance();
952
+ process.stdout.write(
953
+ `wrote ${path.relative(VERIFIER_DIR, PROVENANCE_PATH)} (${Buffer.byteLength(provText)} bytes)\n`
954
+ );
955
+ }
956
+
957
+ module.exports = {
958
+ buildBundle,
959
+ buildSealBundle,
960
+ buildTarget,
961
+ buildProvenanceObject,
962
+ buildProvenanceText,
963
+ moduleProvenance,
964
+ sha256HexOf,
965
+ writeBundle,
966
+ writeAll,
967
+ writeTarget,
968
+ writeProvenance,
969
+ checkTarget,
970
+ checkProvenance,
971
+ runCheck,
972
+ sha256Sidecar,
973
+ sha256SidecarFor,
974
+ OUT_PATH,
975
+ SHA256_PATH,
976
+ SHA256_BASENAME,
977
+ SEAL_OUT_PATH,
978
+ SEAL_SHA256_PATH,
979
+ SEAL_SHA256_BASENAME,
980
+ PROVENANCE_PATH,
981
+ PROVENANCE_BASENAME,
982
+ PROVENANCE_SCHEMA,
983
+ DIST_DIR,
984
+ VERIFIER_DIR,
985
+ MODULES,
986
+ VERIFY_MODULES,
987
+ SEAL_MODULES,
988
+ TARGETS,
989
+ };