@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 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.
@@ -0,0 +1,3 @@
1
+ export { LEGAL_CONTEXT_SCHEMA, X402_PLACEMENT } from "./manifest.js";
2
+ export { x402Placement } from "./placement.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,3 @@
1
+ export { LEGAL_CONTEXT_SCHEMA, X402_PLACEMENT } from "./manifest.js";
2
+ export { x402Placement } from "./placement.js";
3
+ //# sourceMappingURL=index.js.map
@@ -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"}
@@ -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
@@ -0,0 +1,2 @@
1
+ export { LEGAL_CONTEXT_SCHEMA, X402_PLACEMENT } from "./manifest.js";
2
+ export { x402Placement } from "./placement.js";
@@ -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
+ };
@@ -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
+ };