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