verifyhash 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/cli/agent-hook.js +431 -0
- package/docs/ADOPT.md +15 -5
- package/docs/AGENT-HOOK.md +111 -0
- package/docs/ANCHORING.md +43 -22
- package/docs/PUBLISH-VERIFY-VH.md +45 -0
- package/examples/README.md +185 -0
- package/examples/policy.lenient.json +5 -0
- package/examples/policy.strict.json +6 -0
- package/examples/run.js +366 -0
- package/examples/sample-dataset/README.txt +10 -0
- package/examples/sample-dataset/corpus/cc-by-poem.txt +8 -0
- package/examples/sample-dataset/corpus/mit-notes.txt +4 -0
- package/examples/sample-dataset/data/unlabeled.txt +5 -0
- package/examples/sample-dataset/vendored/gpl-snippet.txt +5 -0
- package/examples/sample-dataset.hints.json +7 -0
- package/examples/sample-parcel/data/manifest-of-contents.txt +7 -0
- package/examples/sample-parcel/data/records.csv +4 -0
- package/examples/sample-parcel/delivery-note.txt +9 -0
- package/package.json +26 -3
- package/verifier/README.md +584 -0
- package/verifier/action/README.md +87 -0
- package/verifier/action/action.yml +146 -0
- package/verifier/build-standalone-html.js +1287 -0
- package/verifier/build-standalone.js +989 -0
- package/verifier/ci/journal.generic.sh +96 -0
- package/verifier/ci/journal.github-actions.yml +99 -0
- package/verifier/ci/reproduce-vh.generic.sh +59 -0
- package/verifier/ci/reproduce-vh.github-actions.yml +49 -0
- package/verifier/ci/verify-service.generic.sh +96 -0
- package/verifier/ci/verify-service.github-actions.yml +88 -0
- package/verifier/ci/verify-vh.generic.sh +75 -0
- package/verifier/ci/verify-vh.github-actions.yml +56 -0
- package/verifier/dist/BUILD-PROVENANCE.json +210 -0
- package/verifier/dist/seal-vh-standalone.js +876 -0
- package/verifier/dist/seal-vh-standalone.js.sha256 +1 -0
- package/verifier/dist/verify-vh-standalone.html +3373 -0
- package/verifier/dist/verify-vh-standalone.html.sha256 +1 -0
- package/verifier/dist/verify-vh-standalone.js +5123 -0
- package/verifier/dist/verify-vh-standalone.js.sha256 +1 -0
- package/verifier/lib/canonical.js +141 -0
- package/verifier/lib/keccak.js +30 -0
- package/verifier/lib/keccak256-vendored.js +206 -0
- package/verifier/lib/merkle.js +145 -0
- package/verifier/lib/revocation-core.js +606 -0
- package/verifier/lib/revocation.js +200 -0
- package/verifier/lib/seal-cli.js +374 -0
- package/verifier/lib/seal-evidence.js +237 -0
- package/verifier/lib/secp256k1-recover.js +249 -0
- package/verifier/package.json +39 -0
- package/verifier/verify-vh.js +3376 -0
- package/docs/ADOPTION.json +0 -11
- package/docs/AUDIT.md +0 -55
- package/docs/DECIDE.md +0 -47
- package/docs/DECISIONS-PENDING.md +0 -27
- package/docs/DEPLOY-PUBLIC-SITE.md +0 -301
- package/docs/ENGINE-LEDGER.json +0 -12
- package/docs/LOOP-AUDIT-2026-07-03.json +0 -580
- package/docs/LOOP-HARDENING-PLAN.md +0 -44
- package/docs/METRICS.jsonl +0 -31
- package/docs/MORNING.md +0 -204
- package/docs/STRATEGY-ARCHIVE.md +0 -5055
- package/docs/SUPERVISOR-RUNBOOK.md +0 -52
- package/docs/USAGE-BUDGET.json +0 -121
|
@@ -0,0 +1,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;
|