@integraledger/lcp-verify 0.9.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/CHANGELOG.md +13 -0
- package/LICENSE +202 -0
- package/NOTICE +14 -0
- package/README.md +116 -0
- package/dist/composition.d.ts +53 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +54 -0
- package/dist/composition.js.map +1 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +100 -0
- package/dist/index.js.map +1 -0
- package/dist/report.d.ts +72 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +38 -0
- package/dist/report.js.map +1 -0
- package/dist/required.d.ts +47 -0
- package/dist/required.d.ts.map +1 -0
- package/dist/required.js +63 -0
- package/dist/required.js.map +1 -0
- package/dist/steps.d.ts +182 -0
- package/dist/steps.d.ts.map +1 -0
- package/dist/steps.js +404 -0
- package/dist/steps.js.map +1 -0
- package/package.json +61 -0
- package/src/composition.ts +100 -0
- package/src/index.ts +277 -0
- package/src/report.ts +100 -0
- package/src/required.ts +99 -0
- package/src/steps.ts +517 -0
package/dist/steps.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verification walk as an ordered table of typed steps. At STRUCTURAL depth each step is
|
|
3
|
+
* presence/absence → outcome plus the pure checks it can do over supplied inputs (no live ports):
|
|
4
|
+
* `proved | failed(haltClass) | indeterminate | not-attempted(depth)`. A step whose inputs are absent is
|
|
5
|
+
* `not-attempted(<why>)` — coverage is honest depth, never a silent pass. These outcomes are depth-agnostic;
|
|
6
|
+
* `verified` is computed FROM them by `computeVerified` (index.ts); the live-port gathering that raises
|
|
7
|
+
* coverage to mechanical depth is the buyer gate's.
|
|
8
|
+
*
|
|
9
|
+
* Three rules hold across every step, and all three exist so the walk can never flatter a record:
|
|
10
|
+
* - ABSENT INPUTS NEVER PROVE. An empty authority chain, an acceptance with no verifier to check its
|
|
11
|
+
* signature, an identity with no resolution chain — each is `not-attempted`, never `proved`.
|
|
12
|
+
* - CONTRADICTIONS FAIL, GAPS DO NOT. `failed` is reserved for a record that contradicts itself (a
|
|
13
|
+
* fingerprint that does not match, a link that widens its parent, a signature that does not verify);
|
|
14
|
+
* it impeaches `supportedClass` to TC-0. Missing evidence is a gap: `not-attempted`, which leaves
|
|
15
|
+
* `verified` false without impeaching a record that simply did not carry that rung.
|
|
16
|
+
* - EVERY STEP IS TOTAL. Steps read their slots through `present` and shape-check before dereferencing,
|
|
17
|
+
* because the callers they exist for are untyped — a foreign conformance subject, an unvalidated
|
|
18
|
+
* intake. A malformed slot reads out as a gap, never as a `failed`: the caller's shape error says
|
|
19
|
+
* nothing about whether the record is self-consistent, and a walk that throws cannot report the
|
|
20
|
+
* malformation it was handed.
|
|
21
|
+
*
|
|
22
|
+
* The authority step is the load-bearing one. Every link must have been PERMITTED by its parent (ATA-3 —
|
|
23
|
+
* the parent was delegable, it had depth left, and the link stated a depth that fits beneath what the
|
|
24
|
+
* parent held; an unstated depth is unbounded and therefore an escalation like any other), must ATTENUATE
|
|
25
|
+
* (`authority.isWithin`), and must be UNREVOKED and UNEXPIRED as-of settlement. Each failure is
|
|
26
|
+
* `failed(verification-failure)`, never `proved`, so the walk can never affirm a forged chain — whether the
|
|
27
|
+
* forgery widened the bounds or the authority to delegate them. These are the same four gates
|
|
28
|
+
* `authority.linkAttenuates` enforces at issuance; producer and verifier must not diverge, and
|
|
29
|
+
* `vectors/{authority/link-attenuates,verify/authority-walk}.json` pin both halves against each other.
|
|
30
|
+
*
|
|
31
|
+
* `authorityStep` reads a FLATTENED chain, every field of which is a derived fact it must take on trust.
|
|
32
|
+
* `authorityStepFromWalk` is the path that removes that trust — it consumes `authority.walkChain`'s
|
|
33
|
+
* readout directly, so the caller never flattens anything. Both are exported: the flattened door stays
|
|
34
|
+
* open because a foreign conformance subject may legitimately derive its links some other way, and this
|
|
35
|
+
* module's totality rule means such a caller gets an honest readout rather than a compile wall.
|
|
36
|
+
*/
|
|
37
|
+
import { commitmentWithinLeaf, isWithin, verifyAcceptance, } from "@integraledger/lcp-authority";
|
|
38
|
+
import { atrHashEquals, hashAtr } from "@integraledger/lcp-kernel";
|
|
39
|
+
/**
|
|
40
|
+
* Is the ATR's `terms` slot an `lcp:sha256:` REFERENCE rather than the inlined document?
|
|
41
|
+
*
|
|
42
|
+
* Narrow on purpose. Only the content-addressed form means "the document lives elsewhere and must travel
|
|
43
|
+
* with the package": an inlined string IS the document, and `lcp:ipfs:`/`lcp:ar:` are not what
|
|
44
|
+
* `kernel.assemble` emits for the terms slot. A non-string slot is not a ref — it reads as absent here and
|
|
45
|
+
* is the fingerprint step's business, not this one's.
|
|
46
|
+
*/
|
|
47
|
+
function isRefTerms(terms) {
|
|
48
|
+
return typeof terms === "string" && terms.startsWith("lcp:sha256:");
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* RCS-4's required evidence roles — the specification's own enumeration of a self-contained package: the terms
|
|
52
|
+
* artifact and fingerprint; the acceptance signature and its authority chain (ATA-6); the spend-authorization
|
|
53
|
+
* artifact or a verifiable reference to it (ASP-5); the identity attestations relied on and their assurance
|
|
54
|
+
* levels (IDN-3); the settlement reference and weld; timestamps. Its own conditional clause — fulfillment and
|
|
55
|
+
* order-state, "where performance is disputed" (OPS-2) — is conditional and therefore NOT required here.
|
|
56
|
+
*/
|
|
57
|
+
export const RCS4_REQUIRED_ROLES = [
|
|
58
|
+
"atr",
|
|
59
|
+
"signed acceptance",
|
|
60
|
+
"authority chain",
|
|
61
|
+
"spend artifact",
|
|
62
|
+
"attestation",
|
|
63
|
+
"settlement",
|
|
64
|
+
"weld",
|
|
65
|
+
"timestamp",
|
|
66
|
+
];
|
|
67
|
+
/**
|
|
68
|
+
* Is a slot supplied at all? `=== undefined` is not enough over an untyped caller — a foreign conformance
|
|
69
|
+
* subject or an unvalidated intake can hand in an explicitly `null` slot, which is *present* to that check
|
|
70
|
+
* and then dereferences to a TypeError. `typeof null === "object"` slips it past shape guards too. Every
|
|
71
|
+
* step reads its own slots through this, so a malformed record gets an honest `not-attempted` readout
|
|
72
|
+
* rather than crashing the walk that exists to report on it. Typed callers cannot express the null case.
|
|
73
|
+
*/
|
|
74
|
+
function present(value) {
|
|
75
|
+
return value !== undefined && value !== null;
|
|
76
|
+
}
|
|
77
|
+
/** A bounds-shaped slot: an object `isWithin` can walk. Mirrors `authority.walkableBounds`'s own gate, which
|
|
78
|
+
* exists for the same reason — `isWithin` reads its arguments with `Object.keys`, which THROWS on `undefined`
|
|
79
|
+
* and `null`. `typeof` alone is not that check (`typeof null === "object"`), and an array is not it either:
|
|
80
|
+
* `Object.keys([])` answers indices, so an array would read as a bounds with no dimensions instead of being
|
|
81
|
+
* refused. Only the shape is screened here — UNKNOWN keys are `isWithin`'s fail-closed refusal to make, not
|
|
82
|
+
* a shape gap for this step to pre-empt. */
|
|
83
|
+
function boundsShaped(value) {
|
|
84
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
85
|
+
}
|
|
86
|
+
/** A string that actually STATES something. `present` is not enough where the value IS the claim: `""` is
|
|
87
|
+
* a present string asserting nothing, and a walk that accepts it reports a fact nobody supplied. Blank
|
|
88
|
+
* runs count as empty — whitespace is not a subject and not a resolution method. */
|
|
89
|
+
function nonBlank(value) {
|
|
90
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
91
|
+
}
|
|
92
|
+
/** Recompute the fingerprint over the retrieved ATR bytes and compare to what the settlement committed. */
|
|
93
|
+
export async function fingerprintStep(atrBytes, settledAtrHash) {
|
|
94
|
+
if (!present(atrBytes))
|
|
95
|
+
return { status: "indeterminate" }; // unretrievable ATR — not a failure
|
|
96
|
+
if (!present(settledAtrHash))
|
|
97
|
+
return { status: "not-attempted", depth: "no-settled-hash" };
|
|
98
|
+
const recomputed = await hashAtr(atrBytes);
|
|
99
|
+
// Decoded-byte comparison per LCP §2.5. This is the rung that proves the settlement committed to THIS
|
|
100
|
+
// document, so a comparison that could answer `true` for two malformed strings is the wrong primitive.
|
|
101
|
+
return atrHashEquals(recomputed, settledAtrHash)
|
|
102
|
+
? { status: "proved" }
|
|
103
|
+
: { status: "failed", haltClass: "verification-failure" };
|
|
104
|
+
}
|
|
105
|
+
/** Settlement enumeration across supplied forward-indexable bindings (multiply-settled flag). */
|
|
106
|
+
export function settlementStep(settlements) {
|
|
107
|
+
if (!present(settlements))
|
|
108
|
+
return { status: "not-attempted", depth: "no-enumeration-port" };
|
|
109
|
+
if (settlements.length === 0)
|
|
110
|
+
return { status: "not-attempted", depth: "no-settlement-found" };
|
|
111
|
+
return { status: "proved" };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* TRM-6: the buyer's acceptance over the fingerprint, SIGNED and TIMESTAMPED. The signature is checked
|
|
115
|
+
* through `authority.verifyAcceptance` over the injected `SignatureVerifier` port (the EVM implementation
|
|
116
|
+
* — EOA EIP-191/712 and smart-account ERC-1271/6492 — is `binding-evm-common`'s).
|
|
117
|
+
*
|
|
118
|
+
* WITHOUT A VERIFIER THIS STEP CANNOT PROVE. A signature nobody checked is not evidence of a signature, so
|
|
119
|
+
* an acceptance presented with no verifier is `not-attempted("no-signature-verifier")` — the honest readout
|
|
120
|
+
* of "this record carries an acceptance whose cryptography this verifier was not equipped to check".
|
|
121
|
+
* `signedAt` needs no separate gate: `SignedAcceptance` requires it, so an untimestamped acceptance is
|
|
122
|
+
* unrepresentable rather than rejected at runtime — illegal states are not constructible.
|
|
123
|
+
*/
|
|
124
|
+
export async function acceptanceStep(acceptance, settledAtrHash, verifier) {
|
|
125
|
+
if (!present(acceptance))
|
|
126
|
+
return { status: "not-attempted", depth: "no-acceptance" };
|
|
127
|
+
if (!present(settledAtrHash))
|
|
128
|
+
return { status: "not-attempted", depth: "no-settled-hash" };
|
|
129
|
+
if (!present(verifier))
|
|
130
|
+
return { status: "not-attempted", depth: "no-signature-verifier" };
|
|
131
|
+
const outcome = await verifyAcceptance(acceptance, settledAtrHash, verifier);
|
|
132
|
+
return "refused" in outcome
|
|
133
|
+
? { status: "failed", haltClass: outcome.haltClass }
|
|
134
|
+
: { status: "proved" };
|
|
135
|
+
}
|
|
136
|
+
/** Walk the ATA chain: every link attenuates (isWithin), is unrevoked, and is unexpired as-of settlement. */
|
|
137
|
+
export function authorityStep(chain) {
|
|
138
|
+
// A non-array in the slot is not a chain — `.length` on it is meaningless and `for…of` throws.
|
|
139
|
+
if (!present(chain) || !Array.isArray(chain))
|
|
140
|
+
return { status: "not-attempted", depth: "no-authority-chain" };
|
|
141
|
+
// An empty chain walks nothing. Proving it would let a record with no delegated authority whatsoever
|
|
142
|
+
// clear the ATA rung — the gap that let placeholder chains carry a class they never earned.
|
|
143
|
+
if (chain.length === 0)
|
|
144
|
+
return { status: "not-attempted", depth: "empty-authority-chain" };
|
|
145
|
+
for (const link of chain) {
|
|
146
|
+
// An element that is not a link makes the chain UNWALKABLE. That is a gap, not a contradiction: the
|
|
147
|
+
// caller handed over something malformed, which says nothing about whether the record is self-
|
|
148
|
+
// consistent. Reporting it as `failed` would impeach a record to TC-0 over the caller's shape error.
|
|
149
|
+
if (!present(link) || typeof link !== "object")
|
|
150
|
+
return { status: "not-attempted", depth: "malformed-authority-chain" };
|
|
151
|
+
// ATA-3, gate one: the parent must have permitted delegation at all. Impeccably attenuated bounds
|
|
152
|
+
// re-issued by a holder who was never authorized to re-issue them are still a forged link.
|
|
153
|
+
if (!link.parentDelegable)
|
|
154
|
+
return { status: "failed", haltClass: "verification-failure" };
|
|
155
|
+
// ATA-3, gate two: depth. A depth-exhausted parent admits no link below it, and a link may not mint
|
|
156
|
+
// itself more onward-delegation latitude than the parent held (`isWithin` governs bounds, not depth).
|
|
157
|
+
// An UNSTATED child depth means unbounded, so under a bounded parent it is an escalation like any
|
|
158
|
+
// other — the same reason `parentDelegable` is required rather than defaulted.
|
|
159
|
+
if (present(link.parentMaxDepth)) {
|
|
160
|
+
// FINITE, not merely present. `NaN` compares false on BOTH sides of the arithmetic below
|
|
161
|
+
// (`NaN <= 0`, `x > NaN - 1`), so a non-finite depth on either side disengages this gate and the
|
|
162
|
+
// link reaches `proved` with its depth never actually checked. It reads out as a GAP, not a
|
|
163
|
+
// failure, under this step's own discipline: a caller's type corruption is a malformed slot, and
|
|
164
|
+
// says nothing about whether the RECORD contradicts itself — the same ruling the walk makes on the
|
|
165
|
+
// same value (`authority.walkChainStructure`, which refuses it as `malformed-authority-chain`), so
|
|
166
|
+
// walk and step stay categorically aligned. JSON cannot express NaN, so no corpus vector can pin
|
|
167
|
+
// this; the pins are in this package's tests. Negative and fractional depths stay deliberately
|
|
168
|
+
// un-screened: the arithmetic already fails closed on them.
|
|
169
|
+
if (!Number.isFinite(link.parentMaxDepth))
|
|
170
|
+
return { status: "not-attempted", depth: "malformed-authority-chain" };
|
|
171
|
+
if (link.parentMaxDepth <= 0)
|
|
172
|
+
return { status: "failed", haltClass: "verification-failure" };
|
|
173
|
+
// Absent and non-finite part company HERE, and only here: an unstated child depth is unbounded,
|
|
174
|
+
// which under a bounded parent is a real escalation the record committed (`failed`), while a
|
|
175
|
+
// non-finite one is the caller's corruption (`not-attempted`).
|
|
176
|
+
if (!present(link.maxDepth))
|
|
177
|
+
return { status: "failed", haltClass: "verification-failure" };
|
|
178
|
+
if (!Number.isFinite(link.maxDepth))
|
|
179
|
+
return { status: "not-attempted", depth: "malformed-authority-chain" };
|
|
180
|
+
if (link.maxDepth > link.parentMaxDepth - 1)
|
|
181
|
+
return { status: "failed", haltClass: "verification-failure" };
|
|
182
|
+
}
|
|
183
|
+
// The bounds slots must be WALKABLE before they can be compared. `isWithin` reads both sides with
|
|
184
|
+
// `Object.keys`, so an absent, null or non-object slot throws there — and this module's contract is
|
|
185
|
+
// totality over its inputs, not over well-formed inputs. `not-attempted` rather than `failed`, for the
|
|
186
|
+
// same reason a non-finite depth is: a caller's shape corruption says nothing about whether the RECORD
|
|
187
|
+
// contradicts itself, and answering `failed` would impeach a record to TC-0 over the caller's mistake.
|
|
188
|
+
// A well-formed readout always carries objects — `authority.walkChainStructure` supplies `{}` for a root
|
|
189
|
+
// link's `parentBounds` — and it refuses this same value as `malformed-authority-chain`, so walk and step
|
|
190
|
+
// stay categorically aligned.
|
|
191
|
+
if (!boundsShaped(link.bounds) || !boundsShaped(link.parentBounds))
|
|
192
|
+
return { status: "not-attempted", depth: "malformed-authority-chain" };
|
|
193
|
+
if (!isWithin(link.bounds, link.parentBounds))
|
|
194
|
+
return { status: "failed", haltClass: "verification-failure" }; // forged widening
|
|
195
|
+
// Revocation and liveness, and the ONE place this step used to flatter a record. Both slots are
|
|
196
|
+
// compile-time REQUIRED, so a typed caller cannot reach the absent arm and `authorityStepFromWalk`
|
|
197
|
+
// never could — but this step's whole contract is totality over UNTYPED input, and for that caller an
|
|
198
|
+
// omitted `revoked` used to read as unrevoked and PROVE. That is the permissive direction of the one
|
|
199
|
+
// rule this module states in capitals: "the flattener never consulted a status list" and "the pinned
|
|
200
|
+
// snapshot says unrevoked" are not the same fact, and only one of them may prove.
|
|
201
|
+
//
|
|
202
|
+
// Absence is therefore a gap with its own name, a contradiction still fails, and a non-boolean is the
|
|
203
|
+
// caller's shape error — the same three-way split the depth gate above already makes. The order also
|
|
204
|
+
// matters and is pinned: a link that BOTH widens and omits its status fails, because `isWithin` runs
|
|
205
|
+
// first and a contradiction outranks a gap.
|
|
206
|
+
if (!present(link.revoked))
|
|
207
|
+
return { status: "not-attempted", depth: "no-revocation-stated" };
|
|
208
|
+
if (typeof link.revoked !== "boolean")
|
|
209
|
+
return { status: "not-attempted", depth: "malformed-authority-chain" };
|
|
210
|
+
if (link.revoked)
|
|
211
|
+
return { status: "failed", haltClass: "verification-failure" };
|
|
212
|
+
if (!present(link.active))
|
|
213
|
+
return { status: "not-attempted", depth: "no-liveness-stated" };
|
|
214
|
+
if (typeof link.active !== "boolean")
|
|
215
|
+
return { status: "not-attempted", depth: "malformed-authority-chain" };
|
|
216
|
+
if (!link.active)
|
|
217
|
+
return { status: "failed", haltClass: "verification-failure" }; // expired
|
|
218
|
+
}
|
|
219
|
+
return { status: "proved" };
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The WALK-FED authority step — `authority.walkChain`'s readout mapped onto a `StepOutcome`. Prefer it.
|
|
223
|
+
*
|
|
224
|
+
* `authorityStep` accepts any `AuthorityLink[]`, which makes whoever produced that array a trusted
|
|
225
|
+
* oracle: every field on a link is a DERIVED fact (`parentDelegable`, `parentMaxDepth`, `parentBounds`,
|
|
226
|
+
* `revoked`, `active`), and the step takes each at face value. A flattener that never verified custody —
|
|
227
|
+
* never checked that link N+1 was signed by link N's subject, never dereferenced a status list — yields a
|
|
228
|
+
* confident `proved`. A caller that walks first constructs no link at all and cannot make that mistake.
|
|
229
|
+
*
|
|
230
|
+
* The mapping needs no interpretation, because `ChainWalkResult` already draws this module's own line:
|
|
231
|
+
* `refused` is a reasoned CONTRADICTION (a spliced link, an issuer discontinuity, a forged widening, a
|
|
232
|
+
* revoked grant) and carries the halt class with it; `not-attempted` is the walk's honest GAP, its depth
|
|
233
|
+
* passed through verbatim; `walked` hands over links whose every field the walk STATED rather than
|
|
234
|
+
* defaulted. Re-proving those links through `authorityStep` is deliberate rather than redundant — it is
|
|
235
|
+
* what stops the custody walk and the verification step from drifting apart, and
|
|
236
|
+
* `packages/conformance/the repository's walk-readout tests` pins that the walk's output is exactly what the step
|
|
237
|
+
* proves. Total over untyped input like every step here: an unrecognized readout carries no links, so it
|
|
238
|
+
* falls through to `authorityStep`'s own gap rather than throwing.
|
|
239
|
+
*/
|
|
240
|
+
export function authorityStepFromWalk(walk) {
|
|
241
|
+
if (!present(walk) || typeof walk !== "object")
|
|
242
|
+
return { status: "not-attempted", depth: "no-authority-walk" };
|
|
243
|
+
if (walk.status === "refused")
|
|
244
|
+
return { status: "failed", haltClass: walk.haltClass };
|
|
245
|
+
if (walk.status === "not-attempted")
|
|
246
|
+
return { status: "not-attempted", depth: walk.depth };
|
|
247
|
+
return authorityStep(walk.links);
|
|
248
|
+
}
|
|
249
|
+
/** ATA-4: the accepted commitment must be contained by the leaf grant's bounds. */
|
|
250
|
+
export function commitmentStep(c) {
|
|
251
|
+
// Both halves must be usable bounds objects. `isWithin` is the pure ATA-2 predicate and is deliberately
|
|
252
|
+
// strict — it does `Object.keys` on what it is given — so the walk, not the predicate, owns the totality
|
|
253
|
+
// at this boundary. A half-supplied commitment slot is no commitment: a gap, never a contradiction.
|
|
254
|
+
if (!present(c) ||
|
|
255
|
+
typeof c !== "object" ||
|
|
256
|
+
!present(c.commitment) ||
|
|
257
|
+
typeof c.commitment !== "object" ||
|
|
258
|
+
!present(c.leafBounds) ||
|
|
259
|
+
typeof c.leafBounds !== "object")
|
|
260
|
+
return { status: "not-attempted", depth: "no-commitment" };
|
|
261
|
+
return commitmentWithinLeaf(c.commitment, c.leafBounds)
|
|
262
|
+
? { status: "proved" }
|
|
263
|
+
: { status: "failed", haltClass: "verification-failure" };
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* RCS-1/2/4: the elections the record carries and the package it produced.
|
|
267
|
+
*
|
|
268
|
+
* The elections are read from THE HASHED RECORD ITSELF — `atrBytes` parsed as the LCP envelope — never from
|
|
269
|
+
* a caller-supplied side channel, because RCS-1 requires the forum designation "recorded inside the terms
|
|
270
|
+
* record" (TRM-9). What this step proves is therefore what was welded.
|
|
271
|
+
*
|
|
272
|
+
* This step NEVER returns `failed`. An unelected forum is a record that did not reach for the rung, not a
|
|
273
|
+
* record that contradicts itself, and impeaching `supportedClass` to TC-0 over it would misreport a
|
|
274
|
+
* perfectly coherent TC-2. Likewise a non-machine-readable ATR (a ratified prose template, a PDF) is an
|
|
275
|
+
* honest coverage gap: a human forum can read a governing-law clause a verifier cannot.
|
|
276
|
+
*/
|
|
277
|
+
export function recourseStep(atrBytes, evidenceRoles) {
|
|
278
|
+
if (!present(atrBytes))
|
|
279
|
+
return { status: "not-attempted", depth: "no-atr-bytes" };
|
|
280
|
+
const envelope = parseEnvelope(atrBytes);
|
|
281
|
+
if (!present(envelope))
|
|
282
|
+
return { status: "not-attempted", depth: "atr-not-machine-readable" };
|
|
283
|
+
const recourse = envelope["recourse"];
|
|
284
|
+
if (!present(recourse) || typeof recourse !== "object")
|
|
285
|
+
return { status: "not-attempted", depth: "no-elections-recorded" };
|
|
286
|
+
const elections = recourse;
|
|
287
|
+
if (!stated(elections["forum"]))
|
|
288
|
+
return { status: "not-attempted", depth: "no-forum-elected" }; // RCS-1
|
|
289
|
+
if (!stated(elections["governingLaw"]))
|
|
290
|
+
return { status: "not-attempted", depth: "no-governing-law-elected" }; // RCS-2
|
|
291
|
+
if (!present(evidenceRoles))
|
|
292
|
+
return { status: "not-attempted", depth: "no-evidence-package" }; // RCS-4
|
|
293
|
+
const supplied = new Set(evidenceRoles);
|
|
294
|
+
if (!RCS4_REQUIRED_ROLES.every((role) => supplied.has(role)))
|
|
295
|
+
return { status: "not-attempted", depth: "evidence-package-incomplete" };
|
|
296
|
+
// CONDITIONAL, and this is the rung the role list alone cannot express. An ATR may carry its terms
|
|
297
|
+
// INLINE — then the ATR artifact is the document, and the list above is already complete — or by
|
|
298
|
+
// `lcp:sha256:` REFERENCE, in which case the package holds a fingerprint of a document it does not
|
|
299
|
+
// contain. A hash without a document is a proof without evidence (LCP §5.5, TRM-7), and the forum that
|
|
300
|
+
// opens the package years later receives the fingerprint of something nobody retained.
|
|
301
|
+
//
|
|
302
|
+
// Required IFF the terms slot is a ref, because an unconditional role would fail every honest
|
|
303
|
+
// inline-terms package. `manifest.schema.json` already stated this rule; nothing enforced it.
|
|
304
|
+
if (isRefTerms(envelope["terms"]) &&
|
|
305
|
+
!supplied.has("referenced terms document"))
|
|
306
|
+
return { status: "not-attempted", depth: "referenced-terms-not-retained" };
|
|
307
|
+
return { status: "proved" };
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* IDN-1/IDN-3: both parties resolve, each at a STATED assurance level, over a non-empty resolution chain.
|
|
311
|
+
*
|
|
312
|
+
* IDN-2 — does the chain terminate in an accountable party rather than a bare key? — is deliberately NOT a
|
|
313
|
+
* gate here. It is the counterparty-trust question a BUYER's policy asks (`authority.isConsequentialConformant`,
|
|
314
|
+
* applied by the gate), not a property of the record's class: a record that honestly states
|
|
315
|
+
* `wallet-signature-only` is conformant at its level, and IDN-3's whole discipline is that the low level
|
|
316
|
+
* stated honestly passes while an unstated level does not.
|
|
317
|
+
*/
|
|
318
|
+
export function resolvePartyStep(identity) {
|
|
319
|
+
if (!present(identity))
|
|
320
|
+
return { status: "not-attempted", depth: "no-identity" };
|
|
321
|
+
// A party that is absent or carries no resolution chain is an UNRESOLVED party, not a crash: the step is
|
|
322
|
+
// total over its input, so a foreign conformance subject feeding a half-shaped identity gets an honest
|
|
323
|
+
// readout instead of a TypeError. Typed callers cannot express this — `RecordIdentity` requires both.
|
|
324
|
+
for (const party of [identity.seller, identity.buyer]) {
|
|
325
|
+
if (!present(party) || !present(party.chain))
|
|
326
|
+
return { status: "not-attempted", depth: "no-resolution" };
|
|
327
|
+
if (party.chain.length === 0)
|
|
328
|
+
return { status: "not-attempted", depth: "no-resolution-chain" };
|
|
329
|
+
// IDN-1 — the statement of WHO. A party resolved to nobody is unresolved.
|
|
330
|
+
if (!nonBlank(party.subject))
|
|
331
|
+
return { status: "not-attempted", depth: "no-subject" };
|
|
332
|
+
// IDN-3 — the statement of HOW. Array LENGTH is not evidence: `[{}]` and `[{via:""}]` are non-empty
|
|
333
|
+
// arrays that record nothing, and honouring them proved attribution for an identity naming no method
|
|
334
|
+
// at all. Every entry must say how, or the chain is present in shape only.
|
|
335
|
+
if (!party.chain.every((step) => nonBlank(step?.via)))
|
|
336
|
+
return { status: "not-attempted", depth: "no-resolution-method" };
|
|
337
|
+
// IDN-3 — the statement of AT WHAT LEVEL. A party whose resolution names no assurance stated nothing
|
|
338
|
+
// to pass honestly at: absence is a gap, exactly as a blank `subject` or `via` is. Typed callers cannot
|
|
339
|
+
// omit it (`IdentityResolution` requires `assurance`); this gate exists for the untyped ones.
|
|
340
|
+
if (!nonBlank(party.assurance))
|
|
341
|
+
return { status: "not-attempted", depth: "no-assurance-stated" };
|
|
342
|
+
}
|
|
343
|
+
return { status: "proved" };
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* The reference-placement step — did the reference found in the protocol's native field match this record?
|
|
347
|
+
*
|
|
348
|
+
* REPORTED, NEVER REQUIRED: no class lists it in `REQUIRED_STEPS`, so its absence never blocks, and it
|
|
349
|
+
* impeaches only when it FAILS — the `frc-non-gating` pattern exactly. A protocol that never settles has
|
|
350
|
+
* nothing to enumerate, and letting a placement stand in for a settlement rung would let a record that
|
|
351
|
+
* moved no money read as classed.
|
|
352
|
+
*
|
|
353
|
+
* Total, like every step: the shape checks stay even though the arguments are typed, because the callers
|
|
354
|
+
* this exists for are untyped — a foreign conformance subject, an unvalidated intake. A malformed
|
|
355
|
+
* extraction is a GAP, never a `failed`: the caller's shape error says nothing about whether the record
|
|
356
|
+
* contradicts itself.
|
|
357
|
+
*/
|
|
358
|
+
export function referencePlacementStep(placement, settledAtrHash) {
|
|
359
|
+
if (!present(placement))
|
|
360
|
+
return { status: "not-attempted", depth: "no-placement-input" };
|
|
361
|
+
const extracted = placement.extracted;
|
|
362
|
+
if (extracted === undefined)
|
|
363
|
+
return { status: "not-attempted", depth: "no-reference-extracted" };
|
|
364
|
+
if (typeof extracted !== "object" ||
|
|
365
|
+
extracted === null ||
|
|
366
|
+
typeof extracted.value !== "string")
|
|
367
|
+
return { status: "not-attempted", depth: "malformed-extracted-reference" };
|
|
368
|
+
if (typeof settledAtrHash !== "string")
|
|
369
|
+
return { status: "not-attempted", depth: "no-record-fingerprint" };
|
|
370
|
+
// Both sides are NORMALIZED to the canonical 0x-prefixed lower-case form, not stripped to bare digits.
|
|
371
|
+
// Hex is case-insensitive, so a conformant uppercase counterparty must not be false-negatived. And a
|
|
372
|
+
// missing `0x` is a CARRIER-FORM defect that `binding-core`'s decoder already refuses upstream at
|
|
373
|
+
// extract time — failing here would drive `supportedClass` to TC-0 over formatting, impeaching a record
|
|
374
|
+
// whose reference names it correctly. Impeachment is reserved for a reference naming a DIFFERENT record.
|
|
375
|
+
const canonical = (h) => {
|
|
376
|
+
const lower = h.toLowerCase();
|
|
377
|
+
return lower.startsWith("0x") ? lower : `0x${lower}`;
|
|
378
|
+
};
|
|
379
|
+
if (canonical(extracted.value) !==
|
|
380
|
+
canonical(settledAtrHash))
|
|
381
|
+
return { status: "failed", haltClass: "verification-failure" };
|
|
382
|
+
return { status: "proved" };
|
|
383
|
+
}
|
|
384
|
+
/** A non-empty, non-blank string statement — an empty election is not an election. */
|
|
385
|
+
function stated(value) {
|
|
386
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
387
|
+
}
|
|
388
|
+
/** Parse the ATR bytes as an LCP envelope, or `undefined` when they are not one (prose, PDF, foreign JSON). */
|
|
389
|
+
function parseEnvelope(atrBytes) {
|
|
390
|
+
let parsed;
|
|
391
|
+
try {
|
|
392
|
+
parsed = JSON.parse(new TextDecoder().decode(atrBytes));
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return undefined; // not JSON at all — a prose or binary terms artifact
|
|
396
|
+
}
|
|
397
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
398
|
+
return undefined;
|
|
399
|
+
const envelope = parsed;
|
|
400
|
+
// `lcp` is engine-stamped by kernel.assemble on every record — its absence means these bytes are not an
|
|
401
|
+
// LCP envelope, whatever else they may be.
|
|
402
|
+
return typeof envelope["lcp"] === "string" ? envelope : undefined;
|
|
403
|
+
}
|
|
404
|
+
//# sourceMappingURL=steps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"steps.js","sourceRoot":"","sources":["../src/steps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,EAGL,oBAAoB,EAEpB,QAAQ,EAGR,gBAAgB,GACjB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAuDnE;;;;;;;GAOG;AACH,SAAS,UAAU,CAAC,KAAc;IAChC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAsB;IACpD,KAAK;IACL,mBAAmB;IACnB,iBAAiB;IACjB,gBAAgB;IAChB,aAAa;IACb,YAAY;IACZ,MAAM;IACN,WAAW;CACZ,CAAC;AAEF;;;;;;GAMG;AACH,SAAS,OAAO,CAAI,KAA2B;IAC7C,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,CAAC;AAC/C,CAAC;AAED;;;;;6CAK6C;AAC7C,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED;;qFAEqF;AACrF,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAC9D,CAAC;AAED,2GAA2G;AAC3G,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,QAAgC,EAChC,cAAkC;IAElC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC,oCAAoC;IAChG,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC;QAC1B,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC/D,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC3C,sGAAsG;IACtG,uGAAuG;IACvG,OAAO,aAAa,CAAC,UAAU,EAAE,cAAc,CAAC;QAC9C,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE;QACtB,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC;AAC9D,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,cAAc,CAC5B,WAAkC;IAElC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC;QACvB,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC;IACnE,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAC1B,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC;IACnE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC9B,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,UAAwC,EACxC,cAAkC,EAClC,QAAuC;IAEvC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC;QACtB,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC;IAC7D,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC;QAC1B,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC/D,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;QACpB,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC;IACrE,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,UAAU,EAAE,cAAc,EAAE,QAAQ,CAAC,CAAC;IAC7E,OAAO,SAAS,IAAI,OAAO;QACzB,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE;QACpD,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC3B,CAAC;AAED,6GAA6G;AAC7G,MAAM,UAAU,aAAa,CAAC,KAAkC;IAC9D,+FAA+F;IAC/F,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAC1C,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAClE,qGAAqG;IACrG,4FAA4F;IAC5F,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QACpB,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC;IACrE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,oGAAoG;QACpG,+FAA+F;QAC/F,qGAAqG;QACrG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,OAAO,IAAI,KAAK,QAAQ;YAC5C,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC;QACzE,kGAAkG;QAClG,2FAA2F;QAC3F,IAAI,CAAC,IAAI,CAAC,eAAe;YACvB,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC;QACjE,oGAAoG;QACpG,sGAAsG;QACtG,kGAAkG;QAClG,+EAA+E;QAC/E,IAAI,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC;YACjC,yFAAyF;YACzF,iGAAiG;YACjG,4FAA4F;YAC5F,iGAAiG;YACjG,mGAAmG;YACnG,mGAAmG;YACnG,iGAAiG;YACjG,+FAA+F;YAC/F,4DAA4D;YAC5D,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC;gBACvC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC;YACzE,IAAI,IAAI,CAAC,cAAc,IAAI,CAAC;gBAC1B,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC;YACjE,gGAAgG;YAChG,6FAA6F;YAC7F,+DAA+D;YAC/D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;gBACzB,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC;YACjE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC;gBACjC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC;YACzE,IAAI,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,cAAc,GAAG,CAAC;gBACzC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC;QACnE,CAAC;QACD,kGAAkG;QAClG,oGAAoG;QACpG,uGAAuG;QACvG,uGAAuG;QACvG,uGAAuG;QACvG,yGAAyG;QACzG,0GAA0G;QAC1G,8BAA8B;QAC9B,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC;YAChE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC;QACzE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC;YAC3C,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC,CAAC,kBAAkB;QACpF,gGAAgG;QAChG,mGAAmG;QACnG,sGAAsG;QACtG,qGAAqG;QACrG,qGAAqG;QACrG,kFAAkF;QAClF,EAAE;QACF,sGAAsG;QACtG,qGAAqG;QACrG,qGAAqG;QACrG,4CAA4C;QAC5C,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;YACxB,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC;QACpE,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,SAAS;YACnC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC;QACzE,IAAI,IAAI,CAAC,OAAO;YACd,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC;QACjE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC;YACvB,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;QAClE,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,SAAS;YAClC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC;QACzE,IAAI,CAAC,IAAI,CAAC,MAAM;YACd,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC,CAAC,UAAU;IAC9E,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC9B,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,qBAAqB,CACnC,IAAiC;IAEjC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,OAAO,IAAI,KAAK,QAAQ;QAC5C,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;IACjE,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS;QAC3B,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;IACzD,IAAI,IAAI,CAAC,MAAM,KAAK,eAAe;QACjC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;IACxD,OAAO,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACnC,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,cAAc,CAC5B,CAAyD;IAEzD,wGAAwG;IACxG,yGAAyG;IACzG,oGAAoG;IACpG,IACE,CAAC,OAAO,CAAC,CAAC,CAAC;QACX,OAAO,CAAC,KAAK,QAAQ;QACrB,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC;QACtB,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ;QAChC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC;QACtB,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ;QAEhC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC;IAC7D,OAAO,oBAAoB,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,UAAU,CAAC;QACrD,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE;QACtB,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY,CAC1B,QAAgC,EAChC,aAA4C;IAE5C,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;QACpB,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC;IAC5D,MAAM,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;QACpB,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC;IACxE,MAAM,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;IACtC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,OAAO,QAAQ,KAAK,QAAQ;QACpD,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC;IACrE,MAAM,SAAS,GAAG,QAAmC,CAAC;IACtD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAC7B,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC,CAAC,QAAQ;IACzE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QACpC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAC,QAAQ;IACjF,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC;QACzB,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC,QAAQ;IAC5E,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC;IACxC,IAAI,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1D,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,6BAA6B,EAAE,CAAC;IAC3E,mGAAmG;IACnG,iGAAiG;IACjG,mGAAmG;IACnG,uGAAuG;IACvG,uFAAuF;IACvF,EAAE;IACF,8FAA8F;IAC9F,8FAA8F;IAC9F,IACE,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC7B,CAAC,QAAQ,CAAC,GAAG,CAAC,2BAA2B,CAAC;QAE1C,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,+BAA+B,EAAE,CAAC;IAC7E,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC9B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAAoC;IAEpC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;QACpB,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC;IAC3D,yGAAyG;IACzG,uGAAuG;IACvG,sGAAsG;IACtG,KAAK,MAAM,KAAK,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACtD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;YAC1C,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC;QAC7D,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;YAC1B,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC;QACnE,0EAA0E;QAC1E,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC;YAC1B,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC;QAC1D,oGAAoG;QACpG,qGAAqG;QACrG,2EAA2E;QAC3E,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YACnD,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC;QACpE,qGAAqG;QACrG,wGAAwG;QACxG,8FAA8F;QAC9F,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC;YAC5B,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC;IACrE,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC9B,CAAC;AAQD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,sBAAsB,CACpC,SAAqC,EACrC,cAAkC;IAElC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC;QACrB,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAClE,MAAM,SAAS,GAAY,SAAS,CAAC,SAAS,CAAC;IAC/C,IAAI,SAAS,KAAK,SAAS;QACzB,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,wBAAwB,EAAE,CAAC;IACtE,IACE,OAAO,SAAS,KAAK,QAAQ;QAC7B,SAAS,KAAK,IAAI;QAClB,OAAQ,SAAiC,CAAC,KAAK,KAAK,QAAQ;QAE5D,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,+BAA+B,EAAE,CAAC;IAC7E,IAAI,OAAO,cAAc,KAAK,QAAQ;QACpC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC;IAErE,uGAAuG;IACvG,qGAAqG;IACrG,kGAAkG;IAClG,wGAAwG;IACxG,yGAAyG;IACzG,MAAM,SAAS,GAAG,CAAC,CAAS,EAAU,EAAE;QACtC,MAAM,KAAK,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;IACvD,CAAC,CAAC;IACF,IACE,SAAS,CAAE,SAA+B,CAAC,KAAK,CAAC;QACjD,SAAS,CAAC,cAAc,CAAC;QAEzB,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC;IACjE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC9B,CAAC;AAED,sFAAsF;AACtF,SAAS,MAAM,CAAC,KAAc;IAC5B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAC9D,CAAC;AAED,+GAA+G;AAC/G,SAAS,aAAa,CACpB,QAAoB;IAEpB,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC,CAAC,qDAAqD;IACzE,CAAC;IACD,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QACxE,OAAO,SAAS,CAAC;IACnB,MAAM,QAAQ,GAAG,MAAiC,CAAC;IACnD,wGAAwG;IACxG,2CAA2C;IAC3C,OAAO,OAAO,QAAQ,CAAC,KAAK,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;AACpE,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@integraledger/lcp-verify",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "The LCP verification walk, and the canonical report it emits.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"lcp",
|
|
7
|
+
"legal-context-protocol",
|
|
8
|
+
"legal-terms",
|
|
9
|
+
"agentic-commerce",
|
|
10
|
+
"verification",
|
|
11
|
+
"report"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src",
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"NOTICE"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"registry": "https://registry.npmjs.org",
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/IntegraLedger/integra-protocol.git",
|
|
36
|
+
"directory": "packages/verify"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/IntegraLedger/integra-protocol/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/IntegraLedger/integra-protocol/tree/main/packages/verify#readme",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@integraledger/lcp-authority": "0.9.0",
|
|
44
|
+
"@integraledger/lcp-binding-core": "0.9.0",
|
|
45
|
+
"@integraledger/lcp-kernel": "0.9.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "24.13.3",
|
|
49
|
+
"fast-check": "4.9.0",
|
|
50
|
+
"vitest": "4.1.10"
|
|
51
|
+
},
|
|
52
|
+
"license": "Apache-2.0",
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=24"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc -p tsconfig.build.json",
|
|
58
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
59
|
+
"test": "vitest run"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TC-4 **composition readout** — the pure step evaluators the walk appends when a
|
|
3
|
+
* `composition` slot is supplied. Each evaluator mirrors `fingerprintStep`: presence/absence over the
|
|
4
|
+
* supplied `CompositionInput` → a `StepOutcome`, no I/O, no fetch, no viem. TC-4 is a READOUT of a record
|
|
5
|
+
* complete enough to prove it was complete — these steps never gate; they only confirm or impeach.
|
|
6
|
+
*
|
|
7
|
+
* FRC is DELIBERATELY non-gating (FRC-1): `frcNonGatingStep` is impeachment-only — a signal that GATED is a
|
|
8
|
+
* `failed(risk-block)`, its ABSENCE never blocks TC-4 (it is not in `REQUIRED_STEPS["TC-4"]`).
|
|
9
|
+
*/
|
|
10
|
+
import type { StepOutcome } from "./report.js";
|
|
11
|
+
|
|
12
|
+
/** What the caller can state about a record's composition. **Every field is optional, and absence is
|
|
13
|
+
* meaningful**: an omitted field is a gap the readout reports as unproved, never an assumed `false`.
|
|
14
|
+
* The values are the CALLER's assertions — this module performs no I/O and cannot check any of them. */
|
|
15
|
+
export interface CompositionInput {
|
|
16
|
+
readonly offerBound?: boolean;
|
|
17
|
+
readonly operations?: {
|
|
18
|
+
readonly orderStateRef?: boolean;
|
|
19
|
+
readonly reconciliationIds?: boolean;
|
|
20
|
+
};
|
|
21
|
+
readonly discoveryIntegrity?: "ok" | "mismatch" | "not-checked";
|
|
22
|
+
readonly proportionalityTier?: 1 | 2 | 3 | 4;
|
|
23
|
+
readonly frcSignals?: readonly {
|
|
24
|
+
readonly role: string;
|
|
25
|
+
readonly gated: boolean;
|
|
26
|
+
}[];
|
|
27
|
+
}
|
|
28
|
+
/** The flattened TC-4 readout. It is a READOUT, not a verdict: nothing here gates, and `frcGated: true`
|
|
29
|
+
* is an impeachment signal rather than a refusal. `proportionalityTier` stays `undefined` when the caller
|
|
30
|
+
* stated none, because an unstated tier is not tier 1. */
|
|
31
|
+
export interface CompositionReadout {
|
|
32
|
+
readonly offerBound: boolean;
|
|
33
|
+
readonly operationsBound: boolean;
|
|
34
|
+
readonly discoveryIntegrity: "ok" | "mismatch" | "not-checked";
|
|
35
|
+
readonly proportionalityTier: 1 | 2 | 3 | 4 | undefined;
|
|
36
|
+
readonly frcSignalCount: number;
|
|
37
|
+
readonly frcGated: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const na = (why: string): StepOutcome => ({
|
|
41
|
+
status: "not-attempted",
|
|
42
|
+
depth: why,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** OFR — proved iff the offer slot is bound; an unbound offer is incompleteness (not-attempted), never a failure. */
|
|
46
|
+
export function offerBoundStep(c: CompositionInput | undefined): StepOutcome {
|
|
47
|
+
return c?.offerBound ? { status: "proved" } : na("no-offer");
|
|
48
|
+
}
|
|
49
|
+
/** OPS core — the always-applicable OPS bindings (OPS-2 order/fulfillment ref AND OPS-5 reconciliation). */
|
|
50
|
+
export function operationsStep(c: CompositionInput | undefined): StepOutcome {
|
|
51
|
+
return c?.operations?.orderStateRef && c.operations.reconciliationIds
|
|
52
|
+
? { status: "proved" }
|
|
53
|
+
: na("ops-incomplete");
|
|
54
|
+
}
|
|
55
|
+
/** DSC-1/2 — "ok" proves; "mismatch" is a hard DSC-2 violation (verification-failure); else not-attempted. */
|
|
56
|
+
export function discoveryIntegrityStep(
|
|
57
|
+
c: CompositionInput | undefined,
|
|
58
|
+
): StepOutcome {
|
|
59
|
+
const d = c?.discoveryIntegrity;
|
|
60
|
+
if (d === undefined || d === "not-checked") return na("no-discovery-check");
|
|
61
|
+
return d === "ok"
|
|
62
|
+
? { status: "proved" }
|
|
63
|
+
: { status: "failed", haltClass: "verification-failure" };
|
|
64
|
+
}
|
|
65
|
+
/** CMP-6 — proved iff the proportionality tier is declared. */
|
|
66
|
+
export function proportionalityStep(
|
|
67
|
+
c: CompositionInput | undefined,
|
|
68
|
+
): StepOutcome {
|
|
69
|
+
return c?.proportionalityTier !== undefined
|
|
70
|
+
? { status: "proved" }
|
|
71
|
+
: na("no-tier");
|
|
72
|
+
}
|
|
73
|
+
/** FRC-1 — impeachment-only: any signal that GATED is a `failed(risk-block)`; signals present but non-gating
|
|
74
|
+
* prove the stack recorded-not-gated; no signals is not-attempted (FRC is not a v1 requirement). */
|
|
75
|
+
export function frcNonGatingStep(c: CompositionInput | undefined): StepOutcome {
|
|
76
|
+
const sigs = c?.frcSignals ?? [];
|
|
77
|
+
if (sigs.length === 0) return na("no-frc-signals");
|
|
78
|
+
return sigs.some((s) => s.gated)
|
|
79
|
+
? { status: "failed", haltClass: "risk-block" }
|
|
80
|
+
: { status: "proved" };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Flatten a {@link CompositionInput} into a {@link CompositionReadout}. Pure and total — an `undefined`
|
|
84
|
+
* input is legitimate and reads as "nothing stated" rather than throwing. `operationsBound` requires BOTH
|
|
85
|
+
* operations fields; either alone is a partially-composed record and reads `false`. */
|
|
86
|
+
export function readCompositionSlots(
|
|
87
|
+
input: CompositionInput | undefined,
|
|
88
|
+
): CompositionReadout {
|
|
89
|
+
const sigs = input?.frcSignals ?? [];
|
|
90
|
+
return {
|
|
91
|
+
offerBound: !!input?.offerBound,
|
|
92
|
+
operationsBound: !!(
|
|
93
|
+
input?.operations?.orderStateRef && input.operations.reconciliationIds
|
|
94
|
+
),
|
|
95
|
+
discoveryIntegrity: input?.discoveryIntegrity ?? "not-checked",
|
|
96
|
+
proportionalityTier: input?.proportionalityTier,
|
|
97
|
+
frcSignalCount: sigs.length,
|
|
98
|
+
frcGated: sigs.some((s) => s.gated),
|
|
99
|
+
};
|
|
100
|
+
}
|