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,876 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // seal-vh-standalone.js — the SINGLE-FILE, ZERO-DEPENDENCY, OFFLINE verifyhash SEALER (free tier).
5
+ //
6
+ // SPDX-License-Identifier: Apache-2.0
7
+ // Copyright 2026 verifyhash.com — https://verifyhash.com
8
+ //
9
+ // GENERATED by verifier/build-standalone.js from the in-tree sealer — DO NOT EDIT BY HAND.
10
+ // Re-generate with: node verifier/build-standalone.js (the build is deterministic; see that file.)
11
+ //
12
+ // HOW TO USE IT (no clone, no `npm install`, no node_modules, no package.json, no account):
13
+ // 1. Save THIS one file somewhere.
14
+ // 2. Run: node seal-vh-standalone.js <folder> -o out.vhevidence.json
15
+ // 3. Hand `out.vhevidence.json` (+ your folder) to anyone; they verify it with verify-vh-standalone.js
16
+ // — also zero-install. Exit codes: 0 sealed / 1 IO / 2 usage (incl. >25 files) / 3 seal-build error.
17
+ //
18
+ // SELF-DESCRIBING (needs NO second file): this bundle carries its OWN build-provenance.
19
+ // node seal-vh-standalone.js --self-attest # confirm THIS file's bytes are intact (0 ok / 1 modified)
20
+ // node seal-vh-standalone.js --provenance # print the ordered source modules + sha256 it was built from
21
+ //
22
+ // FREE TIER: an UNSIGNED seal of up to 25 files. Sealing MORE files (`evidence_unlimited`) or a SIGNED
23
+ // wrap (`evidence_signed`) is the PAID surface via `vh evidence seal` — this file has NO --sign/--license
24
+ // /--key flag and uses NO key. It is READ-ONLY apart from the -o file you name, and opens NO network. The
25
+ // seal is TAMPER-EVIDENT + OFFLINE-RECOMPUTABLE, NOT a trusted timestamp. keccak256 is the byte-identical
26
+ // pure-JS implementation the verifier uses, so a seal this builds is accepted verbatim by the verifier.
27
+
28
+ // ---- minimal CommonJS module shim (so the inlined modules keep their require() structure) --------
29
+ var __modules = Object.create(null);
30
+ var __cache = Object.create(null);
31
+ function __require(id) {
32
+ if (id in __cache) return __cache[id].exports;
33
+ var factory = __modules[id];
34
+ if (!factory) throw new Error('standalone bundle: unknown module: ' + id);
35
+ var module = { exports: {} };
36
+ __cache[id] = module;
37
+ factory(module, module.exports, __require);
38
+ return module.exports;
39
+ }
40
+
41
+ // ===== module: keccak256-vendored (from verifier/lib/keccak256-vendored.js) =====
42
+ __modules["keccak256-vendored"] = function (module, exports, __require) {
43
+ "use strict";
44
+
45
+ // verifier/lib/keccak256-vendored.js — a PURE-JS, ZERO-DEPENDENCY keccak256 (T-35.1).
46
+ //
47
+ // WHY THIS FILE EXISTS
48
+ // The whole point of the free verifier is "save ONE file, run it with `node`, no `npm install`, audit it
49
+ // in one sitting." The in-tree verifier core takes keccak256 from `js-sha3` (verifier/lib/keccak.js) — an
50
+ // audited, dependency-free package that is already a project dependency — which is correct for the
51
+ // IN-TREE path. But `js-sha3` is still a RUNTIME dependency: a third party handed a single sealed packet
52
+ // would have to `npm install` it. This module is the LAST piece needed to inline the verifier into one
53
+ // self-contained file: a from-scratch keccak256 that `require`s NOTHING (no `js-sha3`, no Node core, no
54
+ // relative module). It is ADDITIVE — keccak.js and verifier/package.json's `dependencies: ["js-sha3"]`
55
+ // are deliberately left UNCHANGED so the existing tree + isolation test stay green.
56
+ //
57
+ // CORRECTNESS, NOT NOVELTY
58
+ // keccak256 is the FIXED, standardized Keccak[c=512] sponge over the Keccak-f[1600] permutation with the
59
+ // ORIGINAL Keccak padding (a single 0x01 domain byte, NOT SHA3's 0x06) and a 256-bit squeeze — exactly
60
+ // what Ethereum/ethers and `js-sha3.keccak256` compute. This is a textbook implementation of FIPS-202's
61
+ // Keccak-f (theta, rho, pi, chi, iota) done with 32-bit lane halves (lo/hi) so it runs on plain JS numbers
62
+ // with no BigInt and no 64-bit-int dependency. test/verifier.keccak-vendored.test.js proves byte-identical
63
+ // output vs BOTH `js-sha3` AND the production `ethers` keccak path across the empty input, the known
64
+ // vectors, and ≥500 random buffers — a single mismatch FAILS. So this is independent CODE but never an
65
+ // independent ALGORITHM: it cannot silently diverge from the standard.
66
+ //
67
+ // REQUIRES NOTHING: a grep of this source finds no CommonJS require call and no bare-name import.
68
+ // (Intentional — this property is asserted by test/verifier.keccak-vendored.test.js.)
69
+
70
+ // ---- Keccak-f[1600] round constants, split into 32-bit (hi, lo) halves --------------------------------
71
+ // The 24 RC[i] are the canonical Keccak iota constants; here each 64-bit constant is pre-split so we never
72
+ // need a 64-bit integer type. RC_HI[i] is bits 63..32, RC_LO[i] is bits 31..0.
73
+ const RC_HI = [
74
+ 0x00000000, 0x00000000, 0x80000000, 0x80000000, 0x00000000, 0x00000000, 0x80000000, 0x80000000,
75
+ 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x80000000, 0x80000000, 0x80000000,
76
+ 0x80000000, 0x80000000, 0x00000000, 0x80000000, 0x80000000, 0x80000000, 0x00000000, 0x80000000,
77
+ ];
78
+ const RC_LO = [
79
+ 0x00000001, 0x00008082, 0x0000808a, 0x80008000, 0x0000808b, 0x80000001, 0x80008081, 0x00008009,
80
+ 0x0000008a, 0x00000088, 0x80008009, 0x8000000a, 0x8000808b, 0x0000008b, 0x00008089, 0x00008003,
81
+ 0x00008002, 0x00000080, 0x0000800a, 0x8000000a, 0x80008081, 0x00008080, 0x80000001, 0x80008008,
82
+ ];
83
+
84
+ // Rotation offsets r[x,y] for the rho step, indexed by lane number (x + 5*y). Lane 0 is never rotated.
85
+ const RHO = [
86
+ 0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14,
87
+ ];
88
+ // pi permutation: destination lane for each source lane. pi maps (x,y) -> (y, 2x+3y), so source lane
89
+ // (x + 5y) is written to lane (y + 5*((2x+3y) mod 5)); PI[src] = dst.
90
+ const PI = [
91
+ 0, 10, 20, 5, 15, 16, 1, 11, 21, 6, 7, 17, 2, 12, 22, 23, 8, 18, 3, 13, 14, 24, 9, 19, 4,
92
+ ];
93
+
94
+ // The state is 25 lanes; we hold each lane as two 32-bit halves in parallel arrays sLo/sHi (index = lane).
95
+
96
+ // Keccak-f[1600] permutation, in place, on (sLo, sHi). 24 rounds of theta, rho+pi, chi, iota.
97
+ function keccakF(sLo, sHi) {
98
+ const bcLo = new Array(5);
99
+ const bcHi = new Array(5);
100
+ const tLo = new Array(25);
101
+ const tHi = new Array(25);
102
+
103
+ for (let round = 0; round < 24; round++) {
104
+ // --- theta ---
105
+ for (let x = 0; x < 5; x++) {
106
+ bcLo[x] = sLo[x] ^ sLo[x + 5] ^ sLo[x + 10] ^ sLo[x + 15] ^ sLo[x + 20];
107
+ bcHi[x] = sHi[x] ^ sHi[x + 5] ^ sHi[x + 10] ^ sHi[x + 15] ^ sHi[x + 20];
108
+ }
109
+ for (let x = 0; x < 5; x++) {
110
+ // d = bc[x-1] XOR rotl1(bc[x+1])
111
+ const x1 = (x + 1) % 5;
112
+ const x4 = (x + 4) % 5;
113
+ const rotLo = ((bcLo[x1] << 1) | (bcHi[x1] >>> 31)) >>> 0;
114
+ const rotHi = ((bcHi[x1] << 1) | (bcLo[x1] >>> 31)) >>> 0;
115
+ const dLo = (bcLo[x4] ^ rotLo) >>> 0;
116
+ const dHi = (bcHi[x4] ^ rotHi) >>> 0;
117
+ for (let y = 0; y < 25; y += 5) {
118
+ sLo[x + y] = (sLo[x + y] ^ dLo) >>> 0;
119
+ sHi[x + y] = (sHi[x + y] ^ dHi) >>> 0;
120
+ }
121
+ }
122
+
123
+ // --- rho + pi --- (write permuted, rotated lanes into t)
124
+ for (let i = 0; i < 25; i++) {
125
+ const r = RHO[i];
126
+ const dest = PI[i];
127
+ let outLo, outHi;
128
+ if (r === 0) {
129
+ outLo = sLo[i];
130
+ outHi = sHi[i];
131
+ } else if (r < 32) {
132
+ outLo = ((sLo[i] << r) | (sHi[i] >>> (32 - r))) >>> 0;
133
+ outHi = ((sHi[i] << r) | (sLo[i] >>> (32 - r))) >>> 0;
134
+ } else if (r === 32) {
135
+ outLo = sHi[i];
136
+ outHi = sLo[i];
137
+ } else {
138
+ const rr = r - 32;
139
+ outLo = ((sHi[i] << rr) | (sLo[i] >>> (32 - rr))) >>> 0;
140
+ outHi = ((sLo[i] << rr) | (sHi[i] >>> (32 - rr))) >>> 0;
141
+ }
142
+ tLo[dest] = outLo;
143
+ tHi[dest] = outHi;
144
+ }
145
+
146
+ // --- chi --- a[x] = t[x] XOR ((NOT t[x+1]) AND t[x+2]), per row
147
+ for (let y = 0; y < 25; y += 5) {
148
+ for (let x = 0; x < 5; x++) {
149
+ const x1 = y + ((x + 1) % 5);
150
+ const x2 = y + ((x + 2) % 5);
151
+ sLo[y + x] = (tLo[y + x] ^ (~tLo[x1] & tLo[x2])) >>> 0;
152
+ sHi[y + x] = (tHi[y + x] ^ (~tHi[x1] & tHi[x2])) >>> 0;
153
+ }
154
+ }
155
+
156
+ // --- iota ---
157
+ sLo[0] = (sLo[0] ^ RC_LO[round]) >>> 0;
158
+ sHi[0] = (sHi[0] ^ RC_HI[round]) >>> 0;
159
+ }
160
+ }
161
+
162
+ // keccak256 over `bytes` (a Uint8Array/Buffer or array of byte values), returning a 32-byte Uint8Array.
163
+ // Rate r = 1088 bits = 136 bytes (c = 512), original Keccak padding (0x01 .. 0x80), 256-bit output.
164
+ function keccak256Bytes(bytes) {
165
+ const RATE = 136; // bytes absorbed per permutation
166
+ const sLo = new Array(25).fill(0);
167
+ const sHi = new Array(25).fill(0);
168
+
169
+ // Build the padded message: append a single 0x01 domain/pad start byte, zero-fill, set the high bit
170
+ // (0x80) of the final rate block. (If the 0x01 lands on the last byte of a block, it merges to 0x81.)
171
+ const inLen = bytes.length;
172
+ const padLen = RATE - (inLen % RATE); // 1..RATE, guarantees room for the 0x01 and 0x80 markers
173
+ const total = inLen + padLen;
174
+ const msg = new Uint8Array(total);
175
+ for (let i = 0; i < inLen; i++) msg[i] = bytes[i] & 0xff;
176
+ msg[inLen] = 0x01; // start of the original-Keccak pad (NOT SHA3's 0x06)
177
+ msg[total - 1] = (msg[total - 1] | 0x80) & 0xff; // final-block high bit
178
+
179
+ // Absorb: XOR each RATE-byte block into the state (little-endian lanes) and permute.
180
+ for (let off = 0; off < total; off += RATE) {
181
+ for (let i = 0; i < RATE; i += 8) {
182
+ const lane = i >> 3; // lane index within the rate region (0..16), block-relative
183
+ const b = off + i;
184
+ const lo =
185
+ ((msg[b] | (msg[b + 1] << 8) | (msg[b + 2] << 16) | (msg[b + 3] << 24)) >>> 0);
186
+ const hi =
187
+ ((msg[b + 4] | (msg[b + 5] << 8) | (msg[b + 6] << 16) | (msg[b + 7] << 24)) >>> 0);
188
+ sLo[lane] = (sLo[lane] ^ lo) >>> 0;
189
+ sHi[lane] = (sHi[lane] ^ hi) >>> 0;
190
+ }
191
+ keccakF(sLo, sHi);
192
+ }
193
+
194
+ // Squeeze 256 bits = 32 bytes = the first 4 lanes (little-endian), no further permutation needed.
195
+ const out = new Uint8Array(32);
196
+ for (let lane = 0; lane < 4; lane++) {
197
+ const lo = sLo[lane];
198
+ const hi = sHi[lane];
199
+ const base = lane * 8;
200
+ out[base] = lo & 0xff;
201
+ out[base + 1] = (lo >>> 8) & 0xff;
202
+ out[base + 2] = (lo >>> 16) & 0xff;
203
+ out[base + 3] = (lo >>> 24) & 0xff;
204
+ out[base + 4] = hi & 0xff;
205
+ out[base + 5] = (hi >>> 8) & 0xff;
206
+ out[base + 6] = (hi >>> 16) & 0xff;
207
+ out[base + 7] = (hi >>> 24) & 0xff;
208
+ }
209
+ return out;
210
+ }
211
+
212
+ // Lowercase hex (no 0x prefix) of a byte array — used by the hex-string entry point.
213
+ function toHex(bytes) {
214
+ let s = "";
215
+ for (let i = 0; i < bytes.length; i++) {
216
+ const b = bytes[i] & 0xff;
217
+ s += (b < 16 ? "0" : "") + b.toString(16);
218
+ }
219
+ return s;
220
+ }
221
+
222
+ /**
223
+ * keccak256 over a byte buffer.
224
+ * @param {Uint8Array|Buffer|number[]} bytes input bytes
225
+ * @returns {Uint8Array} the 32-byte digest
226
+ */
227
+ function keccak256(bytes) {
228
+ if (
229
+ !(bytes instanceof Uint8Array) &&
230
+ !Array.isArray(bytes) &&
231
+ !(typeof Buffer !== "undefined" && Buffer.isBuffer && Buffer.isBuffer(bytes))
232
+ ) {
233
+ throw new TypeError("keccak256 requires a Uint8Array/Buffer/byte-array of input bytes");
234
+ }
235
+ return keccak256Bytes(bytes);
236
+ }
237
+
238
+ /**
239
+ * keccak256 over a byte buffer, returned as a lowercase hex string WITHOUT a 0x prefix
240
+ * (matching `js-sha3`'s keccak256().hex() output, for drop-in cross-checking).
241
+ * @param {Uint8Array|Buffer|number[]} bytes input bytes
242
+ * @returns {string} 64-char lowercase hex
243
+ */
244
+ function keccak256Hex(bytes) {
245
+ return toHex(keccak256(bytes));
246
+ }
247
+
248
+ module.exports = { keccak256, keccak256Hex };
249
+
250
+ };
251
+
252
+ // ===== module: keccak (from verifier/lib/keccak.js) =====
253
+ __modules["keccak"] = function (module, exports, __require) {
254
+ "use strict";
255
+ // Inlined keccak provider for the standalone bundle: the SAME `keccak256(bytes) -> Buffer` surface as
256
+ // verifier/lib/keccak.js, but backed by the PURE-JS, zero-dependency vendored implementation
257
+ // (verifier/lib/keccak256-vendored.js) instead of js-sha3 — so the bundle requires nothing external.
258
+ var vendored = __require("keccak256-vendored");
259
+ function keccak256(bytes) {
260
+ if (!(bytes instanceof Uint8Array) && !Buffer.isBuffer(bytes)) {
261
+ throw new TypeError("keccak256 requires a Buffer/Uint8Array of input bytes");
262
+ }
263
+ // The vendored routine returns a Uint8Array; wrap it as a Buffer so downstream `.slice(...).toString
264
+ // ("hex")` and `Buffer.concat([...])` callers behave exactly as they do with the js-sha3-backed shim.
265
+ return Buffer.from(vendored.keccak256(bytes));
266
+ }
267
+ module.exports = { keccak256 };
268
+ };
269
+
270
+ // ===== module: merkle (from verifier/lib/merkle.js) =====
271
+ __modules["merkle"] = function (module, exports, __require) {
272
+ "use strict";
273
+
274
+ // verifier/lib/merkle.js — INDEPENDENT re-derivation of the family's path-bound, domain-separated
275
+ // Merkle convention, using ONLY ./keccak (js-sha3). NO ethers, NO hardhat, NO require back into cli/.
276
+ //
277
+ // WHY THIS EXISTS
278
+ // To verify an evidence seal / reconciliation seal / proof bundle OFFLINE without the producer stack,
279
+ // the independent verifier must RE-DERIVE the same per-file leaves and the same Merkle root the
280
+ // producer (cli/hash.js) computes. cli/hash.js uses `ethers` (keccak256/concat/toUtf8Bytes), which the
281
+ // verifier explicitly refuses to depend on. So this file reproduces the EXACT byte composition of
282
+ // pathLeaf / leafHash / nodeHash / buildTree from first principles — and test/verifier.cli.test.js
283
+ // cross-checks the result is byte-identical to the producer's. The two can never silently diverge.
284
+ //
285
+ // THE CONVENTION (must match cli/hash.js VERBATIM)
286
+ // * content digest c = keccak256(file bytes)
287
+ // * DIR_LEAF_DOMAIN = keccak256("verifyhash/dir-leaf/v1") (a fixed 32-byte prefix)
288
+ // * path-bound leaf pathLeaf = keccak256(DIR_LEAF_DOMAIN ++ utf8(relPath) ++ 0x00 ++ c)
289
+ // * tagged leaf leafHash = keccak256(0x00 ++ leaf)
290
+ // * interior node nodeHash = keccak256(0x01 ++ min(a,b) ++ max(a,b)) (sorted 32-byte pair)
291
+ // * tree sorted-leaf, "duplicate the lone odd node" pairing (OpenZeppelin style)
292
+ // relPath is normalized with no leading "./", exactly as the producer's toPosixRel does. CRUCIALLY
293
+ // this must be BYTE-FOR-BYTE the producer's normalization (cli/hash.js#toPosixRel) — see toPosixRel
294
+ // below — or the verifier would re-derive a DIFFERENT root than the producer sealed for some input
295
+ // class and would either falsely reject a genuine artifact or falsely accept the wrong one.
296
+
297
+ const { keccak256 } = __require("keccak");
298
+
299
+ // Domain tags, byte-identical to ContributionRegistry / cli/hash.js LEAF_TAG / NODE_TAG.
300
+ const LEAF_TAG = Buffer.from([0x00]);
301
+ const NODE_TAG = Buffer.from([0x01]);
302
+ const PATH_SEP = Buffer.from([0x00]);
303
+
304
+ // The fixed, versioned domain prefix for path-bound directory leaves: keccak256 of the ASCII tag.
305
+ const DIR_LEAF_DOMAIN_STR = "verifyhash/dir-leaf/v1";
306
+ const DIR_LEAF_DOMAIN = keccak256(Buffer.from(DIR_LEAF_DOMAIN_STR, "utf8")); // 32-byte Buffer
307
+
308
+ const HEX32_RE = /^0x[0-9a-fA-F]{64}$/;
309
+
310
+ // 0x-hex string (no 0x, lowercase) <-> 32-byte Buffer.
311
+ function hexToBuf32(hex) {
312
+ if (typeof hex !== "string" || !HEX32_RE.test(hex)) {
313
+ throw new Error(`expected a 0x-prefixed 32-byte hex string, got: ${String(hex)}`);
314
+ }
315
+ return Buffer.from(hex.slice(2), "hex");
316
+ }
317
+ function bufToHex(buf) {
318
+ return "0x" + Buffer.from(buf).toString("hex");
319
+ }
320
+
321
+ /** keccak256 of raw bytes, returned as a 0x-prefixed 32-byte hex string (matches cli/hash.js hashBytes). */
322
+ function hashBytes(bytes) {
323
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
324
+ return bufToHex(keccak256(buf));
325
+ }
326
+
327
+ /**
328
+ * Normalize a relPath EXACTLY as the producer (cli/hash.js#toPosixRel) does, so the verifier
329
+ * re-derives the IDENTICAL root the producer sealed. The producer is `split(path.sep).join("/")`
330
+ * then `.replace(/^\.\//, "")`. The artifacts the verifier reads carry relPaths the producer wrote,
331
+ * and those are produced on POSIX hosts (cli/evidence.js#loadDirEntries does the same `path.sep`
332
+ * split) — where `path.sep === "/"`, so the split/join is a no-op and a literal backslash byte is a
333
+ * CONTENT byte that survives into the hash. We therefore must NOT collapse backslashes: a previous
334
+ * version unconditionally mapped "\\"->"/", which made the verifier hash `a/b.txt` while the producer
335
+ * hashed `a\b.txt` — a silent root divergence that could falsely REJECT a genuine backslash-named
336
+ * directory or falsely ACCEPT one where `a/b.txt` and `a\b.txt` collide. All we strip is the leading
337
+ * "./", which the producer also strips on every host. (Windows-authored relPaths, if ever needed,
338
+ * must be converted to "/" on BOTH the producer and verifier sides identically — not only here.)
339
+ */
340
+ function toPosixRel(relPath) {
341
+ return String(relPath).replace(/^\.\//, "");
342
+ }
343
+
344
+ /**
345
+ * pathLeaf(relPath, contentDigest) = keccak256(DIR_LEAF_DOMAIN ++ utf8(relPath) ++ 0x00 ++ c).
346
+ * @param {string} relPath
347
+ * @param {string} contentDigest 0x bytes32
348
+ * @returns {string} 0x bytes32
349
+ */
350
+ function pathLeaf(relPath, contentDigest) {
351
+ const relBytes = Buffer.from(toPosixRel(relPath), "utf8");
352
+ const c = hexToBuf32(contentDigest);
353
+ return bufToHex(keccak256(Buffer.concat([DIR_LEAF_DOMAIN, relBytes, PATH_SEP, c])));
354
+ }
355
+
356
+ /** leafHash(c) = keccak256(LEAF_TAG ++ c). */
357
+ function leafHash(c) {
358
+ return bufToHex(keccak256(Buffer.concat([LEAF_TAG, hexToBuf32(c)])));
359
+ }
360
+
361
+ /** nodeHash(a,b) = keccak256(NODE_TAG ++ min(a,b) ++ max(a,b)) comparing as 32-byte big-endian values. */
362
+ function nodeHash(a, b) {
363
+ const A = hexToBuf32(a);
364
+ const B = hexToBuf32(b);
365
+ const [lo, hi] = Buffer.compare(A, B) <= 0 ? [A, B] : [B, A];
366
+ return bufToHex(keccak256(Buffer.concat([NODE_TAG, lo, hi])));
367
+ }
368
+
369
+ /**
370
+ * Build the sorted-leaf, domain-separated Merkle root from an array of per-file PATH-BOUND leaves
371
+ * (the same values pathLeaf produces). Leaves are sorted ascending by their 32-byte value, tagged via
372
+ * leafHash, then folded with nodeHash, pairing a lone odd node with itself — byte-identical to
373
+ * cli/hash.js buildTree's root.
374
+ * @param {string[]} leaves array of 0x bytes32 path-bound leaves
375
+ * @returns {string} the 0x bytes32 root
376
+ */
377
+ function rootFromLeaves(leaves) {
378
+ if (!Array.isArray(leaves) || leaves.length === 0) {
379
+ throw new Error("cannot build a Merkle tree from zero leaves");
380
+ }
381
+ const sorted = leaves
382
+ .slice()
383
+ .sort((a, b) => Buffer.compare(hexToBuf32(a), hexToBuf32(b)));
384
+ let layer = sorted.map((c) => leafHash(c));
385
+ while (layer.length > 1) {
386
+ const next = [];
387
+ for (let i = 0; i < layer.length; i += 2) {
388
+ const right = i + 1 < layer.length ? layer[i + 1] : layer[i];
389
+ next.push(nodeHash(layer[i], right));
390
+ }
391
+ layer = next;
392
+ }
393
+ return layer[0];
394
+ }
395
+
396
+ /**
397
+ * Re-derive the top-level root from a flat list of { relPath, contentHash } — the SAME computation the
398
+ * seal cores use: pathLeaf each, then rootFromLeaves. PURE.
399
+ * @param {{relPath:string, contentHash:string}[]} flat
400
+ * @returns {string} 0x bytes32 root
401
+ */
402
+ function rootFromFlat(flat) {
403
+ return rootFromLeaves(flat.map((e) => pathLeaf(e.relPath, e.contentHash)));
404
+ }
405
+
406
+ module.exports = {
407
+ HEX32_RE,
408
+ DIR_LEAF_DOMAIN_STR,
409
+ hashBytes,
410
+ toPosixRel,
411
+ pathLeaf,
412
+ leafHash,
413
+ nodeHash,
414
+ rootFromLeaves,
415
+ rootFromFlat,
416
+ };
417
+
418
+ };
419
+
420
+ // ===== module: seal-cli (from verifier/lib/seal-cli.js) =====
421
+ __modules["seal-cli"] = function (module, exports, __require) {
422
+ "use strict";
423
+
424
+ // verifier/lib/seal-cli.js — the FREE, ZERO-INSTALL "seal your own folder" PRODUCER, inlined verbatim into
425
+ // verifier/dist/seal-vh-standalone.js by verifier/build-standalone.js (T-36.2).
426
+ //
427
+ // WHY THIS EXISTS
428
+ // EPIC-35 made the FREE VERIFY side zero-install: a counterparty handed ONE sealed packet saves a single
429
+ // file (verify-vh-standalone.js) and runs it — no clone, no `npm install`. The symmetric gap was the FREE
430
+ // PRODUCE side: a stranger who wants to SEAL up to 25 of their OWN files (the free tier) still had to clone
431
+ // the repo and `npm install` the heavy ethers/hardhat stack, because `vh evidence seal` routes through
432
+ // cli/evidence.js -> cli/core/packetseal.js -> cli/hash.js, and cli/hash.js pulls keccak256 from `ethers`.
433
+ // This module is the LAST piece that closes the loop: a from-scratch sealer that `require`s NOTHING but
434
+ // Node core (fs/path) and the verifier's OWN merkle lib (which itself is zero-third-party in the bundle —
435
+ // keccak256 comes from the inlined pure-JS vendored implementation). The emitted bundle lets a prospect
436
+ // PRODUCE a free `vh.evidence-seal` of up to 25 files with NO install, hand it to a counterparty, and have
437
+ // THEM verify it with NO install — the whole organic adoption loop, self-service, before any sales call.
438
+ //
439
+ // FREE-TIER BOUNDARY (enforced here, not advisory)
440
+ // The free tier is an UNSIGNED seal of up to SAMPLE_LIMIT (25) files. This sealer:
441
+ // * HARD-ERRORS (exit 2) on a folder of MORE than 25 files, naming the paid `evidence_unlimited`
442
+ // entitlement + the full `vh evidence seal` command that unlocks it. It never silently truncates.
443
+ // * has NO `--sign` / `--license` / `--key` flag AT ALL — signing (`evidence_signed`) is the PAID
444
+ // surface and lives only in the full CLI. There is no way to produce a signed packet from this file.
445
+ // So the standalone is strictly the FREE half of the product: a try-before-you-buy producer whose output
446
+ // the paid signed wrap is layered on top of (the bytes this emits are the exact canonical bytes the paid
447
+ // `vh evidence seal --sign` would wrap, so an upgrade re-uses, never re-does, the free seal).
448
+ //
449
+ // BYTE-FOR-BYTE COMPATIBLE with the producer
450
+ // The seal this emits is BYTE-IDENTICAL to cli/evidence.js#serializeSeal over the same directory: the same
451
+ // `kind`, `schemaVersion`, `note`, `root`, `fileCount`, and per-file { relPath, contentHash, leaf } in the
452
+ // same canonical key order, terminated with one "\n". That is WHY the standalone-produced seal is accepted
453
+ // verbatim by verify-vh-standalone.js (and the in-tree verifier) — the free PRODUCE and free VERIFY halves
454
+ // interoperate with zero install on either side. The merkle convention (pathLeaf / leafHash / nodeHash /
455
+ // sorted-leaf tree) is the verifier's own ./merkle lib, the SAME math the verifier re-derives on the other
456
+ // side, so a seal this builds always re-derives to the same root the verifier recomputes from the bytes.
457
+ //
458
+ // HONEST POSTURE + I/O DISCIPLINE
459
+ // The seal is TAMPER-EVIDENT + OFFLINE-RECOMPUTABLE, NOT a trusted timestamp (the load-bearing `note` is
460
+ // stated once, below, byte-identical to the producer's). This file reads the named folder and writes ONLY
461
+ // the single output file the user names with `-o`/`--out` (or prints to stdout) — it NEVER writes cwd
462
+ // otherwise, opens NO socket, and uses NO key. Same inputs -> byte-identical bytes.
463
+
464
+ const fs = require("fs");
465
+ const path = require("path");
466
+
467
+ // The verifier's INDEPENDENT merkle convention (pathLeaf / hashBytes / rootFromFlat). In the bundle this is
468
+ // the inlined verifier/lib/merkle.js, whose keccak256 is the inlined pure-JS vendored implementation — so the
469
+ // whole sealer is zero-third-party. Out of the bundle (direct `node verifier/lib/seal-cli.js`) it resolves to
470
+ // the same in-tree merkle lib, which uses js-sha3; either way the math is byte-identical.
471
+ const merkle = __require("merkle");
472
+
473
+ // Exit contract — the SAME as cli/evidence.js's EXIT: 0 ok / 1 IO / 2 usage / 3 gate-fail. The free-tier
474
+ // >25-files boundary is a USAGE error (2): the invocation asked for a paid surface the free sealer cannot do.
475
+ const EXIT = Object.freeze({ OK: 0, IO: 1, USAGE: 2, FAIL: 3 });
476
+
477
+ // The free SAMPLE size, byte-identical to cli/evidence.js SAMPLE_LIMIT (25). Sealing MORE requires the paid
478
+ // `evidence_unlimited` entitlement via the full `vh evidence seal` command — this free sealer hard-errors.
479
+ const SAMPLE_LIMIT = 25;
480
+
481
+ const SEAL_KIND = "vh.evidence-seal";
482
+ const SEAL_SCHEMA_VERSION = 1;
483
+
484
+ // The TRUST-BOUNDARIES one-liner — BYTE-IDENTICAL to cli/evidence.js EVIDENCE_TRUST_NOTE. The seal's `note`
485
+ // field MUST equal this verbatim or the verifier's strict structural check (note must not drift) rejects the
486
+ // packet. Stated once here so the standalone can never silently soften the caveat.
487
+ const EVIDENCE_TRUST_NOTE =
488
+ "This evidence seal is TAMPER-EVIDENT + OFFLINE-RECOMPUTABLE, NOT a trusted timestamp. Its Merkle " +
489
+ "`root` commits to the full set of (relPath, content) pairs in the directory: any edit, rename, add, " +
490
+ "or remove changes the root, and verify RE-DERIVES the root from the bytes you hold and LOCALIZES the " +
491
+ "change to the exact file (MATCH / CHANGED / MISSING / UNEXPECTED). It does NOT prove WHEN the sealing " +
492
+ 'happened ("sealed at T" rides the human-owned signing/timestamp trust-root, STRATEGY.md P-3) and it ' +
493
+ "is NOT a legal opinion. The packet is an UNTRUSTED transport container: verify never trusts the " +
494
+ "packet's own stored hashes.";
495
+
496
+ // ---------------------------------------------------------------------------
497
+ // FILESYSTEM WALK — recursively collect every regular file under dirAbs (skipping sockets/fifos/symlinks,
498
+ // exactly as cli/hash.js#listFiles does — they have no stable content hash). Returns absolute paths.
499
+ // ---------------------------------------------------------------------------
500
+ function listFiles(dirAbs) {
501
+ const out = [];
502
+ const entries = fs.readdirSync(dirAbs, { withFileTypes: true });
503
+ for (const entry of entries) {
504
+ const full = path.join(dirAbs, entry.name);
505
+ if (entry.isDirectory()) {
506
+ out.push(...listFiles(full));
507
+ } else if (entry.isFile()) {
508
+ out.push(full);
509
+ }
510
+ // sockets/fifos/symlinks are intentionally skipped (no stable content hash) — same as cli/hash.js.
511
+ }
512
+ return out;
513
+ }
514
+
515
+ // Load a directory into a sorted [{ relPath, bytes }] list. relPath is POSIX-normalized + relative to dirAbs,
516
+ // matching cli/evidence.js#loadDirEntries EXACTLY (split on path.sep, join "/"), so the standalone seal
517
+ // travels with the directory identically to a producer-built one. Sorted by relPath for determinism.
518
+ function loadDirEntries(dirAbs) {
519
+ const files = listFiles(dirAbs);
520
+ const entries = files.map((abs) => {
521
+ const rel = path.relative(dirAbs, abs).split(path.sep).join("/");
522
+ return { relPath: rel, bytes: fs.readFileSync(abs) };
523
+ });
524
+ entries.sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0));
525
+ return entries;
526
+ }
527
+
528
+ // ---------------------------------------------------------------------------
529
+ // PURE SEAL BUILD — over the verifier's own merkle convention. Mirrors cli/core/packetseal.js#buildSeal +
530
+ // cli/evidence.js#serializeSeal so the emitted bytes are byte-identical to the producer's. Throws a plain
531
+ // Error (named in the message) on a structural problem (e.g. a duplicate relPath) — the CLI maps it to exit 3.
532
+ // ---------------------------------------------------------------------------
533
+
534
+ function buildSeal(entries) {
535
+ if (!Array.isArray(entries) || entries.length === 0) {
536
+ throw new Error("cannot build an evidence seal from zero files");
537
+ }
538
+ // Per-file (relPath, contentHash, leaf), de-duplicated on relPath (a duplicate is a hard error — every
539
+ // entry must occupy a distinct path, matching the producer core's invariant).
540
+ const seen = new Set();
541
+ const files = entries.map((e) => {
542
+ if (typeof e.relPath !== "string" || e.relPath.length === 0) {
543
+ throw new Error("evidence seal entry relPath must be a non-empty string");
544
+ }
545
+ if (seen.has(e.relPath)) {
546
+ throw new Error(`evidence seal has a duplicate relPath across the file set: ${JSON.stringify(e.relPath)}`);
547
+ }
548
+ seen.add(e.relPath);
549
+ const contentHash = merkle.hashBytes(e.bytes);
550
+ const leaf = merkle.pathLeaf(e.relPath, contentHash);
551
+ return { relPath: e.relPath, contentHash, leaf };
552
+ });
553
+ // Emit per-file leaves sorted by relPath so the seal bytes are deterministic regardless of input order
554
+ // (the producer core does the same), then re-derive the root over the SAME convention the verifier uses.
555
+ files.sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0));
556
+ const root = merkle.rootFromFlat(files.map((f) => ({ relPath: f.relPath, contentHash: f.contentHash })));
557
+ return {
558
+ kind: SEAL_KIND,
559
+ schemaVersion: SEAL_SCHEMA_VERSION,
560
+ note: EVIDENCE_TRUST_NOTE,
561
+ root,
562
+ fileCount: files.length,
563
+ files,
564
+ };
565
+ }
566
+
567
+ // Serialize a built seal to canonical, byte-deterministic bytes — BYTE-IDENTICAL to cli/evidence.js#
568
+ // serializeSeal: an EXPLICIT key order, no insignificant whitespace, one trailing "\n". The producer builds
569
+ // the same ordered object literal and JSON.stringify(...)+"\n"; reproducing that literal here yields the
570
+ // identical bytes the verifier (and `sha256sum`) expect.
571
+ function serializeSeal(seal) {
572
+ const canonical = {
573
+ kind: seal.kind,
574
+ schemaVersion: seal.schemaVersion,
575
+ note: seal.note,
576
+ root: seal.root,
577
+ fileCount: seal.fileCount,
578
+ files: seal.files.map((e) => ({
579
+ relPath: e.relPath,
580
+ contentHash: e.contentHash,
581
+ leaf: e.leaf,
582
+ })),
583
+ };
584
+ return JSON.stringify(canonical) + "\n";
585
+ }
586
+
587
+ // ---------------------------------------------------------------------------
588
+ // CLI — `seal-vh-standalone.js <folder> [-o <out>] [--json]`.
589
+ // Walks <folder>, enforces the free-tier boundary, builds the UNSIGNED seal, and writes it to -o/--out
590
+ // (caller-named; NEVER cwd) or prints it. There is DELIBERATELY no --sign/--license/--key flag: signing is
591
+ // the paid surface. Exit: 0 ok / 1 IO / 2 usage (incl. the >25-files paid boundary) / 3 seal-build error.
592
+ // ---------------------------------------------------------------------------
593
+
594
+ function usage() {
595
+ return [
596
+ "seal-vh-standalone.js — FREE, zero-install evidence sealer (seal your own folder, hand it to anyone)",
597
+ "",
598
+ "Usage:",
599
+ " node seal-vh-standalone.js <folder> [-o <out.vhevidence.json>] [--json]",
600
+ "",
601
+ "Walks <folder> and binds every file into ONE tamper-evident `vh.evidence-seal` you can hand to a",
602
+ "counterparty; they verify it with verify-vh-standalone.js — no clone, no `npm install`, on either side.",
603
+ "",
604
+ "FREE tier: an UNSIGNED seal of up to " + SAMPLE_LIMIT + " files. Sealing MORE files, or a SIGNED",
605
+ "attestation wrap, is the PAID surface (`evidence_unlimited` / `evidence_signed`) via `vh evidence seal`.",
606
+ "There is no --sign/--license/--key flag here: this file produces only the free, unsigned seal.",
607
+ "",
608
+ "Exit codes: 0 sealed / 1 IO error / 2 usage (incl. >" + SAMPLE_LIMIT + " files) / 3 seal-build error.",
609
+ "It is READ-ONLY apart from the -o file you name, opens NO network, and uses NO key.",
610
+ "",
611
+ ].join("\n");
612
+ }
613
+
614
+ function parseArgs(argv) {
615
+ const opts = { folder: undefined, out: undefined, json: false, _positionals: [] };
616
+ for (let i = 0; i < argv.length; i++) {
617
+ const a = argv[i];
618
+ const need = (flag) => {
619
+ const v = argv[++i];
620
+ if (v === undefined) {
621
+ const e = new Error(`${flag} requires a value`);
622
+ e.usage = true;
623
+ throw e;
624
+ }
625
+ return v;
626
+ };
627
+ switch (a) {
628
+ case "-o":
629
+ case "--out":
630
+ opts.out = need(a);
631
+ break;
632
+ case "--json":
633
+ opts.json = true;
634
+ break;
635
+ case "-h":
636
+ case "--help":
637
+ opts.help = true;
638
+ break;
639
+ default:
640
+ if (a && a.startsWith("-")) {
641
+ const e = new Error(`unknown flag: ${a}`);
642
+ e.usage = true;
643
+ throw e;
644
+ }
645
+ opts._positionals.push(a);
646
+ }
647
+ }
648
+ if (opts._positionals.length > 1) {
649
+ const e = new Error(
650
+ `unexpected extra argument: ${opts._positionals[1]} (seal takes exactly one <folder>)`
651
+ );
652
+ e.usage = true;
653
+ throw e;
654
+ }
655
+ opts.folder = opts._positionals[0];
656
+ return opts;
657
+ }
658
+
659
+ // Run the sealer with an injectable io ({ write, writeErr }) so it is unit-testable without spawning a
660
+ // process. Returns the exit code. PURE except for the directory read + the single -o write.
661
+ function run(argv, io = {}) {
662
+ const write = io.write || ((s) => process.stdout.write(s));
663
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
664
+
665
+ let opts;
666
+ try {
667
+ opts = parseArgs(argv);
668
+ } catch (e) {
669
+ writeErr(`error: ${e.message}\n`);
670
+ return EXIT.USAGE;
671
+ }
672
+ if (opts.help) {
673
+ write(usage());
674
+ return EXIT.OK;
675
+ }
676
+ if (!opts.folder) {
677
+ writeErr("error: seal-vh-standalone requires a <folder> to seal\n\n");
678
+ writeErr(usage());
679
+ return EXIT.USAGE;
680
+ }
681
+
682
+ // Walk the folder (the only read I/O). A missing/unreadable folder or a non-directory is an IO error.
683
+ const dirAbs = path.resolve(opts.folder);
684
+ let stat;
685
+ try {
686
+ stat = fs.statSync(dirAbs);
687
+ } catch (e) {
688
+ writeErr(`error: cannot read folder ${opts.folder}: ${e.message}\n`);
689
+ return EXIT.IO;
690
+ }
691
+ if (!stat.isDirectory()) {
692
+ writeErr(`error: ${opts.folder} is not a directory\n`);
693
+ return EXIT.IO;
694
+ }
695
+ let entries;
696
+ try {
697
+ entries = loadDirEntries(dirAbs);
698
+ } catch (e) {
699
+ writeErr(`error: cannot read folder ${opts.folder}: ${e.message}\n`);
700
+ return EXIT.IO;
701
+ }
702
+ if (entries.length === 0) {
703
+ writeErr(`error: ${opts.folder} contains no files to seal\n`);
704
+ return EXIT.FAIL;
705
+ }
706
+
707
+ // FREE-TIER BOUNDARY — hard-error (exit 2) on more than SAMPLE_LIMIT files, naming the paid entitlement +
708
+ // the full command that unlocks it. The free sealer NEVER silently truncates or downgrades.
709
+ if (entries.length > SAMPLE_LIMIT) {
710
+ writeErr(
711
+ `error: this folder has ${entries.length} files, but the FREE sealer seals at most ${SAMPLE_LIMIT}.\n` +
712
+ `Sealing more than ${SAMPLE_LIMIT} files is the PAID "evidence_unlimited" entitlement — use the full ` +
713
+ "command:\n" +
714
+ " vh evidence seal <folder> --license <file> --vendor <0xaddr>\n" +
715
+ "(The free, zero-install sealer is strictly try-before-you-buy: up to " +
716
+ SAMPLE_LIMIT +
717
+ " files, unsigned.)\n"
718
+ );
719
+ return EXIT.USAGE;
720
+ }
721
+
722
+ // Build the UNSIGNED seal. A structural problem (e.g. a duplicate relPath) is a seal-build error (3).
723
+ let seal;
724
+ try {
725
+ seal = buildSeal(entries);
726
+ } catch (e) {
727
+ writeErr(`error: cannot build evidence seal: ${e.message}\n`);
728
+ return EXIT.FAIL;
729
+ }
730
+ const artifactStr = serializeSeal(seal);
731
+
732
+ // Write to -o/--out (caller-chosen path; NEVER cwd) or print to stdout (writes nothing to disk).
733
+ let outAbs = null;
734
+ if (opts.out) {
735
+ outAbs = path.resolve(opts.out);
736
+ try {
737
+ fs.writeFileSync(outAbs, artifactStr);
738
+ } catch (e) {
739
+ writeErr(`error: cannot write -o file ${opts.out}: ${e.message}\n`);
740
+ return EXIT.IO;
741
+ }
742
+ }
743
+
744
+ if (opts.json) {
745
+ write(
746
+ JSON.stringify(
747
+ {
748
+ ok: true,
749
+ note: EVIDENCE_TRUST_NOTE,
750
+ kind: SEAL_KIND,
751
+ root: seal.root,
752
+ fileCount: seal.fileCount,
753
+ signed: false,
754
+ out: outAbs,
755
+ // With no -o the artifact rides in `artifact` so --json never drops it (parity with the producer).
756
+ artifact: outAbs ? null : artifactStr,
757
+ },
758
+ null,
759
+ 2
760
+ ) + "\n"
761
+ );
762
+ } else {
763
+ write(EVIDENCE_TRUST_NOTE + "\n\n");
764
+ write(
765
+ `sealed ${seal.fileCount} file${seal.fileCount === 1 ? "" : "s"} into an evidence packet — root ${seal.root}\n`
766
+ );
767
+ if (outAbs) {
768
+ write(` written: ${outAbs}\n`);
769
+ write(` verify it: node verify-vh-standalone.js ${path.basename(outAbs)} --dir <folder>\n`);
770
+ } else {
771
+ write(artifactStr);
772
+ }
773
+ }
774
+ return EXIT.OK;
775
+ }
776
+
777
+ module.exports = {
778
+ EXIT,
779
+ SAMPLE_LIMIT,
780
+ SEAL_KIND,
781
+ SEAL_SCHEMA_VERSION,
782
+ EVIDENCE_TRUST_NOTE,
783
+ listFiles,
784
+ loadDirEntries,
785
+ buildSeal,
786
+ serializeSeal,
787
+ parseArgs,
788
+ usage,
789
+ run,
790
+ };
791
+
792
+ // CLI shim when this file is run directly (out of the bundle). Inside the bundle the boot wrapper drives run().
793
+ if (require.main === module) {
794
+ process.exit(run(process.argv.slice(2)));
795
+ }
796
+
797
+ };
798
+
799
+ // ---- embedded build-provenance (this file's own): see `--provenance` / `--self-attest` below. ----
800
+ var __SELF_SHA256_SENTINEL = "0000000000000000000000000000000000000000000000000000000000000000";
801
+ var __PROVENANCE = {
802
+ "schema": "verifyhash/build-provenance@1",
803
+ "target": "seal",
804
+ "note": "This bundle's OWN provenance, embedded so the single file is self-describing. Run `node seal-vh-standalone.js --self-attest` to recompute selfSha256 from these very bytes, or `--provenance` to print the ordered source modules + hashes it was built from. Cross-check against verifier/dist/BUILD-PROVENANCE.json (the same data) with: node verifier/build-standalone.js --check",
805
+ "selfSha256": "f1777ad496b73f6b94379b4eda13b9949862b0d56bdac938de3b27d1323011b4",
806
+ "modules": [
807
+ {
808
+ "id": "keccak256-vendored",
809
+ "synthetic": false,
810
+ "sourceFile": "verifier/lib/keccak256-vendored.js",
811
+ "sourceSha256": "4f5f2dda618a5889ab5b3f8498dc64ddeacdd22b57349d97824f60960ee334a1",
812
+ "inlinedSha256": "4f5f2dda618a5889ab5b3f8498dc64ddeacdd22b57349d97824f60960ee334a1",
813
+ "entry": false
814
+ },
815
+ {
816
+ "id": "keccak",
817
+ "synthetic": true,
818
+ "sourceFile": null,
819
+ "sourceSha256": null,
820
+ "inlinedSha256": "7ead489c805c4e62e8338dbcfde77a85ca52076e2a3639e2ba57ce3b2415828e",
821
+ "note": "swapped body (keccak provider shim) — defined in build-standalone.js, not a source file"
822
+ },
823
+ {
824
+ "id": "merkle",
825
+ "synthetic": false,
826
+ "sourceFile": "verifier/lib/merkle.js",
827
+ "sourceSha256": "1bea7bab4b479962225279f4590c5524fad5295c7c70cf01d4978dbf9f37f34b",
828
+ "inlinedSha256": "e65be5614998f9a00ca9b58a6c79b05abe7f70ed2e0dad61ece07a41ee5da876",
829
+ "entry": false
830
+ },
831
+ {
832
+ "id": "seal-cli",
833
+ "synthetic": false,
834
+ "sourceFile": "verifier/lib/seal-cli.js",
835
+ "sourceSha256": "7a68adacb21ace3c3a77a3bc9bc27ec8a4732c77df3e8d6a6c5864d44a7a13bc",
836
+ "inlinedSha256": "8ba0e03a6a9bcaa5ee400d045fc13c4eb7e99b2d7f4effb307135e7b02d8804d",
837
+ "entry": true
838
+ }
839
+ ]
840
+ };
841
+ function __maybeProvenance(argv) {
842
+ var wantProv = argv.indexOf('--provenance') !== -1;
843
+ var wantAttest = argv.indexOf('--self-attest') !== -1;
844
+ if (!wantProv && !wantAttest) return null;
845
+ if (wantProv) { process.stdout.write(JSON.stringify(__PROVENANCE, null, 2) + '\n'); }
846
+ if (wantAttest) {
847
+ var fs = require('fs');
848
+ var crypto = require('crypto');
849
+ var selfText;
850
+ try { selfText = fs.readFileSync(__filename, 'utf8'); }
851
+ catch (e) { process.stderr.write('self-attest: cannot read this file: ' + e.message + '\n'); return 1; }
852
+ // Re-blank our own selfSha256 line back to the sentinel, then hash — reproducing the build's pass-1 hash.
853
+ var blanked = selfText.replace(
854
+ '"selfSha256": "' + __PROVENANCE.selfSha256 + '"',
855
+ '"selfSha256": "' + __SELF_SHA256_SENTINEL + '"'
856
+ );
857
+ var got = crypto.createHash('sha256').update(Buffer.from(blanked, 'utf8')).digest('hex');
858
+ if (got === __PROVENANCE.selfSha256) {
859
+ process.stdout.write('[MATCH] self-attest: this file is intact (selfSha256 ' + got + ').\n');
860
+ return 0;
861
+ }
862
+ process.stderr.write('[MISMATCH] self-attest: this file has been MODIFIED ' +
863
+ '(embedded selfSha256 ' + __PROVENANCE.selfSha256 + ' != recomputed ' + got + ').\n');
864
+ return 1;
865
+ }
866
+ return 0;
867
+ }
868
+
869
+ // ---- boot: run the inlined sealer CLI with this process's argv. ----
870
+ var __entry = __require("seal-cli");
871
+ if (require.main === module) {
872
+ var __code = __maybeProvenance(process.argv.slice(2));
873
+ if (__code !== null) process.exit(__code);
874
+ process.exit(__entry.run(process.argv.slice(2)));
875
+ }
876
+ module.exports = __entry;