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,448 @@
1
+ "use strict";
2
+
3
+ // cli/core/agent-commit.js — the PURE commit-claim core (T-69.1, EPIC-69).
4
+ //
5
+ // WHAT THIS IS
6
+ // The canonical "this session claims commit X of tree-root R" payload, plus its strict
7
+ // verifier, layered on the T-68.1 agent-session core. A commit claim is an ordinary
8
+ // canonical session event (`type: "note"`) whose payload is ONE canonical JSON string:
9
+ //
10
+ // {"commit":"<40-hex oid>","gitRoot":"0x<64-hex>","kind":"vh-agent-commit-claim@1"[,"scope":"<posix hint>"]}
11
+ //
12
+ // Keys SORTED, no whitespace, lowercase hex — so the claim has exactly ONE byte
13
+ // representation and the payload commitment (agent-session `payloadHash`) is reproducible
14
+ // by anyone from the facts alone. Because the claim rides the T-68.1 redaction-safe leaf,
15
+ // sealing a session that contains it and then redacting ANY OTHER event leaves the head
16
+ // UNCHANGED and the claim still disclosed, findable and verifiable.
17
+ //
18
+ // WHAT THE FIELDS MEAN (all CALLER-SUPPLIED — this core never derives them)
19
+ // - `commit`: the git commit oid (40-hex lowercase; what `cli/git.js resolveCommit` returns).
20
+ // - `gitRoot`: the 0x-bytes32 tracked-set root (what `cli/hash.js hashGit` returns).
21
+ // - `scope`: OPTIONAL repo-relative POSIX path hint (which subtree the session touched).
22
+ // `scope` is an UNVERIFIED hint: verifyCommitClaim checks `commit` and `gitRoot` only.
23
+ //
24
+ // TRUST BOUNDARY (honest — carried into docs by T-69.3)
25
+ // The core proves the sealed log CONTAINS an unaltered claim to exactly (commit, gitRoot).
26
+ // It does NOT prove the session's events PRODUCED that commit (containment, not causation),
27
+ // does not touch git, a clock, or the filesystem, and trusts the caller for every fact:
28
+ // re-deriving the facts from a real clone is the T-69.2 CLI's job.
29
+ //
30
+ // PURITY (a hard acceptance criterion, statically guarded by the test)
31
+ // No fs / git / child_process / http / https / net / dns, no process.env, no clock, no
32
+ // randomness, no signing material, NO new crypto and NO new dependency. The ONLY require is
33
+ // `./agent-session` (itself statically guarded pure), reused NOT forked: event validation and
34
+ // the payload commitment come from that core verbatim. Every exported function is TOTAL:
35
+ // hostile input yields a named `{ ok:false, reason }` verdict, never an exception.
36
+
37
+ const {
38
+ validateEvent,
39
+ validateSession,
40
+ payloadHash: sessionPayloadHash,
41
+ } = require("./agent-session");
42
+
43
+ // ---------------------------------------------------------------------------------------------------
44
+ // Canonical schema constants.
45
+ // ---------------------------------------------------------------------------------------------------
46
+
47
+ // The versioned kind tag bound INSIDE the payload bytes. Any schema change bumps the version;
48
+ // an unknown kind/version is a NAMED reject (CLAIM_BAD_KIND), never silently accepted.
49
+ const CLAIM_KIND = "vh-agent-commit-claim@1";
50
+
51
+ // A commit claim is always a canonical `note` event — the self-describing payload plus the
52
+ // closed T-68.1 type set make the claim unambiguous without extending that schema.
53
+ const CLAIM_EVENT_TYPE = "note";
54
+
55
+ // Default `actor` for a built claim event when the caller does not name one.
56
+ const DEFAULT_ACTOR = "agent";
57
+
58
+ // The exhaustive field set of a canonical claim object (payload JSON). Sorted — this IS the
59
+ // serialization order. Any other key is CLAIM_UNKNOWN_FIELD.
60
+ const CLAIM_FIELDS = Object.freeze(["commit", "gitRoot", "kind", "scope"]);
61
+
62
+ // Size caps that keep parsing total and O(cap) on hostile input. A canonical claim is ~150
63
+ // bytes + the scope hint; JSON escaping can inflate a 4096-char scope at most 6x, so 32 KiB
64
+ // leaves generous headroom while rejecting megabyte "payloads" in O(1) BEFORE JSON.parse.
65
+ const MAX_SCOPE_LENGTH = 4096;
66
+ const MAX_PAYLOAD_LENGTH = 32768;
67
+
68
+ // Stable, named reason codes — the verdict contract callers (and the T-69.2 CLI) rely on.
69
+ // The three verifyCommitClaim verdicts are the lowercase names the backlog fixes; note that
70
+ // findCommitClaims/buildCommitClaimEvent may also pass through agent-session REASONS codes
71
+ // (EVENT_*/SESSION_*) verbatim when the EVENT layer (not the claim) is what is malformed.
72
+ const REASONS = Object.freeze({
73
+ CLAIM_NOT_OBJECT: "CLAIM_NOT_OBJECT",
74
+ CLAIM_UNKNOWN_FIELD: "CLAIM_UNKNOWN_FIELD",
75
+ CLAIM_BAD_KIND: "CLAIM_BAD_KIND",
76
+ CLAIM_BAD_COMMIT: "CLAIM_BAD_COMMIT",
77
+ CLAIM_BAD_GIT_ROOT: "CLAIM_BAD_GIT_ROOT",
78
+ CLAIM_BAD_SCOPE: "CLAIM_BAD_SCOPE",
79
+ CLAIM_REDACTED: "CLAIM_REDACTED",
80
+ CLAIM_BAD_EVENT_TYPE: "CLAIM_BAD_EVENT_TYPE",
81
+ PAYLOAD_NOT_STRING: "PAYLOAD_NOT_STRING",
82
+ PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE",
83
+ PAYLOAD_NOT_JSON: "PAYLOAD_NOT_JSON",
84
+ PAYLOAD_NOT_CANONICAL: "PAYLOAD_NOT_CANONICAL",
85
+ VERIFY_BAD_INPUT: "VERIFY_BAD_INPUT",
86
+ VERIFY_BAD_EXPECTED: "VERIFY_BAD_EXPECTED",
87
+ OID_MISMATCH: "oid-mismatch",
88
+ ROOT_MISMATCH: "root-mismatch",
89
+ BAD_CLAIM: "bad-claim",
90
+ HOSTILE_INPUT: "HOSTILE_INPUT",
91
+ });
92
+
93
+ // STRICT lowercase — `git rev-parse` and `hashGit` both emit lowercase, and accepting a
94
+ // case-variant would mint a second byte representation of the "same" claim.
95
+ const COMMIT_RE = /^[0-9a-f]{40}$/;
96
+ const GIT_ROOT_RE = /^0x[0-9a-f]{64}$/;
97
+
98
+ // A "plain" object: prototype is Object.prototype or null (same discipline as agent-session —
99
+ // what we serialize is exactly the JSON-shaped data the caller could write and read back).
100
+ function _isPlainObject(v) {
101
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
102
+ const proto = Object.getPrototypeOf(v);
103
+ return proto === Object.prototype || proto === null;
104
+ }
105
+
106
+ // A valid repo-relative POSIX scope hint: non-empty, capped, forward slashes only, no
107
+ // control characters, no empty/"."/".." segments (so no absolute paths, no traversal, no
108
+ // trailing slash), and UTF-8-encodable (a lone UTF-16 surrogate — checked via the REUSED
109
+ // agent-session payloadHash, which returns null exactly for unencodable strings).
110
+ function _isValidScope(s) {
111
+ if (typeof s !== "string") return false;
112
+ if (s.length === 0 || s.length > MAX_SCOPE_LENGTH) return false;
113
+ if (s.includes("\\")) return false;
114
+ // eslint-disable-next-line no-control-regex
115
+ if (/[\u0000-\u001f\u007f]/.test(s)) return false;
116
+ if (sessionPayloadHash(s) === null) return false;
117
+ for (const seg of s.split("/")) {
118
+ if (seg === "" || seg === "." || seg === "..") return false;
119
+ }
120
+ return true;
121
+ }
122
+
123
+ // Validate the claim FIELDS of a plain object (builder input or parsed payload alike).
124
+ // Returns { ok:true, claim } with the canonical claim object, or the named reject.
125
+ // `kind` is optional on input (a parsed claim carries it; a fresh build may omit it) but,
126
+ // when present, MUST be exactly CLAIM_KIND — that one check covers both "unknown kind"
127
+ // and "unknown version" since the version lives inside the kind string.
128
+ function _validateClaimFields(input, extraAllowedKeys) {
129
+ if (!_isPlainObject(input)) return { ok: false, reason: REASONS.CLAIM_NOT_OBJECT };
130
+ if ("kind" in input && input.kind !== CLAIM_KIND) {
131
+ return { ok: false, reason: REASONS.CLAIM_BAD_KIND, field: "kind" };
132
+ }
133
+ for (const k of Object.keys(input)) {
134
+ if (!CLAIM_FIELDS.includes(k) && !extraAllowedKeys.includes(k)) {
135
+ return { ok: false, reason: REASONS.CLAIM_UNKNOWN_FIELD, field: k };
136
+ }
137
+ }
138
+ if (typeof input.commit !== "string" || !COMMIT_RE.test(input.commit)) {
139
+ return { ok: false, reason: REASONS.CLAIM_BAD_COMMIT, field: "commit" };
140
+ }
141
+ if (typeof input.gitRoot !== "string" || !GIT_ROOT_RE.test(input.gitRoot)) {
142
+ return { ok: false, reason: REASONS.CLAIM_BAD_GIT_ROOT, field: "gitRoot" };
143
+ }
144
+ if ("scope" in input && !_isValidScope(input.scope)) {
145
+ return { ok: false, reason: REASONS.CLAIM_BAD_SCOPE, field: "scope" };
146
+ }
147
+ const claim = { kind: CLAIM_KIND, commit: input.commit, gitRoot: input.gitRoot };
148
+ if ("scope" in input) claim.scope = input.scope;
149
+ return { ok: true, claim };
150
+ }
151
+
152
+ // The ONE byte representation of a claim: keys in sorted (CLAIM_FIELDS) order, JSON string
153
+ // escaping, no whitespace. Assumes a validated claim object.
154
+ function _serializeClaim(claim) {
155
+ const parts = [];
156
+ for (const k of CLAIM_FIELDS) {
157
+ if (k in claim) parts.push(JSON.stringify(k) + ":" + JSON.stringify(claim[k]));
158
+ }
159
+ return "{" + parts.join(",") + "}";
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------------------------------
163
+ // (a) commitClaimPayload — build the canonical claim string.
164
+ // ---------------------------------------------------------------------------------------------------
165
+
166
+ /**
167
+ * Build the canonical commit-claim payload STRING from caller-supplied git facts.
168
+ * DETERMINISTIC: the same facts yield BYTE-IDENTICAL output regardless of the input object's
169
+ * key insertion order, on every call. TOTAL: every failure is a named reject; never throws.
170
+ *
171
+ * @param {{ commit: string, gitRoot: string, scope?: string, kind?: string }} input
172
+ * `commit` 40-hex lowercase oid; `gitRoot` 0x-bytes32 lowercase hex (the hashGit root);
173
+ * `scope` optional repo-relative POSIX hint; `kind` optional but must equal CLAIM_KIND
174
+ * when present (so a parsed claim round-trips straight back through this builder).
175
+ * @returns {{ ok: true, payload: string, claim: object } | { ok: false, reason: string, field?: string }}
176
+ * On ok: `payload` is the canonical string; `claim` the canonical claim object
177
+ * ({ kind, commit, gitRoot, scope? }) — parseCommitClaim(payload).claim deep-equals it.
178
+ */
179
+ function commitClaimPayload(input) {
180
+ try {
181
+ const v = _validateClaimFields(input, []);
182
+ if (!v.ok) return v;
183
+ return { ok: true, payload: _serializeClaim(v.claim), claim: v.claim };
184
+ } catch (_) {
185
+ return { ok: false, reason: REASONS.HOSTILE_INPUT };
186
+ }
187
+ }
188
+
189
+ // ---------------------------------------------------------------------------------------------------
190
+ // (b) parseCommitClaim — the strict inverse.
191
+ // ---------------------------------------------------------------------------------------------------
192
+
193
+ /**
194
+ * Parse and STRICTLY validate a claim payload string. Accepts ONLY the canonical bytes:
195
+ * unknown kind/version, extra/missing/malformed fields, oversize input, non-JSON, and any
196
+ * NON-CANONICAL byte representation (reordered keys, whitespace, uppercase hex, duplicate
197
+ * keys, escape-sequence variants) are each a NAMED reject. NEVER throws.
198
+ *
199
+ * Round-trip invariants (tested):
200
+ * parseCommitClaim(commitClaimPayload(x).payload).claim deep-equals commitClaimPayload(x).claim
201
+ * commitClaimPayload(parseCommitClaim(s).claim).payload === s
202
+ *
203
+ * @param {string} payloadString
204
+ * @returns {{ ok: true, claim: { kind: string, commit: string, gitRoot: string, scope?: string } }
205
+ * | { ok: false, reason: string, field?: string }}
206
+ */
207
+ function parseCommitClaim(payloadString) {
208
+ try {
209
+ if (typeof payloadString !== "string") {
210
+ return { ok: false, reason: REASONS.PAYLOAD_NOT_STRING };
211
+ }
212
+ if (payloadString.length > MAX_PAYLOAD_LENGTH) {
213
+ return { ok: false, reason: REASONS.PAYLOAD_TOO_LARGE };
214
+ }
215
+ let parsed;
216
+ try {
217
+ parsed = JSON.parse(payloadString);
218
+ } catch (_) {
219
+ return { ok: false, reason: REASONS.PAYLOAD_NOT_JSON };
220
+ }
221
+ // JSON.parse yields plain objects only ("__proto__" arrives as an ordinary own key and is
222
+ // caught by the exhaustive-field check); `kind` here is REQUIRED, not optional.
223
+ if (!_isPlainObject(parsed)) return { ok: false, reason: REASONS.CLAIM_NOT_OBJECT };
224
+ if (parsed.kind !== CLAIM_KIND) {
225
+ return { ok: false, reason: REASONS.CLAIM_BAD_KIND, field: "kind" };
226
+ }
227
+ const v = _validateClaimFields(parsed, []);
228
+ if (!v.ok) return v;
229
+ // Canonical-bytes check: the ONLY accepted representation is the one this core emits.
230
+ if (_serializeClaim(v.claim) !== payloadString) {
231
+ return { ok: false, reason: REASONS.PAYLOAD_NOT_CANONICAL };
232
+ }
233
+ return { ok: true, claim: v.claim };
234
+ } catch (_) {
235
+ return { ok: false, reason: REASONS.HOSTILE_INPUT };
236
+ }
237
+ }
238
+
239
+ // ---------------------------------------------------------------------------------------------------
240
+ // (c) buildCommitClaimEvent — the canonical T-68.1 event carrying the claim.
241
+ // ---------------------------------------------------------------------------------------------------
242
+
243
+ /**
244
+ * Build the canonical claim EVENT: a full (disclosed) `note` event whose payload is the
245
+ * canonical claim string, ready to append to a session log and seal via the T-68.1 core.
246
+ * The returned event is asserted through agent-session `validateEvent` (REUSED verbatim)
247
+ * before it is handed back, so what this returns is BY CONSTRUCTION a canonical event —
248
+ * bad seq/ts/actor surface as that core's own named rejects (EVENT_BAD_SEQ, ...), and the
249
+ * event's leaf enjoys every T-68.1 guarantee (in particular: redacting any OTHER event in
250
+ * the session leaves the head unchanged and this claim disclosed). Never throws.
251
+ *
252
+ * @param {{ seq: number, ts: string, actor?: string, commit: string, gitRoot: string, scope?: string }} input
253
+ * `seq`/`ts` as in the T-68.1 schema (`ts` SELF-ASSERTED, untrusted); `actor`
254
+ * defaults to DEFAULT_ACTOR; git facts as in commitClaimPayload.
255
+ * @returns {{ ok: true, event: object, payload: string, claim: object }
256
+ * | { ok: false, reason: string, field?: string }}
257
+ */
258
+ function buildCommitClaimEvent(input) {
259
+ try {
260
+ const v = _validateClaimFields(input, ["seq", "ts", "actor"]);
261
+ if (!v.ok) return v;
262
+ const event = {
263
+ seq: input.seq,
264
+ ts: input.ts,
265
+ actor: "actor" in input ? input.actor : DEFAULT_ACTOR,
266
+ type: CLAIM_EVENT_TYPE,
267
+ payload: _serializeClaim(v.claim),
268
+ };
269
+ const ev = validateEvent(event);
270
+ if (!ev.ok) return { ok: false, reason: ev.reason, field: ev.field };
271
+ return { ok: true, event, payload: event.payload, claim: v.claim };
272
+ } catch (_) {
273
+ return { ok: false, reason: REASONS.HOSTILE_INPUT };
274
+ }
275
+ }
276
+
277
+ // ---------------------------------------------------------------------------------------------------
278
+ // (d) findCommitClaims — every DISCLOSED claim in a session.
279
+ // ---------------------------------------------------------------------------------------------------
280
+
281
+ /**
282
+ * Scan a VALID session (agent-session `validateSession`, reused verbatim — an invalid session
283
+ * is that core's own named, located reject) for every DISCLOSED commit claim: a `note` event
284
+ * whose full payload parses as a canonical claim. A REDACTED claim event is by definition not
285
+ * disclosable — its payload bytes are withheld, only the commitment remains — so it is
286
+ * deliberately NOT returned here (the holder re-discloses by including the full event).
287
+ * Non-claim notes and unparseable payloads are simply skipped, never errors. Never throws.
288
+ *
289
+ * @param {object[]} events the session (full or partially redacted).
290
+ * @returns {{ ok: true, claims: { index: number, seq: number, claim: object, payload: string, event: object }[] }
291
+ * | { ok: false, reason: string, index?: number, field?: string }}
292
+ * `index` === `seq` === the event's tree position (what proveEvent/verifyEvent bind);
293
+ * `event` is a deep copy — the result never aliases caller-mutable state.
294
+ */
295
+ function findCommitClaims(events) {
296
+ try {
297
+ const s = validateSession(events);
298
+ if (!s.ok) return s;
299
+ const claims = [];
300
+ for (let i = 0; i < events.length; i++) {
301
+ const e = events[i];
302
+ if (e.type !== CLAIM_EVENT_TYPE) continue;
303
+ if (typeof e.payload !== "string") continue; // redacted or absent: not disclosed
304
+ const p = parseCommitClaim(e.payload);
305
+ if (!p.ok) continue;
306
+ claims.push({
307
+ index: i,
308
+ seq: e.seq,
309
+ claim: p.claim,
310
+ payload: e.payload,
311
+ event: JSON.parse(JSON.stringify(e)),
312
+ });
313
+ }
314
+ return { ok: true, claims };
315
+ } catch (_) {
316
+ return { ok: false, reason: REASONS.HOSTILE_INPUT };
317
+ }
318
+ }
319
+
320
+ // ---------------------------------------------------------------------------------------------------
321
+ // (e) verifyCommitClaim — the strict verifier.
322
+ // ---------------------------------------------------------------------------------------------------
323
+
324
+ /**
325
+ * Verify ONE disclosed claim against the EXPECTED facts the auditor re-derived themselves
326
+ * (oid from their own clone, root from their own hashGit run — this core never derives them).
327
+ *
328
+ * Give EXACTLY ONE of:
329
+ * - `event`: a canonical claim event (validated via agent-session `validateEvent` verbatim;
330
+ * a redacted event is `bad-claim`/CLAIM_REDACTED — withheld bytes cannot be verified;
331
+ * a non-`note` type is `bad-claim`/CLAIM_BAD_EVENT_TYPE), or
332
+ * - `payloadString`: the raw canonical claim string.
333
+ *
334
+ * Verdicts (never a throw):
335
+ * { ok:true, claim, seq? } — facts match exactly;
336
+ * { ok:false, reason:"oid-mismatch", field:"commit", claimed, expected }
337
+ * { ok:false, reason:"root-mismatch", field:"gitRoot", claimed, expected }
338
+ * { ok:false, reason:"bad-claim", detail, field? } — the claim itself is invalid
339
+ * (detail = the underlying code);
340
+ * { ok:false, reason:"VERIFY_BAD_INPUT"|"VERIFY_BAD_EXPECTED", field? } — malformed CALL.
341
+ *
342
+ * NOTE `scope` is a hint and is NOT verified — only `commit` and `gitRoot` are facts.
343
+ * Mismatches are reported oid-first; both fields may of course differ.
344
+ *
345
+ * @param {{ event?: object, payloadString?: string, expected: { commit: string, gitRoot: string } }} args
346
+ * @returns {{ ok: true, claim: object, seq?: number } | { ok: false, reason: string, [k: string]: any }}
347
+ */
348
+ function verifyCommitClaim(args) {
349
+ try {
350
+ if (!_isPlainObject(args)) return { ok: false, reason: REASONS.VERIFY_BAD_INPUT };
351
+ for (const k of Object.keys(args)) {
352
+ if (!["event", "payloadString", "expected"].includes(k)) {
353
+ return { ok: false, reason: REASONS.VERIFY_BAD_INPUT, field: k };
354
+ }
355
+ }
356
+ const hasEvent = "event" in args;
357
+ const hasPayload = "payloadString" in args;
358
+ if (hasEvent === hasPayload) {
359
+ // neither, or both: ambiguous call
360
+ return { ok: false, reason: REASONS.VERIFY_BAD_INPUT, field: "event" };
361
+ }
362
+
363
+ const exp = args.expected;
364
+ if (!_isPlainObject(exp)) {
365
+ return { ok: false, reason: REASONS.VERIFY_BAD_EXPECTED, field: "expected" };
366
+ }
367
+ for (const k of Object.keys(exp)) {
368
+ if (!["commit", "gitRoot"].includes(k)) {
369
+ return { ok: false, reason: REASONS.VERIFY_BAD_EXPECTED, field: k };
370
+ }
371
+ }
372
+ if (typeof exp.commit !== "string" || !COMMIT_RE.test(exp.commit)) {
373
+ return { ok: false, reason: REASONS.VERIFY_BAD_EXPECTED, field: "commit" };
374
+ }
375
+ if (typeof exp.gitRoot !== "string" || !GIT_ROOT_RE.test(exp.gitRoot)) {
376
+ return { ok: false, reason: REASONS.VERIFY_BAD_EXPECTED, field: "gitRoot" };
377
+ }
378
+
379
+ let payload;
380
+ let seq;
381
+ if (hasEvent) {
382
+ const ev = validateEvent(args.event);
383
+ if (!ev.ok) {
384
+ return { ok: false, reason: REASONS.BAD_CLAIM, detail: ev.reason, field: ev.field };
385
+ }
386
+ if (ev.redacted) {
387
+ return { ok: false, reason: REASONS.BAD_CLAIM, detail: REASONS.CLAIM_REDACTED };
388
+ }
389
+ if (args.event.type !== CLAIM_EVENT_TYPE) {
390
+ return {
391
+ ok: false,
392
+ reason: REASONS.BAD_CLAIM,
393
+ detail: REASONS.CLAIM_BAD_EVENT_TYPE,
394
+ field: "type",
395
+ };
396
+ }
397
+ payload = args.event.payload;
398
+ seq = args.event.seq;
399
+ } else {
400
+ payload = args.payloadString;
401
+ }
402
+
403
+ const p = parseCommitClaim(payload);
404
+ if (!p.ok) {
405
+ return { ok: false, reason: REASONS.BAD_CLAIM, detail: p.reason, field: p.field };
406
+ }
407
+ if (p.claim.commit !== exp.commit) {
408
+ return {
409
+ ok: false,
410
+ reason: REASONS.OID_MISMATCH,
411
+ field: "commit",
412
+ claimed: p.claim.commit,
413
+ expected: exp.commit,
414
+ };
415
+ }
416
+ if (p.claim.gitRoot !== exp.gitRoot) {
417
+ return {
418
+ ok: false,
419
+ reason: REASONS.ROOT_MISMATCH,
420
+ field: "gitRoot",
421
+ claimed: p.claim.gitRoot,
422
+ expected: exp.gitRoot,
423
+ };
424
+ }
425
+ const out = { ok: true, claim: p.claim };
426
+ if (seq !== undefined) out.seq = seq;
427
+ return out;
428
+ } catch (_) {
429
+ return { ok: false, reason: REASONS.HOSTILE_INPUT };
430
+ }
431
+ }
432
+
433
+ module.exports = {
434
+ // Schema + verdict contract.
435
+ CLAIM_KIND,
436
+ CLAIM_EVENT_TYPE,
437
+ CLAIM_FIELDS,
438
+ DEFAULT_ACTOR,
439
+ MAX_SCOPE_LENGTH,
440
+ MAX_PAYLOAD_LENGTH,
441
+ REASONS,
442
+ // The core operations.
443
+ commitClaimPayload,
444
+ parseCommitClaim,
445
+ buildCommitClaimEvent,
446
+ findCommitClaims,
447
+ verifyCommitClaim,
448
+ };