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.
Files changed (154) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +883 -0
  3. package/cli/abi/ContributionRegistry.json +881 -0
  4. package/cli/agent.js +2173 -0
  5. package/cli/anchor-artifact.js +853 -0
  6. package/cli/anchor.js +400 -0
  7. package/cli/claim.js +881 -0
  8. package/cli/core/agent-commit.js +448 -0
  9. package/cli/core/agent-session.js +598 -0
  10. package/cli/core/anchor-binding.js +663 -0
  11. package/cli/core/attestation.js +580 -0
  12. package/cli/core/evidence-plans.js +495 -0
  13. package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
  14. package/cli/core/fulfill-intake.js +1082 -0
  15. package/cli/core/go-live-preflight.js +481 -0
  16. package/cli/core/license.js +534 -0
  17. package/cli/core/manifest.js +243 -0
  18. package/cli/core/packetseal.js +591 -0
  19. package/cli/core/registryArtifact.js +49 -0
  20. package/cli/core/revocation.js +539 -0
  21. package/cli/core/rfc3161.js +389 -0
  22. package/cli/core/timestamp.js +482 -0
  23. package/cli/core/trust-asof.js +479 -0
  24. package/cli/dataset.js +2950 -0
  25. package/cli/evidence.js +2227 -0
  26. package/cli/fulfill-webhook-http.js +438 -0
  27. package/cli/git.js +220 -0
  28. package/cli/hash.js +550 -0
  29. package/cli/identity.js +1072 -0
  30. package/cli/journal-cli.js +1110 -0
  31. package/cli/journal-log.js +454 -0
  32. package/cli/journal.js +334 -0
  33. package/cli/lineage.js +447 -0
  34. package/cli/list.js +287 -0
  35. package/cli/parcel.js +1509 -0
  36. package/cli/proof.js +578 -0
  37. package/cli/prove.js +300 -0
  38. package/cli/receipt.js +631 -0
  39. package/cli/registry.js +331 -0
  40. package/cli/reputation.js +344 -0
  41. package/cli/revocation.js +495 -0
  42. package/cli/serve-verify-http.js +298 -0
  43. package/cli/serve-verify.js +333 -0
  44. package/cli/show.js +339 -0
  45. package/cli/verify.js +383 -0
  46. package/cli/vh.js +3927 -0
  47. package/docs/ADOPT.md +183 -0
  48. package/docs/ADOPTION.json +11 -0
  49. package/docs/AGENTTRACE.md +247 -0
  50. package/docs/ANCHORING.md +167 -0
  51. package/docs/AUDIT.md +55 -0
  52. package/docs/CONFORMANCE.md +107 -0
  53. package/docs/DATALEDGER.md +638 -0
  54. package/docs/DECIDE.md +47 -0
  55. package/docs/DECISIONS-PENDING.md +27 -0
  56. package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
  57. package/docs/ENGINE-LEDGER.json +12 -0
  58. package/docs/EVIDENCE.md +519 -0
  59. package/docs/GO-LIVE.md +66 -0
  60. package/docs/IDENTITY.md +123 -0
  61. package/docs/INDEPENDENT-VERIFICATION.md +377 -0
  62. package/docs/INTEGRITY-JOURNAL.md +337 -0
  63. package/docs/KEY-LIFECYCLE.md +179 -0
  64. package/docs/LICENSING.md +46 -0
  65. package/docs/LINEAGE.md +307 -0
  66. package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
  67. package/docs/LOOP-HARDENING-PLAN.md +44 -0
  68. package/docs/MERKLE-LEAVES.md +113 -0
  69. package/docs/METRICS.jsonl +31 -0
  70. package/docs/MORNING.md +204 -0
  71. package/docs/PILOT.md +444 -0
  72. package/docs/PROOFPARCEL.md +227 -0
  73. package/docs/PROOFS.md +262 -0
  74. package/docs/RECEIPTS.md +341 -0
  75. package/docs/REPUTATION.md +158 -0
  76. package/docs/SDK.md +301 -0
  77. package/docs/STRATEGY-ARCHIVE.md +5055 -0
  78. package/docs/SUPERVISOR-RUNBOOK.md +52 -0
  79. package/docs/TRUST-BOUNDARIES.md +335 -0
  80. package/docs/TRUSTLEDGER.md +1976 -0
  81. package/docs/USAGE-BUDGET.json +121 -0
  82. package/docs/VERIFY-SERVICE.md +168 -0
  83. package/index.js +160 -0
  84. package/package.json +41 -0
  85. package/trustledger/build-standalone.js +796 -0
  86. package/trustledger/cli.js +3179 -0
  87. package/trustledger/close.js +391 -0
  88. package/trustledger/corpus.js +159 -0
  89. package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
  90. package/trustledger/dist/trustledger-standalone.html +6197 -0
  91. package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
  92. package/trustledger/door-core.js +442 -0
  93. package/trustledger/fixtures/bank.csv +7 -0
  94. package/trustledger/fixtures/bank.malformed.csv +3 -0
  95. package/trustledger/fixtures/bank.noalias.csv +5 -0
  96. package/trustledger/fixtures/bank.ofx +34 -0
  97. package/trustledger/fixtures/bank.real.csv +5 -0
  98. package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
  99. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
  100. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
  101. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
  102. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
  103. package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
  104. package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
  105. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
  106. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
  107. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
  108. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
  109. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
  110. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
  111. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
  112. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
  113. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
  114. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
  115. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
  116. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
  117. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
  118. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
  119. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
  120. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
  121. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
  122. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
  123. package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
  124. package/trustledger/fixtures/e2e/bank.csv +4 -0
  125. package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
  126. package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
  127. package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
  128. package/trustledger/fixtures/e2e/rentroll.csv +6 -0
  129. package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
  130. package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
  131. package/trustledger/fixtures/plans/baseline.json +25 -0
  132. package/trustledger/fixtures/plans/price-binding.example.json +27 -0
  133. package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
  134. package/trustledger/fixtures/policy/baseline.json +19 -0
  135. package/trustledger/fixtures/policy/ca-example.json +12 -0
  136. package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
  137. package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
  138. package/trustledger/fixtures/quickbooks.csv +7 -0
  139. package/trustledger/fixtures/quickbooks.real.csv +5 -0
  140. package/trustledger/fixtures/rentroll.csv +6 -0
  141. package/trustledger/fixtures/rentroll.real.csv +4 -0
  142. package/trustledger/ingest.js +1163 -0
  143. package/trustledger/lib/policy-bundled-loader.js +44 -0
  144. package/trustledger/lib/sha256-vendored.js +227 -0
  145. package/trustledger/license.js +563 -0
  146. package/trustledger/match.js +551 -0
  147. package/trustledger/plans.js +551 -0
  148. package/trustledger/policy.js +398 -0
  149. package/trustledger/public/index.html +512 -0
  150. package/trustledger/reconcile.js +1486 -0
  151. package/trustledger/report.js +887 -0
  152. package/trustledger/seal.js +854 -0
  153. package/trustledger/server.js +391 -0
  154. package/trustledger/valueproof.js +350 -0
@@ -0,0 +1,663 @@
1
+ "use strict";
2
+
3
+ // cli/core/anchor-binding.js — the PURE ANCHOR-BINDING core (T-70.1, EPIC-70 "chain-anchor bridge").
4
+ //
5
+ // WHAT THIS IS
6
+ // The one place that knows how to turn ANY sealed product artifact into the single canonical
7
+ // 32-byte digest a chain registry record can bind — and how to verify, OFFLINE, that an
8
+ // anchored receipt really is about the exact artifact bytes in hand. Three functions:
9
+ //
10
+ // (a) artifactDigest(artifact) — strict dispatch over a CLOSED, frozen kind table; each
11
+ // leg REUSES the artifact's SHIPPED validator VERBATIM
12
+ // before extracting its digest. Returns
13
+ // { ok:true, digest, kind, how } or a NAMED
14
+ // { ok:false, reason, detail? }. TOTAL: never throws.
15
+ // (b) buildAnchoredReceipt(params) — the canonical, versioned, SORTED-KEY
16
+ // `kind:"vh-anchored-receipt@1"` container embedding the
17
+ // digest + derivation rule + chain facts + the honest
18
+ // trust note VERBATIM. Strict field validation, named
19
+ // rejects.
20
+ // (c) verifyAnchoredReceipt(args) — parse+validate the receipt strictly, recompute
21
+ // artifactDigest(artifact) via the SAME closed table, and
22
+ // return { ok:true, digest, chain } on match or the
23
+ // SPECIFIC named mismatch. NEVER consults a network — the
24
+ // on-chain read-back is T-70.2's `--rpc` mode.
25
+ //
26
+ // THE CLOSED KIND TABLE (extending it is a deliberate edit here + in the test, never implicit)
27
+ // kind string shipped validator REUSED VERBATIM digest
28
+ // ----------------------------------- ----------------------------------------- ------------------
29
+ // vh.evidence-seal cli/evidence.js readSeal seal `root`
30
+ // vh.agent-session-packet cli/agent.js validatePacketShape + verified head root
31
+ // verifyPacket (which delegates every event
32
+ // leaf/head recompute to the T-68.1 core
33
+ // cli/core/agent-session.js)
34
+ // vh.journal-tree-head cli/journal-log.js head shape { size, head `root`
35
+ // root } (the Signed-Tree-Head-SHAPED
36
+ // commitment `vh journal tree-head` prints)
37
+ // + its exported EMPTY_ROOT constant
38
+ // trustledger.reconcile-seal trustledger/seal.js readSeal seal `root`
39
+ // verifyhash.dataset-attestation cli/dataset.js serializeAttestation 0x + sha256 over
40
+ // (validates first) the canonical bytes
41
+ // verifyhash.parcel-attestation cli/parcel.js serializeParcelAttestation 0x + sha256 over
42
+ // (validates first) the canonical bytes
43
+ //
44
+ // The attestation digests are computed with cli/core/timestamp.js `sha256Hex` — the EXACT
45
+ // function `vh dataset timestamp-request` / `vh parcel timestamp-request` use — so the anchored
46
+ // digest for an attestation is 0x + the very digest the owner's RFC-3161 TSA flow already
47
+ // stamps (one digest per artifact, never two).
48
+ //
49
+ // THE JOURNAL TREE-HEAD LEG (why `how` carries the size)
50
+ // A journal tree head is the bare RFC-6962 commitment { size, root } (cli/journal-log.js's own
51
+ // head shape — `vh journal tree-head` prints exactly these two facts; a kind-tagged twin
52
+ // { kind:"vh.journal-tree-head", size, root } is also accepted so the artifact can be
53
+ // self-describing on disk). The digest is the `root`; the `size` is part of the RFC-6962 head
54
+ // SEMANTICS but is NOT derivable from the root alone, so this leg binds it into the receipt via
55
+ // the derivation-rule string (`how`), and verifyAnchoredReceipt compares the FULL recomputed
56
+ // { digest, kind, how } triple — an edited size is a NAMED `how-mismatch`, never a silent pass.
57
+ //
58
+ // CASE NORMALIZATION
59
+ // The packetseal-family validators accept mixed-case hex and compare case-insensitively; the
60
+ // receipt digest is canonical LOWERCASE (one logical value, one wire encoding — the family's
61
+ // byte-determinism discipline), so seal roots are lowercased on extraction. All shipped builders
62
+ // already emit lowercase.
63
+ //
64
+ // TRUST BOUNDARY (embedded VERBATIM in every receipt as `note`; pinned by the test and by T-70.3)
65
+ // See ANCHOR_TRUST_NOTE below. The load-bearing honesty: a local dev chain proves mechanism
66
+ // only; a public-chain record is as trustworthy as the chain + YOUR pinned contract address; the
67
+ // binding proves existence-by-block-time of the digest, never the artifact's truth. The `chain`
68
+ // facts inside a receipt are the ANCHORER'S CLAIM until re-checked against the chain — this pure
69
+ // core validates their FORM strictly but cannot (and does not pretend to) confirm them; that is
70
+ // T-70.2's `--rpc` read-back.
71
+ //
72
+ // PURITY (a hard acceptance criterion, statically guarded by the test)
73
+ // This module's own source requires NO fs / http / https / net / dns / tls / dgram /
74
+ // child_process, touches NO process.env, reads NO clock (no Date), has NO randomness and NO key
75
+ // material, and invents NO crypto: every hash it returns was computed by a shipped, already-
76
+ // tested validator/serializer, reused verbatim. Every exported function is TOTAL on hostile
77
+ // input: a failure is a NAMED { ok:false, reason } verdict, never an exception.
78
+
79
+ const evidence = require("../evidence");
80
+ const agent = require("../agent");
81
+ const journalLog = require("../journal-log");
82
+ const dataset = require("../dataset");
83
+ const parcel = require("../parcel");
84
+ const tlSeal = require("../../trustledger/seal");
85
+ const coreTimestamp = require("./timestamp");
86
+
87
+ // ---------------------------------------------------------------------------------------------------
88
+ // The receipt container framing.
89
+ // ---------------------------------------------------------------------------------------------------
90
+
91
+ // The container kind. The schema version rides IN the kind string ("@1") — a future breaking change
92
+ // is a NEW kind ("@2"), so an old verifier can never half-read a new receipt.
93
+ const ANCHORED_RECEIPT_KIND = "vh-anchored-receipt@1";
94
+
95
+ // The standing trust note, embedded VERBATIM in every built receipt (the T-70.1 acceptance pins it;
96
+ // T-70.3 carries the same sentences into docs). The two load-bearing sentences — "local dev chain
97
+ // proves MECHANISM only" and "as trustworthy as the chain + YOUR pinned contract address" — must
98
+ // never drift.
99
+ const ANCHOR_TRUST_NOTE =
100
+ "This anchored receipt binds the artifact digest above to an on-chain registry record. A receipt " +
101
+ "from a LOCAL dev chain proves MECHANISM only and is worth NOTHING publicly until a human deploys " +
102
+ "the registry (STRATEGY.md P-2). On a public chain it proves ONLY that an on-chain record binds " +
103
+ "this exact digest at a block whose timestamp BOUNDS existence — as trustworthy as the chain + " +
104
+ "YOUR pinned contract address — NOT the artifact's truth, NOT faithful recording, NOT attribution " +
105
+ "beyond the anchoring key. The `chain` facts in this receipt are the anchorer's claim until " +
106
+ "re-checked against the chain (`vh verify-anchored --rpc`).";
107
+
108
+ // Stable, named reason codes — the verdict contract callers (and the T-70.2 CLI) rely on. Hyphenated
109
+ // lowercase, matching the backlog's documented `digest-mismatch` / `kind-mismatch` / `bad-receipt`.
110
+ const REASONS = Object.freeze({
111
+ NOT_AN_OBJECT: "not-an-object",
112
+ UNKNOWN_KIND: "unknown-kind",
113
+ EVIDENCE_SEAL_INVALID: "evidence-seal-invalid",
114
+ AGENT_PACKET_INVALID: "agent-packet-invalid",
115
+ JOURNAL_TREE_HEAD_INVALID: "journal-tree-head-invalid",
116
+ TRUSTLEDGER_SEAL_INVALID: "trustledger-seal-invalid",
117
+ DATASET_ATTESTATION_INVALID: "dataset-attestation-invalid",
118
+ PARCEL_ATTESTATION_INVALID: "parcel-attestation-invalid",
119
+ BAD_ARGS: "bad-args",
120
+ BAD_DIGEST: "bad-digest",
121
+ BAD_HOW: "bad-how",
122
+ BAD_LABEL: "bad-label",
123
+ BAD_CHAIN: "bad-chain",
124
+ BAD_RECEIPT: "bad-receipt",
125
+ DIGEST_MISMATCH: "digest-mismatch",
126
+ KIND_MISMATCH: "kind-mismatch",
127
+ HOW_MISMATCH: "how-mismatch",
128
+ });
129
+
130
+ // The journal tree head carries no `kind` of its own (it is the bare { size, root } commitment), so
131
+ // the closed table names it here; the other five kinds are the products' OWN shipped constants,
132
+ // reused so the table can never drift from the artifacts.
133
+ const JOURNAL_TREE_HEAD_KIND = "vh.journal-tree-head";
134
+
135
+ // The CLOSED, frozen kind table (the six anchorable sealed-product artifacts).
136
+ const ARTIFACT_KINDS = Object.freeze([
137
+ evidence.SEAL_KIND, // "vh.evidence-seal"
138
+ agent.PACKET_KIND, // "vh.agent-session-packet"
139
+ JOURNAL_TREE_HEAD_KIND, // "vh.journal-tree-head"
140
+ tlSeal.SEAL_KIND, // "trustledger.reconcile-seal"
141
+ dataset.ATTESTATION_KIND, // "verifyhash.dataset-attestation"
142
+ parcel.PARCEL_ATTESTATION_KIND, // "verifyhash.parcel-attestation"
143
+ ]);
144
+
145
+ // ---------------------------------------------------------------------------------------------------
146
+ // Small strict-shape helpers (no clock, no randomness — pure predicates).
147
+ // ---------------------------------------------------------------------------------------------------
148
+
149
+ const HEX32_LC_RE = /^0x[0-9a-f]{64}$/; // canonical lowercase bytes32
150
+ const ADDRESS_LC_RE = /^0x[0-9a-f]{40}$/; // canonical lowercase address
151
+ const CONTROL_CHAR_RE = /[\u0000-\u001f\u007f]/;
152
+
153
+ function isPlainObject(v) {
154
+ return v != null && typeof v === "object" && !Array.isArray(v);
155
+ }
156
+
157
+ function _detail(e) {
158
+ return e && typeof e.message === "string" ? e.message : String(e);
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------------------------------
162
+ // The per-kind derivation rules (`how`) — human-readable, deterministic, and BOUND into the receipt.
163
+ // For the five self-contained kinds the rule is a FIXED string; the journal leg interpolates the
164
+ // head size (see the module header for why). verifyAnchoredReceipt compares the recomputed rule
165
+ // against the receipt's, so a drifted rule (or an edited journal size) is a NAMED reject.
166
+ // ---------------------------------------------------------------------------------------------------
167
+
168
+ const HOW_FIXED = Object.freeze({
169
+ [evidence.SEAL_KIND]:
170
+ "digest = the evidence packet's `root` (sorted-pair Merkle root over its path-bound file leaves), " +
171
+ "re-derived by cli/evidence.js readSeal before extraction",
172
+ [agent.PACKET_KIND]:
173
+ "digest = the agent-session packet's verified head `root` (RFC-6962 ordered Merkle root over the " +
174
+ "event leaves), re-derived by cli/agent.js verifyPacket before extraction",
175
+ [tlSeal.SEAL_KIND]:
176
+ "digest = the TrustLedger sealfile's `root` (Merkle root over its committed input/output leaves + " +
177
+ "verdict header), re-derived by trustledger/seal.js readSeal before extraction",
178
+ [dataset.ATTESTATION_KIND]:
179
+ "digest = 0x + sha256 over the canonical UNSIGNED dataset-attestation bytes, exactly as " +
180
+ "`vh dataset timestamp-request` computes it (cli/core/timestamp.js sha256Hex)",
181
+ [parcel.PARCEL_ATTESTATION_KIND]:
182
+ "digest = 0x + sha256 over the canonical UNSIGNED parcel-attestation bytes, exactly as " +
183
+ "`vh parcel timestamp-request` computes it (cli/core/timestamp.js sha256Hex)",
184
+ });
185
+
186
+ function _journalHow(size) {
187
+ return (
188
+ `digest = the journal tree head \`root\` (RFC-6962 ordered Merkle root, cli/journal-log.js ` +
189
+ `treeHead) over ${size} entries; the head size is bound into this derivation rule`
190
+ );
191
+ }
192
+
193
+ const JOURNAL_HOW_RE =
194
+ /^digest = the journal tree head `root` \(RFC-6962 ordered Merkle root, cli\/journal-log\.js treeHead\) over (0|[1-9][0-9]*) entries; the head size is bound into this derivation rule$/;
195
+
196
+ /** Is `how` the valid derivation rule for `kind`? (kind must already be a table kind.) */
197
+ function _howValidFor(kind, how) {
198
+ if (typeof how !== "string") return false;
199
+ if (kind === JOURNAL_TREE_HEAD_KIND) {
200
+ const m = JOURNAL_HOW_RE.exec(how);
201
+ return m !== null && Number.isSafeInteger(Number(m[1]));
202
+ }
203
+ return how === HOW_FIXED[kind];
204
+ }
205
+
206
+ // ---------------------------------------------------------------------------------------------------
207
+ // (a) artifactDigest(artifact) — the closed-table digest extraction.
208
+ // ---------------------------------------------------------------------------------------------------
209
+
210
+ function _ok(digest, kind, how) {
211
+ return { ok: true, digest, kind, how };
212
+ }
213
+
214
+ function _no(reason, detail) {
215
+ return detail === undefined ? { ok: false, reason } : { ok: false, reason, detail };
216
+ }
217
+
218
+ // vh.evidence-seal — cli/evidence.js readSeal (strict structure + root re-derivation), then `root`.
219
+ function _evidenceDigest(artifact) {
220
+ try {
221
+ evidence.readSeal(artifact);
222
+ } catch (e) {
223
+ return _no(REASONS.EVIDENCE_SEAL_INVALID, _detail(e));
224
+ }
225
+ return _ok(artifact.root.toLowerCase(), evidence.SEAL_KIND, HOW_FIXED[evidence.SEAL_KIND]);
226
+ }
227
+
228
+ // vh.agent-session-packet — cli/agent.js validatePacketShape (strict container) + verifyPacket (the
229
+ // AUTHORITATIVE per-event leaf/head recompute, delegating to cli/core/agent-session.js), then the
230
+ // VERIFIED head root. Using verifyPacket (not the packet's own stored head) means a packet whose
231
+ // stored head, leaves, counts, or any event byte was edited is the packet's OWN named reject.
232
+ function _agentDigest(artifact) {
233
+ try {
234
+ agent.validatePacketShape(artifact);
235
+ } catch (e) {
236
+ return _no(REASONS.AGENT_PACKET_INVALID, _detail(e));
237
+ }
238
+ let v;
239
+ try {
240
+ v = agent.verifyPacket(artifact);
241
+ } catch (e) {
242
+ // verifyPacket is documented never to throw; fail CLOSED anyway.
243
+ return _no(REASONS.AGENT_PACKET_INVALID, _detail(e));
244
+ }
245
+ if (!v || v.accepted !== true) {
246
+ const seq = v && v.seq !== null && v.seq !== undefined ? ` at seq ${v.seq}` : "";
247
+ return _no(REASONS.AGENT_PACKET_INVALID, `packet verify REJECTED: ${v ? v.reason : "no verdict"}${seq}`);
248
+ }
249
+ return _ok(v.head.root, agent.PACKET_KIND, HOW_FIXED[agent.PACKET_KIND]);
250
+ }
251
+
252
+ // vh.journal-tree-head — the bare { size, root } commitment (cli/journal-log.js's own head shape),
253
+ // or its kind-tagged twin. Strict: exact key set, canonical-lowercase root, non-negative safe-integer
254
+ // size, and the EMPTY_ROOT consistency both ways (size 0 <=> the exported domain-separated empty
255
+ // root — the ONE structural fact checkable without the journal's leaves).
256
+ function _journalHeadDigest(artifact, tagged) {
257
+ const allowed = tagged ? ["kind", "size", "root"] : ["size", "root"];
258
+ for (const k of Object.keys(artifact)) {
259
+ if (!allowed.includes(k)) {
260
+ return _no(REASONS.JOURNAL_TREE_HEAD_INVALID, `journal tree head has unknown field: ${JSON.stringify(k)}`);
261
+ }
262
+ }
263
+ if (!Number.isSafeInteger(artifact.size) || artifact.size < 0) {
264
+ return _no(
265
+ REASONS.JOURNAL_TREE_HEAD_INVALID,
266
+ `journal tree head size must be a non-negative integer, got: ${String(artifact.size)}`
267
+ );
268
+ }
269
+ if (typeof artifact.root !== "string" || !HEX32_LC_RE.test(artifact.root)) {
270
+ return _no(
271
+ REASONS.JOURNAL_TREE_HEAD_INVALID,
272
+ `journal tree head root must be a LOWERCASE 0x-bytes32 hex string, got: ${String(artifact.root)}`
273
+ );
274
+ }
275
+ if (artifact.size === 0 && artifact.root !== journalLog.EMPTY_ROOT) {
276
+ return _no(
277
+ REASONS.JOURNAL_TREE_HEAD_INVALID,
278
+ `an EMPTY journal tree head (size 0) must carry the documented empty root ${journalLog.EMPTY_ROOT}`
279
+ );
280
+ }
281
+ if (artifact.size > 0 && artifact.root === journalLog.EMPTY_ROOT) {
282
+ return _no(
283
+ REASONS.JOURNAL_TREE_HEAD_INVALID,
284
+ "a non-empty journal tree head cannot carry the domain-separated EMPTY root"
285
+ );
286
+ }
287
+ return _ok(artifact.root, JOURNAL_TREE_HEAD_KIND, _journalHow(artifact.size));
288
+ }
289
+
290
+ // trustledger.reconcile-seal — trustledger/seal.js readSeal (strict structure + root re-derivation
291
+ // over files + the verdict/role header), then `root`.
292
+ function _trustledgerDigest(artifact) {
293
+ try {
294
+ tlSeal.readSeal(artifact);
295
+ } catch (e) {
296
+ return _no(REASONS.TRUSTLEDGER_SEAL_INVALID, _detail(e));
297
+ }
298
+ return _ok(artifact.root.toLowerCase(), tlSeal.SEAL_KIND, HOW_FIXED[tlSeal.SEAL_KIND]);
299
+ }
300
+
301
+ // The canonical UNSIGNED attestation field set (dataset and parcel share it). The shipped canonical
302
+ // serializers emit EXACTLY these keys and DROP anything else, so an unknown key would otherwise ride
303
+ // along unbound by the digest — reject it instead (defense on top of the shipped validator, which is
304
+ // still reused verbatim inside the serializer).
305
+ const ATTESTATION_FIELDS = Object.freeze([
306
+ "kind",
307
+ "schemaVersion",
308
+ "note",
309
+ "root",
310
+ "fileCount",
311
+ "manifestDigest",
312
+ "signed",
313
+ "signature",
314
+ ]);
315
+
316
+ function _attestationDigest(artifact, serialize, kind, reason) {
317
+ for (const k of Object.keys(artifact)) {
318
+ if (!ATTESTATION_FIELDS.includes(k)) {
319
+ return _no(
320
+ reason,
321
+ `attestation has unknown field ${JSON.stringify(k)} (the canonical bytes would not bind it)`
322
+ );
323
+ }
324
+ }
325
+ let canonical;
326
+ try {
327
+ canonical = serialize(artifact); // validates first (the shipped validator, reused verbatim)
328
+ } catch (e) {
329
+ return _no(reason, _detail(e));
330
+ }
331
+ let digest;
332
+ try {
333
+ digest = "0x" + coreTimestamp.sha256Hex(canonical);
334
+ } catch (e) {
335
+ return _no(reason, _detail(e)); // unreachable for a string; kept total
336
+ }
337
+ return _ok(digest, kind, HOW_FIXED[kind]);
338
+ }
339
+
340
+ /**
341
+ * Extract the ONE canonical 32-byte digest a chain record binds for `artifact` — a caller-supplied
342
+ * PARSED object (this core does no I/O; read + JSON.parse the file yourself, like every cli/core/*).
343
+ * Strict dispatch over the CLOSED kind table; each leg reuses the shipped validator VERBATIM before
344
+ * extracting. TOTAL: hostile input yields a named { ok:false, reason, detail? }, never a throw.
345
+ *
346
+ * @param {any} artifact a parsed sealed-product artifact (see the module-header table)
347
+ * @returns {{ ok:true, digest:string, kind:string, how:string } |
348
+ * { ok:false, reason:string, detail?:string }}
349
+ * digest is a canonical LOWERCASE 0x-bytes32; `how` the human-readable derivation rule.
350
+ */
351
+ function artifactDigest(artifact) {
352
+ try {
353
+ if (!isPlainObject(artifact)) {
354
+ return _no(REASONS.NOT_AN_OBJECT, "artifact must be a parsed JSON object");
355
+ }
356
+ const kind = artifact.kind;
357
+ if (kind === undefined) {
358
+ // The bare journal tree head { size, root } is the ONE table entry that carries no kind of
359
+ // its own (it is cli/journal-log.js's head shape verbatim). Anything else without a kind is
360
+ // not dispatchable — a named reject, never a guess.
361
+ if ("size" in artifact || "root" in artifact) {
362
+ return _journalHeadDigest(artifact, false);
363
+ }
364
+ return _no(REASONS.UNKNOWN_KIND, "artifact carries no `kind` and is not a { size, root } journal tree head");
365
+ }
366
+ if (typeof kind !== "string") {
367
+ return _no(REASONS.UNKNOWN_KIND, "artifact `kind` must be a string");
368
+ }
369
+ switch (kind) {
370
+ case evidence.SEAL_KIND:
371
+ return _evidenceDigest(artifact);
372
+ case agent.PACKET_KIND:
373
+ return _agentDigest(artifact);
374
+ case JOURNAL_TREE_HEAD_KIND:
375
+ return _journalHeadDigest(artifact, true);
376
+ case tlSeal.SEAL_KIND:
377
+ return _trustledgerDigest(artifact);
378
+ case dataset.ATTESTATION_KIND:
379
+ return _attestationDigest(
380
+ artifact,
381
+ dataset.serializeAttestation,
382
+ dataset.ATTESTATION_KIND,
383
+ REASONS.DATASET_ATTESTATION_INVALID
384
+ );
385
+ case parcel.PARCEL_ATTESTATION_KIND:
386
+ return _attestationDigest(
387
+ artifact,
388
+ parcel.serializeParcelAttestation,
389
+ parcel.PARCEL_ATTESTATION_KIND,
390
+ REASONS.PARCEL_ATTESTATION_INVALID
391
+ );
392
+ default:
393
+ return _no(
394
+ REASONS.UNKNOWN_KIND,
395
+ `unknown artifact kind ${JSON.stringify(kind)} (the closed table: ${ARTIFACT_KINDS.join(", ")})`
396
+ );
397
+ }
398
+ } catch (e) {
399
+ // The legs are individually total; this is the fail-CLOSED belt for truly hostile shapes.
400
+ return _no(REASONS.NOT_AN_OBJECT, _detail(e));
401
+ }
402
+ }
403
+
404
+ // ---------------------------------------------------------------------------------------------------
405
+ // Chain-facts validation, shared by build (reason bad-chain) and receipt validation (bad-receipt).
406
+ // STRICT FORM ONLY: this pure core has no network, so it pins types/canonical-case/ranges — whether
407
+ // the VALUES are true on chain is exactly what T-70.2's `--rpc` read-back re-checks.
408
+ // ---------------------------------------------------------------------------------------------------
409
+
410
+ const CHAIN_FIELDS = Object.freeze([
411
+ "authorBound",
412
+ "blockNumber",
413
+ "blockTime",
414
+ "chainId",
415
+ "contract",
416
+ "contributor",
417
+ "txHash",
418
+ ]);
419
+
420
+ function _checkChain(chain) {
421
+ if (!isPlainObject(chain)) {
422
+ return { ok: false, field: "chain", detail: "chain must be an object of the seven recorded chain facts" };
423
+ }
424
+ for (const k of Object.keys(chain)) {
425
+ if (!CHAIN_FIELDS.includes(k)) {
426
+ return { ok: false, field: `chain.${k}`, detail: `chain has unknown field: ${JSON.stringify(k)}` };
427
+ }
428
+ }
429
+ for (const k of CHAIN_FIELDS) {
430
+ if (!(k in chain)) {
431
+ return { ok: false, field: `chain.${k}`, detail: `chain is missing required field: ${JSON.stringify(k)}` };
432
+ }
433
+ }
434
+ if (typeof chain.authorBound !== "boolean") {
435
+ return { ok: false, field: "chain.authorBound", detail: "authorBound must be a boolean" };
436
+ }
437
+ for (const k of ["blockNumber", "blockTime"]) {
438
+ if (!Number.isSafeInteger(chain[k]) || chain[k] < 0) {
439
+ return { ok: false, field: `chain.${k}`, detail: `${k} must be a non-negative integer, got: ${String(chain[k])}` };
440
+ }
441
+ }
442
+ if (!Number.isSafeInteger(chain.chainId) || chain.chainId < 1) {
443
+ return { ok: false, field: "chain.chainId", detail: `chainId must be a positive integer, got: ${String(chain.chainId)}` };
444
+ }
445
+ for (const k of ["contract", "contributor"]) {
446
+ if (typeof chain[k] !== "string" || !ADDRESS_LC_RE.test(chain[k])) {
447
+ return {
448
+ ok: false,
449
+ field: `chain.${k}`,
450
+ detail: `${k} must be a LOWERCASE 0x-address (canonical case), got: ${String(chain[k])}`,
451
+ };
452
+ }
453
+ }
454
+ if (typeof chain.txHash !== "string" || !HEX32_LC_RE.test(chain.txHash)) {
455
+ return {
456
+ ok: false,
457
+ field: "chain.txHash",
458
+ detail: `txHash must be a LOWERCASE 0x-bytes32 hex string, got: ${String(chain.txHash)}`,
459
+ };
460
+ }
461
+ return { ok: true };
462
+ }
463
+
464
+ function _checkLabel(label) {
465
+ if (typeof label !== "string" || label.length === 0 || label.length > 200 || CONTROL_CHAR_RE.test(label)) {
466
+ return {
467
+ ok: false,
468
+ detail: "artifactLabel, when present, must be a 1..200-char string with no control characters",
469
+ };
470
+ }
471
+ return { ok: true };
472
+ }
473
+
474
+ /** A canonical chain-facts copy in sorted key order (build re-emits, never aliases caller state). */
475
+ function _canonicalChain(chain) {
476
+ return {
477
+ authorBound: chain.authorBound,
478
+ blockNumber: chain.blockNumber,
479
+ blockTime: chain.blockTime,
480
+ chainId: chain.chainId,
481
+ contract: chain.contract,
482
+ contributor: chain.contributor,
483
+ txHash: chain.txHash,
484
+ };
485
+ }
486
+
487
+ // ---------------------------------------------------------------------------------------------------
488
+ // (b) buildAnchoredReceipt(params) — the canonical, versioned, sorted-key receipt container.
489
+ // ---------------------------------------------------------------------------------------------------
490
+
491
+ /**
492
+ * Build the anchored-receipt container from a digest extraction ({ digest, kind, how } — normally
493
+ * artifactDigest's own ok-result) plus the chain facts of the registry record that anchored it.
494
+ * The result's keys are in SORTED order at every level, so `JSON.stringify(receipt) + "\n"` IS the
495
+ * canonical byte serialization — no separate serializer to drift. TOTAL: named rejects, no throws.
496
+ *
497
+ * @param {object} params
498
+ * @param {string} params.digest the anchored digest (LOWERCASE 0x-bytes32)
499
+ * @param {string} params.kind the artifact's kind (one of the closed table)
500
+ * @param {string} params.how the derivation rule artifactDigest returned for that kind
501
+ * @param {string} [params.artifactLabel] optional presentation label (e.g. a file name) — NOT
502
+ * digest-bound; edits to it are not tamper-evident
503
+ * @param {object} params.chain { authorBound, blockNumber, blockTime, chainId, contract,
504
+ * contributor, txHash } — strict form, see _checkChain
505
+ * @returns {{ ok:true, receipt:object } | { ok:false, reason:string, field?:string, detail?:string }}
506
+ */
507
+ function buildAnchoredReceipt(params) {
508
+ try {
509
+ if (!isPlainObject(params)) {
510
+ return _no(REASONS.BAD_ARGS, "buildAnchoredReceipt requires { digest, kind, how, chain }");
511
+ }
512
+ if (typeof params.digest !== "string" || !HEX32_LC_RE.test(params.digest)) {
513
+ return _no(REASONS.BAD_DIGEST, `digest must be a LOWERCASE 0x-bytes32 hex string, got: ${String(params.digest)}`);
514
+ }
515
+ if (typeof params.kind !== "string" || !ARTIFACT_KINDS.includes(params.kind)) {
516
+ return _no(
517
+ REASONS.UNKNOWN_KIND,
518
+ `unknown artifact kind ${JSON.stringify(params.kind)} (the closed table: ${ARTIFACT_KINDS.join(", ")})`
519
+ );
520
+ }
521
+ if (!_howValidFor(params.kind, params.how)) {
522
+ return _no(
523
+ REASONS.BAD_HOW,
524
+ `\`how\` must be the documented derivation rule for ${params.kind} (pass artifactDigest's own \`how\` through)`
525
+ );
526
+ }
527
+ if (params.artifactLabel !== undefined) {
528
+ const l = _checkLabel(params.artifactLabel);
529
+ if (!l.ok) return _no(REASONS.BAD_LABEL, l.detail);
530
+ }
531
+ const c = _checkChain(params.chain);
532
+ if (!c.ok) return { ok: false, reason: REASONS.BAD_CHAIN, field: c.field, detail: c.detail };
533
+
534
+ // Sorted-key assembly (artifactKind < artifactLabel < chain < digest < how < kind < note).
535
+ const receipt = {};
536
+ receipt.artifactKind = params.kind;
537
+ if (params.artifactLabel !== undefined) receipt.artifactLabel = params.artifactLabel;
538
+ receipt.chain = _canonicalChain(params.chain);
539
+ receipt.digest = params.digest;
540
+ receipt.how = params.how;
541
+ receipt.kind = ANCHORED_RECEIPT_KIND;
542
+ receipt.note = ANCHOR_TRUST_NOTE;
543
+ return { ok: true, receipt };
544
+ } catch (e) {
545
+ return _no(REASONS.BAD_ARGS, _detail(e));
546
+ }
547
+ }
548
+
549
+ // ---------------------------------------------------------------------------------------------------
550
+ // Receipt validation (strict; every deviation a named `bad-receipt` with the offending field).
551
+ // ---------------------------------------------------------------------------------------------------
552
+
553
+ const RECEIPT_FIELDS = Object.freeze(["artifactKind", "artifactLabel", "chain", "digest", "how", "kind", "note"]);
554
+ const RECEIPT_REQUIRED = Object.freeze(["artifactKind", "chain", "digest", "how", "kind", "note"]);
555
+
556
+ function _badReceipt(field, detail) {
557
+ return { ok: false, reason: REASONS.BAD_RECEIPT, field, detail };
558
+ }
559
+
560
+ function _validateReceipt(receipt) {
561
+ if (!isPlainObject(receipt)) {
562
+ return _badReceipt("receipt", "receipt must be a parsed JSON object");
563
+ }
564
+ for (const k of Object.keys(receipt)) {
565
+ if (!RECEIPT_FIELDS.includes(k)) {
566
+ return _badReceipt(k, `receipt has unknown field: ${JSON.stringify(k)}`);
567
+ }
568
+ }
569
+ for (const k of RECEIPT_REQUIRED) {
570
+ if (!(k in receipt)) {
571
+ return _badReceipt(k, `receipt is missing required field: ${JSON.stringify(k)}`);
572
+ }
573
+ }
574
+ if (receipt.kind !== ANCHORED_RECEIPT_KIND) {
575
+ return _badReceipt(
576
+ "kind",
577
+ `not an anchored receipt this build understands (kind: ${JSON.stringify(receipt.kind)}; expected ${JSON.stringify(ANCHORED_RECEIPT_KIND)})`
578
+ );
579
+ }
580
+ if (receipt.note !== ANCHOR_TRUST_NOTE) {
581
+ return _badReceipt("note", "receipt `note` must be the standing trust note VERBATIM (the caveat must not drift)");
582
+ }
583
+ if (typeof receipt.digest !== "string" || !HEX32_LC_RE.test(receipt.digest)) {
584
+ return _badReceipt("digest", `receipt digest must be a LOWERCASE 0x-bytes32 hex string, got: ${String(receipt.digest)}`);
585
+ }
586
+ if (typeof receipt.artifactKind !== "string" || !ARTIFACT_KINDS.includes(receipt.artifactKind)) {
587
+ return _badReceipt(
588
+ "artifactKind",
589
+ `receipt artifactKind ${JSON.stringify(receipt.artifactKind)} is not in the closed table (${ARTIFACT_KINDS.join(", ")})`
590
+ );
591
+ }
592
+ if (!_howValidFor(receipt.artifactKind, receipt.how)) {
593
+ return _badReceipt("how", `receipt \`how\` is not the documented derivation rule for ${receipt.artifactKind}`);
594
+ }
595
+ if (receipt.artifactLabel !== undefined) {
596
+ const l = _checkLabel(receipt.artifactLabel);
597
+ if (!l.ok) return _badReceipt("artifactLabel", l.detail);
598
+ }
599
+ const c = _checkChain(receipt.chain);
600
+ if (!c.ok) return _badReceipt(c.field, c.detail);
601
+ return { ok: true };
602
+ }
603
+
604
+ // ---------------------------------------------------------------------------------------------------
605
+ // (c) verifyAnchoredReceipt({ receipt, artifact }) — the pure, offline binding check.
606
+ // ---------------------------------------------------------------------------------------------------
607
+
608
+ /**
609
+ * Verify that `receipt` is a well-formed anchored receipt AND that it binds EXACTLY the supplied
610
+ * `artifact`: the receipt is validated strictly, the digest is RECOMPUTED from the artifact via the
611
+ * SAME closed table (never trusted from either side), and the full { kind, digest, how } triple must
612
+ * match. NEVER consults a network — the receipt's chain facts are returned for the caller (T-70.2's
613
+ * `--rpc` mode re-checks them against the chain). TOTAL: named rejects, no throws.
614
+ *
615
+ * @param {object} args { receipt, artifact } — both caller-supplied PARSED objects
616
+ * @returns {{ ok:true, digest:string, chain:object } |
617
+ * { ok:false, reason:string, field?:string, detail?:string }}
618
+ */
619
+ function verifyAnchoredReceipt(args) {
620
+ try {
621
+ if (!isPlainObject(args)) {
622
+ return _no(REASONS.BAD_ARGS, "verifyAnchoredReceipt requires { receipt, artifact }");
623
+ }
624
+ const r = _validateReceipt(args.receipt);
625
+ if (!r.ok) return r;
626
+ const d = artifactDigest(args.artifact);
627
+ if (!d.ok) return d; // the artifact's OWN named validation reject, propagated verbatim
628
+ const receipt = args.receipt;
629
+ if (d.kind !== receipt.artifactKind) {
630
+ return _no(
631
+ REASONS.KIND_MISMATCH,
632
+ `receipt anchors a ${receipt.artifactKind} but the supplied artifact is a ${d.kind}`
633
+ );
634
+ }
635
+ if (d.digest !== receipt.digest) {
636
+ return _no(
637
+ REASONS.DIGEST_MISMATCH,
638
+ `recomputed digest ${d.digest} != receipt digest ${receipt.digest} — this receipt does not bind this artifact`
639
+ );
640
+ }
641
+ if (d.how !== receipt.how) {
642
+ // Same kind + same digest but a different derivation rule — for the journal leg this is
643
+ // exactly an edited head `size` (bound into `how` because it is not derivable from the root).
644
+ return _no(REASONS.HOW_MISMATCH, `recomputed derivation rule != receipt \`how\` (recomputed: ${d.how})`);
645
+ }
646
+ return { ok: true, digest: d.digest, chain: _canonicalChain(receipt.chain) };
647
+ } catch (e) {
648
+ return _no(REASONS.BAD_ARGS, _detail(e));
649
+ }
650
+ }
651
+
652
+ module.exports = {
653
+ // Container framing + the closed table.
654
+ ANCHORED_RECEIPT_KIND,
655
+ ANCHOR_TRUST_NOTE,
656
+ ARTIFACT_KINDS,
657
+ JOURNAL_TREE_HEAD_KIND,
658
+ REASONS,
659
+ // The three core operations.
660
+ artifactDigest,
661
+ buildAnchoredReceipt,
662
+ verifyAnchoredReceipt,
663
+ };