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,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
|
+
};
|