verifyhash 0.1.0
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/LICENSE +201 -0
- package/README.md +883 -0
- package/cli/abi/ContributionRegistry.json +881 -0
- package/cli/agent.js +2173 -0
- package/cli/anchor-artifact.js +853 -0
- package/cli/anchor.js +400 -0
- package/cli/claim.js +881 -0
- package/cli/core/agent-commit.js +448 -0
- package/cli/core/agent-session.js +598 -0
- package/cli/core/anchor-binding.js +663 -0
- package/cli/core/attestation.js +580 -0
- package/cli/core/evidence-plans.js +495 -0
- package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
- package/cli/core/fulfill-intake.js +1082 -0
- package/cli/core/go-live-preflight.js +481 -0
- package/cli/core/license.js +534 -0
- package/cli/core/manifest.js +243 -0
- package/cli/core/packetseal.js +591 -0
- package/cli/core/registryArtifact.js +49 -0
- package/cli/core/revocation.js +539 -0
- package/cli/core/rfc3161.js +389 -0
- package/cli/core/timestamp.js +482 -0
- package/cli/core/trust-asof.js +479 -0
- package/cli/dataset.js +2950 -0
- package/cli/evidence.js +2227 -0
- package/cli/fulfill-webhook-http.js +438 -0
- package/cli/git.js +220 -0
- package/cli/hash.js +550 -0
- package/cli/identity.js +1072 -0
- package/cli/journal-cli.js +1110 -0
- package/cli/journal-log.js +454 -0
- package/cli/journal.js +334 -0
- package/cli/lineage.js +447 -0
- package/cli/list.js +287 -0
- package/cli/parcel.js +1509 -0
- package/cli/proof.js +578 -0
- package/cli/prove.js +300 -0
- package/cli/receipt.js +631 -0
- package/cli/registry.js +331 -0
- package/cli/reputation.js +344 -0
- package/cli/revocation.js +495 -0
- package/cli/serve-verify-http.js +298 -0
- package/cli/serve-verify.js +333 -0
- package/cli/show.js +339 -0
- package/cli/verify.js +383 -0
- package/cli/vh.js +3927 -0
- package/docs/ADOPT.md +183 -0
- package/docs/ADOPTION.json +11 -0
- package/docs/AGENTTRACE.md +247 -0
- package/docs/ANCHORING.md +167 -0
- package/docs/AUDIT.md +55 -0
- package/docs/CONFORMANCE.md +107 -0
- package/docs/DATALEDGER.md +638 -0
- package/docs/DECIDE.md +47 -0
- package/docs/DECISIONS-PENDING.md +27 -0
- package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
- package/docs/ENGINE-LEDGER.json +12 -0
- package/docs/EVIDENCE.md +519 -0
- package/docs/GO-LIVE.md +66 -0
- package/docs/IDENTITY.md +123 -0
- package/docs/INDEPENDENT-VERIFICATION.md +377 -0
- package/docs/INTEGRITY-JOURNAL.md +337 -0
- package/docs/KEY-LIFECYCLE.md +179 -0
- package/docs/LICENSING.md +46 -0
- package/docs/LINEAGE.md +307 -0
- package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
- package/docs/LOOP-HARDENING-PLAN.md +44 -0
- package/docs/MERKLE-LEAVES.md +113 -0
- package/docs/METRICS.jsonl +31 -0
- package/docs/MORNING.md +204 -0
- package/docs/PILOT.md +444 -0
- package/docs/PROOFPARCEL.md +227 -0
- package/docs/PROOFS.md +262 -0
- package/docs/RECEIPTS.md +341 -0
- package/docs/REPUTATION.md +158 -0
- package/docs/SDK.md +301 -0
- package/docs/STRATEGY-ARCHIVE.md +5055 -0
- package/docs/SUPERVISOR-RUNBOOK.md +52 -0
- package/docs/TRUST-BOUNDARIES.md +335 -0
- package/docs/TRUSTLEDGER.md +1976 -0
- package/docs/USAGE-BUDGET.json +121 -0
- package/docs/VERIFY-SERVICE.md +168 -0
- package/index.js +160 -0
- package/package.json +41 -0
- package/trustledger/build-standalone.js +796 -0
- package/trustledger/cli.js +3179 -0
- package/trustledger/close.js +391 -0
- package/trustledger/corpus.js +159 -0
- package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
- package/trustledger/dist/trustledger-standalone.html +6197 -0
- package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
- package/trustledger/door-core.js +442 -0
- package/trustledger/fixtures/bank.csv +7 -0
- package/trustledger/fixtures/bank.malformed.csv +3 -0
- package/trustledger/fixtures/bank.noalias.csv +5 -0
- package/trustledger/fixtures/bank.ofx +34 -0
- package/trustledger/fixtures/bank.real.csv +5 -0
- package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
- package/trustledger/fixtures/e2e/bank.csv +4 -0
- package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
- package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
- package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.csv +6 -0
- package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
- package/trustledger/fixtures/plans/baseline.json +25 -0
- package/trustledger/fixtures/plans/price-binding.example.json +27 -0
- package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
- package/trustledger/fixtures/policy/baseline.json +19 -0
- package/trustledger/fixtures/policy/ca-example.json +12 -0
- package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
- package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
- package/trustledger/fixtures/quickbooks.csv +7 -0
- package/trustledger/fixtures/quickbooks.real.csv +5 -0
- package/trustledger/fixtures/rentroll.csv +6 -0
- package/trustledger/fixtures/rentroll.real.csv +4 -0
- package/trustledger/ingest.js +1163 -0
- package/trustledger/lib/policy-bundled-loader.js +44 -0
- package/trustledger/lib/sha256-vendored.js +227 -0
- package/trustledger/license.js +563 -0
- package/trustledger/match.js +551 -0
- package/trustledger/plans.js +551 -0
- package/trustledger/policy.js +398 -0
- package/trustledger/public/index.html +512 -0
- package/trustledger/reconcile.js +1486 -0
- package/trustledger/report.js +887 -0
- package/trustledger/seal.js +854 -0
- package/trustledger/server.js +391 -0
- package/trustledger/valueproof.js +350 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// cli/core/packetseal.js — the GENERIC, product-agnostic TAMPER-EVIDENT PACKET SEAL core.
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS EXISTS
|
|
6
|
+
// A "packet seal" is a content-addressed wrapper that binds a SET of already-loaded files
|
|
7
|
+
// ({ relPath, bytes }) — PLUS an OPTIONAL, opaque, caller-supplied HEADER ({ relPath, content }) —
|
|
8
|
+
// into ONE Merkle root, then LOCALIZES any later tamper to the exact file (MATCH / CHANGED / MISSING /
|
|
9
|
+
// UNEXPECTED) or to the header. TrustLedger's reconciliation seal was the first such product, but the
|
|
10
|
+
// machinery is entirely product-agnostic: the only product-specific things are (a) the seal's `kind`
|
|
11
|
+
// discriminator and (b) WHAT the product chooses to bind into the header. This module is the SINGLE,
|
|
12
|
+
// tested implementation of that machinery; each product is a THIN adapter (see trustledger/seal.js).
|
|
13
|
+
//
|
|
14
|
+
// It reuses cli/core/manifest.js's hashing / path-leaf convention VERBATIM — pathLeaf / buildTree /
|
|
15
|
+
// hashEntries, the SAME path-bound, domain-separated convention `vh hash <dir>` and the on-chain
|
|
16
|
+
// verifyLeaf use — so a seal root re-derives with NO new crypto, NO contract change, NO network, NO
|
|
17
|
+
// key. NO reconcile/verdict/period vocabulary lives here: that is purely the CALLER's header content.
|
|
18
|
+
//
|
|
19
|
+
// THE OPTIONAL HEADER (the product's binding seam)
|
|
20
|
+
// A product MAY bind product-specific facts (e.g. TrustLedger's pass/reportDate/period verdict + each
|
|
21
|
+
// input's logical role) into the SAME committed root as the files by supplying a HEADER: an opaque
|
|
22
|
+
// { relPath, content } pair, where `content` is the product's deterministic canonical bytes (a Buffer).
|
|
23
|
+
// The header is folded in as ONE MORE (relPath, content) leaf via the EXACT same pathLeaf convention —
|
|
24
|
+
// no second hashing scheme. The header's content is NOT stored in the seal: on validate/verify the
|
|
25
|
+
// CALLER re-derives it (from the seal's own recorded fields) via a `headerFor(seal)` hook, so an edit to
|
|
26
|
+
// any bound field changes the header leaf → the root → the seal stops re-deriving. The header `relPath`
|
|
27
|
+
// is RESERVED: a real file may not occupy it.
|
|
28
|
+
//
|
|
29
|
+
// PURE + I/O-FREE
|
|
30
|
+
// Every function here is pure: the CALLER reads the files and hands in already-loaded { relPath, bytes }
|
|
31
|
+
// entries (and, for header products, the header content). Nothing here touches the filesystem, the
|
|
32
|
+
// clock, the network, or a key — same inputs → byte-identical results.
|
|
33
|
+
//
|
|
34
|
+
// HONEST POSTURE
|
|
35
|
+
// The seal is TAMPER-EVIDENT, not a trusted timestamp and not a legal/semantic opinion. It is an
|
|
36
|
+
// UNTRUSTED transport container: verifySeal is AUTHORITATIVE by RE-COMPUTING the root from the supplied
|
|
37
|
+
// bytes; the seal's stored hashes are merely the EXPECTATION it checks against.
|
|
38
|
+
|
|
39
|
+
const coreManifest = require("../core/manifest");
|
|
40
|
+
const { hashEntries, pathLeaf, hashBytes, buildTree } = require("../hash");
|
|
41
|
+
|
|
42
|
+
// Same 0x + 64-hex shape the manifest core validates against — imported so the two can never drift.
|
|
43
|
+
const HEX32_RE = coreManifest.HEX32_RE;
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Errors — STRICT. A malformed/ambiguous seal raises a NAMED error rather than
|
|
47
|
+
// being silently dropped, coerced, or partially accepted.
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
class PacketSealError extends Error {
|
|
51
|
+
constructor(message) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "PacketSealError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isPlainObject(v) {
|
|
58
|
+
return v != null && typeof v === "object" && !Array.isArray(v);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// _requireCfg(cfg) — assert a product passed a structurally complete seal config.
|
|
63
|
+
// `cfg` parameterizes ONLY the product framing:
|
|
64
|
+
// * kind (required string) — the seal's `kind` discriminator
|
|
65
|
+
// * schemaVersion (required number) — version stamped on a built seal
|
|
66
|
+
// * supportedSchemaVersions (required array) — versions validate accepts
|
|
67
|
+
// * note (required string) — the in-band trust caveat
|
|
68
|
+
// * label (optional string) — the human noun in error messages
|
|
69
|
+
// * headerRelPath (optional string) — RESERVED relPath of the header leaf
|
|
70
|
+
// (required iff the product uses a header)
|
|
71
|
+
// * headerContentFor(seal) (optional fn) — returns the header's canonical content
|
|
72
|
+
// bytes (Buffer) re-derived from a seal object,
|
|
73
|
+
// for validate's root re-derivation
|
|
74
|
+
// A product WITHOUT a header omits headerRelPath/headerContentFor entirely.
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
function _requireCfg(cfg) {
|
|
78
|
+
if (!isPlainObject(cfg)) {
|
|
79
|
+
throw new PacketSealError(
|
|
80
|
+
"packet-seal core requires a { kind, schemaVersion, supportedSchemaVersions, note } config"
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (typeof cfg.kind !== "string" || cfg.kind.length === 0) {
|
|
84
|
+
throw new PacketSealError("packet-seal config requires a non-empty string `kind`");
|
|
85
|
+
}
|
|
86
|
+
if (typeof cfg.schemaVersion !== "number") {
|
|
87
|
+
throw new PacketSealError("packet-seal config requires a numeric `schemaVersion`");
|
|
88
|
+
}
|
|
89
|
+
if (!Array.isArray(cfg.supportedSchemaVersions) || cfg.supportedSchemaVersions.length === 0) {
|
|
90
|
+
throw new PacketSealError("packet-seal config requires a non-empty `supportedSchemaVersions` array");
|
|
91
|
+
}
|
|
92
|
+
if (typeof cfg.note !== "string" || cfg.note.length === 0) {
|
|
93
|
+
throw new PacketSealError("packet-seal config requires a non-empty string `note`");
|
|
94
|
+
}
|
|
95
|
+
// The header is OPTIONAL, but if a product uses one it must supply BOTH the reserved relPath and the
|
|
96
|
+
// re-derivation hook (so validate can recompute the header leaf from a seal's own recorded fields).
|
|
97
|
+
const hasRel = cfg.headerRelPath !== undefined && cfg.headerRelPath !== null;
|
|
98
|
+
const hasFn = cfg.headerContentFor !== undefined && cfg.headerContentFor !== null;
|
|
99
|
+
if (hasRel !== hasFn) {
|
|
100
|
+
throw new PacketSealError(
|
|
101
|
+
"packet-seal config header is all-or-nothing: supply BOTH `headerRelPath` and `headerContentFor`, or NEITHER"
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (hasRel) {
|
|
105
|
+
if (typeof cfg.headerRelPath !== "string" || cfg.headerRelPath.length === 0) {
|
|
106
|
+
throw new PacketSealError("packet-seal config `headerRelPath` must be a non-empty string when present");
|
|
107
|
+
}
|
|
108
|
+
if (typeof cfg.headerContentFor !== "function") {
|
|
109
|
+
throw new PacketSealError("packet-seal config `headerContentFor` must be a function when present");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _usesHeader(cfg) {
|
|
115
|
+
return cfg.headerRelPath !== undefined && cfg.headerRelPath !== null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function _asBuffer(bytes, where, label) {
|
|
119
|
+
if (!(bytes instanceof Uint8Array) && !Buffer.isBuffer(bytes)) {
|
|
120
|
+
throw new PacketSealError(
|
|
121
|
+
`${label} ${where} bytes must be a Buffer/Uint8Array ` +
|
|
122
|
+
"(the core is I/O-free; the caller reads the file and hands in its bytes)"
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// _normalizeEntries(files, cfg, { strictNonEmpty }) — normalize + strictly
|
|
130
|
+
// validate a caller-supplied flat file set into the ordered entry list.
|
|
131
|
+
//
|
|
132
|
+
// `files` is { entries: [{ relPath, bytes }] }. Each relPath must be a non-empty
|
|
133
|
+
// string, unique across the set, and (when the product uses a header) NOT the
|
|
134
|
+
// reserved header relPath. Each `bytes` must be a Buffer/Uint8Array.
|
|
135
|
+
//
|
|
136
|
+
// `strictNonEmpty` is true for BUILD (a seal over zero files is meaningless) and
|
|
137
|
+
// false for VERIFY (which must tolerate a PARTIAL supplied set so it can localize
|
|
138
|
+
// MISSING). Per-entry strictness is identical either way.
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
function _normalizeEntries(files, cfg, opts) {
|
|
142
|
+
const label = cfg.label || "packet seal";
|
|
143
|
+
if (!isPlainObject(files)) {
|
|
144
|
+
throw new PacketSealError(`${label} requires a { entries } file set object`);
|
|
145
|
+
}
|
|
146
|
+
const raw = Array.isArray(files.entries) ? files.entries : null;
|
|
147
|
+
if (raw === null) {
|
|
148
|
+
throw new PacketSealError(`${label} \`entries\` must be an array of { relPath, bytes }`);
|
|
149
|
+
}
|
|
150
|
+
if (opts.strictNonEmpty && raw.length === 0) {
|
|
151
|
+
throw new PacketSealError(`${label} \`entries\` must be a non-empty array of { relPath, bytes }`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const seenRelPath = new Set();
|
|
155
|
+
const entries = raw.map((e) => {
|
|
156
|
+
if (!isPlainObject(e)) {
|
|
157
|
+
throw new PacketSealError(`${label} entry must be an object with relPath + bytes`);
|
|
158
|
+
}
|
|
159
|
+
if (typeof e.relPath !== "string" || e.relPath.length === 0) {
|
|
160
|
+
throw new PacketSealError(`${label} entry relPath must be a non-empty string`);
|
|
161
|
+
}
|
|
162
|
+
if (_usesHeader(cfg) && e.relPath === cfg.headerRelPath) {
|
|
163
|
+
throw new PacketSealError(
|
|
164
|
+
`${label} entry relPath ${JSON.stringify(e.relPath)} is reserved for the seal header ` +
|
|
165
|
+
"(a real file may not occupy the bound header slot)"
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (seenRelPath.has(e.relPath)) {
|
|
169
|
+
throw new PacketSealError(
|
|
170
|
+
`${label} has a duplicate relPath across the file set: ${JSON.stringify(e.relPath)} ` +
|
|
171
|
+
"(every entry must occupy a distinct path)"
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
seenRelPath.add(e.relPath);
|
|
175
|
+
return { relPath: e.relPath, bytes: _asBuffer(e.bytes, `entry ${JSON.stringify(e.relPath)}`, label) };
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return { entries };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// _validateHeaderArg(header, cfg) — normalize a caller-supplied build HEADER. A
|
|
183
|
+
// header is an opaque { relPath, content } pair: relPath must equal the config's
|
|
184
|
+
// reserved headerRelPath (so the product can't bind into an arbitrary slot), and
|
|
185
|
+
// content must be a Buffer/Uint8Array (the product's deterministic canonical bytes).
|
|
186
|
+
// Returns the content as a Buffer. Throws if the product supplied a header without
|
|
187
|
+
// declaring headerRelPath/headerContentFor (or vice versa).
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
function _validateHeaderArg(header, cfg) {
|
|
191
|
+
const label = cfg.label || "packet seal";
|
|
192
|
+
if (header === undefined || header === null) {
|
|
193
|
+
if (_usesHeader(cfg)) {
|
|
194
|
+
throw new PacketSealError(
|
|
195
|
+
`${label} config declares a header (headerRelPath) but buildSeal got no \`header\` { relPath, content }`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
if (!_usesHeader(cfg)) {
|
|
201
|
+
throw new PacketSealError(
|
|
202
|
+
`${label} got a \`header\` but its config declares none (set headerRelPath/headerContentFor to use one)`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (!isPlainObject(header)) {
|
|
206
|
+
throw new PacketSealError(`${label} \`header\` must be a { relPath, content } object`);
|
|
207
|
+
}
|
|
208
|
+
if (header.relPath !== cfg.headerRelPath) {
|
|
209
|
+
throw new PacketSealError(
|
|
210
|
+
`${label} header relPath must be the reserved ${JSON.stringify(cfg.headerRelPath)}, got: ` +
|
|
211
|
+
`${JSON.stringify(header.relPath)}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return _asBuffer(header.content, "header", label);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// The manifest framing the GENERIC manifest core uses for a seal's file hashing. Derived from the seal
|
|
218
|
+
// cfg so each product's manifest `kind` is disjoint. The manifest core does the shared Merkle/manifest
|
|
219
|
+
// math + structural validation; this supplies only the framing.
|
|
220
|
+
function _manifestCfg(cfg) {
|
|
221
|
+
return {
|
|
222
|
+
kind: `${cfg.kind}-manifest`,
|
|
223
|
+
schemaVersion: 1,
|
|
224
|
+
supportedSchemaVersions: [1],
|
|
225
|
+
note: coreManifest.TRUST_NOTE,
|
|
226
|
+
label: `${cfg.label || "packet seal"} manifest`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// _committedEntries(fileEntries, headerContent, cfg) — the FULL ordered list the root commits to: every
|
|
231
|
+
// real file PLUS (when the product uses a header) the synthetic HEADER entry. Every caller of the hasher
|
|
232
|
+
// routes through here so build/validate/verify commit to the SAME structure. The header is one more
|
|
233
|
+
// { relPath, content } pair fed to the SAME hashEntries convention; no second hashing scheme.
|
|
234
|
+
function _committedEntries(fileEntries, headerContent, cfg) {
|
|
235
|
+
if (!_usesHeader(cfg) || headerContent == null) return [...fileEntries];
|
|
236
|
+
return [...fileEntries, { relPath: cfg.headerRelPath, bytes: headerContent }];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// _manifestOver(entries, cfg) — compute the per-file manifest (contentHash + leaf) over a flat
|
|
240
|
+
// { relPath, bytes } list by REUSING the existing core VERBATIM: cli/hash.js hashEntries for the
|
|
241
|
+
// path-bound Merkle leaves, then cli/core/manifest.js buildItemManifest for the strict manifest. NO
|
|
242
|
+
// hashing/leaf construction is re-implemented here.
|
|
243
|
+
function _manifestOver(entries, cfg) {
|
|
244
|
+
const built = hashEntries(entries.map((e) => ({ path: e.relPath, content: e.bytes })));
|
|
245
|
+
const manifest = coreManifest.buildItemManifest(built, _manifestCfg(cfg));
|
|
246
|
+
const byRelPath = new Map();
|
|
247
|
+
for (const f of manifest.files) {
|
|
248
|
+
byRelPath.set(f.relPath, { relPath: f.relPath, contentHash: f.contentHash, leaf: f.leaf });
|
|
249
|
+
}
|
|
250
|
+
return { manifest, byRelPath };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Internal: re-derive the top-level root from a flat list of { relPath, contentHash } using the SAME
|
|
254
|
+
// convention as build — pathLeaf for each, then buildTree — via the shared hash module. We reuse buildTree
|
|
255
|
+
// (the exact builder hashEntries uses) so this stays a re-derivation of the same math, never a parallel one.
|
|
256
|
+
function _rootFromLeafEntries(flat) {
|
|
257
|
+
const leaves = flat.map((e) => pathLeaf(e.relPath, e.contentHash));
|
|
258
|
+
return buildTree(leaves).root;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// buildSeal({ files, header? }, cfg) — assemble + strictly validate a seal.
|
|
263
|
+
//
|
|
264
|
+
// `files` is { entries: [{ relPath, bytes }] } (the caller read these). `header`,
|
|
265
|
+
// when the product uses one, is the opaque { relPath: <reserved>, content: Buffer }
|
|
266
|
+
// pair to bind into the SAME root. The product fills the seal's OWN recorded fields
|
|
267
|
+
// (verdict, roles, …) AFTER this returns, then re-validates — but the root + file
|
|
268
|
+
// leaves + header binding are produced here.
|
|
269
|
+
//
|
|
270
|
+
// PURE + deterministic: same files + header → byte-identical root. The `files` array
|
|
271
|
+
// in the returned seal is emitted sorted by relPath so the seal bytes are deterministic
|
|
272
|
+
// regardless of the caller's array order.
|
|
273
|
+
//
|
|
274
|
+
// Returns a BARE seal object: { kind, schemaVersion, note, root, fileCount, files, [header:{relPath}] }.
|
|
275
|
+
// The header's CONTENT is not stored (it is re-derivable by the caller via headerContentFor); only its
|
|
276
|
+
// reserved relPath is recorded so a reader knows a header was bound.
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
function buildSeal(params, cfg) {
|
|
280
|
+
_requireCfg(cfg);
|
|
281
|
+
if (!isPlainObject(params)) {
|
|
282
|
+
throw new PacketSealError("buildSeal requires { files, [header] }");
|
|
283
|
+
}
|
|
284
|
+
const { entries } = _normalizeEntries(params.files, cfg, { strictNonEmpty: true });
|
|
285
|
+
const headerContent = _validateHeaderArg(params.header, cfg);
|
|
286
|
+
|
|
287
|
+
// Re-derive the manifest/root over the WHOLE committed set — every real file PLUS (when used) the
|
|
288
|
+
// synthetic HEADER entry — via the shared core (no re-implementation).
|
|
289
|
+
const committed = _committedEntries(entries, headerContent, cfg);
|
|
290
|
+
const { manifest, byRelPath } = _manifestOver(committed, cfg);
|
|
291
|
+
|
|
292
|
+
// Emit the per-file leaves sorted by relPath so the seal bytes are deterministic regardless of caller
|
|
293
|
+
// order. The header leaf is NOT listed (it is re-derived on validate/verify).
|
|
294
|
+
const files = entries
|
|
295
|
+
.map((e) => {
|
|
296
|
+
const leaf = byRelPath.get(e.relPath);
|
|
297
|
+
return { relPath: leaf.relPath, contentHash: leaf.contentHash, leaf: leaf.leaf };
|
|
298
|
+
})
|
|
299
|
+
.sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0));
|
|
300
|
+
|
|
301
|
+
const seal = {
|
|
302
|
+
kind: cfg.kind,
|
|
303
|
+
schemaVersion: cfg.schemaVersion,
|
|
304
|
+
note: cfg.note,
|
|
305
|
+
root: manifest.root,
|
|
306
|
+
fileCount: entries.length,
|
|
307
|
+
files,
|
|
308
|
+
};
|
|
309
|
+
if (_usesHeader(cfg)) {
|
|
310
|
+
seal.header = { relPath: cfg.headerRelPath };
|
|
311
|
+
}
|
|
312
|
+
return seal;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// _checkCommonSeal(obj, cfg) — STRICT structural validation of the GENERIC parts of
|
|
317
|
+
// a seal: kind, schemaVersion, note, hex root, the files array, per-file leaf
|
|
318
|
+
// self-consistency (leaf === pathLeaf(relPath, contentHash)), relPath-uniqueness,
|
|
319
|
+
// and fileCount agreement. Returns the flat [{ relPath, contentHash }] file list so
|
|
320
|
+
// the caller can re-derive the root (folding in the header). Throws on the FIRST
|
|
321
|
+
// problem; never half-accepts. Does NOT touch the header (that is validateSeal's job,
|
|
322
|
+
// driven by the product's headerContentFor).
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
function _checkCommonSeal(obj, cfg) {
|
|
326
|
+
const label = cfg.label || "packet seal";
|
|
327
|
+
if (!isPlainObject(obj)) {
|
|
328
|
+
throw new PacketSealError(`${label} must be a JSON object`);
|
|
329
|
+
}
|
|
330
|
+
if (obj.kind !== cfg.kind) {
|
|
331
|
+
throw new PacketSealError(
|
|
332
|
+
`not a ${label} (kind: ${JSON.stringify(obj.kind)}; expected ${JSON.stringify(cfg.kind)})`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
if (!cfg.supportedSchemaVersions.includes(obj.schemaVersion)) {
|
|
336
|
+
throw new PacketSealError(
|
|
337
|
+
`unsupported ${label} schemaVersion: ${JSON.stringify(obj.schemaVersion)} ` +
|
|
338
|
+
`(this build understands ${JSON.stringify(cfg.supportedSchemaVersions)})`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if (obj.note !== cfg.note) {
|
|
342
|
+
throw new PacketSealError(`${label} \`note\` must be the standing trust note (caveat must not drift)`);
|
|
343
|
+
}
|
|
344
|
+
if (typeof obj.root !== "string" || !HEX32_RE.test(obj.root)) {
|
|
345
|
+
throw new PacketSealError(
|
|
346
|
+
`${label} root must be a 0x-prefixed 32-byte hex string, got: ${String(obj.root)}`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (!Array.isArray(obj.files) || obj.files.length === 0) {
|
|
350
|
+
throw new PacketSealError(`${label} \`files\` must be a non-empty array`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const seenRelPath = new Set();
|
|
354
|
+
const flat = []; // { relPath, contentHash }
|
|
355
|
+
obj.files.forEach((entry, i) => {
|
|
356
|
+
if (!isPlainObject(entry)) {
|
|
357
|
+
throw new PacketSealError(`${label} files[${i}] must be an object`);
|
|
358
|
+
}
|
|
359
|
+
if (typeof entry.relPath !== "string" || entry.relPath.length === 0) {
|
|
360
|
+
throw new PacketSealError(`${label} files[${i}].relPath must be a non-empty string`);
|
|
361
|
+
}
|
|
362
|
+
if (_usesHeader(cfg) && entry.relPath === cfg.headerRelPath) {
|
|
363
|
+
throw new PacketSealError(
|
|
364
|
+
`${label} files[${i}].relPath ${JSON.stringify(entry.relPath)} is reserved for the seal header`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
if (seenRelPath.has(entry.relPath)) {
|
|
368
|
+
throw new PacketSealError(
|
|
369
|
+
`${label} has a duplicate relPath across the file set: ${JSON.stringify(entry.relPath)}`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
seenRelPath.add(entry.relPath);
|
|
373
|
+
for (const f of ["contentHash", "leaf"]) {
|
|
374
|
+
if (typeof entry[f] !== "string" || !HEX32_RE.test(entry[f])) {
|
|
375
|
+
throw new PacketSealError(
|
|
376
|
+
`${label} files[${i}].${f} must be a 0x-prefixed 32-byte hex string, got: ${String(entry[f])}`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const expectedLeaf = pathLeaf(entry.relPath, entry.contentHash);
|
|
381
|
+
if (entry.leaf.toLowerCase() !== expectedLeaf.toLowerCase()) {
|
|
382
|
+
throw new PacketSealError(
|
|
383
|
+
`${label} files[${i}].leaf is inconsistent with its relPath+contentHash ` +
|
|
384
|
+
`(expected ${expectedLeaf}, got ${entry.leaf})`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
flat.push({ relPath: entry.relPath, contentHash: entry.contentHash });
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
if (obj.fileCount !== undefined && obj.fileCount !== obj.files.length) {
|
|
391
|
+
throw new PacketSealError(
|
|
392
|
+
`${label} fileCount (${String(obj.fileCount)}) does not match the files length (${obj.files.length})`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return flat;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// validateSeal(obj, cfg) — STRICT structural + self-consistency validation. Throws
|
|
401
|
+
// a named PacketSealError on the FIRST problem; returns the object unchanged on success.
|
|
402
|
+
//
|
|
403
|
+
// On top of the generic structural checks (_checkCommonSeal), the LOAD-BEARING check:
|
|
404
|
+
// the top-level `root` must RE-DERIVE from the listed file (relPath, contentHash) leaves
|
|
405
|
+
// PLUS (when the product uses a header) the synthetic HEADER leaf — whose content is
|
|
406
|
+
// re-derived from the seal's OWN recorded fields via cfg.headerContentFor(obj). A seal
|
|
407
|
+
// whose root was edited to mask a changed file is caught here, AND so is one whose bound
|
|
408
|
+
// header fields were edited: that changes the header content → its leaf → the root.
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
function validateSeal(obj, cfg) {
|
|
412
|
+
_requireCfg(cfg);
|
|
413
|
+
const label = cfg.label || "packet seal";
|
|
414
|
+
const flat = _checkCommonSeal(obj, cfg);
|
|
415
|
+
|
|
416
|
+
let committedFlat = flat;
|
|
417
|
+
if (_usesHeader(cfg)) {
|
|
418
|
+
// The product re-derives the header content from the seal's own recorded fields, so an edit to any
|
|
419
|
+
// bound field changes the header content here and the root stops re-deriving below.
|
|
420
|
+
const headerContent = cfg.headerContentFor(obj);
|
|
421
|
+
const headerBuf = _asBuffer(headerContent, "header content", label);
|
|
422
|
+
// Sanity: the seal must carry the header marker (so a reader knows a header was bound).
|
|
423
|
+
if (!isPlainObject(obj.header) || obj.header.relPath !== cfg.headerRelPath) {
|
|
424
|
+
throw new PacketSealError(
|
|
425
|
+
`${label} must carry a header marker { relPath: ${JSON.stringify(cfg.headerRelPath)} }`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
committedFlat = [
|
|
429
|
+
...flat,
|
|
430
|
+
{ relPath: cfg.headerRelPath, contentHash: hashBytes(headerBuf) },
|
|
431
|
+
];
|
|
432
|
+
} else if (obj.header !== undefined) {
|
|
433
|
+
throw new PacketSealError(`${label} carries a header but its config declares none`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const rederived = _rootFromLeafEntries(committedFlat);
|
|
437
|
+
if (rederived.toLowerCase() !== obj.root.toLowerCase()) {
|
|
438
|
+
const headerNote = _usesHeader(cfg) ? " + header" : "";
|
|
439
|
+
throw new PacketSealError(
|
|
440
|
+
`${label} root does not re-derive from its listed entries${headerNote} ` +
|
|
441
|
+
`(expected ${rederived}, got ${obj.root}) — the seal is internally inconsistent ` +
|
|
442
|
+
"(a file" +
|
|
443
|
+
(_usesHeader(cfg) ? " or a bound header field" : "") +
|
|
444
|
+
" was edited without updating the root)"
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return obj;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
// committedLeaves(seal, cfg) — the FULL ordered { relPath, contentHash } list the
|
|
453
|
+
// seal's `root` commits to: every listed file PLUS (when used) the synthetic header
|
|
454
|
+
// leaf (re-derived from the seal's own fields via cfg.headerContentFor). Validates
|
|
455
|
+
// the seal first. buildTree(map(pathLeaf)) over it re-derives `seal.root` byte-for-byte
|
|
456
|
+
// via the SAME shared convention. PURE.
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
function committedLeaves(seal, cfg) {
|
|
460
|
+
validateSeal(seal, cfg);
|
|
461
|
+
const flat = [];
|
|
462
|
+
for (const e of seal.files) flat.push({ relPath: e.relPath, contentHash: e.contentHash });
|
|
463
|
+
if (_usesHeader(cfg)) {
|
|
464
|
+
const headerBuf = _asBuffer(cfg.headerContentFor(seal), "header content", cfg.label || "packet seal");
|
|
465
|
+
flat.push({ relPath: cfg.headerRelPath, contentHash: hashBytes(headerBuf) });
|
|
466
|
+
}
|
|
467
|
+
return flat;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
// verifySeal(seal, files, cfg, { headerContent? }) — the AUTHORITATIVE, PURE verify.
|
|
472
|
+
//
|
|
473
|
+
// Recompute the per-file content hashes + the root from the SUPPLIED { relPath, bytes }
|
|
474
|
+
// set and compare them, per file, against the seal's stored EXPECTATION. The authoritative
|
|
475
|
+
// check is the RECOMPUTE: the seal is an untrusted container, so a verdict is decided by the
|
|
476
|
+
// bytes the caller holds, never by the seal's own hashes.
|
|
477
|
+
//
|
|
478
|
+
// `files` is { entries: [{ relPath, bytes }] } (the SAME shape buildSeal took). For a header
|
|
479
|
+
// product the caller passes the header CONTENT it re-derived from the SUPPLIED facts as
|
|
480
|
+
// `opts.headerContent` (a Buffer) — so a header edit on the supplied side changes the recomputed
|
|
481
|
+
// root and rootMatches goes false. (Validate already guarantees the seal's OWN header re-derives.)
|
|
482
|
+
//
|
|
483
|
+
// Returns a structured result naming EXACTLY which files MATCH / CHANGED / MISSING / UNEXPECTED,
|
|
484
|
+
// plus the recomputed/sealed roots and rootMatches. The overall verdict is ACCEPTED only when every
|
|
485
|
+
// sealed file MATCHes, none is MISSING/UNEXPECTED, AND the recomputed root equals the sealed root.
|
|
486
|
+
//
|
|
487
|
+
// PURE: no I/O, no key, no network, no clock.
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
function verifySeal(seal, files, cfg, opts = {}) {
|
|
491
|
+
validateSeal(seal, cfg);
|
|
492
|
+
const { entries } = _normalizeEntries(files, cfg, { strictNonEmpty: false });
|
|
493
|
+
|
|
494
|
+
// The supplied header content (when the product uses a header). Folded into the recomputed root so a
|
|
495
|
+
// header edit on the supplied side flips rootMatches.
|
|
496
|
+
let headerContent = null;
|
|
497
|
+
if (_usesHeader(cfg)) {
|
|
498
|
+
if (opts.headerContent === undefined || opts.headerContent === null) {
|
|
499
|
+
throw new PacketSealError(
|
|
500
|
+
`${cfg.label || "packet seal"} verifySeal requires \`headerContent\` (a header product binds it into the root)`
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
headerContent = _asBuffer(opts.headerContent, "header content", cfg.label || "packet seal");
|
|
504
|
+
} else if (opts.headerContent !== undefined && opts.headerContent !== null) {
|
|
505
|
+
throw new PacketSealError(
|
|
506
|
+
`${cfg.label || "packet seal"} verifySeal got headerContent but the config declares no header`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Per-file recomputed contentHash from the SUPPLIED bytes via the SHARED hasher — the authoritative
|
|
511
|
+
// re-derivation, done per file so a PARTIAL set still yields honest per-file findings.
|
|
512
|
+
const suppliedByRel = new Map();
|
|
513
|
+
for (const e of entries) suppliedByRel.set(e.relPath, hashBytes(e.bytes));
|
|
514
|
+
|
|
515
|
+
// The recomputed ROOT folds in the supplied header (when used). A tree needs ≥1 leaf and a partial set
|
|
516
|
+
// can never re-derive the sealed root anyway, so we compute it only when at least one committed leaf
|
|
517
|
+
// exists; otherwise it is null and rootMatches is false.
|
|
518
|
+
const committed = _committedEntries(entries, headerContent, cfg);
|
|
519
|
+
const recomputedRoot = committed.length > 0 ? _rootFromSupplied(committed) : null;
|
|
520
|
+
|
|
521
|
+
// Sealed expectation: relPath -> contentHash.
|
|
522
|
+
const sealedByRel = new Map();
|
|
523
|
+
for (const e of seal.files) sealedByRel.set(e.relPath, e.contentHash);
|
|
524
|
+
|
|
525
|
+
const matched = [];
|
|
526
|
+
const changed = [];
|
|
527
|
+
const missing = [];
|
|
528
|
+
const unexpected = [];
|
|
529
|
+
|
|
530
|
+
for (const [relPath, expHash] of sealedByRel) {
|
|
531
|
+
const got = suppliedByRel.get(relPath);
|
|
532
|
+
if (got === undefined) {
|
|
533
|
+
missing.push({ relPath });
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (got.toLowerCase() === expHash.toLowerCase()) {
|
|
537
|
+
matched.push({ relPath, contentHash: got });
|
|
538
|
+
} else {
|
|
539
|
+
changed.push({ relPath, expectedContentHash: expHash, actualContentHash: got });
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
for (const [relPath, got] of suppliedByRel) {
|
|
543
|
+
if (!sealedByRel.has(relPath)) unexpected.push({ relPath, contentHash: got });
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const byRel = (a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0);
|
|
547
|
+
matched.sort(byRel);
|
|
548
|
+
changed.sort(byRel);
|
|
549
|
+
missing.sort(byRel);
|
|
550
|
+
unexpected.sort(byRel);
|
|
551
|
+
|
|
552
|
+
const rootMatches = recomputedRoot != null && recomputedRoot.toLowerCase() === seal.root.toLowerCase();
|
|
553
|
+
const accepted =
|
|
554
|
+
changed.length === 0 && missing.length === 0 && unexpected.length === 0 && rootMatches;
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
verdict: accepted ? "ACCEPTED" : "REJECTED",
|
|
558
|
+
accepted,
|
|
559
|
+
sealedRoot: seal.root,
|
|
560
|
+
recomputedRoot,
|
|
561
|
+
rootMatches,
|
|
562
|
+
counts: {
|
|
563
|
+
matched: matched.length,
|
|
564
|
+
changed: changed.length,
|
|
565
|
+
missing: missing.length,
|
|
566
|
+
unexpected: unexpected.length,
|
|
567
|
+
},
|
|
568
|
+
matched,
|
|
569
|
+
changed,
|
|
570
|
+
missing,
|
|
571
|
+
unexpected,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Internal: recompute the top-level root from the SUPPLIED { relPath, bytes } entries via the SAME
|
|
576
|
+
// convention buildSeal used (hashEntries). Kept separate so verifySeal's root is always the authoritative
|
|
577
|
+
// re-derivation from bytes, never copied from the seal.
|
|
578
|
+
function _rootFromSupplied(entries) {
|
|
579
|
+
const built = hashEntries(entries.map((e) => ({ path: e.relPath, content: e.bytes })));
|
|
580
|
+
return built.root;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
module.exports = {
|
|
584
|
+
HEX32_RE,
|
|
585
|
+
PacketSealError,
|
|
586
|
+
// pure seal core (parameterized by the product cfg)
|
|
587
|
+
buildSeal,
|
|
588
|
+
validateSeal,
|
|
589
|
+
verifySeal,
|
|
590
|
+
committedLeaves,
|
|
591
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Single source of truth for the ContributionRegistry ABI used by the on-chain
|
|
4
|
+
// `vh` subcommands (anchor/claim/verify/list/show/…).
|
|
5
|
+
//
|
|
6
|
+
// Why this exists: the CLI must ship and run as a published npm package whose
|
|
7
|
+
// `files` allowlist contains only `cli/` — it does NOT ship hardhat's build
|
|
8
|
+
// output under `artifacts/`. If the on-chain modules required
|
|
9
|
+
// `../artifacts/contracts/ContributionRegistry.sol/ContributionRegistry.json`
|
|
10
|
+
// directly (as they historically did), then a clean install would CRASH on
|
|
11
|
+
// load — even for on-chain-free commands like `vh hash`, because vh.js eagerly
|
|
12
|
+
// requires those modules.
|
|
13
|
+
//
|
|
14
|
+
// Resolution order:
|
|
15
|
+
// 1. The committed, shipped copy bundled in `cli/abi/…` (always present in a
|
|
16
|
+
// published install).
|
|
17
|
+
// 2. The hardhat artifact under `artifacts/…` when developing inside the repo
|
|
18
|
+
// (kept as a freshness cross-check; see test/cli.packaging.test.js).
|
|
19
|
+
//
|
|
20
|
+
// Either source yields the same ABI. We prefer the bundled copy so the package
|
|
21
|
+
// is self-contained and never depends on a compile step at runtime.
|
|
22
|
+
|
|
23
|
+
const path = require("path");
|
|
24
|
+
|
|
25
|
+
function tryRequire(p) {
|
|
26
|
+
try {
|
|
27
|
+
return require(p);
|
|
28
|
+
} catch (_err) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Bundled, version-controlled copy that ships with the package.
|
|
34
|
+
const bundled = tryRequire(path.join(__dirname, "..", "abi", "ContributionRegistry.json"));
|
|
35
|
+
|
|
36
|
+
// Hardhat build output — only present in a dev checkout after `hardhat compile`.
|
|
37
|
+
const hardhat = tryRequire(
|
|
38
|
+
path.join(__dirname, "..", "..", "artifacts", "contracts", "ContributionRegistry.sol", "ContributionRegistry.json")
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const ARTIFACT = bundled || hardhat;
|
|
42
|
+
|
|
43
|
+
if (!ARTIFACT || !Array.isArray(ARTIFACT.abi)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"ContributionRegistry ABI unavailable: neither the bundled cli/abi copy nor the hardhat artifact could be loaded."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = ARTIFACT;
|