@integraledger/lcp-binding-evm-mpp 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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-type.js","sourceRoot":"","sources":["../src/credential-type.ts"],"names":[],"mappings":"AAiCA;;;;;;;GAOG;AACH,MAAM,UAAU,8BAA8B,CAAC,KAAa;IAC1D,OAAO;QACL,OAAO,EAAE,IAAI;QACb,SAAS,EAAE,sBAAsB;QACjC,IAAI,EAAE,2CAA2C;QACjD,MAAM,EAAE,SAAS,KAAK,2ZAA2Z;KAClb,CAAC;AACJ,CAAC"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * MPP-EVM Id-Reuse (LCP §8.3.5): the host's derivation rule, the weld it produces, and the *only* kind of
3
+ * check the resulting on-chain value permits.
4
+ *
5
+ * **THE HOST RULE, quoted.** `draft-evm-charge-00` §5.3.1 (paymentauth.org, read 2026-07-30):
6
+ *
7
+ * nonce = keccak256(abi.encodePacked(challenge.id, challenge.realm))
8
+ *
9
+ * — with the accompanying sentence "This specification requires the nonce to be set to the `challengeHash`".
10
+ * The nonce is therefore a *derivation*, not a slot: there is nothing here for LCP to occupy. What LCP does
11
+ * instead is supply an input — the seller sets `challenge.id = atrHash` — so the host's own required
12
+ * derivation carries the record into the settlement transaction.
13
+ *
14
+ * **WHAT MAKES THAT SOUND IS NOT UNIQUENESS, AND AN EARLIER DOCBLOCK SAID IT WAS.** It argued that MPP's
15
+ * "Unique challenge identifier" requirement (`draft-httpauth-payment` §5.1.1) was satisfied by making each
16
+ * ATR unique per transaction. Every citation in it was exact; the sufficiency was not. LCP v1.38 §C.1
17
+ * (:1282-1289):
18
+ *
19
+ * > **It is not simply available, and §8.3.5's uniqueness advice is not sufficient for it.** MPP requires
20
+ * > the server to bind the challenge `id` *to the challenge parameters* … expressly to prevent a client
21
+ * > altering the challenge it presents. **Making each ATR unique per transaction satisfies uniqueness but
22
+ * > not that binding.** A per-transaction ATR that itself states the transaction parameters (Section 6.1)
23
+ * > satisfies both, because the parameters are then inside the hashed document; a terms document made
24
+ * > unique by a timestamp alone does not.
25
+ *
26
+ * **THE REQUIREMENT IS ON THE ATR, AND THE TREE ALREADY VERIFIES IT.** This binding holds a hash, never the
27
+ * document, so it cannot inspect what the ATR says — and it should not: where the ATR lives is the seller's
28
+ * and buyer's business, and a rail binding that needed the document would assert a custody LCP does not
29
+ * require. The property §C.1 asks for is **OFR**: the ATR's offer slot is bound, meaning the transaction
30
+ * parameters are inside the hashed document. `verify` implements it as `offerBoundStep`
31
+ * (`verify/src/composition.ts`) and requires it at **TC-4** (`verify/src/required.ts`). So a record whose
32
+ * class is TC-4 has had this checked by the layer that holds the document; a record below TC-4 has not, and
33
+ * the §8.3.5 discharge on this rail is only as good as that rung.
34
+ *
35
+ * The host is internally inconsistent here and the split is kept visible deliberately: the core draft
36
+ * states the challenge-parameter binding as a **MUST** in the `id` field description and as a **SHOULD** in
37
+ * its challenge-binding section. §C.1 names that inconsistency rather than resolving it, and so does this.
38
+ *
39
+ * **Zero-party-recoverable on-chain binding on this rail still requires an Overlay Contract per §8.3.2**
40
+ * (§C.1:1289). Nothing here recovers an atrHash, and nothing may be added that does — see the closing note.
41
+ *
42
+ * **`abi.encodePacked` over two strings is raw UTF-8 concatenation** — no length prefix, no padding. That was
43
+ * confirmed against Foundry's own encoder rather than assumed, and every oracle in
44
+ * `vectors/binding/mpp-evm-id-reuse.json` was produced by two independent keccak-256 implementations,
45
+ * neither of them the one used here.
46
+ *
47
+ * **The atrHash's SPELLING is part of the preimage.** The derivation hashes the id *as a string*, so
48
+ * `0x7f83…` and `7f83…` are different challenge ids with different nonces. This module therefore canonicalizes
49
+ * to LCP's own spelling — lowercase, `0x`-prefixed, 32 bytes — before deriving OR comparing, on both the
50
+ * candidate and the observed nonce. An uppercase-hex spelling of the same bytes is a legal input that reaches
51
+ * the same nonce, not a second wire convention: it is normalized, and the vectors pin that both ways round.
52
+ * What is REJECTED is a value that is not 32 hex bytes at all — a missing `0x`, a wrong length, a non-hex
53
+ * digit. Those are different challenge ids rather than spellings of this one, and there is no canonical form
54
+ * to map them to.
55
+ *
56
+ * **Packed concatenation is undelimited, and the fixed length is what makes that safe.** `('ab','c')` and
57
+ * `('a','bc')` pack identically; the vectors pin that as an observed property of the host rule. It is
58
+ * unreachable for this binding because `challenge.id` is always exactly 66 characters, so no other split of
59
+ * the preimage produces a legal id. The length check below is therefore load-bearing, not defensive noise.
60
+ *
61
+ * **NOTHING HERE RECOVERS AN atrHash, and no future edit may add such a path.** keccak-256 has no inverse:
62
+ * the honest surface is confirmation of a candidate the auditor already holds. Re-deriving from a stored
63
+ * challenge would be service-record recovery wearing the name of zero-party recovery, and would falsify
64
+ * `zeroPartyRecoverable: false` in the manifest.
65
+ */
66
+ import type { Outcome, Refusal } from "@integraledger/lcp-binding-core";
67
+ import { type Hex } from "viem";
68
+ /** The challenge the seller emits, and the nonce the buyer's authorization will therefore carry. */
69
+ export interface MppEvmChallengeBinding {
70
+ /** `challenge.id` — the atrHash itself, in LCP's canonical lowercase `0x` spelling. */
71
+ readonly challengeId: Hex;
72
+ /** `challenge.realm` — MPP's protection space, the second half of the preimage. */
73
+ readonly realm: string;
74
+ /** The EIP-3009 nonce `keccak256(abi.encodePacked(id, realm))` requires. DERIVED, never chosen. */
75
+ readonly nonce: Hex;
76
+ }
77
+ /** A candidate atrHash confirmed against a settlement's on-chain nonce. Confirmation, never recovery. */
78
+ export interface MppEvmCandidateConfirmation {
79
+ readonly confirmed: true;
80
+ /** The candidate that reproduced the on-chain nonce, canonicalized. */
81
+ readonly atrHash: Hex;
82
+ /** The protection space the derivation was performed under — the confirmation is scoped to it. */
83
+ readonly realm: string;
84
+ /** The on-chain nonce the candidate reproduced. */
85
+ readonly nonce: Hex;
86
+ }
87
+ /**
88
+ * MPP's derivation, exactly as its EVM method specifies it: `keccak256(abi.encodePacked(id, realm))`.
89
+ *
90
+ * Total over any two non-empty strings, because it implements MPP's rule and not an LCP-specific one — an
91
+ * ordinary opaque challenge id derives here too, which is what makes this the host's function rather than
92
+ * ours. THROWS on an empty id or realm: MPP requires both on every challenge (`realm` is a MUST per
93
+ * §5.1.1), so an empty one is a wiring defect in the caller, not a value to hash.
94
+ */
95
+ export declare function deriveChallengeHash(challengeId: string, realm: string): Hex;
96
+ /**
97
+ * The Id-Reuse weld at PROPOSAL time: `challenge.id = atrHash`, and the nonce that follows from it.
98
+ *
99
+ * THROWS on a malformed atrHash, via `canonicalAtrHash` — a seller welding a hash that is not 32 bytes is a
100
+ * wiring defect, and the guard is also what keeps the fixed-length argument above true of the code.
101
+ *
102
+ * **Do not reach for `binding-evm-common`'s `assertBytes32` here.** It enforces the same 32 bytes but
103
+ * explains them as the value that will "ride as the EIP-3009 nonce" — true on x402, false on this rail, and
104
+ * precisely the cross-rail misreading `binding-evm-x402`'s KNOWN-BAD note exists to stop. On MPP-EVM the
105
+ * atrHash rides `challenge.id` and the nonce is DERIVED from it, never occupied by it. `canonicalAtrHash`
106
+ * states the shape requirement and nothing about where the value rides, which is why it is the right guard:
107
+ * the rail-specific reasoning belongs in this docblock and in the manifest, not in an error string.
108
+ */
109
+ export declare function bindAtrHash(atrHash: string, realm: string): MppEvmChallengeBinding;
110
+ /**
111
+ * Confirm a candidate atrHash against the `AuthorizationUsed` nonces ONE settlement emitted.
112
+ *
113
+ * The auditor brings the atrHash (from the record, or from `legal-context.json`); the chain confirms it.
114
+ * Refusals rather than throws, because both the candidate and the observed nonces are data under audit —
115
+ * the realm is not: it is the verifier's own configuration, so an empty one throws.
116
+ *
117
+ * Every observed nonce is validated BEFORE any is matched: a malformed entry refuses loudly instead of being
118
+ * skipped past, because silently narrowing a settlement view could turn "this transaction is not what you
119
+ * think" into "no match found".
120
+ */
121
+ export declare function checkCandidate(atrHash: string, realm: string, observedNonces: readonly string[]): Outcome<MppEvmCandidateConfirmation>;
122
+ /**
123
+ * The refusal recovery always returns, stated in one place so the reason travels with the code.
124
+ *
125
+ * Not a "not implemented yet" and not a gap to be filled: the on-chain value is a hash over the atrHash, so
126
+ * there is no function from settlement to atrHash to write. A caller wanting the record must bring the
127
+ * candidate and use `verifyCandidate`.
128
+ */
129
+ export declare function notRecoverableByConstruction(): Refusal;
130
+ //# sourceMappingURL=id-reuse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id-reuse.d.ts","sourceRoot":"","sources":["../src/id-reuse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAExE,OAAO,EAAU,KAAK,GAAG,EAA4B,MAAM,MAAM,CAAC;AAKlE,oGAAoG;AACpG,MAAM,WAAW,sBAAsB;IACrC,uFAAuF;IACvF,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,mGAAmG;IACnG,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC;CACrB;AAED,yGAAyG;AACzG,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IACzB,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC;IACtB,kGAAkG;IAClG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,mDAAmD;IACnD,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,CAU3E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,sBAAsB,CAOxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,cAAc,EAAE,SAAS,MAAM,EAAE,GAChC,OAAO,CAAC,2BAA2B,CAAC,CAsCtC;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,IAAI,OAAO,CAQtD"}
@@ -0,0 +1,166 @@
1
+ /**
2
+ * MPP-EVM Id-Reuse (LCP §8.3.5): the host's derivation rule, the weld it produces, and the *only* kind of
3
+ * check the resulting on-chain value permits.
4
+ *
5
+ * **THE HOST RULE, quoted.** `draft-evm-charge-00` §5.3.1 (paymentauth.org, read 2026-07-30):
6
+ *
7
+ * nonce = keccak256(abi.encodePacked(challenge.id, challenge.realm))
8
+ *
9
+ * — with the accompanying sentence "This specification requires the nonce to be set to the `challengeHash`".
10
+ * The nonce is therefore a *derivation*, not a slot: there is nothing here for LCP to occupy. What LCP does
11
+ * instead is supply an input — the seller sets `challenge.id = atrHash` — so the host's own required
12
+ * derivation carries the record into the settlement transaction.
13
+ *
14
+ * **WHAT MAKES THAT SOUND IS NOT UNIQUENESS, AND AN EARLIER DOCBLOCK SAID IT WAS.** It argued that MPP's
15
+ * "Unique challenge identifier" requirement (`draft-httpauth-payment` §5.1.1) was satisfied by making each
16
+ * ATR unique per transaction. Every citation in it was exact; the sufficiency was not. LCP v1.38 §C.1
17
+ * (:1282-1289):
18
+ *
19
+ * > **It is not simply available, and §8.3.5's uniqueness advice is not sufficient for it.** MPP requires
20
+ * > the server to bind the challenge `id` *to the challenge parameters* … expressly to prevent a client
21
+ * > altering the challenge it presents. **Making each ATR unique per transaction satisfies uniqueness but
22
+ * > not that binding.** A per-transaction ATR that itself states the transaction parameters (Section 6.1)
23
+ * > satisfies both, because the parameters are then inside the hashed document; a terms document made
24
+ * > unique by a timestamp alone does not.
25
+ *
26
+ * **THE REQUIREMENT IS ON THE ATR, AND THE TREE ALREADY VERIFIES IT.** This binding holds a hash, never the
27
+ * document, so it cannot inspect what the ATR says — and it should not: where the ATR lives is the seller's
28
+ * and buyer's business, and a rail binding that needed the document would assert a custody LCP does not
29
+ * require. The property §C.1 asks for is **OFR**: the ATR's offer slot is bound, meaning the transaction
30
+ * parameters are inside the hashed document. `verify` implements it as `offerBoundStep`
31
+ * (`verify/src/composition.ts`) and requires it at **TC-4** (`verify/src/required.ts`). So a record whose
32
+ * class is TC-4 has had this checked by the layer that holds the document; a record below TC-4 has not, and
33
+ * the §8.3.5 discharge on this rail is only as good as that rung.
34
+ *
35
+ * The host is internally inconsistent here and the split is kept visible deliberately: the core draft
36
+ * states the challenge-parameter binding as a **MUST** in the `id` field description and as a **SHOULD** in
37
+ * its challenge-binding section. §C.1 names that inconsistency rather than resolving it, and so does this.
38
+ *
39
+ * **Zero-party-recoverable on-chain binding on this rail still requires an Overlay Contract per §8.3.2**
40
+ * (§C.1:1289). Nothing here recovers an atrHash, and nothing may be added that does — see the closing note.
41
+ *
42
+ * **`abi.encodePacked` over two strings is raw UTF-8 concatenation** — no length prefix, no padding. That was
43
+ * confirmed against Foundry's own encoder rather than assumed, and every oracle in
44
+ * `vectors/binding/mpp-evm-id-reuse.json` was produced by two independent keccak-256 implementations,
45
+ * neither of them the one used here.
46
+ *
47
+ * **The atrHash's SPELLING is part of the preimage.** The derivation hashes the id *as a string*, so
48
+ * `0x7f83…` and `7f83…` are different challenge ids with different nonces. This module therefore canonicalizes
49
+ * to LCP's own spelling — lowercase, `0x`-prefixed, 32 bytes — before deriving OR comparing, on both the
50
+ * candidate and the observed nonce. An uppercase-hex spelling of the same bytes is a legal input that reaches
51
+ * the same nonce, not a second wire convention: it is normalized, and the vectors pin that both ways round.
52
+ * What is REJECTED is a value that is not 32 hex bytes at all — a missing `0x`, a wrong length, a non-hex
53
+ * digit. Those are different challenge ids rather than spellings of this one, and there is no canonical form
54
+ * to map them to.
55
+ *
56
+ * **Packed concatenation is undelimited, and the fixed length is what makes that safe.** `('ab','c')` and
57
+ * `('a','bc')` pack identically; the vectors pin that as an observed property of the host rule. It is
58
+ * unreachable for this binding because `challenge.id` is always exactly 66 characters, so no other split of
59
+ * the preimage produces a legal id. The length check below is therefore load-bearing, not defensive noise.
60
+ *
61
+ * **NOTHING HERE RECOVERS AN atrHash, and no future edit may add such a path.** keccak-256 has no inverse:
62
+ * the honest surface is confirmation of a candidate the auditor already holds. Re-deriving from a stored
63
+ * challenge would be service-record recovery wearing the name of zero-party recovery, and would falsify
64
+ * `zeroPartyRecoverable: false` in the manifest.
65
+ */
66
+ import { canonicalAtrHash } from "@integraledger/lcp-kernel";
67
+ import { concat, keccak256, stringToBytes } from "viem";
68
+ /** A 32-byte value in the canonical lowercase-`0x` spelling this binding hashes and compares. */
69
+ const BYTES32 = /^0x[0-9a-fA-F]{64}$/;
70
+ /**
71
+ * MPP's derivation, exactly as its EVM method specifies it: `keccak256(abi.encodePacked(id, realm))`.
72
+ *
73
+ * Total over any two non-empty strings, because it implements MPP's rule and not an LCP-specific one — an
74
+ * ordinary opaque challenge id derives here too, which is what makes this the host's function rather than
75
+ * ours. THROWS on an empty id or realm: MPP requires both on every challenge (`realm` is a MUST per
76
+ * §5.1.1), so an empty one is a wiring defect in the caller, not a value to hash.
77
+ */
78
+ export function deriveChallengeHash(challengeId, realm) {
79
+ if (challengeId === "")
80
+ throw new Error("MPP challenge id must be non-empty — it is half the nonce preimage");
81
+ if (realm === "")
82
+ throw new Error("MPP realm must be non-empty — the core scheme makes realm a MUST on every challenge, and it is half the nonce preimage");
83
+ return keccak256(concat([stringToBytes(challengeId), stringToBytes(realm)]));
84
+ }
85
+ /**
86
+ * The Id-Reuse weld at PROPOSAL time: `challenge.id = atrHash`, and the nonce that follows from it.
87
+ *
88
+ * THROWS on a malformed atrHash, via `canonicalAtrHash` — a seller welding a hash that is not 32 bytes is a
89
+ * wiring defect, and the guard is also what keeps the fixed-length argument above true of the code.
90
+ *
91
+ * **Do not reach for `binding-evm-common`'s `assertBytes32` here.** It enforces the same 32 bytes but
92
+ * explains them as the value that will "ride as the EIP-3009 nonce" — true on x402, false on this rail, and
93
+ * precisely the cross-rail misreading `binding-evm-x402`'s KNOWN-BAD note exists to stop. On MPP-EVM the
94
+ * atrHash rides `challenge.id` and the nonce is DERIVED from it, never occupied by it. `canonicalAtrHash`
95
+ * states the shape requirement and nothing about where the value rides, which is why it is the right guard:
96
+ * the rail-specific reasoning belongs in this docblock and in the manifest, not in an error string.
97
+ */
98
+ export function bindAtrHash(atrHash, realm) {
99
+ const challengeId = canonicalAtrHash(atrHash, "bindAtrHash", "on this rail it rides challenge.id and the EIP-3009 nonce is DERIVED from it (draft-evm-charge-00 §5.3.1), never occupied by it");
100
+ return { challengeId, realm, nonce: deriveChallengeHash(challengeId, realm) };
101
+ }
102
+ /**
103
+ * Confirm a candidate atrHash against the `AuthorizationUsed` nonces ONE settlement emitted.
104
+ *
105
+ * The auditor brings the atrHash (from the record, or from `legal-context.json`); the chain confirms it.
106
+ * Refusals rather than throws, because both the candidate and the observed nonces are data under audit —
107
+ * the realm is not: it is the verifier's own configuration, so an empty one throws.
108
+ *
109
+ * Every observed nonce is validated BEFORE any is matched: a malformed entry refuses loudly instead of being
110
+ * skipped past, because silently narrowing a settlement view could turn "this transaction is not what you
111
+ * think" into "no match found".
112
+ */
113
+ export function checkCandidate(atrHash, realm, observedNonces) {
114
+ if (!BYTES32.test(atrHash))
115
+ return {
116
+ refused: true,
117
+ haltClass: "verification-failure",
118
+ code: "mpp-evm/candidate-malformed",
119
+ detail: `a candidate atrHash must be a 0x-prefixed 32-byte value to be a legal MPP challenge id, got "${atrHash}"`,
120
+ };
121
+ for (const observed of observedNonces)
122
+ if (!BYTES32.test(observed))
123
+ return {
124
+ refused: true,
125
+ haltClass: "verification-failure",
126
+ code: "mpp-evm/nonce-malformed",
127
+ detail: `an observed EIP-3009 nonce must be a 0x-prefixed 32-byte value, got "${observed}"`,
128
+ };
129
+ if (observedNonces.length === 0)
130
+ return {
131
+ refused: true,
132
+ haltClass: "verification-failure",
133
+ code: "mpp-evm/no-settlement-event",
134
+ detail: "no EIP-3009 AuthorizationUsed nonce was observed for this settlement — there is nothing to verify against",
135
+ };
136
+ const candidate = canonicalAtrHash(atrHash, "checkCandidate");
137
+ const nonce = deriveChallengeHash(candidate, realm);
138
+ const matched = observedNonces.find((o) => o.toLowerCase() === nonce);
139
+ if (matched === undefined)
140
+ return {
141
+ refused: true,
142
+ haltClass: "verification-failure",
143
+ code: "mpp-evm/candidate-mismatch",
144
+ detail: `no observed nonce equals keccak256(packed("${candidate}", "${realm}")) = ${nonce}`,
145
+ };
146
+ return {
147
+ ok: true,
148
+ value: { confirmed: true, atrHash: candidate, realm, nonce },
149
+ };
150
+ }
151
+ /**
152
+ * The refusal recovery always returns, stated in one place so the reason travels with the code.
153
+ *
154
+ * Not a "not implemented yet" and not a gap to be filled: the on-chain value is a hash over the atrHash, so
155
+ * there is no function from settlement to atrHash to write. A caller wanting the record must bring the
156
+ * candidate and use `verifyCandidate`.
157
+ */
158
+ export function notRecoverableByConstruction() {
159
+ return {
160
+ refused: true,
161
+ haltClass: "verification-failure",
162
+ code: "mpp-evm/not-recoverable-by-construction",
163
+ detail: "MPP-EVM is an Id-Reuse binding (LCP §8.3.5): the on-chain EIP-3009 nonce is keccak256 over the atrHash and the realm, so no atrHash can be recovered from a settlement — bring the candidate atrHash and use verifyCandidate",
164
+ };
165
+ }
166
+ //# sourceMappingURL=id-reuse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id-reuse.js","sourceRoot":"","sources":["../src/id-reuse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export { createMppEvmAdapter, type MppEvmAdapter, type MppEvmAdapterConfig, } from "./adapter.js";
2
+ export { notAuthorizationCredentialType } from "./credential-type.js";
3
+ export { bindAtrHash, checkCandidate, deriveChallengeHash, type MppEvmCandidateConfirmation, type MppEvmChallengeBinding, notRecoverableByConstruction, } from "./id-reuse.js";
4
+ export { MPP_EVM_MANIFEST } from "./manifest.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,KAAK,aAAa,EAClB,KAAK,mBAAmB,GACzB,MAAM,cAAc,CAAC;AAItB,OAAO,EAAE,8BAA8B,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EACL,WAAW,EACX,cAAc,EACd,mBAAmB,EACnB,KAAK,2BAA2B,EAChC,KAAK,sBAAsB,EAC3B,4BAA4B,GAC7B,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { createMppEvmAdapter, } from "./adapter.js";
2
+ // `assetWasTransferred` and `ERC20_TRANSFER_TOPIC0` moved to `@integraledger/lcp-binding-evm-common` when
3
+ // binding-evm-x402 needed the same predicate. They are NOT re-exported from here: a symbol with two homes
4
+ // is how the two copies drift, which is the thing the move was for.
5
+ export { notAuthorizationCredentialType } from "./credential-type.js";
6
+ export { bindAtrHash, checkCandidate, deriveChallengeHash, notRecoverableByConstruction, } from "./id-reuse.js";
7
+ export { MPP_EVM_MANIFEST } from "./manifest.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,GAGpB,MAAM,cAAc,CAAC;AACtB,0GAA0G;AAC1G,0GAA0G;AAC1G,oEAAoE;AACpE,OAAO,EAAE,8BAA8B,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EACL,WAAW,EACX,cAAc,EACd,mBAAmB,EAGnB,4BAA4B,GAC7B,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,66 @@
1
+ import type { BindingManifest } from "@integraledger/lcp-binding-core";
2
+ /**
3
+ * The MPP-EVM Id-Reuse binding manifest (published as `mpp-evm-derived-v1`).
4
+ *
5
+ * **pattern = "id-reuse"** (LCP §8.3.5), and this is the one member of the family that differs *at the
6
+ * weld*. On MPP's EVM charge method the EIP-3009 nonce is not ours to occupy: `draft-evm-charge-00` §5.3.1
7
+ * REQUIRES `nonce = keccak256(abi.encodePacked(challenge.id, challenge.realm))`, and §5.2.3 fixes the
8
+ * Permit2 witness type string with a MUST ("Implementations MUST use the exact type string above"). There is
9
+ * no free field on this path. What remains available is Id-Reuse: the seller sets `challenge.id = atrHash`,
10
+ * so the host's own derivation carries the record into the settlement transaction.
11
+ *
12
+ * **Do NOT reconcile this manifest against `binding-evm-x402`'s.** That package's docblock records an
13
+ * archived declaration marked KNOWN-BAD for reading MPP-EVM's derivation-MUST onto the x402 path, where no
14
+ * derivation exists. This is where that reading is correct, and the two manifests legitimately differ: x402
15
+ * is off-canonical Native Field on the same nonce field, MPP-EVM is Id-Reuse over a derivation. "Fixing" one
16
+ * to look like the other reintroduces the bug the archive comment exists to stop.
17
+ *
18
+ * **`nativeField` is absent, and its absence is substantive** — not merely the schema's `iff`. Nothing of
19
+ * ours occupies a protocol field here; the atrHash rides an identifier the host protocol already requires.
20
+ *
21
+ * **SCOPE — this manifest declares ONE of MPP's four credential types, and the scope is load-bearing.**
22
+ * `draft-evm-charge-00` defines four ways a client may present payment, and the derived `challengeHash`
23
+ * reaches the chain in exactly one of them:
24
+ * - **§5.3 `authorization`** — the derived value IS the EIP-3009 nonce, so it is committed on-chain. Opt-in
25
+ * and token-conditional: §4.2.2 "Servers MUST only include `"authorization"` when the `currency` token is
26
+ * known to implement EIP-3009", and §5.3.2 the same rule as a prohibition ("Servers MUST NOT advertise
27
+ * `"authorization"` in `credentialTypes` unless the `currency` token is known to implement EIP-3009").
28
+ * **This is the credential type this binding covers, and the only one it can.**
29
+ * - **§5.2 `permit2`** — "The RECOMMENDED credential type", and it carries the SAME derived `challengeHash`
30
+ * inside the EIP-712 `PaymentWitness` struct, which the client signs but which never appears in calldata or
31
+ * an event log; §10.4 places it "in the EIP-712 witness data (Permit2)". Signature-committed, off-chain,
32
+ * unreadable from a settlement.
33
+ * - **§5.4 `transaction`** and **§5.5 `hash`** — plain ERC-20 transfers. No challengeHash anywhere; §10.4
34
+ * says so outright ("weaker challenge binding than Permit2 credentials").
35
+ *
36
+ * So `weldGrades` is keyed by MPP's own credential-type name rather than by a coinage like `derived`: the key
37
+ * IS the scope, machine-readably, and `finality.note` repeats it in prose for whoever reads only the published
38
+ * profile JSON. A grade for `permit2` would be declared from the specification rather than from this binding —
39
+ * the package constructs no witness — and `transaction`/`hash` have no weld to grade. Generalizing the
40
+ * strongest leg to the whole rail is the failure this scoping exists to prevent, and the adapter enforces the
41
+ * same scope at runtime: a settlement that moved the token without an `AuthorizationUsed` beside it is refused
42
+ * by credential type, never reported as an absence of settlement.
43
+ *
44
+ * **The WLD-3 triple, under that scope:**
45
+ * - `onChain: true` — the derived value is the EIP-3009 nonce, emitted as an indexed topic of
46
+ * `AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)` and consumed on-chain by the
47
+ * token contract (§8: "The nonce is consumed on-chain by the token contract itself"). **Read from the host
48
+ * specification and from the adapter's own behaviour on synthetic logs; NOT yet from a live MPP-EVM
49
+ * settlement** — the repository's opt-in on-chain integration suite discharges that against a real transaction, and until
50
+ * it runs, this member is the one still owed a live proof.
51
+ * - `zeroPartyRecoverable: false` — keccak-256 has no inverse. An auditor holding only the transaction hash
52
+ * reads the nonce and cannot obtain the atrHash from it. Verification of a *candidate* is the whole of
53
+ * what this rail offers, which is why the adapter exposes `verifyCandidate` and why `recover` refuses.
54
+ * Observed of the code: no member maps a settlement to an atrHash, and `recover` takes no arguments.
55
+ * - `forwardIndexable: false` — §8.3's criterion is enumeration "bound to a given `atrHash`", and §8.3.5
56
+ * states it directly: "Not forward-indexable by `atrHash` alone — the on-chain value is a hash over
57
+ * `atrHash` and other inputs." Knowing the realm as well, one *could* derive the nonce and topic-filter it,
58
+ * but that is realm-scoped, and it is degenerate besides: §8.3.5's uniqueness satisfaction makes each ATR
59
+ * unique per transaction, so there is at most one settlement per atrHash to enumerate. Hence `indexing:
60
+ * "none"` and no `enumerate` member. Observed of the code: no `enumerate` exists to call.
61
+ *
62
+ * `weldGrades.authorization = "signature"`: the payer's EIP-3009 typed-data message covers the `nonce` field,
63
+ * so the buyer's signature — not merely an inclusion in a transaction — commits the derived value.
64
+ */
65
+ export declare const MPP_EVM_MANIFEST: BindingManifest;
66
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,eAAO,MAAM,gBAAgB,EAAE,eAkB9B,CAAC"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The MPP-EVM Id-Reuse binding manifest (published as `mpp-evm-derived-v1`).
3
+ *
4
+ * **pattern = "id-reuse"** (LCP §8.3.5), and this is the one member of the family that differs *at the
5
+ * weld*. On MPP's EVM charge method the EIP-3009 nonce is not ours to occupy: `draft-evm-charge-00` §5.3.1
6
+ * REQUIRES `nonce = keccak256(abi.encodePacked(challenge.id, challenge.realm))`, and §5.2.3 fixes the
7
+ * Permit2 witness type string with a MUST ("Implementations MUST use the exact type string above"). There is
8
+ * no free field on this path. What remains available is Id-Reuse: the seller sets `challenge.id = atrHash`,
9
+ * so the host's own derivation carries the record into the settlement transaction.
10
+ *
11
+ * **Do NOT reconcile this manifest against `binding-evm-x402`'s.** That package's docblock records an
12
+ * archived declaration marked KNOWN-BAD for reading MPP-EVM's derivation-MUST onto the x402 path, where no
13
+ * derivation exists. This is where that reading is correct, and the two manifests legitimately differ: x402
14
+ * is off-canonical Native Field on the same nonce field, MPP-EVM is Id-Reuse over a derivation. "Fixing" one
15
+ * to look like the other reintroduces the bug the archive comment exists to stop.
16
+ *
17
+ * **`nativeField` is absent, and its absence is substantive** — not merely the schema's `iff`. Nothing of
18
+ * ours occupies a protocol field here; the atrHash rides an identifier the host protocol already requires.
19
+ *
20
+ * **SCOPE — this manifest declares ONE of MPP's four credential types, and the scope is load-bearing.**
21
+ * `draft-evm-charge-00` defines four ways a client may present payment, and the derived `challengeHash`
22
+ * reaches the chain in exactly one of them:
23
+ * - **§5.3 `authorization`** — the derived value IS the EIP-3009 nonce, so it is committed on-chain. Opt-in
24
+ * and token-conditional: §4.2.2 "Servers MUST only include `"authorization"` when the `currency` token is
25
+ * known to implement EIP-3009", and §5.3.2 the same rule as a prohibition ("Servers MUST NOT advertise
26
+ * `"authorization"` in `credentialTypes` unless the `currency` token is known to implement EIP-3009").
27
+ * **This is the credential type this binding covers, and the only one it can.**
28
+ * - **§5.2 `permit2`** — "The RECOMMENDED credential type", and it carries the SAME derived `challengeHash`
29
+ * inside the EIP-712 `PaymentWitness` struct, which the client signs but which never appears in calldata or
30
+ * an event log; §10.4 places it "in the EIP-712 witness data (Permit2)". Signature-committed, off-chain,
31
+ * unreadable from a settlement.
32
+ * - **§5.4 `transaction`** and **§5.5 `hash`** — plain ERC-20 transfers. No challengeHash anywhere; §10.4
33
+ * says so outright ("weaker challenge binding than Permit2 credentials").
34
+ *
35
+ * So `weldGrades` is keyed by MPP's own credential-type name rather than by a coinage like `derived`: the key
36
+ * IS the scope, machine-readably, and `finality.note` repeats it in prose for whoever reads only the published
37
+ * profile JSON. A grade for `permit2` would be declared from the specification rather than from this binding —
38
+ * the package constructs no witness — and `transaction`/`hash` have no weld to grade. Generalizing the
39
+ * strongest leg to the whole rail is the failure this scoping exists to prevent, and the adapter enforces the
40
+ * same scope at runtime: a settlement that moved the token without an `AuthorizationUsed` beside it is refused
41
+ * by credential type, never reported as an absence of settlement.
42
+ *
43
+ * **The WLD-3 triple, under that scope:**
44
+ * - `onChain: true` — the derived value is the EIP-3009 nonce, emitted as an indexed topic of
45
+ * `AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)` and consumed on-chain by the
46
+ * token contract (§8: "The nonce is consumed on-chain by the token contract itself"). **Read from the host
47
+ * specification and from the adapter's own behaviour on synthetic logs; NOT yet from a live MPP-EVM
48
+ * settlement** — the repository's opt-in on-chain integration suite discharges that against a real transaction, and until
49
+ * it runs, this member is the one still owed a live proof.
50
+ * - `zeroPartyRecoverable: false` — keccak-256 has no inverse. An auditor holding only the transaction hash
51
+ * reads the nonce and cannot obtain the atrHash from it. Verification of a *candidate* is the whole of
52
+ * what this rail offers, which is why the adapter exposes `verifyCandidate` and why `recover` refuses.
53
+ * Observed of the code: no member maps a settlement to an atrHash, and `recover` takes no arguments.
54
+ * - `forwardIndexable: false` — §8.3's criterion is enumeration "bound to a given `atrHash`", and §8.3.5
55
+ * states it directly: "Not forward-indexable by `atrHash` alone — the on-chain value is a hash over
56
+ * `atrHash` and other inputs." Knowing the realm as well, one *could* derive the nonce and topic-filter it,
57
+ * but that is realm-scoped, and it is degenerate besides: §8.3.5's uniqueness satisfaction makes each ATR
58
+ * unique per transaction, so there is at most one settlement per atrHash to enumerate. Hence `indexing:
59
+ * "none"` and no `enumerate` member. Observed of the code: no `enumerate` exists to call.
60
+ *
61
+ * `weldGrades.authorization = "signature"`: the payer's EIP-3009 typed-data message covers the `nonce` field,
62
+ * so the buyer's signature — not merely an inclusion in a transaction — commits the derived value.
63
+ */
64
+ export const MPP_EVM_MANIFEST = {
65
+ rail: "evm:mpp",
66
+ protocol: "mpp",
67
+ pattern: "id-reuse",
68
+ recovery: {
69
+ onChain: true,
70
+ zeroPartyRecoverable: false,
71
+ forwardIndexable: false,
72
+ },
73
+ assetBinding: "filtered", // candidate verification filters the configured token's own log (AuthorizationUsed / assetWasTransferred)
74
+ successGate: "structural", // a reverted tx emits no logs, so the weld event cannot exist
75
+ indexing: "none",
76
+ finality: {
77
+ reversible: false,
78
+ note: "scoped to MPP's `authorization` credential type (§5.3), the only one of the four whose challengeHash reaches the chain — under the RECOMMENDED `permit2` type (§5.2) the same derived value is signature-committed in an off-chain EIP-712 witness, and `transaction`/`hash` (§5.4/§5.5) bind no challenge at all. Within that scope: final on settlement — the EIP-3009 authorization is consumed on-chain and there is no on-rail reversal; the weld is derivation-bound (nonce = keccak256(abi.encodePacked(challenge.id, challenge.realm)), draft-evm-charge-00 §5.3.1), so a candidate atrHash is VERIFIED against the on-chain nonce and never recovered from it; recourse is the record's elected forum (PAY-3/RCS-5), never dispute resolution. The §8.3.5 discharge rests on the ATR STATING the transaction parameters (LCP §6.1), not on ATR uniqueness: MPP binds the challenge id to the challenge parameters, and uniqueness alone does not satisfy that (§C.1). The tree checks it as OFR — `offerBoundStep`, required at TC-4 — so this weld is only as strong as the record's class. Zero-party-recoverable on-chain binding on this rail still requires an Overlay Contract per §8.3.2",
79
+ },
80
+ weldGrades: { authorization: "signature" },
81
+ lifecycleStates: ["proposed", "settled"],
82
+ };
83
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAoB;IAC/C,IAAI,EAAE,SAAS;IACf,QAAQ,EAAE,KAAK;IACf,OAAO,EAAE,UAAU;IACnB,QAAQ,EAAE;QACR,OAAO,EAAE,IAAI;QACb,oBAAoB,EAAE,KAAK;QAC3B,gBAAgB,EAAE,KAAK;KACxB;IACD,YAAY,EAAE,UAAU,EAAE,0GAA0G;IACpI,WAAW,EAAE,YAAY,EAAE,8DAA8D;IACzF,QAAQ,EAAE,MAAM;IAChB,QAAQ,EAAE;QACR,UAAU,EAAE,KAAK;QACjB,IAAI,EAAE,0oCAA0oC;KACjpC;IACD,UAAU,EAAE,EAAE,aAAa,EAAE,WAAW,EAAE;IAC1C,eAAe,EAAE,CAAC,UAAU,EAAE,SAAS,CAAC;CACzC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@integraledger/lcp-binding-evm-mpp",
3
+ "version": "0.9.0",
4
+ "description": "Welds an LCP record into an MPP settlement on EVM by Id-Reuse (LCP §8.3.5).",
5
+ "keywords": [
6
+ "lcp",
7
+ "legal-context-protocol",
8
+ "legal-terms",
9
+ "agentic-commerce",
10
+ "settlement",
11
+ "atr-hash",
12
+ "evm",
13
+ "mpp"
14
+ ],
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src",
27
+ "CHANGELOG.md",
28
+ "LICENSE",
29
+ "NOTICE"
30
+ ],
31
+ "publishConfig": {
32
+ "registry": "https://registry.npmjs.org",
33
+ "access": "public"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/IntegraLedger/integra-protocol.git",
38
+ "directory": "packages/binding-evm-mpp"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/IntegraLedger/integra-protocol/issues"
42
+ },
43
+ "homepage": "https://github.com/IntegraLedger/integra-protocol/tree/main/packages/binding-evm-mpp#readme",
44
+ "dependencies": {
45
+ "viem": "2.55.11",
46
+ "@integraledger/lcp-binding-core": "0.9.0",
47
+ "@integraledger/lcp-binding-evm-common": "0.9.0",
48
+ "@integraledger/lcp-kernel": "0.9.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "24.13.3",
52
+ "vitest": "4.1.10"
53
+ },
54
+ "license": "Apache-2.0",
55
+ "engines": {
56
+ "node": ">=24"
57
+ },
58
+ "scripts": {
59
+ "build": "tsc -p tsconfig.build.json",
60
+ "typecheck": "tsc -p tsconfig.json --noEmit",
61
+ "test": "vitest run"
62
+ }
63
+ }