@integraledger/lcp-placement-x402 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/manifest.js CHANGED
@@ -1,46 +1,67 @@
1
1
  /**
2
- * The carrier's JSON Schema, INLINED rather than referenced.
2
+ * The carrier's JSON Schema, INLINED rather than referenced — and equal, member for member, to the
3
+ * AUTHORITY document at `https://integraledger.com/lcp/x402/legal-context/v1.schema.json` minus its `$id`
4
+ * and `$defs`. That equality is load-bearing and drift-gated (the conformance suite compares this literal
5
+ * to `@integraledger/lcp-discovery`'s shipped copy of the authority file), because its absence was a
6
+ * published defect: from 0.10.1's release until this version, this schema said `required: ["type",
7
+ * "value"]` while the authority document said `required: ["type", "value", "legalContextUrl"]` — two
8
+ * definitions of the same `info` in the published ecosystem, self-consistent halves, no document valid
9
+ * against both (integra-protocol#8). Both being conformant to the schema each carried is exactly why no
10
+ * package's own tests could catch it; only comparing the two could, and now something does.
3
11
  *
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.)
12
+ * WHY INLINE AT ALL: x402 makes `schema` a REQUIRED member of an extension entry — "JSON Schema defining
13
+ * the expected structure of info" — so whatever goes here is on the wire of every challenge. **All nine
14
+ * extensions published in the x402 repository inline a complete JSON Schema** rather than referencing an
15
+ * external document (`x402-foundation/x402` HEAD, read 2026-08-11), and one of the nine makes it a rule:
16
+ * the Bazaar extension requires a `schema`'s `$ref`/`$id` values to be "same-document JSON Pointer
17
+ * fragments (starting with `#`); external references … are not allowed", and says a facilitator "must not
18
+ * resolve external `$ref`/`$id` values … when validating an untrusted `schema`". So an external `$ref`
19
+ * here would not merely be unresolvable to a counterparty that declines to fetch — wherever Bazaar
20
+ * governs, it is rejected outright. LCP v1.38 §C.4 draws the same conclusion ("publish a resolvable
21
+ * schema or inline it — and inlining is the safer of the two"). Dropping `$id` and `$defs` from the
22
+ * inlined form is that rule applied: the authority document's `$id` is an absolute URL, and its `$defs`
23
+ * carries the RECEIPT-time definition, which is not this challenge-time `info` and would bloat every 402.
9
24
  *
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.
25
+ * The shape is the §8.1 reference object PLUS the locator the reference is verified through:
26
+ * `legalContextUrl` is REQUIRED here because `value` is a digest — a buyer verifies the terms by fetching
27
+ * the document and hashing it, so a challenge advertising the hash without the locator advertises
28
+ * something no counterparty who lacks the document can check. Every shipped buyer parser already refuses
29
+ * such a challenge; the schema now says on the wire what the readers always demanded. It describes the
30
+ * carrier, not the terms behind it, and asserts nothing about any agreement's lawfulness — the
31
+ * description says so in as many words because the schema travels alone.
17
32
  *
18
33
  * 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.
34
+ * value assembled at call time, and the drift gate is what keeps the frozen copy honest.
20
35
  */
21
36
  export const LEGAL_CONTEXT_SCHEMA = Object.freeze({
22
37
  $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.",
38
+ title: "legalContext — x402 extension info",
39
+ description: "The `info` payload of the `legalContext` x402 extension at challenge time: a Legal Context Protocol reference to the terms governing this transaction, plus the URL the terms document can be fetched from. The reference identifies the exact terms document; it is not the terms. This describes a technology harness and asserts nothing about whether any agreement is lawful, sound or enforceable.",
25
40
  type: "object",
26
- required: ["type", "value"],
27
41
  additionalProperties: false,
42
+ required: ["type", "value", "legalContextUrl"],
28
43
  properties: {
29
44
  type: {
45
+ description: "The digest algorithm over the terms document. `sha256` is the only value this version defines.",
30
46
  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.",
47
+ const: "sha256",
33
48
  },
34
49
  value: {
50
+ description: "The atrHash — SHA-256 of the terms document, lowercase hex with an 0x prefix.",
51
+ type: "string",
52
+ pattern: "^0x[0-9a-f]{64}$",
53
+ },
54
+ legalContextUrl: {
55
+ description: "Where the terms document this hash covers can be fetched. A reader verifies the document against `value`; the URL is a locator and never the authority.",
35
56
  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.",
57
+ format: "uri",
38
58
  },
39
59
  },
40
60
  });
41
61
  /**
42
62
  * 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).
63
+ * (`x402-foundation/x402@db5da2e65952`, `specs/x402-specification-v2.md`, read 2026-08-11; gate discharged
64
+ * in the README).
44
65
  *
45
66
  * **THE CANONICAL REPOSITORY IS THE FOUNDATION'S, and the citation moved on 2026-08-08.** Every
46
67
  * reference here named `coinbase/x402`, which the GitHub API reports as `"fork": true`;
@@ -83,39 +104,47 @@ export const LEGAL_CONTEXT_SCHEMA = Object.freeze({
83
104
  * would emit something no x402 counterparty parses. This is the reason an alias declares its own `encoding`
84
105
  * at all.
85
106
  *
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.
107
+ * **The alias IS WRITTEN, and this reverses a recorded stance — deliberately, on three grounds.** The
108
+ * predecessor declared no `write` on the reasoning that `extra` is "Scheme-specific additional information"
109
+ * and writing into another party's namespace is not our act. That reasoning has been overtaken. First, the
110
+ * host itself no longer treats `extra` as wholly scheme-private: §6.1 reserves `assetTransferMethod` and
111
+ * `paymentFlow` inside it as protocol-governed names, so `extra` is a host-managed extension surface with
112
+ * scheme-specific residue, not a foreign namespace. Second, LCP v1.38 §C.4's own Tier A illustration puts
113
+ * `atrHash` AND `legalContextUrl` in `accepts[].extra` — a third-party reader built from the spec's example
114
+ * reads `extra` first, and a challenge that leaves it empty is invisible to that reader. Third, the shipped
115
+ * buyer parser reads BOTH carriers and reconciles, refusing disagreement — so the mirror cannot drift
116
+ * silently: two slots either agree or the document refuses at the counterparty. The write lands only in
117
+ * `accepts[0]`, the requirement buyer parsers read (see the index-0 rule below), and never touches the
118
+ * reserved names.
90
119
  *
91
120
  * **The alias is index 0 only.** A locator names one path. `accepts[0]` is what buyer parsers read, and the
92
121
  * reason is substantive: the reference must bind to the requirement actually being paid, and searching every
93
122
  * requirement would let a seller park a second set of terms on an alternative it never expects to be chosen.
94
123
  *
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.
124
+ * **`termsUrlFields` declares BOTH slots the wire carries, and both are written.** The predecessor member
125
+ * (`termsUrlField`, singular) named only the `extensions` slot, and it was read-only in every published
126
+ * package — the write path did not exist anywhere, so a seller assembling from published parts emitted a
127
+ * challenge advertising a hash with no locator, which the published buyer refuses
128
+ * (integra-protocol#8). Declaring both slots makes the manifest state what actually lands on the wire:
129
+ * `place` writes the URL beside the reference in `info` (where the authority schema requires it) and
130
+ * mirrors it at `accepts[0].extra.legalContextUrl` (where §C.4's illustration carries it), and `extract`
131
+ * reconciles the two, refusing disagreement. The kit REQUIRES the URL of any integrity-bearing
132
+ * advertisement on this manifest — a hash no counterparty can resolve is unverifiable by construction,
133
+ * which is the defect the readers always guarded against and the emitters never did.
103
134
  *
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.
135
+ * **`carrierTypes` is `sha256` alone, and the `url` admission is WITHDRAWN — a defect resolved, not a
136
+ * preference.** The predecessor admitted `url` as the §8.1 discovery form and recorded, in the same
137
+ * docblock, that the permission was "WIDER than any shipped x402 reader": a buyer parser that requires an
138
+ * integrity carrier refuses `info.type !== "sha256"` outright, so a `url` placed in this slot was
139
+ * well-formed against the manifest and rejected at read time — a permission no reader accepts, which is a
140
+ * claim about the ecosystem rather than a description of it. The withdrawal ground is now structural: the
141
+ * `schema` member this package puts on the wire is the AUTHORITY document's shape, whose `type` is
142
+ * `const: "sha256"`, so a `url` reference would emit a challenge that violates its own adjacent schema.
143
+ * The predecessor declined to narrow because "narrowing the reference field to one type is a change to
144
+ * what the SLOT may hold across the set" — that set-wide decision has since been made, by the authority
145
+ * document. `ipfs`/`ar` remain excluded on the original ground: alternative integrity carriers add no
146
+ * capability `sha256` does not already discharge while advertising a content-addressed transport no x402
147
+ * counterparty resolves.
119
148
  */
120
149
  export const X402_PLACEMENT = {
121
150
  protocol: "x402",
@@ -129,10 +158,14 @@ export const X402_PLACEMENT = {
129
158
  path: "accepts.0.extra.atrHash",
130
159
  encoding: "bare-value",
131
160
  bareType: "sha256",
161
+ write: true,
132
162
  },
133
163
  ],
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)",
164
+ termsUrlFields: [
165
+ "extensions.legalContext.info.legalContextUrl",
166
+ "accepts.0.extra.legalContextUrl",
167
+ ],
168
+ carrierTypes: ["sha256"],
169
+ specRef: "x402 v2 (x402-foundation/x402@db5da2e65952 specs/x402-specification-v2.md, read 2026-08-11) — top-level extensions map carried on PaymentRequired/PaymentPayload/SettlementResponse, each entry {info, schema}; accepts[].extra carries the §C.4 mirror (reserved names per §6.1 untouched; gate discharged: see README)",
137
170
  };
138
171
  //# sourceMappingURL=manifest.js.map
@@ -1 +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"}
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAC/B,MAAM,CAAC,MAAM,CAAC;IACZ,OAAO,EAAE,8CAA8C;IACvD,KAAK,EAAE,oCAAoC;IAC3C,WAAW,EACT,0YAA0Y;IAC5Y,IAAI,EAAE,QAAQ;IACd,oBAAoB,EAAE,KAAK;IAC3B,QAAQ,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,iBAAiB,CAAC;IAC9C,UAAU,EAAE;QACV,IAAI,EAAE;YACJ,WAAW,EACT,gGAAgG;YAClG,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,QAAQ;SAChB;QACD,KAAK,EAAE;YACL,WAAW,EACT,+EAA+E;YACjF,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,kBAAkB;SAC5B;QACD,eAAe,EAAE;YACf,WAAW,EACT,yJAAyJ;YAC3J,IAAI,EAAE,QAAQ;YACd,MAAM,EAAE,KAAK;SACd;KACF;CACF,CAAC,CAAC;AAEL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuFG;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;YAClB,KAAK,EAAE,IAAI;SACZ;KACF;IACD,cAAc,EAAE;QACd,8CAA8C;QAC9C,iCAAiC;KAClC;IACD,YAAY,EAAE,CAAC,QAAQ,CAAC;IACxB,OAAO,EACL,0TAA0T;CAC7T,CAAC"}
@@ -1,16 +1,29 @@
1
1
  import { type ReferencePlacementAdapter } from "@integraledger/lcp-binding-core";
2
2
  /**
3
- * The x402 reference placement — the kit's `extract`, and the ONE `place` in the set that the kit cannot
4
- * supply.
3
+ * The x402 reference placement — the kit, plus the ONE wrapper the kit cannot know.
5
4
  *
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.
5
+ * x402's slot does not hold the reference directly: it holds `{ info, schema }`, where `info` is the
6
+ * carrier payload and `schema` is the inlined JSON Schema describing it (a REQUIRED member per x402
7
+ * §5.1.2). That wrapper is a protocol fact no container kind models, so `place` is overridden — but as
8
+ * COMPOSITION over `base.place`, not as a reimplementation beside it. The kit performs the entire
9
+ * placement first: the advertisement rules (terms URL demanded of an integrity-bearing reference,
10
+ * refused where malformed), the canonical write into `extensions.legalContext.info`, the bare-hash
11
+ * mirror into `accepts[0].extra.atrHash`, the terms-URL writes into BOTH declared slots, every
12
+ * malformed-container refusal, and purity. The override then does exactly one thing: rebuilds our entry
13
+ * as `{ info, schema }` so the wrapper's second member lands beside the payload the kit just wrote.
14
+ *
15
+ * The predecessor override reimplemented the write wholesale — sibling preservation, own-property
16
+ * walks, the malformed-`extensions` refusal, all restated beside the kit's copies — which is how it
17
+ * could drift: it froze a schema and never wrote a terms URL while the kit's manifest declared one
18
+ * (integra-protocol#8). A one-member rebuild has no room to disagree with the manifest, because
19
+ * everything the manifest declares is discharged before it runs.
20
+ *
21
+ * Rebuilding the ENTRY wholesale (rather than merging `schema` into whatever sits there) is the
22
+ * predecessor's ratified behaviour, kept: our entry is the direct holder and is REPLACED, so junk a
23
+ * counterparty parked inside `extensions.legalContext` does not ride our wire; sibling entries in
24
+ * `extensions` survive untouched, per the host's own echo rule. `extract` is the kit's, unchanged —
25
+ * reading `info` is an ordinary object-path read, the alias and both terms-URL slots are declared data,
26
+ * and reconciliation lives where every protocol shares it.
14
27
  */
15
28
  export declare const x402Placement: ReferencePlacementAdapter;
16
29
  //# sourceMappingURL=placement.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"placement.d.ts","sourceRoot":"","sources":["../src/placement.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,yBAAyB,EAI/B,MAAM,iCAAiC,CAAC;AAKzC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,aAAa,EAAE,yBA4B3B,CAAC"}
package/dist/placement.js CHANGED
@@ -1,91 +1,51 @@
1
- import { CarrierError, encodeForField, makePlacement, } from "@integraledger/lcp-binding-core";
1
+ import { makePlacement, readAtPath, requireWritten, writeToContainer, } from "@integraledger/lcp-binding-core";
2
2
  import { LEGAL_CONTEXT_SCHEMA, X402_PLACEMENT } from "./manifest.js";
3
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
4
  /**
15
- * The x402 reference placement — the kit's `extract`, and the ONE `place` in the set that the kit cannot
16
- * supply.
5
+ * The x402 reference placement — the kit, plus the ONE wrapper the kit cannot know.
17
6
  *
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.
7
+ * x402's slot does not hold the reference directly: it holds `{ info, schema }`, where `info` is the
8
+ * carrier payload and `schema` is the inlined JSON Schema describing it (a REQUIRED member per x402
9
+ * §5.1.2). That wrapper is a protocol fact no container kind models, so `place` is overridden — but as
10
+ * COMPOSITION over `base.place`, not as a reimplementation beside it. The kit performs the entire
11
+ * placement first: the advertisement rules (terms URL demanded of an integrity-bearing reference,
12
+ * refused where malformed), the canonical write into `extensions.legalContext.info`, the bare-hash
13
+ * mirror into `accepts[0].extra.atrHash`, the terms-URL writes into BOTH declared slots, every
14
+ * malformed-container refusal, and purity. The override then does exactly one thing: rebuilds our entry
15
+ * as `{ info, schema }` so the wrapper's second member lands beside the payload the kit just wrote.
16
+ *
17
+ * The predecessor override reimplemented the write wholesale — sibling preservation, own-property
18
+ * walks, the malformed-`extensions` refusal, all restated beside the kit's copies — which is how it
19
+ * could drift: it froze a schema and never wrote a terms URL while the kit's manifest declared one
20
+ * (integra-protocol#8). A one-member rebuild has no room to disagree with the manifest, because
21
+ * everything the manifest declares is discharged before it runs.
22
+ *
23
+ * Rebuilding the ENTRY wholesale (rather than merging `schema` into whatever sits there) is the
24
+ * predecessor's ratified behaviour, kept: our entry is the direct holder and is REPLACED, so junk a
25
+ * counterparty parked inside `extensions.legalContext` does not ride our wire; sibling entries in
26
+ * `extensions` survive untouched, per the host's own echo rule. `extract` is the kit's, unchanged —
27
+ * reading `info` is an ordinary object-path read, the alias and both terms-URL slots are declared data,
28
+ * and reconciliation lives where every protocol shares it.
26
29
  */
27
30
  export const x402Placement = {
28
31
  ...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
- };
32
+ place(ad, doc) {
33
+ const placed = base.place(ad, doc);
34
+ if ("refused" in placed)
35
+ return placed;
36
+ // The kit's write just created this path on its own output; reading it back — rather than re-encoding
37
+ // the reference a second time — keeps one codepath responsible for what `info` contains.
38
+ const info = readAtPath(placed.value, X402_PLACEMENT.field);
39
+ // TOTAL by the kit's own postconditions: `place` succeeded, so `placed.value` is a record; the write's
40
+ // parent path is the one-segment `extensions`, and a direct holder that is present-but-unmergeable is
41
+ // REPLACED under the ratified container rule, never refused. `requireWritten` states that rather than
42
+ // asserting it with a cast — if the postcondition ever breaks, this throws instead of returning a
43
+ // success carrying no document.
44
+ const wrapped = requireWritten(writeToContainer(placed.value, X402_PLACEMENT.container, "extensions.legalContext", {
45
+ info,
46
+ schema: LEGAL_CONTEXT_SCHEMA,
47
+ }), "x402Placement.place");
48
+ return { ok: true, value: wrapped };
89
49
  },
90
50
  };
91
51
  //# sourceMappingURL=placement.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"placement.js","sourceRoot":"","sources":["../src/placement.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"placement.js","sourceRoot":"","sources":["../src/placement.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,aAAa,EAGb,UAAU,EACV,cAAc,EACd,gBAAgB,GACjB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAErE,MAAM,IAAI,GAAG,aAAa,CAAC,cAAc,CAAC,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,aAAa,GAA8B;IACtD,GAAG,IAAI;IAEP,KAAK,CAAC,EAA6B,EAAE,GAAY;QAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;QACnC,IAAI,SAAS,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QACvC,sGAAsG;QACtG,yFAAyF;QACzF,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC;QAC5D,uGAAuG;QACvG,sGAAsG;QACtG,sGAAsG;QACtG,kGAAkG;QAClG,gCAAgC;QAChC,MAAM,OAAO,GAAG,cAAc,CAC5B,gBAAgB,CACd,MAAM,CAAC,KAAK,EACZ,cAAc,CAAC,SAAS,EACxB,yBAAyB,EACzB;YACE,IAAI;YACJ,MAAM,EAAE,oBAAoB;SAC7B,CACF,EACD,qBAAqB,CACtB,CAAC;QACF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IACtC,CAAC;CACF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integraledger/lcp-placement-x402",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Places an LCP reference into an x402 v2 payment challenge, and reads it back.",
5
5
  "keywords": [
6
6
  "lcp",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "homepage": "https://github.com/IntegraLedger/integra-protocol/tree/main/packages/placement-x402#readme",
42
42
  "dependencies": {
43
- "@integraledger/lcp-binding-core": "0.9.0"
43
+ "@integraledger/lcp-binding-core": "0.11.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@cfworker/json-schema": "4.1.1",