@integraledger/lcp-placement-x402 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 +207 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +103 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +138 -0
- package/dist/manifest.js.map +1 -0
- package/dist/placement.d.ts +16 -0
- package/dist/placement.d.ts.map +1 -0
- package/dist/placement.js +91 -0
- package/dist/placement.js.map +1 -0
- package/package.json +59 -0
- package/src/index.ts +2 -0
- package/src/manifest.ts +145 -0
- package/src/placement.ts +116 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @integraledger/lcp-placement-x402
|
|
2
|
+
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
First public release.
|
|
6
|
+
|
|
7
|
+
`0.9.0` is deliberate: this is a release candidate for 1.0, not a preview. The implementation is complete
|
|
8
|
+
against LCP v1.38 and certified by the conformance corpus, and the remaining distance to 1.0 is the
|
|
9
|
+
specification's own — the standard is still moving through its steering committee, and this package will not
|
|
10
|
+
claim a stability its protocol has not yet promised.
|
|
11
|
+
|
|
12
|
+
Development before this release happened in a private repository and is not reproduced here; no earlier
|
|
13
|
+
version was ever available to install.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Integra Protocol
|
|
2
|
+
Copyright 2026 Integra Ledger
|
|
3
|
+
|
|
4
|
+
This product includes software developed by Integra Ledger (https://integraledger.com).
|
|
5
|
+
|
|
6
|
+
"Integra", "Integra Ledger", and the Integra logo are trademarks of Integra Ledger.
|
|
7
|
+
Trademark use is not granted by the Apache License 2.0 (see Section 6).
|
|
8
|
+
|
|
9
|
+
The Legal Context Protocol is co-stewarded by Integra Ledger and AAA-ICDR. This repository is a
|
|
10
|
+
reference implementation of the standard's open layer; the specification itself is published at
|
|
11
|
+
https://legalcontextprotocol.org/standard and is not part of this distribution.
|
|
12
|
+
|
|
13
|
+
The vendored submodule under lib/commerce-payments is Coinbase's AuthCaptureEscrow and carries its own
|
|
14
|
+
MIT licence at lib/commerce-payments/LICENSE.md. It is not covered by the Apache License above.
|
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# @integraledger/lcp-placement-x402
|
|
2
|
+
|
|
3
|
+
Places an LCP reference into an [x402](https://x402.org) v2 payment challenge, and reads it back out.
|
|
4
|
+
|
|
5
|
+
**This is the HTTP-layer carrier, not the x402 weld.** [`@integraledger/lcp-binding-evm-x402`](../binding-evm-x402#readme) binds `atrHash`
|
|
6
|
+
into the EIP-3009 authorization nonce — that is the settlement binding, and it answers *what did the money
|
|
7
|
+
commit to?* This package answers a different question — *where does the reference ride on the wire?* — and
|
|
8
|
+
both are true at the same time. x402 is the proof that one protocol can need a binding **and** a placement.
|
|
9
|
+
It is also the reason this package exists at all: the carrier it declares was previously only a private
|
|
10
|
+
TypeScript interface inside a seller implementation, and a private convention nobody outside that codebase
|
|
11
|
+
can read is not an artifact.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @integraledger/lcp-placement-x402
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
| | |
|
|
18
|
+
|---|---|
|
|
19
|
+
| **Chain** | none here — settlement is `binding-evm-x402`'s, on whichever EVM rail the scheme selects |
|
|
20
|
+
| **Pattern** | `http-advisory` (LCP §8.3.7, Tier A) |
|
|
21
|
+
| **Field** | `extensions.legalContext.info` — the top-level extensions map, the carrier x402 protects |
|
|
22
|
+
| **Read also** | `accepts.0.extra.atrHash` — a **bare** hash, its own encoding (integrity) |
|
|
23
|
+
| **Terms URL** | `extensions.legalContext.info.legalContextUrl` — declared, never written by `place` |
|
|
24
|
+
| **Carrier types** | `sha256`, `url` |
|
|
25
|
+
| **Spec** | x402 v2 (`x402-foundation/x402@1fec3aa04e41`, `specs/x402-specification-v2.md`), gate discharged **2026-07-30** |
|
|
26
|
+
|
|
27
|
+
## Use
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { X402_PLACEMENT, x402Placement } from "@integraledger/lcp-placement-x402";
|
|
31
|
+
|
|
32
|
+
declare const challenge: unknown; // the x402 402 challenge document, as received
|
|
33
|
+
|
|
34
|
+
const placed = x402Placement.place({ type: "sha256", value: "0x…" }, challenge);
|
|
35
|
+
const ref = x402Placement.extract(challenge); // reads either carrier, canonical first
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Both members are total: a refusal is a returned value, never a thrown exception. Entries **beside** our own in
|
|
39
|
+
the `extensions` map are preserved on every `place`. That is narrower than x402's echo rule, which protects the
|
|
40
|
+
`legalContext` entry's *contents* too — see *Known limitations*: `place` replaces our entry whole, so it is a
|
|
41
|
+
seller's write verb, not a buyer's echo verb.
|
|
42
|
+
|
|
43
|
+
## Specification provenance — verified against the live host, 2026-07-30
|
|
44
|
+
|
|
45
|
+
Read against the live x402 v2 specification in the x402 repository (`specs/x402-specification-v2.md`, §5.1.2
|
|
46
|
+
and the PaymentRequirements table), four facts were confirmed and each one decides something here:
|
|
47
|
+
|
|
48
|
+
1. **`extensions` is carried on `PaymentRequired`, `PaymentPayload` and `SettlementResponse`.** A reference
|
|
49
|
+
placed there is on the **receipt**, not only the proposal — so the record can bind at execution time.
|
|
50
|
+
2. **Each entry carries `info` and `schema`** — `info` is "Extension-specific data provided by the server",
|
|
51
|
+
`schema` is a "JSON Schema defining the expected structure of `info`". Nothing in the spec fetches or
|
|
52
|
+
validates `schema`; it is a pointer. That `{info, schema}` wrapper is why this is the one placement in the
|
|
53
|
+
plan that overrides the kit's `place` (see below).
|
|
54
|
+
3. **The echo rule:** "The client must include at least the info received; it may append additional info but
|
|
55
|
+
cannot delete or overwrite existing info." This is the carrier the **protocol itself protects**, which is
|
|
56
|
+
why it is canonical here rather than the per-requirement object.
|
|
57
|
+
4. **`extra` is "Scheme-specific additional information"** on a `PaymentRequirements` entry — the payment
|
|
58
|
+
scheme's object, whose contents that scheme defines. So `place` **never writes there**; the alias is
|
|
59
|
+
read-only, and that is a decision about whose namespace it is, not an omission.
|
|
60
|
+
|
|
61
|
+
Extension identifiers are implementation-defined strings — no registry, no reverse-domain rule — so the
|
|
62
|
+
`legalContext` key is available today and this placement is Tier A on the wire.
|
|
63
|
+
|
|
64
|
+
### Drift from LCP v1.37 §C.4, recorded rather than absorbed
|
|
65
|
+
|
|
66
|
+
The host governs: its live specification is binding and LCP's Appendix C is an illustration.
|
|
67
|
+
Two differences, neither of which changes the design:
|
|
68
|
+
|
|
69
|
+
- v1.37 renders the echo rule in RFC-2119 capitals ("MAY append to but **MUST NOT** delete or overwrite")
|
|
70
|
+
and states it about the extensions map. The live spec states it in lower case and about the `info` payload.
|
|
71
|
+
Same rule, weaker modality, one level lower.
|
|
72
|
+
- The live spec also carries `extra` on `PaymentPayload.accepted`, which §C.4 does not mention.
|
|
73
|
+
|
|
74
|
+
### Drift from a strict buyer-side reader, recorded for the same reason
|
|
75
|
+
|
|
76
|
+
Buyer gates that parse this wire document already exist. The **paths and shapes** match field for field — `accepts[0].extra.atrHash`, `accepts[0].extra.legalContextUrl`,
|
|
77
|
+
`extensions.legalContext.info.{type,value,legalContextUrl}`. The **resolution semantics do not**, and three
|
|
78
|
+
differences are real. Recording them is the whole point of declaring the carrier: an undisclosed
|
|
79
|
+
divergence between two readers of one document is the drift this package exists to end, not a smaller version
|
|
80
|
+
of it.
|
|
81
|
+
|
|
82
|
+
1. **Carrier precedence is inverted.** This placement answers with the **canonical `extensions` slot** when
|
|
83
|
+
both carriers are present — the manifest's declared field wins, which is `binding-core`'s rule for every
|
|
84
|
+
protocol in the set. A buyer gate may instead prefer **`accepts[].extra`**, on the ground that it is the
|
|
85
|
+
per-requirement carrier and binds to the requirement actually being paid.
|
|
86
|
+
Measured on one challenge carrying `0xaa…` in `extensions` and `0xbb…` in `extra`, this package answers
|
|
87
|
+
`0xaa…`.
|
|
88
|
+
2. **Carrier disagreement is resolved here and REFUSED by a strict reader.** A buyer gate may refuse outright
|
|
89
|
+
rather than pick, because two values on one challenge would let a seller advertise different terms to
|
|
90
|
+
different readers. A placement is structural — it reads the strongest
|
|
91
|
+
declared carrier and does not adjudicate the host's document — so it answers with the canonical value and
|
|
92
|
+
says nothing. A caller that needs the commerce reader's guarantee must compare both carriers itself:
|
|
93
|
+
`readDeclaredPaths` returns the first hit, not the set.
|
|
94
|
+
3. **A `url` carrier is placeable here and rejected there.** `carrierTypes` permits `sha256` and `url`;
|
|
95
|
+
a strict buyer gate refuses `extensions.legalContext.info.type !== "sha256"` outright, because it compares
|
|
96
|
+
the advertised value against a recomputed record hash and nothing but a hash can be. See
|
|
97
|
+
*Known limitations*.
|
|
98
|
+
|
|
99
|
+
Reconciling 1 and 2 belongs to a universal buyer parser that reads both carriers through this manifest; a
|
|
100
|
+
follow-on re-expresses the seller's private carrier interface in terms of this manifest and touches the
|
|
101
|
+
**writer** only. Neither is done here.
|
|
102
|
+
|
|
103
|
+
### Tier B forward work — prose only, no manifest
|
|
104
|
+
|
|
105
|
+
§C.4's two forward paths are real and neither is shippable: a reference inside the **signed** Offer/Receipt
|
|
106
|
+
artifact (the EIP-712 types are closed structures and any change to them is a breaking version change; under
|
|
107
|
+
JWS an added claim is signed but explicitly uninterpreted), and a **registered** extension identifier with a
|
|
108
|
+
published schema (a standardization step, not a protocol change). No Tier B manifest is declared for either —
|
|
109
|
+
a manifest carrying a shape whose owner has not defined it is exactly the assertion this seam refuses.
|
|
110
|
+
|
|
111
|
+
## The one override, and why it earns it
|
|
112
|
+
|
|
113
|
+
`extract` is the kit's, unchanged: reading `extensions.legalContext.info` is an ordinary object-path read,
|
|
114
|
+
and the bare-hash alias is handled by its own declared `encoding`. `place` is overridden, because x402's slot
|
|
115
|
+
does not hold the reference — it holds `{ info, schema }`, a **wrapper** that no container kind models.
|
|
116
|
+
Inventing an `x402-extension` container kind would put one protocol's name inside a generic enum, which is
|
|
117
|
+
the abstraction leaking. One overridden member is composition; the test suite asserts `extract` still behaves
|
|
118
|
+
exactly as a freshly built kit adapter does, over every accept-and-refuse path, so a later edit cannot
|
|
119
|
+
quietly fork the read half too.
|
|
120
|
+
|
|
121
|
+
**The override changes the shape it writes, never which documents it will write into.** `place` refuses
|
|
122
|
+
exactly what a kit adapter built from this manifest alone refuses, over the same inputs and with the same
|
|
123
|
+
codes, and a test pins that equality. Two rules do the work:
|
|
124
|
+
|
|
125
|
+
- **Own properties only.** A challenge with zero own properties does not inherit an `extensions` map into the
|
|
126
|
+
document we emit. `extract` reports such a document as `reference-absent`, and the two halves must agree
|
|
127
|
+
about what is present — a `place` document is exactly as attacker-influenced as an `extract` one.
|
|
128
|
+
- **An `extensions` that is present and is not a map REFUSES** (`x402/document-malformed`), rather than being
|
|
129
|
+
replaced. The declared field is `extensions.legalContext.info`, so `legalContext` is the field's direct
|
|
130
|
+
holder and `extensions` sits one level **above** it: `binding-core`'s malformed-container rule replaces at
|
|
131
|
+
the holder and refuses above it, because replacing an intermediate discards everything beneath. Absent is
|
|
132
|
+
still created — that is the extension point working — and our own entry, being the direct holder, is still
|
|
133
|
+
replaced.
|
|
134
|
+
|
|
135
|
+
## Two carriers, and the alias has a different SHAPE
|
|
136
|
+
|
|
137
|
+
x402 is the only protocol in the set whose alias is encoded differently from its canonical field. The
|
|
138
|
+
canonical slot holds an LCP §8.1 `{type, value}` object; `accepts[0].extra.atrHash` holds a **bare** hash.
|
|
139
|
+
Writing `lcp:sha256:0x…` into `extra.atrHash` would emit a field neither our own seller nor any x402
|
|
140
|
+
counterparty parses. This is the reason a `readAlso` entry declares its own encoding at all.
|
|
141
|
+
|
|
142
|
+
The alias is **index 0 only**. A locator names one path, and `accepts[0]` is what the shipped buyer parser
|
|
143
|
+
reads: the reference must bind to the requirement actually being paid, and searching every requirement would
|
|
144
|
+
let a seller park a second set of terms on an alternative it never expects to be chosen.
|
|
145
|
+
|
|
146
|
+
Reaching that path is also the reason `binding-core`'s `readAtPath` now indexes arrays — narrowly, on a
|
|
147
|
+
canonical non-negative integer segment only, so `length` and every other array property stay unreachable.
|
|
148
|
+
That relaxation lives in the kit rather than in a private loop here, because a read rule nobody can find is
|
|
149
|
+
the same defect as a carrier nobody can find.
|
|
150
|
+
|
|
151
|
+
## Known limitations
|
|
152
|
+
|
|
153
|
+
- **`place` writes the hash, never the terms URL.** `place(ref, doc)` holds one reference; the terms URL is a
|
|
154
|
+
second datum. `termsUrlField` is declared so a parser can find the URL our seller does emit, the same
|
|
155
|
+
division `placement-acp` draws with `metadata.legal_context_url`.
|
|
156
|
+
- **The shipped carrier repeats the URL at `accepts[0].extra.legalContextUrl`.** A single `termsUrlField`
|
|
157
|
+
cannot express two spellings, so that one is recorded here rather than half-declared in the manifest.
|
|
158
|
+
- **A `url` carrier is placeable here and no shipped x402 reader accepts one.** `carrierTypes` permits
|
|
159
|
+
`sha256` and `url` — the §8.1 integrity form and the §8.1 discovery form, the same pair `placement-acp` and
|
|
160
|
+
`placement-ucp` permit — while a strict buyer gate refuses any `info.type` but `sha256`. The permission is not
|
|
161
|
+
narrowed away here because narrowing the reference field to one type is a statement about what the slot may
|
|
162
|
+
hold across the whole set, not a fact about x402. **Related and not ours to fix in this package:**
|
|
163
|
+
`readDeclaredPaths` labels a canonical-field hit `carrierClass: "integrity"` unconditionally, so a `url`
|
|
164
|
+
read from `extensions.legalContext.info` is labelled `integrity` and passes `requireIntegrity()` (from
|
|
165
|
+
[`@integraledger/lcp-binding-core`](../binding-core#readme), like `makePlacement`). That is
|
|
166
|
+
`binding-core` behaviour that `placement-acp` and `placement-ucp` already share on `main`; this manifest
|
|
167
|
+
only adds a third reference field with the same property, and `requireIntegrity` has no production caller
|
|
168
|
+
yet.
|
|
169
|
+
- **Our own `legalContext` entry is replaced whole, not merged — and the key that loses is `legalContextUrl`,
|
|
170
|
+
the one `termsUrlField` points at.** `place` writes `{info, schema}`, so nothing previously inside our entry
|
|
171
|
+
survives, and on the document sellers actually emit that is concretely the terms-URL half: measured on
|
|
172
|
+
the vector case for the long-standing shipped carrier, `termsUrlField` reads
|
|
173
|
+
`https://seller.example/.well-known/legal-context.json` before `place` and `undefined` after. This is the
|
|
174
|
+
kit's leaf-write semantics, not an override defect — `makePlacement(X402_PLACEMENT).place` drops it
|
|
175
|
+
identically — and it is why the previous bullet says `place` holds one reference: a caller that needs both
|
|
176
|
+
halves on the wire writes the URL itself.
|
|
177
|
+
**Consequence for a buyer.** Do not build a `PaymentPayload` echo with `place`. x402's rule is that the
|
|
178
|
+
client "cannot delete or overwrite existing `info`", and re-placing over a server-sent entry deletes the
|
|
179
|
+
URL the server put there. Echo the received entry verbatim and place only where no entry exists yet; the
|
|
180
|
+
reconciled read/echo path belongs to a universal buyer parser, not this package.
|
|
181
|
+
x402 defines only `info` and `schema` for an entry and the `legalContext` key is ours, so owning the entry is
|
|
182
|
+
the point for the *seller* direction; entries *beside* ours in the `extensions` map are preserved
|
|
183
|
+
unconditionally.
|
|
184
|
+
- **`LEGAL_CONTEXT_SCHEMA_REF` points at a document the LCP TSC has not published yet**
|
|
185
|
+
(`https://legalcontextprotocol.org/schemas/lcp-extension.json`). It is byte-identical to what emitters
|
|
186
|
+
emits, and it is never fetched by anything in x402. Publishing it — or repointing it — is a **wire change
|
|
187
|
+
and must be made in both repos at once**; repointing it here alone would recreate the drift this package
|
|
188
|
+
exists to close.
|
|
189
|
+
|
|
190
|
+
## Provenance
|
|
191
|
+
|
|
192
|
+
Cut against x402 v2 (`x402-foundation/x402@1fec3aa04e41`, `specs/x402-specification-v2.md`, read 2026-07-30) and reconciled
|
|
193
|
+
against LCP v1.37 §C.4 the same day. The **paths and shapes** are matched field-for-field against the shipped
|
|
194
|
+
shipped seller carrier and the buyer parsers that read it; the **resolution semantics diverge from a strict
|
|
195
|
+
buyer gate in three recorded ways**, and *Drift from a strict buyer-side reader*
|
|
196
|
+
above states each one and who closes it. Both drift sections are the disclosure, not a to-do list — a
|
|
197
|
+
divergence written down is governed; the same divergence unwritten is the undisclosed-drift defect wearing
|
|
198
|
+
a new name.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
**Requires Node >= 24.** Part of the
|
|
203
|
+
[Legal Context Protocol open layer](https://github.com/IntegraLedger/integra-protocol) — see the
|
|
204
|
+
[documentation](https://github.com/IntegraLedger/integra-protocol/blob/main/docs/developer/index.md)
|
|
205
|
+
and the
|
|
206
|
+
[package index](https://github.com/IntegraLedger/integra-protocol/blob/main/docs/developer/reference.md).
|
|
207
|
+
Apache-2.0.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { PlacementManifest } from "@integraledger/lcp-binding-core";
|
|
2
|
+
/**
|
|
3
|
+
* The carrier's JSON Schema, INLINED rather than referenced.
|
|
4
|
+
*
|
|
5
|
+
* x402 makes `schema` a REQUIRED member of an extension entry — "JSON Schema defining the expected
|
|
6
|
+
* structure of info" — so whatever goes here is on the wire of every challenge. A `$ref` to a URL nobody
|
|
7
|
+
* serves would be a required member no counterparty can resolve, and "x402 never fetches it" is a reason
|
|
8
|
+
* that does not break, not a reason to ship it. (`https://legalcontextprotocol.org/schemas/lcp-extension.json`
|
|
9
|
+
* returns **404**, measured 2026-08-08 — the schema is inlined instead.)
|
|
10
|
+
*
|
|
11
|
+
* **Both extensions published in the x402 repository inline a complete JSON Schema** rather than
|
|
12
|
+
* referencing an external document, and LCP v1.38 §C.4 says to do one or the other: "Because `schema` is a
|
|
13
|
+
* REQUIRED member, publish a resolvable schema or inline it." Inlining also removes a hosting dependency
|
|
14
|
+
* the deployment does not currently meet — the same dependency the UCP capability still owes.
|
|
15
|
+
*
|
|
16
|
+
* The shape is the §8.1 reference object this placement writes into `info`, and nothing more: it describes
|
|
17
|
+
* the carrier, not the terms behind it.
|
|
18
|
+
*
|
|
19
|
+
* Changing this is a WIRE change — it appears in every challenge — so it is a frozen literal rather than a
|
|
20
|
+
* value assembled at call time.
|
|
21
|
+
*/
|
|
22
|
+
export declare const LEGAL_CONTEXT_SCHEMA: Readonly<Record<string, unknown>>;
|
|
23
|
+
/**
|
|
24
|
+
* x402 reference placement — the HTTP-layer carrier, cut against the live x402 v2 specification
|
|
25
|
+
* (`x402-foundation/x402@1fec3aa04e41`, `specs/x402-specification-v2.md`; gate discharged in the README).
|
|
26
|
+
*
|
|
27
|
+
* **THE CANONICAL REPOSITORY IS THE FOUNDATION'S, and the citation moved on 2026-08-08.** Every
|
|
28
|
+
* reference here named `coinbase/x402`, which the GitHub API reports as `"fork": true`;
|
|
29
|
+
* `x402-foundation/x402` is `"fork": false` and is what LCP v1.38 §C.4's own *Checked against* line
|
|
30
|
+
* reads. Citing a fork invites a reader to diff against a copy that may lag. The revision is pinned
|
|
31
|
+
* rather than left as a bare repo name, because "the live spec" with no commit is not a claim anyone
|
|
32
|
+
* can re-check.
|
|
33
|
+
*
|
|
34
|
+
* **This is NOT the x402 weld.** `@integraledger/lcp-binding-evm-x402` binds `atrHash` into the EIP-3009 nonce —
|
|
35
|
+
* that is the SETTLEMENT binding and it answers "what did the money commit to?". This manifest answers a
|
|
36
|
+
* different question — "where does the reference ride on the wire?" — and both are true at once. x402 is the
|
|
37
|
+
* proof that one protocol can need a binding AND a placement.
|
|
38
|
+
*
|
|
39
|
+
* **Tier A in both slots, and the canonical one is the map the protocol PROTECTS.** x402 v2 defines a
|
|
40
|
+
* per-requirement `accepts[].extra` object and a top-level `extensions` map, the latter carried on the
|
|
41
|
+
* `PaymentRequired` challenge, the `PaymentPayload` AND the `SettlementResponse` — so a reference placed
|
|
42
|
+
* there is on the receipt, not only the proposal. Each `extensions` entry carries `info` ("Extension-specific
|
|
43
|
+
* data provided by the server") and `schema` ("JSON Schema defining the expected structure of `info`"). The
|
|
44
|
+
* live spec's echo rule — "The client must include at least the info received; it may append additional info
|
|
45
|
+
* but cannot delete or overwrite existing info" — is what makes that map the more durable carrier, and it is
|
|
46
|
+
* why it is canonical here.
|
|
47
|
+
*
|
|
48
|
+
* **`pattern` is `http-advisory`, not `protocol-extension`.** §8.3.6 means the HOST protocol's own
|
|
49
|
+
* verification and settlement procedure understands the hash; no x402 facilitator does. It is also Tier B by
|
|
50
|
+
* definition, which would misdescribe a carrier that works today against stock x402 — extension identifiers
|
|
51
|
+
* are implementation-defined strings, so no registration gates this. The same determination is made for ACP
|
|
52
|
+
* and UCP. §C.4's Tier B forward paths (a reference inside the signed Offer/Receipt artifact; a registered
|
|
53
|
+
* extension identifier) are declared in the README as prose and in NO manifest — asserting a shape whose
|
|
54
|
+
* owner has not defined it is the asserting-a-shape defect.
|
|
55
|
+
*
|
|
56
|
+
* **The extension key is `legalContext`, deliberately not a reverse-domain name.** x402 imposes no namespace
|
|
57
|
+
* rule, and `legalContext` is the spelling emitters put on the wire, the spelling buyer parsers read, and the
|
|
58
|
+
* one LCP v1.38 §C.4's own illustration shows. The `com.integraledger.*` reverse-domain namespace applies to
|
|
59
|
+
* hosts that REQUIRE one — UCP does; x402 does not — and renaming this key to suit a convention x402 does not
|
|
60
|
+
* have would put a spelling on the wire that no counterparty reads.
|
|
61
|
+
*
|
|
62
|
+
* **The alias carries a DIFFERENT SHAPE, and x402 is the only protocol in the set where that is true.** The
|
|
63
|
+
* canonical slot holds a §8.1 object; `accepts[0].extra.atrHash` holds a **bare** hash — that is the form
|
|
64
|
+
* emitters write there and the form x402 integrators recognize. Writing an `lcp:` string into `extra.atrHash`
|
|
65
|
+
* would emit something no x402 counterparty parses. This is the reason an alias declares its own `encoding`
|
|
66
|
+
* at all.
|
|
67
|
+
*
|
|
68
|
+
* **The alias declares no `write`, and the live spec is the reason.** `extra` is "Scheme-specific additional
|
|
69
|
+
* information" — the payment scheme's object, whose contents that scheme defines. An `atrHash` is READ there
|
|
70
|
+
* because sellers put one there; writing into another party's namespace is not the same act. The
|
|
71
|
+
* `extensions` map is the protocol's own declared extension point and is where `place` writes.
|
|
72
|
+
*
|
|
73
|
+
* **The alias is index 0 only.** A locator names one path. `accepts[0]` is what buyer parsers read, and the
|
|
74
|
+
* reason is substantive: the reference must bind to the requirement actually being paid, and searching every
|
|
75
|
+
* requirement would let a seller park a second set of terms on an alternative it never expects to be chosen.
|
|
76
|
+
*
|
|
77
|
+
* **`termsUrlField` is DECLARED — this is the protocol whose wire carries both halves.** `binding-core`'s own
|
|
78
|
+
* contract cites x402 for exactly that: a buyer-side parser may demand the URL because x402 carries it, and
|
|
79
|
+
* emitters put `legalContextUrl` inside `info` beside `type`/`value`. Declaring the path
|
|
80
|
+
* makes that half machine-readable instead of a second private convention; `place` never writes it, because
|
|
81
|
+
* `place(ref, doc)` holds one reference and the terms URL is a different datum (the same division ACP draws
|
|
82
|
+
* with `metadata.legal_context_url`). The shipped carrier repeats the URL at `accepts[0].extra.legalContextUrl`
|
|
83
|
+
* too, which a single `termsUrlField` cannot express — recorded in the README as a known limitation rather
|
|
84
|
+
* than half-declared here.
|
|
85
|
+
*
|
|
86
|
+
* **`carrierTypes` permits `sha256` and `url`, and the two are admitted on DIFFERENT grounds** — one reason
|
|
87
|
+
* cannot cover both. `sha256` is the integrity carrier: §C.4's illustration carries one, emitters carry one,
|
|
88
|
+
* and the bare alias is fixed to it. `url` is the §8.1 discovery form, admitted because the canonical
|
|
89
|
+
* slot is a general reference-object slot and the kit puts the integrity-versus-discovery decision at the
|
|
90
|
+
* READER — `carrierClass` plus `requireIntegrity` — not in the permission list; ACP and UCP permit it for the
|
|
91
|
+
* same reason. `ipfs`/`ar` are excluded on a ground that does NOT apply to `url`: they are ALTERNATIVE
|
|
92
|
+
* integrity carriers, so admitting one adds no capability `sha256` does not already discharge while
|
|
93
|
+
* advertising a content-addressed transport no x402 counterparty resolves — a claim about the ecosystem
|
|
94
|
+
* rather than a description of it.
|
|
95
|
+
*
|
|
96
|
+
* The `url` permission is nonetheless WIDER than any shipped x402 reader: a buyer parser that requires an
|
|
97
|
+
* integrity carrier refuses `info.type !== "sha256"` outright, so a `url` placed in this slot is well-formed
|
|
98
|
+
* against this manifest and would still be rejected at read time. Recorded in the README as a limitation
|
|
99
|
+
* rather than narrowed away here, because narrowing the reference field to one type is a change to what the
|
|
100
|
+
* SLOT may hold across the set, not a fact about x402.
|
|
101
|
+
*/
|
|
102
|
+
export declare const X402_PLACEMENT: PlacementManifest;
|
|
103
|
+
//# 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,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuB/D,CAAC;AAEL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8EG;AACH,eAAO,MAAM,cAAc,EAAE,iBAkB5B,CAAC"}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The carrier's JSON Schema, INLINED rather than referenced.
|
|
3
|
+
*
|
|
4
|
+
* x402 makes `schema` a REQUIRED member of an extension entry — "JSON Schema defining the expected
|
|
5
|
+
* structure of info" — so whatever goes here is on the wire of every challenge. A `$ref` to a URL nobody
|
|
6
|
+
* serves would be a required member no counterparty can resolve, and "x402 never fetches it" is a reason
|
|
7
|
+
* that does not break, not a reason to ship it. (`https://legalcontextprotocol.org/schemas/lcp-extension.json`
|
|
8
|
+
* returns **404**, measured 2026-08-08 — the schema is inlined instead.)
|
|
9
|
+
*
|
|
10
|
+
* **Both extensions published in the x402 repository inline a complete JSON Schema** rather than
|
|
11
|
+
* referencing an external document, and LCP v1.38 §C.4 says to do one or the other: "Because `schema` is a
|
|
12
|
+
* REQUIRED member, publish a resolvable schema or inline it." Inlining also removes a hosting dependency
|
|
13
|
+
* the deployment does not currently meet — the same dependency the UCP capability still owes.
|
|
14
|
+
*
|
|
15
|
+
* The shape is the §8.1 reference object this placement writes into `info`, and nothing more: it describes
|
|
16
|
+
* the carrier, not the terms behind it.
|
|
17
|
+
*
|
|
18
|
+
* Changing this is a WIRE change — it appears in every challenge — so it is a frozen literal rather than a
|
|
19
|
+
* value assembled at call time.
|
|
20
|
+
*/
|
|
21
|
+
export const LEGAL_CONTEXT_SCHEMA = Object.freeze({
|
|
22
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
23
|
+
title: "LCP legal-context reference",
|
|
24
|
+
description: "A Legal Context Protocol reference to the terms governing this transaction. The reference identifies the exact terms document; it is not the terms.",
|
|
25
|
+
type: "object",
|
|
26
|
+
required: ["type", "value"],
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
properties: {
|
|
29
|
+
type: {
|
|
30
|
+
type: "string",
|
|
31
|
+
enum: ["sha256", "url", "ipfs", "ar"],
|
|
32
|
+
description: "Carrier type. sha256, ipfs and ar are content-addressed and bear integrity; url only locates a document.",
|
|
33
|
+
},
|
|
34
|
+
value: {
|
|
35
|
+
type: "string",
|
|
36
|
+
minLength: 1,
|
|
37
|
+
description: "The reference itself — for sha256, a 0x-prefixed lowercase 32-byte hex digest of the complete ATR file.",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* x402 reference placement — the HTTP-layer carrier, cut against the live x402 v2 specification
|
|
43
|
+
* (`x402-foundation/x402@1fec3aa04e41`, `specs/x402-specification-v2.md`; gate discharged in the README).
|
|
44
|
+
*
|
|
45
|
+
* **THE CANONICAL REPOSITORY IS THE FOUNDATION'S, and the citation moved on 2026-08-08.** Every
|
|
46
|
+
* reference here named `coinbase/x402`, which the GitHub API reports as `"fork": true`;
|
|
47
|
+
* `x402-foundation/x402` is `"fork": false` and is what LCP v1.38 §C.4's own *Checked against* line
|
|
48
|
+
* reads. Citing a fork invites a reader to diff against a copy that may lag. The revision is pinned
|
|
49
|
+
* rather than left as a bare repo name, because "the live spec" with no commit is not a claim anyone
|
|
50
|
+
* can re-check.
|
|
51
|
+
*
|
|
52
|
+
* **This is NOT the x402 weld.** `@integraledger/lcp-binding-evm-x402` binds `atrHash` into the EIP-3009 nonce —
|
|
53
|
+
* that is the SETTLEMENT binding and it answers "what did the money commit to?". This manifest answers a
|
|
54
|
+
* different question — "where does the reference ride on the wire?" — and both are true at once. x402 is the
|
|
55
|
+
* proof that one protocol can need a binding AND a placement.
|
|
56
|
+
*
|
|
57
|
+
* **Tier A in both slots, and the canonical one is the map the protocol PROTECTS.** x402 v2 defines a
|
|
58
|
+
* per-requirement `accepts[].extra` object and a top-level `extensions` map, the latter carried on the
|
|
59
|
+
* `PaymentRequired` challenge, the `PaymentPayload` AND the `SettlementResponse` — so a reference placed
|
|
60
|
+
* there is on the receipt, not only the proposal. Each `extensions` entry carries `info` ("Extension-specific
|
|
61
|
+
* data provided by the server") and `schema` ("JSON Schema defining the expected structure of `info`"). The
|
|
62
|
+
* live spec's echo rule — "The client must include at least the info received; it may append additional info
|
|
63
|
+
* but cannot delete or overwrite existing info" — is what makes that map the more durable carrier, and it is
|
|
64
|
+
* why it is canonical here.
|
|
65
|
+
*
|
|
66
|
+
* **`pattern` is `http-advisory`, not `protocol-extension`.** §8.3.6 means the HOST protocol's own
|
|
67
|
+
* verification and settlement procedure understands the hash; no x402 facilitator does. It is also Tier B by
|
|
68
|
+
* definition, which would misdescribe a carrier that works today against stock x402 — extension identifiers
|
|
69
|
+
* are implementation-defined strings, so no registration gates this. The same determination is made for ACP
|
|
70
|
+
* and UCP. §C.4's Tier B forward paths (a reference inside the signed Offer/Receipt artifact; a registered
|
|
71
|
+
* extension identifier) are declared in the README as prose and in NO manifest — asserting a shape whose
|
|
72
|
+
* owner has not defined it is the asserting-a-shape defect.
|
|
73
|
+
*
|
|
74
|
+
* **The extension key is `legalContext`, deliberately not a reverse-domain name.** x402 imposes no namespace
|
|
75
|
+
* rule, and `legalContext` is the spelling emitters put on the wire, the spelling buyer parsers read, and the
|
|
76
|
+
* one LCP v1.38 §C.4's own illustration shows. The `com.integraledger.*` reverse-domain namespace applies to
|
|
77
|
+
* hosts that REQUIRE one — UCP does; x402 does not — and renaming this key to suit a convention x402 does not
|
|
78
|
+
* have would put a spelling on the wire that no counterparty reads.
|
|
79
|
+
*
|
|
80
|
+
* **The alias carries a DIFFERENT SHAPE, and x402 is the only protocol in the set where that is true.** The
|
|
81
|
+
* canonical slot holds a §8.1 object; `accepts[0].extra.atrHash` holds a **bare** hash — that is the form
|
|
82
|
+
* emitters write there and the form x402 integrators recognize. Writing an `lcp:` string into `extra.atrHash`
|
|
83
|
+
* would emit something no x402 counterparty parses. This is the reason an alias declares its own `encoding`
|
|
84
|
+
* at all.
|
|
85
|
+
*
|
|
86
|
+
* **The alias declares no `write`, and the live spec is the reason.** `extra` is "Scheme-specific additional
|
|
87
|
+
* information" — the payment scheme's object, whose contents that scheme defines. An `atrHash` is READ there
|
|
88
|
+
* because sellers put one there; writing into another party's namespace is not the same act. The
|
|
89
|
+
* `extensions` map is the protocol's own declared extension point and is where `place` writes.
|
|
90
|
+
*
|
|
91
|
+
* **The alias is index 0 only.** A locator names one path. `accepts[0]` is what buyer parsers read, and the
|
|
92
|
+
* reason is substantive: the reference must bind to the requirement actually being paid, and searching every
|
|
93
|
+
* requirement would let a seller park a second set of terms on an alternative it never expects to be chosen.
|
|
94
|
+
*
|
|
95
|
+
* **`termsUrlField` is DECLARED — this is the protocol whose wire carries both halves.** `binding-core`'s own
|
|
96
|
+
* contract cites x402 for exactly that: a buyer-side parser may demand the URL because x402 carries it, and
|
|
97
|
+
* emitters put `legalContextUrl` inside `info` beside `type`/`value`. Declaring the path
|
|
98
|
+
* makes that half machine-readable instead of a second private convention; `place` never writes it, because
|
|
99
|
+
* `place(ref, doc)` holds one reference and the terms URL is a different datum (the same division ACP draws
|
|
100
|
+
* with `metadata.legal_context_url`). The shipped carrier repeats the URL at `accepts[0].extra.legalContextUrl`
|
|
101
|
+
* too, which a single `termsUrlField` cannot express — recorded in the README as a known limitation rather
|
|
102
|
+
* than half-declared here.
|
|
103
|
+
*
|
|
104
|
+
* **`carrierTypes` permits `sha256` and `url`, and the two are admitted on DIFFERENT grounds** — one reason
|
|
105
|
+
* cannot cover both. `sha256` is the integrity carrier: §C.4's illustration carries one, emitters carry one,
|
|
106
|
+
* and the bare alias is fixed to it. `url` is the §8.1 discovery form, admitted because the canonical
|
|
107
|
+
* slot is a general reference-object slot and the kit puts the integrity-versus-discovery decision at the
|
|
108
|
+
* READER — `carrierClass` plus `requireIntegrity` — not in the permission list; ACP and UCP permit it for the
|
|
109
|
+
* same reason. `ipfs`/`ar` are excluded on a ground that does NOT apply to `url`: they are ALTERNATIVE
|
|
110
|
+
* integrity carriers, so admitting one adds no capability `sha256` does not already discharge while
|
|
111
|
+
* advertising a content-addressed transport no x402 counterparty resolves — a claim about the ecosystem
|
|
112
|
+
* rather than a description of it.
|
|
113
|
+
*
|
|
114
|
+
* The `url` permission is nonetheless WIDER than any shipped x402 reader: a buyer parser that requires an
|
|
115
|
+
* integrity carrier refuses `info.type !== "sha256"` outright, so a `url` placed in this slot is well-formed
|
|
116
|
+
* against this manifest and would still be rejected at read time. Recorded in the README as a limitation
|
|
117
|
+
* rather than narrowed away here, because narrowing the reference field to one type is a change to what the
|
|
118
|
+
* SLOT may hold across the set, not a fact about x402.
|
|
119
|
+
*/
|
|
120
|
+
export const X402_PLACEMENT = {
|
|
121
|
+
protocol: "x402",
|
|
122
|
+
pattern: "http-advisory",
|
|
123
|
+
tier: "A",
|
|
124
|
+
encoding: "reference-object",
|
|
125
|
+
container: { kind: "object-path" },
|
|
126
|
+
field: "extensions.legalContext.info",
|
|
127
|
+
readAlso: [
|
|
128
|
+
{
|
|
129
|
+
path: "accepts.0.extra.atrHash",
|
|
130
|
+
encoding: "bare-value",
|
|
131
|
+
bareType: "sha256",
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
termsUrlField: "extensions.legalContext.info.legalContextUrl",
|
|
135
|
+
carrierTypes: ["sha256", "url"],
|
|
136
|
+
specRef: "x402 v2 (x402-foundation/x402@1fec3aa04e41 specs/x402-specification-v2.md, read 2026-07-30) — top-level extensions map carried on PaymentRequired/PaymentPayload/SettlementResponse, each entry {info, schema}; accepts[].extra is scheme-specific (gate discharged: see README)",
|
|
137
|
+
};
|
|
138
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAC/B,MAAM,CAAC,MAAM,CAAC;IACZ,OAAO,EAAE,8CAA8C;IACvD,KAAK,EAAE,6BAA6B;IACpC,WAAW,EACT,qJAAqJ;IACvJ,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC;IAC3B,oBAAoB,EAAE,KAAK;IAC3B,UAAU,EAAE;QACV,IAAI,EAAE;YACJ,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC;YACrC,WAAW,EACT,0GAA0G;SAC7G;QACD,KAAK,EAAE;YACL,IAAI,EAAE,QAAQ;YACd,SAAS,EAAE,CAAC;YACZ,WAAW,EACT,yGAAyG;SAC5G;KACF;CACF,CAAC,CAAC;AAEL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8EG;AACH,MAAM,CAAC,MAAM,cAAc,GAAsB;IAC/C,QAAQ,EAAE,MAAM;IAChB,OAAO,EAAE,eAAe;IACxB,IAAI,EAAE,GAAG;IACT,QAAQ,EAAE,kBAAkB;IAC5B,SAAS,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE;IAClC,KAAK,EAAE,8BAA8B;IACrC,QAAQ,EAAE;QACR;YACE,IAAI,EAAE,yBAAyB;YAC/B,QAAQ,EAAE,YAAY;YACtB,QAAQ,EAAE,QAAQ;SACnB;KACF;IACD,aAAa,EAAE,8CAA8C;IAC7D,YAAY,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC;IAC/B,OAAO,EACL,kRAAkR;CACrR,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ReferencePlacementAdapter } from "@integraledger/lcp-binding-core";
|
|
2
|
+
/**
|
|
3
|
+
* The x402 reference placement — the kit's `extract`, and the ONE `place` in the set that the kit cannot
|
|
4
|
+
* supply.
|
|
5
|
+
*
|
|
6
|
+
* x402's slot does not hold the reference directly: it holds `{ info, schema }`, where `info` is the §8.1
|
|
7
|
+
* reference object and `schema` is a `$ref` pointing at the carrier schema. That is a WRAPPER, and no
|
|
8
|
+
* container kind models it. Inventing an `x402-extension` container kind would put one protocol's name inside
|
|
9
|
+
* a generic enum — the abstraction leaking — so the write half is overridden here, in this package, where it
|
|
10
|
+
* is reviewed like any other code. `extract` is the kit's unchanged: reading `extensions.legalContext.info` is
|
|
11
|
+
* an ordinary object-path read, and the bare-hash alias at `accepts.0.extra.atrHash` is handled by its own
|
|
12
|
+
* declared encoding (S2/S6). One overridden member is composition; a second would mean this package had
|
|
13
|
+
* stopped using the kit, and the test suite says so.
|
|
14
|
+
*/
|
|
15
|
+
export declare const x402Placement: ReferencePlacementAdapter;
|
|
16
|
+
//# sourceMappingURL=placement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placement.d.ts","sourceRoot":"","sources":["../src/placement.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,yBAAyB,EAC/B,MAAM,iCAAiC,CAAC;AAgBzC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,EAAE,yBA+E3B,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { CarrierError, encodeForField, makePlacement, } from "@integraledger/lcp-binding-core";
|
|
2
|
+
import { LEGAL_CONTEXT_SCHEMA, X402_PLACEMENT } from "./manifest.js";
|
|
3
|
+
const base = makePlacement(X402_PLACEMENT);
|
|
4
|
+
/** Refusals namespaced from the protocol id, exactly as the kit does — so `x402/document-malformed` means the
|
|
5
|
+
* same thing whether it came from the override or from the generic read path. */
|
|
6
|
+
function refuse(code, detail) {
|
|
7
|
+
return {
|
|
8
|
+
refused: true,
|
|
9
|
+
haltClass: "verification-failure",
|
|
10
|
+
code: `x402/${code}`,
|
|
11
|
+
detail,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The x402 reference placement — the kit's `extract`, and the ONE `place` in the set that the kit cannot
|
|
16
|
+
* supply.
|
|
17
|
+
*
|
|
18
|
+
* x402's slot does not hold the reference directly: it holds `{ info, schema }`, where `info` is the §8.1
|
|
19
|
+
* reference object and `schema` is a `$ref` pointing at the carrier schema. That is a WRAPPER, and no
|
|
20
|
+
* container kind models it. Inventing an `x402-extension` container kind would put one protocol's name inside
|
|
21
|
+
* a generic enum — the abstraction leaking — so the write half is overridden here, in this package, where it
|
|
22
|
+
* is reviewed like any other code. `extract` is the kit's unchanged: reading `extensions.legalContext.info` is
|
|
23
|
+
* an ordinary object-path read, and the bare-hash alias at `accepts.0.extra.atrHash` is handled by its own
|
|
24
|
+
* declared encoding (S2/S6). One overridden member is composition; a second would mean this package had
|
|
25
|
+
* stopped using the kit, and the test suite says so.
|
|
26
|
+
*/
|
|
27
|
+
export const x402Placement = {
|
|
28
|
+
...base,
|
|
29
|
+
place(ref, doc) {
|
|
30
|
+
if (!X402_PLACEMENT.carrierTypes.includes(ref.type))
|
|
31
|
+
return refuse("carrier-type-not-permitted", `${X402_PLACEMENT.field} permits ${X402_PLACEMENT.carrierTypes.join("/")}, got ${ref.type}`);
|
|
32
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc))
|
|
33
|
+
return refuse("document-malformed", "an x402 challenge is a non-null object");
|
|
34
|
+
// Rendered through the codec, never by interpolation — a value that does not meet its type's rule refuses
|
|
35
|
+
// here instead of putting an extension on the wire that a buyer's parser would reject.
|
|
36
|
+
let encoded;
|
|
37
|
+
try {
|
|
38
|
+
encoded = encodeForField(ref, X402_PLACEMENT.encoding);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
if (!(e instanceof CarrierError))
|
|
42
|
+
throw e; // never swallow a non-carrier bug
|
|
43
|
+
return refuse("reference-malformed", `not a valid carrier value for its type: ${ref.value}`);
|
|
44
|
+
}
|
|
45
|
+
// OWN PROPERTY ONLY. `place`'s document is exactly as attacker-influenced as `extract`'s, so a challenge
|
|
46
|
+
// with ZERO own properties must not walk into its prototype's `extensions` and put those entries on the
|
|
47
|
+
// wire — `extract` reports that document as `reference-absent`, and the two halves have to agree about
|
|
48
|
+
// what is present. binding-core's own writer states the rule and proves it for the generic path; the
|
|
49
|
+
// override is the one write path that has to restate it. The DECLARED view rather than a
|
|
50
|
+
// `Record<string, unknown>` is deliberate: reading `extensions` off an index signature is TS4111, and the
|
|
51
|
+
// bracket form biome would then ask for is the fix it classes as unsafe. The document is still spread
|
|
52
|
+
// wholesale below, so nothing is narrowed away.
|
|
53
|
+
const ext = Object.hasOwn(doc, "extensions")
|
|
54
|
+
? doc.extensions
|
|
55
|
+
: undefined;
|
|
56
|
+
// Sibling extensions are PRESERVED: the live x402 v2 spec states a client "must include at least the info
|
|
57
|
+
// received; it may append additional info but cannot delete or overwrite existing info". That is the host
|
|
58
|
+
// protocol's rule about its own map, and a placement that pruned a sibling would make the client
|
|
59
|
+
// using it non-conformant.
|
|
60
|
+
//
|
|
61
|
+
// An `extensions` that is PRESENT but cannot be merged into REFUSES, and that is binding-core's ratified
|
|
62
|
+
// malformed-container rule read against the manifest this package PUBLISHES rather than against the
|
|
63
|
+
// granularity the override happens to write at: `field` is `extensions.legalContext.info`, so `legalContext`
|
|
64
|
+
// is the field's direct holder and `extensions` sits one level ABOVE it — replaced at the holder, refused
|
|
65
|
+
// above it, because replacing an intermediate discards everything beneath it. A stranger holding only the
|
|
66
|
+
// manifest and the kit computes that refusal for the same document, and an override that emitted a
|
|
67
|
+
// challenge instead would make the manifest a description rather than an artifact. ABSENT is still
|
|
68
|
+
// created; our own entry, being the direct holder, is still replaced.
|
|
69
|
+
const siblings = ext === undefined
|
|
70
|
+
? {}
|
|
71
|
+
: typeof ext === "object" && ext !== null && !Array.isArray(ext)
|
|
72
|
+
? ext
|
|
73
|
+
: undefined;
|
|
74
|
+
if (siblings === undefined)
|
|
75
|
+
return refuse("document-malformed", `extensions is present and is not a map, so ${X402_PLACEMENT.field} has no holder to write into: ${JSON.stringify(ext)}`);
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
value: {
|
|
79
|
+
...doc,
|
|
80
|
+
extensions: {
|
|
81
|
+
...siblings,
|
|
82
|
+
legalContext: {
|
|
83
|
+
info: encoded,
|
|
84
|
+
schema: LEGAL_CONTEXT_SCHEMA,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=placement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placement.js","sourceRoot":"","sources":["../src/placement.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,cAAc,EAEd,aAAa,GAGd,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAErE,MAAM,IAAI,GAAG,aAAa,CAAC,cAAc,CAAC,CAAC;AAE3C;kFACkF;AAClF,SAAS,MAAM,CAAC,IAAY,EAAE,MAAc;IAC1C,OAAO;QACL,OAAO,EAAE,IAAI;QACb,SAAS,EAAE,sBAAsB;QACjC,IAAI,EAAE,QAAQ,IAAI,EAAE;QACpB,MAAM;KACP,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,aAAa,GAA8B;IACtD,GAAG,IAAI;IAEP,KAAK,CAAC,GAAoB,EAAE,GAAY;QACtC,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;YACjD,OAAO,MAAM,CACX,4BAA4B,EAC5B,GAAG,cAAc,CAAC,KAAK,YAAY,cAAc,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,GAAG,CAAC,IAAI,EAAE,CAC5F,CAAC;QACJ,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;YAC/D,OAAO,MAAM,CACX,oBAAoB,EACpB,wCAAwC,CACzC,CAAC;QAEJ,0GAA0G;QAC1G,uFAAuF;QACvF,IAAI,OAAgB,CAAC;QACrB,IAAI,CAAC;YACH,OAAO,GAAG,cAAc,CAAC,GAAG,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC;QACzD,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,CAAC,CAAC,YAAY,YAAY,CAAC;gBAAE,MAAM,CAAC,CAAC,CAAC,kCAAkC;YAC7E,OAAO,MAAM,CACX,qBAAqB,EACrB,2CAA2C,GAAG,CAAC,KAAK,EAAE,CACvD,CAAC;QACJ,CAAC;QAED,yGAAyG;QACzG,wGAAwG;QACxG,uGAAuG;QACvG,qGAAqG;QACrG,yFAAyF;QACzF,0GAA0G;QAC1G,sGAAsG;QACtG,gDAAgD;QAChD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC;YAC1C,CAAC,CAAE,GAAyC,CAAC,UAAU;YACvD,CAAC,CAAC,SAAS,CAAC;QAEd,0GAA0G;QAC1G,0GAA0G;QAC1G,iGAAiG;QACjG,2BAA2B;QAC3B,EAAE;QACF,yGAAyG;QACzG,oGAAoG;QACpG,6GAA6G;QAC7G,0GAA0G;QAC1G,0GAA0G;QAC1G,mGAAmG;QACnG,mGAAmG;QACnG,sEAAsE;QACtE,MAAM,QAAQ,GACZ,GAAG,KAAK,SAAS;YACf,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;gBAC9D,CAAC,CAAE,GAA+B;gBAClC,CAAC,CAAC,SAAS,CAAC;QAClB,IAAI,QAAQ,KAAK,SAAS;YACxB,OAAO,MAAM,CACX,oBAAoB,EACpB,8CAA8C,cAAc,CAAC,KAAK,iCAAiC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CACzH,CAAC;QAEJ,OAAO;YACL,EAAE,EAAE,IAAI;YACR,KAAK,EAAE;gBACL,GAAI,GAAc;gBAClB,UAAU,EAAE;oBACV,GAAG,QAAQ;oBACX,YAAY,EAAE;wBACZ,IAAI,EAAE,OAAO;wBACb,MAAM,EAAE,oBAAoB;qBAC7B;iBACF;aACF;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@integraledger/lcp-placement-x402",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Places an LCP reference into an x402 v2 payment challenge, and reads it back.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"lcp",
|
|
7
|
+
"legal-context-protocol",
|
|
8
|
+
"legal-terms",
|
|
9
|
+
"agentic-commerce",
|
|
10
|
+
"placement",
|
|
11
|
+
"x402"
|
|
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/placement-x402"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/IntegraLedger/integra-protocol/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/IntegraLedger/integra-protocol/tree/main/packages/placement-x402#readme",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@integraledger/lcp-binding-core": "0.9.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@cfworker/json-schema": "4.1.1",
|
|
47
|
+
"@types/node": "24.13.3",
|
|
48
|
+
"vitest": "4.1.10"
|
|
49
|
+
},
|
|
50
|
+
"license": "Apache-2.0",
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=24"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsc -p tsconfig.build.json",
|
|
56
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
57
|
+
"test": "vitest run"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/index.ts
ADDED
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { PlacementManifest } from "@integraledger/lcp-binding-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The carrier's JSON Schema, INLINED rather than referenced.
|
|
5
|
+
*
|
|
6
|
+
* x402 makes `schema` a REQUIRED member of an extension entry — "JSON Schema defining the expected
|
|
7
|
+
* structure of info" — so whatever goes here is on the wire of every challenge. A `$ref` to a URL nobody
|
|
8
|
+
* serves would be a required member no counterparty can resolve, and "x402 never fetches it" is a reason
|
|
9
|
+
* that does not break, not a reason to ship it. (`https://legalcontextprotocol.org/schemas/lcp-extension.json`
|
|
10
|
+
* returns **404**, measured 2026-08-08 — the schema is inlined instead.)
|
|
11
|
+
*
|
|
12
|
+
* **Both extensions published in the x402 repository inline a complete JSON Schema** rather than
|
|
13
|
+
* referencing an external document, and LCP v1.38 §C.4 says to do one or the other: "Because `schema` is a
|
|
14
|
+
* REQUIRED member, publish a resolvable schema or inline it." Inlining also removes a hosting dependency
|
|
15
|
+
* the deployment does not currently meet — the same dependency the UCP capability still owes.
|
|
16
|
+
*
|
|
17
|
+
* The shape is the §8.1 reference object this placement writes into `info`, and nothing more: it describes
|
|
18
|
+
* the carrier, not the terms behind it.
|
|
19
|
+
*
|
|
20
|
+
* Changing this is a WIRE change — it appears in every challenge — so it is a frozen literal rather than a
|
|
21
|
+
* value assembled at call time.
|
|
22
|
+
*/
|
|
23
|
+
export const LEGAL_CONTEXT_SCHEMA: Readonly<Record<string, unknown>> =
|
|
24
|
+
Object.freeze({
|
|
25
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
26
|
+
title: "LCP legal-context reference",
|
|
27
|
+
description:
|
|
28
|
+
"A Legal Context Protocol reference to the terms governing this transaction. The reference identifies the exact terms document; it is not the terms.",
|
|
29
|
+
type: "object",
|
|
30
|
+
required: ["type", "value"],
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
properties: {
|
|
33
|
+
type: {
|
|
34
|
+
type: "string",
|
|
35
|
+
enum: ["sha256", "url", "ipfs", "ar"],
|
|
36
|
+
description:
|
|
37
|
+
"Carrier type. sha256, ipfs and ar are content-addressed and bear integrity; url only locates a document.",
|
|
38
|
+
},
|
|
39
|
+
value: {
|
|
40
|
+
type: "string",
|
|
41
|
+
minLength: 1,
|
|
42
|
+
description:
|
|
43
|
+
"The reference itself — for sha256, a 0x-prefixed lowercase 32-byte hex digest of the complete ATR file.",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* x402 reference placement — the HTTP-layer carrier, cut against the live x402 v2 specification
|
|
50
|
+
* (`x402-foundation/x402@1fec3aa04e41`, `specs/x402-specification-v2.md`; gate discharged in the README).
|
|
51
|
+
*
|
|
52
|
+
* **THE CANONICAL REPOSITORY IS THE FOUNDATION'S, and the citation moved on 2026-08-08.** Every
|
|
53
|
+
* reference here named `coinbase/x402`, which the GitHub API reports as `"fork": true`;
|
|
54
|
+
* `x402-foundation/x402` is `"fork": false` and is what LCP v1.38 §C.4's own *Checked against* line
|
|
55
|
+
* reads. Citing a fork invites a reader to diff against a copy that may lag. The revision is pinned
|
|
56
|
+
* rather than left as a bare repo name, because "the live spec" with no commit is not a claim anyone
|
|
57
|
+
* can re-check.
|
|
58
|
+
*
|
|
59
|
+
* **This is NOT the x402 weld.** `@integraledger/lcp-binding-evm-x402` binds `atrHash` into the EIP-3009 nonce —
|
|
60
|
+
* that is the SETTLEMENT binding and it answers "what did the money commit to?". This manifest answers a
|
|
61
|
+
* different question — "where does the reference ride on the wire?" — and both are true at once. x402 is the
|
|
62
|
+
* proof that one protocol can need a binding AND a placement.
|
|
63
|
+
*
|
|
64
|
+
* **Tier A in both slots, and the canonical one is the map the protocol PROTECTS.** x402 v2 defines a
|
|
65
|
+
* per-requirement `accepts[].extra` object and a top-level `extensions` map, the latter carried on the
|
|
66
|
+
* `PaymentRequired` challenge, the `PaymentPayload` AND the `SettlementResponse` — so a reference placed
|
|
67
|
+
* there is on the receipt, not only the proposal. Each `extensions` entry carries `info` ("Extension-specific
|
|
68
|
+
* data provided by the server") and `schema` ("JSON Schema defining the expected structure of `info`"). The
|
|
69
|
+
* live spec's echo rule — "The client must include at least the info received; it may append additional info
|
|
70
|
+
* but cannot delete or overwrite existing info" — is what makes that map the more durable carrier, and it is
|
|
71
|
+
* why it is canonical here.
|
|
72
|
+
*
|
|
73
|
+
* **`pattern` is `http-advisory`, not `protocol-extension`.** §8.3.6 means the HOST protocol's own
|
|
74
|
+
* verification and settlement procedure understands the hash; no x402 facilitator does. It is also Tier B by
|
|
75
|
+
* definition, which would misdescribe a carrier that works today against stock x402 — extension identifiers
|
|
76
|
+
* are implementation-defined strings, so no registration gates this. The same determination is made for ACP
|
|
77
|
+
* and UCP. §C.4's Tier B forward paths (a reference inside the signed Offer/Receipt artifact; a registered
|
|
78
|
+
* extension identifier) are declared in the README as prose and in NO manifest — asserting a shape whose
|
|
79
|
+
* owner has not defined it is the asserting-a-shape defect.
|
|
80
|
+
*
|
|
81
|
+
* **The extension key is `legalContext`, deliberately not a reverse-domain name.** x402 imposes no namespace
|
|
82
|
+
* rule, and `legalContext` is the spelling emitters put on the wire, the spelling buyer parsers read, and the
|
|
83
|
+
* one LCP v1.38 §C.4's own illustration shows. The `com.integraledger.*` reverse-domain namespace applies to
|
|
84
|
+
* hosts that REQUIRE one — UCP does; x402 does not — and renaming this key to suit a convention x402 does not
|
|
85
|
+
* have would put a spelling on the wire that no counterparty reads.
|
|
86
|
+
*
|
|
87
|
+
* **The alias carries a DIFFERENT SHAPE, and x402 is the only protocol in the set where that is true.** The
|
|
88
|
+
* canonical slot holds a §8.1 object; `accepts[0].extra.atrHash` holds a **bare** hash — that is the form
|
|
89
|
+
* emitters write there and the form x402 integrators recognize. Writing an `lcp:` string into `extra.atrHash`
|
|
90
|
+
* would emit something no x402 counterparty parses. This is the reason an alias declares its own `encoding`
|
|
91
|
+
* at all.
|
|
92
|
+
*
|
|
93
|
+
* **The alias declares no `write`, and the live spec is the reason.** `extra` is "Scheme-specific additional
|
|
94
|
+
* information" — the payment scheme's object, whose contents that scheme defines. An `atrHash` is READ there
|
|
95
|
+
* because sellers put one there; writing into another party's namespace is not the same act. The
|
|
96
|
+
* `extensions` map is the protocol's own declared extension point and is where `place` writes.
|
|
97
|
+
*
|
|
98
|
+
* **The alias is index 0 only.** A locator names one path. `accepts[0]` is what buyer parsers read, and the
|
|
99
|
+
* reason is substantive: the reference must bind to the requirement actually being paid, and searching every
|
|
100
|
+
* requirement would let a seller park a second set of terms on an alternative it never expects to be chosen.
|
|
101
|
+
*
|
|
102
|
+
* **`termsUrlField` is DECLARED — this is the protocol whose wire carries both halves.** `binding-core`'s own
|
|
103
|
+
* contract cites x402 for exactly that: a buyer-side parser may demand the URL because x402 carries it, and
|
|
104
|
+
* emitters put `legalContextUrl` inside `info` beside `type`/`value`. Declaring the path
|
|
105
|
+
* makes that half machine-readable instead of a second private convention; `place` never writes it, because
|
|
106
|
+
* `place(ref, doc)` holds one reference and the terms URL is a different datum (the same division ACP draws
|
|
107
|
+
* with `metadata.legal_context_url`). The shipped carrier repeats the URL at `accepts[0].extra.legalContextUrl`
|
|
108
|
+
* too, which a single `termsUrlField` cannot express — recorded in the README as a known limitation rather
|
|
109
|
+
* than half-declared here.
|
|
110
|
+
*
|
|
111
|
+
* **`carrierTypes` permits `sha256` and `url`, and the two are admitted on DIFFERENT grounds** — one reason
|
|
112
|
+
* cannot cover both. `sha256` is the integrity carrier: §C.4's illustration carries one, emitters carry one,
|
|
113
|
+
* and the bare alias is fixed to it. `url` is the §8.1 discovery form, admitted because the canonical
|
|
114
|
+
* slot is a general reference-object slot and the kit puts the integrity-versus-discovery decision at the
|
|
115
|
+
* READER — `carrierClass` plus `requireIntegrity` — not in the permission list; ACP and UCP permit it for the
|
|
116
|
+
* same reason. `ipfs`/`ar` are excluded on a ground that does NOT apply to `url`: they are ALTERNATIVE
|
|
117
|
+
* integrity carriers, so admitting one adds no capability `sha256` does not already discharge while
|
|
118
|
+
* advertising a content-addressed transport no x402 counterparty resolves — a claim about the ecosystem
|
|
119
|
+
* rather than a description of it.
|
|
120
|
+
*
|
|
121
|
+
* The `url` permission is nonetheless WIDER than any shipped x402 reader: a buyer parser that requires an
|
|
122
|
+
* integrity carrier refuses `info.type !== "sha256"` outright, so a `url` placed in this slot is well-formed
|
|
123
|
+
* against this manifest and would still be rejected at read time. Recorded in the README as a limitation
|
|
124
|
+
* rather than narrowed away here, because narrowing the reference field to one type is a change to what the
|
|
125
|
+
* SLOT may hold across the set, not a fact about x402.
|
|
126
|
+
*/
|
|
127
|
+
export const X402_PLACEMENT: PlacementManifest = {
|
|
128
|
+
protocol: "x402",
|
|
129
|
+
pattern: "http-advisory",
|
|
130
|
+
tier: "A",
|
|
131
|
+
encoding: "reference-object",
|
|
132
|
+
container: { kind: "object-path" },
|
|
133
|
+
field: "extensions.legalContext.info",
|
|
134
|
+
readAlso: [
|
|
135
|
+
{
|
|
136
|
+
path: "accepts.0.extra.atrHash",
|
|
137
|
+
encoding: "bare-value",
|
|
138
|
+
bareType: "sha256",
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
termsUrlField: "extensions.legalContext.info.legalContextUrl",
|
|
142
|
+
carrierTypes: ["sha256", "url"],
|
|
143
|
+
specRef:
|
|
144
|
+
"x402 v2 (x402-foundation/x402@1fec3aa04e41 specs/x402-specification-v2.md, read 2026-07-30) — top-level extensions map carried on PaymentRequired/PaymentPayload/SettlementResponse, each entry {info, schema}; accepts[].extra is scheme-specific (gate discharged: see README)",
|
|
145
|
+
};
|
package/src/placement.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CarrierError,
|
|
3
|
+
encodeForField,
|
|
4
|
+
type LegalContextRef,
|
|
5
|
+
makePlacement,
|
|
6
|
+
type Outcome,
|
|
7
|
+
type ReferencePlacementAdapter,
|
|
8
|
+
} from "@integraledger/lcp-binding-core";
|
|
9
|
+
import { LEGAL_CONTEXT_SCHEMA, X402_PLACEMENT } from "./manifest.js";
|
|
10
|
+
|
|
11
|
+
const base = makePlacement(X402_PLACEMENT);
|
|
12
|
+
|
|
13
|
+
/** Refusals namespaced from the protocol id, exactly as the kit does — so `x402/document-malformed` means the
|
|
14
|
+
* same thing whether it came from the override or from the generic read path. */
|
|
15
|
+
function refuse(code: string, detail: string): Outcome<never> {
|
|
16
|
+
return {
|
|
17
|
+
refused: true,
|
|
18
|
+
haltClass: "verification-failure",
|
|
19
|
+
code: `x402/${code}`,
|
|
20
|
+
detail,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The x402 reference placement — the kit's `extract`, and the ONE `place` in the set that the kit cannot
|
|
26
|
+
* supply.
|
|
27
|
+
*
|
|
28
|
+
* x402's slot does not hold the reference directly: it holds `{ info, schema }`, where `info` is the §8.1
|
|
29
|
+
* reference object and `schema` is a `$ref` pointing at the carrier schema. That is a WRAPPER, and no
|
|
30
|
+
* container kind models it. Inventing an `x402-extension` container kind would put one protocol's name inside
|
|
31
|
+
* a generic enum — the abstraction leaking — so the write half is overridden here, in this package, where it
|
|
32
|
+
* is reviewed like any other code. `extract` is the kit's unchanged: reading `extensions.legalContext.info` is
|
|
33
|
+
* an ordinary object-path read, and the bare-hash alias at `accepts.0.extra.atrHash` is handled by its own
|
|
34
|
+
* declared encoding (S2/S6). One overridden member is composition; a second would mean this package had
|
|
35
|
+
* stopped using the kit, and the test suite says so.
|
|
36
|
+
*/
|
|
37
|
+
export const x402Placement: ReferencePlacementAdapter = {
|
|
38
|
+
...base,
|
|
39
|
+
|
|
40
|
+
place(ref: LegalContextRef, doc: unknown): Outcome<unknown> {
|
|
41
|
+
if (!X402_PLACEMENT.carrierTypes.includes(ref.type))
|
|
42
|
+
return refuse(
|
|
43
|
+
"carrier-type-not-permitted",
|
|
44
|
+
`${X402_PLACEMENT.field} permits ${X402_PLACEMENT.carrierTypes.join("/")}, got ${ref.type}`,
|
|
45
|
+
);
|
|
46
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc))
|
|
47
|
+
return refuse(
|
|
48
|
+
"document-malformed",
|
|
49
|
+
"an x402 challenge is a non-null object",
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Rendered through the codec, never by interpolation — a value that does not meet its type's rule refuses
|
|
53
|
+
// here instead of putting an extension on the wire that a buyer's parser would reject.
|
|
54
|
+
let encoded: unknown;
|
|
55
|
+
try {
|
|
56
|
+
encoded = encodeForField(ref, X402_PLACEMENT.encoding);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
if (!(e instanceof CarrierError)) throw e; // never swallow a non-carrier bug
|
|
59
|
+
return refuse(
|
|
60
|
+
"reference-malformed",
|
|
61
|
+
`not a valid carrier value for its type: ${ref.value}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// OWN PROPERTY ONLY. `place`'s document is exactly as attacker-influenced as `extract`'s, so a challenge
|
|
66
|
+
// with ZERO own properties must not walk into its prototype's `extensions` and put those entries on the
|
|
67
|
+
// wire — `extract` reports that document as `reference-absent`, and the two halves have to agree about
|
|
68
|
+
// what is present. binding-core's own writer states the rule and proves it for the generic path; the
|
|
69
|
+
// override is the one write path that has to restate it. The DECLARED view rather than a
|
|
70
|
+
// `Record<string, unknown>` is deliberate: reading `extensions` off an index signature is TS4111, and the
|
|
71
|
+
// bracket form biome would then ask for is the fix it classes as unsafe. The document is still spread
|
|
72
|
+
// wholesale below, so nothing is narrowed away.
|
|
73
|
+
const ext = Object.hasOwn(doc, "extensions")
|
|
74
|
+
? (doc as { readonly extensions?: unknown }).extensions
|
|
75
|
+
: undefined;
|
|
76
|
+
|
|
77
|
+
// Sibling extensions are PRESERVED: the live x402 v2 spec states a client "must include at least the info
|
|
78
|
+
// received; it may append additional info but cannot delete or overwrite existing info". That is the host
|
|
79
|
+
// protocol's rule about its own map, and a placement that pruned a sibling would make the client
|
|
80
|
+
// using it non-conformant.
|
|
81
|
+
//
|
|
82
|
+
// An `extensions` that is PRESENT but cannot be merged into REFUSES, and that is binding-core's ratified
|
|
83
|
+
// malformed-container rule read against the manifest this package PUBLISHES rather than against the
|
|
84
|
+
// granularity the override happens to write at: `field` is `extensions.legalContext.info`, so `legalContext`
|
|
85
|
+
// is the field's direct holder and `extensions` sits one level ABOVE it — replaced at the holder, refused
|
|
86
|
+
// above it, because replacing an intermediate discards everything beneath it. A stranger holding only the
|
|
87
|
+
// manifest and the kit computes that refusal for the same document, and an override that emitted a
|
|
88
|
+
// challenge instead would make the manifest a description rather than an artifact. ABSENT is still
|
|
89
|
+
// created; our own entry, being the direct holder, is still replaced.
|
|
90
|
+
const siblings =
|
|
91
|
+
ext === undefined
|
|
92
|
+
? {}
|
|
93
|
+
: typeof ext === "object" && ext !== null && !Array.isArray(ext)
|
|
94
|
+
? (ext as Record<string, unknown>)
|
|
95
|
+
: undefined;
|
|
96
|
+
if (siblings === undefined)
|
|
97
|
+
return refuse(
|
|
98
|
+
"document-malformed",
|
|
99
|
+
`extensions is present and is not a map, so ${X402_PLACEMENT.field} has no holder to write into: ${JSON.stringify(ext)}`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
ok: true,
|
|
104
|
+
value: {
|
|
105
|
+
...(doc as object),
|
|
106
|
+
extensions: {
|
|
107
|
+
...siblings,
|
|
108
|
+
legalContext: {
|
|
109
|
+
info: encoded,
|
|
110
|
+
schema: LEGAL_CONTEXT_SCHEMA,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
};
|