@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/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":"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"}
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
+ }