@integraledger/lcp-placement-ucp 0.10.1 → 0.12.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 CHANGED
@@ -1,5 +1,111 @@
1
1
  # @integraledger/lcp-placement-ucp
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Patch Changes
6
+
7
+ - @integraledger/lcp-binding-core@0.12.0
8
+
9
+ ## 0.11.0
10
+
11
+ ### Minor Changes
12
+
13
+ - b2ffecc: Report the class a record actually supports, place a terms URL on UCP, and stop refusing a conformant UCP
14
+ profile — the remediation of the 2026-08-19 conformance re-audit.
15
+
16
+ **`verify` now computes `supportedClass` instead of echoing the claim.** It was `anyFailed ? "TC-0" :
17
+ claimedClass`, so a record proving nothing — no settlement, no acceptance, no authority chain — reported
18
+ whatever class the caller named, while the field's own published docblock promised "what the record
19
+ honestly supports, not what the caller asked for". It is now the highest class every one of whose required
20
+ steps is `proved`, `TC-0` on any failure, computed from the steps alone: neither capped by the claim (rungs
21
+ that reach TC-3 read TC-3 where the caller claimed TC-2) nor lifted by it. White paper #4 §5 defines the
22
+ class of a transaction as "the highest class whose criteria it fully meets", and this is that.
23
+
24
+ The claim is not discarded — the report gains **`claimedClass`**, a required member, because `verified`
25
+ answers "did the record reach the class it claimed?" and cannot be read without it. The two fields are the
26
+ report's two halves: an input echoed, and a finding computed. Where they differ, the record did not reach
27
+ its own shape. An out-of-taxonomy claim now lands only in the echo and can no longer masquerade as a
28
+ finding.
29
+
30
+ **UCP can advertise a terms URL.** Its policy object declares `url` — "Optional link to the full policy
31
+ document", `format: uri` — on the very entry this placement writes, and §C.3's illustration carries `url`
32
+ and `atrHash` side by side there. The manifest previously said the protocol had no slot, citing `links[]`,
33
+ which §C.3 separates as "a standing page, not a per-transaction record". The obstacle was mechanical:
34
+ `termsUrlFields` addresses document paths, and a tagged-array entry's index is chosen at write time. The
35
+ `tagged-array` container therefore gains `termsUrlField`, written onto the same entry in the same write, and
36
+ read back through the same first-match rule. UCP was the last shipped protocol that refused an
37
+ advertisement carrying its own locator (integra-protocol#8).
38
+
39
+ **`readUcpProfile` no longer refuses a conformant business profile.** `requireHttps` mapped an ABSENT `spec`
40
+ to the same branch as a malformed one, and the live host requires `spec` only of a platform declaration —
41
+ as this repository's own README already said. It is now `requireHttpsIfDeclared`: absence is absence, and a
42
+ declared value is still held to the host's https MUST.
43
+
44
+ Also: `requireWritten` replaces an unchecked cast in the x402 override, so a broken postcondition throws
45
+ instead of returning a success carrying no document; six x402 citations move to the revision that actually
46
+ touches the file they name, and a new gate refuses any `owner/repo@sha` in source that `spec-pins.json` does
47
+ not record; four spec citations move from line anchors to section anchors; the escrow binding states why it
48
+ declares no §8.3.1 off-canonical variant, and asserts it; and §C.3's `policies[]` illustration is recorded as
49
+ invalid against the live UCP schema, which shows `description` as a bare string where the host requires an
50
+ object — owed upstream, not a defect here.
51
+
52
+ Corpus 844 → 847, root `ec4ad1b02a81538b…`.
53
+
54
+ - 822190a: Give the terms URL the write path the published set never had, and certify the composition that broke
55
+ without it (integra-protocol#8).
56
+
57
+ A third party assembling a seller from published parts emitted a 402 the published buyer refuses: every
58
+ published reader demanded `legalContextUrl` and no published writer placed it, and the schema
59
+ `placement-x402` inlined onto the wire (`required: ["type","value"]`, closed) contradicted the authority
60
+ document integraledger.com serves (`required: ["type","value","legalContextUrl"]`, closed) — two
61
+ definitions of one `info`, no document valid against both, each package self-consistent. Three structural
62
+ gaps let it ship: the manifest's `termsUrlField` was singular and read-only (declared, hygiene-checked,
63
+ never written — and x402's wire carries the URL in two slots, so one path could not even name the shape),
64
+ nothing compared the inlined schema to the authority document, and the corpus certified `place` and
65
+ `extract` separately but never fed one to the other.
66
+
67
+ `binding-core` — the placement seam now moves an ADVERTISEMENT, not a bare reference. `place` takes
68
+ `{ ref, termsUrl? }` and writes the URL at every slot the manifest's new `termsUrlFields` (plural,
69
+ replacing `termsUrlField`) declares; it REFUSES an integrity-bearing advertisement with no URL where slots
70
+ are declared (a hash no counterparty can resolve is unverifiable by construction), a URL where no slot
71
+ exists (silent dropping is fail-open), and a non-https URL on either side of the seam. `extract` returns
72
+ `{ ref, termsUrl }` with absence as a typed value — `no-field-declared` is a fact about the protocol,
73
+ `declared-fields-empty` a fact about the document, and the gate decides what an absence means — while two
74
+ slots that disagree, or a malformed value in either, refuse. The object-path writer learned to descend
75
+ into an EXISTING array element (never minting one, never extending a list, refusing an index segment it
76
+ would have to create), which is what lets x402's `accepts[0].extra` mirrors land.
77
+
78
+ `placement-x402` — the inlined wire schema now IS the authority document minus `$id` and `$defs`
79
+ (Bazaar forbids both on the wire), drift-gated in `lcp-conformance` where the two packages meet.
80
+ `termsUrlFields` declares both slots the wire carries; the bare-hash alias is written (`extra` stopped
81
+ being wholly scheme-private when x402 §6.1 reserved names inside it, and LCP v1.38 §C.4's own Tier A
82
+ illustration carries the pair there); the `url` carrier admission is withdrawn (`carrierTypes` is
83
+ `sha256` alone — the schema on the wire is `const: "sha256"`, and no shipped reader ever accepted a url
84
+ in this slot). The `place` override shrinks to composition: the kit performs the whole placement and the
85
+ override adds only the `{info, schema}` wrapper.
86
+
87
+ `placement-mpp` / `placement-acp` — the singular member becomes the one-entry `termsUrlFields`; the kit
88
+ now writes the slot their buyer parsers always demanded and refuses first at the seller.
89
+
90
+ `lcp-conformance` — the corpus grows 812 → 844: a `roundtrip` op (place then extract in one case, the
91
+ composition certification whose absence let two separately-conformant halves ship jointly broken),
92
+ advertisement-rule refusals for every manifest, and the authority↔wire drift gate. Extract expectations
93
+ across every placement area become the extracted advertisement.
94
+
95
+ `lcp-verify` — `referencePlacementStep` reads the advertisement (`extracted.ref.value`) and deliberately
96
+ ignores `termsUrl`: where the terms live is the gate's fetch concern, not a fact the record can
97
+ contradict.
98
+
99
+ `lcp-discovery` — the x402 authority document restates the atrHash pattern inline in both definitions
100
+ (no `$defs` indirection the wire copy would have to rewrite) and moves the two-definitions rationale into
101
+ `$defs.receipt`, so the challenge-time root is byte-derivable for the wire.
102
+
103
+ ### Patch Changes
104
+
105
+ - Updated dependencies [b2ffecc]
106
+ - Updated dependencies [822190a]
107
+ - @integraledger/lcp-binding-core@0.11.0
108
+
3
109
  ## 0.10.1
4
110
 
5
111
  **0.10.0 was staged and withdrawn before approval; this is that release, re-cut.** The conformance corpus
package/README.md CHANGED
@@ -37,7 +37,7 @@ import { UCP_PLACEMENT, ucpPlacement } from "@integraledger/lcp-placement-ucp";
37
37
 
38
38
  declare const checkout: unknown; // the UCP checkout response, as received
39
39
 
40
- const placed = ucpPlacement.place({ type: "sha256", value: "0x…" }, checkout);
40
+ const placed = ucpPlacement.place({ ref: { type: "sha256", value: "0x…" } }, checkout);
41
41
  const ref = ucpPlacement.extract(checkout);
42
42
  ```
43
43
 
@@ -108,7 +108,7 @@ alone. Both arms are pinned as vectors.
108
108
 
109
109
  ## Why the links alias has no `write` flag
110
110
 
111
- The links entry carries the terms **URL** — a different datum than the atrHash — and `place(ref, doc)`
111
+ The links entry carries the terms **URL** — a different datum than the atrHash — and `place(ad, doc)`
112
112
  holds one reference, not two data: writing `ref.value` into `links[].url` would put a bare hash where every
113
113
  UCP client expects a URL. Publishing the links entry is the deployment's act (UCP already makes `links[]`
114
114
  required on checkout responses), and this placement **reads** it: `extract` falls to the alias when no policy entry is
@@ -66,12 +66,18 @@ import type { PlacementManifest } from "@integraledger/lcp-binding-core";
66
66
  * `links[].type` is an OPEN set ("Businesses MAY define custom types"); the well-known `terms_of_service`
67
67
  * tag is used because it is the spelling counterparties already read, not because it is the only one legal.
68
68
  *
69
- * **`termsUrlField` is deliberately OMITTED.** The terms URL rides `links[type=terms_of_service].url` a
70
- * tagged-array locator that a dotted-path parser cannot resolve. Declaring it as `termsUrlField` would
71
- * repeat the defect the `field` rule exists to prevent: a declared property that is not the declared thing.
72
- * The discovery
73
- * alias states the same fact WITH its machine-readable container, and the `PlacementManifest` contract says
74
- * an absent `termsUrlField` means a parser must not demand one.
69
+ * **The terms URL rides the policies entry, declared as the container's `termsUrlField`.** UCP's policy
70
+ * object is `additionalProperties: true` and declares `url` outright "Optional link to the full policy
71
+ * document", `format: uri` (re-verified at UCP HEAD, `source/schemas/shopping/types/policy.json`) and
72
+ * §C.3's own illustration carries `url` and `atrHash` side by side in ONE entry. So the per-transaction
73
+ * locator belongs on the per-transaction record, which is the entry this placement writes.
74
+ *
75
+ * It is declared container-relative rather than in `termsUrlFields` because the entry's index is chosen at
76
+ * write time (replace-by-tag, else append), so no dotted path names it — not because the host lacks a slot.
77
+ * `links[]` is NOT that slot and never was: §C.3 separates the two explicitly, `links[]` being "a standing
78
+ * page, not a per-transaction record". A predecessor of this manifest gave the links entry as the reason
79
+ * this placement could advertise no terms URL at all, which read the wrong carrier and left UCP the one
80
+ * shipped protocol that refused an advertisement carrying its locator (integra-protocol#8).
75
81
  *
76
82
  * **`carrierTypes` permits `sha256` and `url`, and `url` is load-bearing.** extract checks every decoded
77
83
  * reference against this list, so the discovery alias's url-typed hits would refuse
@@ -1 +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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AACH,eAAO,MAAM,aAAa,EAAE,iBAyC3B,CAAC"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2FG;AACH,eAAO,MAAM,aAAa,EAAE,iBA6C3B,CAAC"}
package/dist/manifest.js CHANGED
@@ -65,12 +65,18 @@
65
65
  * `links[].type` is an OPEN set ("Businesses MAY define custom types"); the well-known `terms_of_service`
66
66
  * tag is used because it is the spelling counterparties already read, not because it is the only one legal.
67
67
  *
68
- * **`termsUrlField` is deliberately OMITTED.** The terms URL rides `links[type=terms_of_service].url` a
69
- * tagged-array locator that a dotted-path parser cannot resolve. Declaring it as `termsUrlField` would
70
- * repeat the defect the `field` rule exists to prevent: a declared property that is not the declared thing.
71
- * The discovery
72
- * alias states the same fact WITH its machine-readable container, and the `PlacementManifest` contract says
73
- * an absent `termsUrlField` means a parser must not demand one.
68
+ * **The terms URL rides the policies entry, declared as the container's `termsUrlField`.** UCP's policy
69
+ * object is `additionalProperties: true` and declares `url` outright "Optional link to the full policy
70
+ * document", `format: uri` (re-verified at UCP HEAD, `source/schemas/shopping/types/policy.json`) and
71
+ * §C.3's own illustration carries `url` and `atrHash` side by side in ONE entry. So the per-transaction
72
+ * locator belongs on the per-transaction record, which is the entry this placement writes.
73
+ *
74
+ * It is declared container-relative rather than in `termsUrlFields` because the entry's index is chosen at
75
+ * write time (replace-by-tag, else append), so no dotted path names it — not because the host lacks a slot.
76
+ * `links[]` is NOT that slot and never was: §C.3 separates the two explicitly, `links[]` being "a standing
77
+ * page, not a per-transaction record". A predecessor of this manifest gave the links entry as the reason
78
+ * this placement could advertise no terms URL at all, which read the wrong carrier and left UCP the one
79
+ * shipped protocol that refused an advertisement carrying its locator (integra-protocol#8).
74
80
  *
75
81
  * **`carrierTypes` permits `sha256` and `url`, and `url` is load-bearing.** extract checks every decoded
76
82
  * reference against this list, so the discovery alias's url-typed hits would refuse
@@ -104,6 +110,10 @@ export const UCP_PLACEMENT = {
104
110
  plain: "Terms of sale for this order, identified by a Legal Context Protocol reference. The reference identifies the exact terms document; it is not itself the terms.",
105
111
  },
106
112
  },
113
+ // `url` is declared on this very object by the host — "Optional link to the full policy document",
114
+ // format uri — and §C.3's illustration carries it beside `atrHash` in one entry. Verified at UCP HEAD
115
+ // (source/schemas/shopping/types/policy.json).
116
+ termsUrlField: "url",
107
117
  },
108
118
  field: "policies[type=com.integraledger.policy.legal_context]",
109
119
  readAlso: [
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AACH,MAAM,CAAC,MAAM,aAAa,GAAsB;IAC9C,QAAQ,EAAE,KAAK;IACf,OAAO,EAAE,eAAe;IACxB,IAAI,EAAE,GAAG;IACT,QAAQ,EAAE,kBAAkB;IAC5B,SAAS,EAAE;QACT,IAAI,EAAE,cAAc;QACpB,EAAE,EAAE,UAAU;QACd,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,wCAAwC;QAC7C,UAAU,EAAE,iCAAiC;QAC7C,mGAAmG;QACnG,iGAAiG;QACjG,yGAAyG;QACzG,uCAAuC;QACvC,SAAS,EAAE;YACT,WAAW,EAAE;gBACX,KAAK,EACH,gKAAgK;aACnK;SACF;KACF;IACD,KAAK,EAAE,uDAAuD;IAC9D,QAAQ,EAAE;QACR;YACE,IAAI,EAAE,kCAAkC;YACxC,QAAQ,EAAE,YAAY;YACtB,QAAQ,EAAE,KAAK;YACf,YAAY,EAAE,WAAW;YACzB,SAAS,EAAE;gBACT,IAAI,EAAE,cAAc;gBACpB,EAAE,EAAE,OAAO;gBACX,QAAQ,EAAE,MAAM;gBAChB,GAAG,EAAE,kBAAkB;gBACvB,UAAU,EAAE,KAAK;aAClB;SACF;KACF;IACD,YAAY,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC;IAC/B,OAAO,EACL,sWAAsW;CACzW,CAAC"}
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2FG;AACH,MAAM,CAAC,MAAM,aAAa,GAAsB;IAC9C,QAAQ,EAAE,KAAK;IACf,OAAO,EAAE,eAAe;IACxB,IAAI,EAAE,GAAG;IACT,QAAQ,EAAE,kBAAkB;IAC5B,SAAS,EAAE;QACT,IAAI,EAAE,cAAc;QACpB,EAAE,EAAE,UAAU;QACd,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,wCAAwC;QAC7C,UAAU,EAAE,iCAAiC;QAC7C,mGAAmG;QACnG,iGAAiG;QACjG,yGAAyG;QACzG,uCAAuC;QACvC,SAAS,EAAE;YACT,WAAW,EAAE;gBACX,KAAK,EACH,gKAAgK;aACnK;SACF;QACD,mGAAmG;QACnG,sGAAsG;QACtG,+CAA+C;QAC/C,aAAa,EAAE,KAAK;KACrB;IACD,KAAK,EAAE,uDAAuD;IAC9D,QAAQ,EAAE;QACR;YACE,IAAI,EAAE,kCAAkC;YACxC,QAAQ,EAAE,YAAY;YACtB,QAAQ,EAAE,KAAK;YACf,YAAY,EAAE,WAAW;YACzB,SAAS,EAAE;gBACT,IAAI,EAAE,cAAc;gBACpB,EAAE,EAAE,OAAO;gBACX,QAAQ,EAAE,MAAM;gBAChB,GAAG,EAAE,kBAAkB;gBACvB,UAAU,EAAE,KAAK;aAClB;SACF;KACF;IACD,YAAY,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC;IAC/B,OAAO,EACL,sWAAsW;CACzW,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"placement.d.ts","sourceRoot":"","sources":["../src/placement.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,iCAAiC,CAAC;AAKzC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,YAAY,EAAE,yBAc1B,CAAC"}
1
+ {"version":3,"file":"placement.d.ts","sourceRoot":"","sources":["../src/placement.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,iCAAiC,CAAC;AAKzC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,YAAY,EAAE,yBAe1B,CAAC"}
package/dist/placement.js CHANGED
@@ -30,12 +30,13 @@ export const ucpPlacement = {
30
30
  const out = base.extract(doc);
31
31
  if ("refused" in out)
32
32
  return out;
33
- if (out.value.type === "url" && !out.value.value.startsWith("https://"))
33
+ const { ref } = out.value;
34
+ if (ref.type === "url" && !ref.value.startsWith("https://"))
34
35
  return {
35
36
  refused: true,
36
37
  haltClass: "verification-failure",
37
38
  code: "ucp/insecure-terms-url",
38
- detail: `a terms link must be HTTPS — an http: reference is rewritable in transit: ${out.value.value}`,
39
+ detail: `a terms link must be HTTPS — an http: reference is rewritable in transit: ${ref.value}`,
39
40
  };
40
41
  return out;
41
42
  },
@@ -1 +1 @@
1
- {"version":3,"file":"placement.js","sourceRoot":"","sources":["../src/placement.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,GAEd,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,MAAM,IAAI,GAAG,aAAa,CAAC,aAAa,CAAC,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,YAAY,GAA8B;IACrD,GAAG,IAAI;IACP,OAAO,CAAC,GAAY;QAClB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,SAAS,IAAI,GAAG;YAAE,OAAO,GAAG,CAAC;QACjC,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC;YACrE,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,SAAS,EAAE,sBAAsB;gBACjC,IAAI,EAAE,wBAAwB;gBAC9B,MAAM,EAAE,6EAA6E,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE;aACvG,CAAC;QACJ,OAAO,GAAG,CAAC;IACb,CAAC;CACF,CAAC"}
1
+ {"version":3,"file":"placement.js","sourceRoot":"","sources":["../src/placement.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,GAEd,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,MAAM,IAAI,GAAG,aAAa,CAAC,aAAa,CAAC,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,YAAY,GAA8B;IACrD,GAAG,IAAI;IACP,OAAO,CAAC,GAAY;QAClB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,SAAS,IAAI,GAAG;YAAE,OAAO,GAAG,CAAC;QACjC,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC;QAC1B,IAAI,GAAG,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC;YACzD,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,SAAS,EAAE,sBAAsB;gBACjC,IAAI,EAAE,wBAAwB;gBAC9B,MAAM,EAAE,6EAA6E,GAAG,CAAC,KAAK,EAAE;aACjG,CAAC;QACJ,OAAO,GAAG,CAAC;IACb,CAAC;CACF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integraledger/lcp-placement-ucp",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "Places an LCP reference into a UCP (Universal Commerce Protocol) checkout, 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-ucp#readme",
42
42
  "dependencies": {
43
- "@integraledger/lcp-binding-core": "0.10.1"
43
+ "@integraledger/lcp-binding-core": "0.12.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@cfworker/json-schema": "4.1.1",
package/src/manifest.ts CHANGED
@@ -67,12 +67,18 @@ import type { PlacementManifest } from "@integraledger/lcp-binding-core";
67
67
  * `links[].type` is an OPEN set ("Businesses MAY define custom types"); the well-known `terms_of_service`
68
68
  * tag is used because it is the spelling counterparties already read, not because it is the only one legal.
69
69
  *
70
- * **`termsUrlField` is deliberately OMITTED.** The terms URL rides `links[type=terms_of_service].url` a
71
- * tagged-array locator that a dotted-path parser cannot resolve. Declaring it as `termsUrlField` would
72
- * repeat the defect the `field` rule exists to prevent: a declared property that is not the declared thing.
73
- * The discovery
74
- * alias states the same fact WITH its machine-readable container, and the `PlacementManifest` contract says
75
- * an absent `termsUrlField` means a parser must not demand one.
70
+ * **The terms URL rides the policies entry, declared as the container's `termsUrlField`.** UCP's policy
71
+ * object is `additionalProperties: true` and declares `url` outright "Optional link to the full policy
72
+ * document", `format: uri` (re-verified at UCP HEAD, `source/schemas/shopping/types/policy.json`) and
73
+ * §C.3's own illustration carries `url` and `atrHash` side by side in ONE entry. So the per-transaction
74
+ * locator belongs on the per-transaction record, which is the entry this placement writes.
75
+ *
76
+ * It is declared container-relative rather than in `termsUrlFields` because the entry's index is chosen at
77
+ * write time (replace-by-tag, else append), so no dotted path names it — not because the host lacks a slot.
78
+ * `links[]` is NOT that slot and never was: §C.3 separates the two explicitly, `links[]` being "a standing
79
+ * page, not a per-transaction record". A predecessor of this manifest gave the links entry as the reason
80
+ * this placement could advertise no terms URL at all, which read the wrong carrier and left UCP the one
81
+ * shipped protocol that refused an advertisement carrying its locator (integra-protocol#8).
76
82
  *
77
83
  * **`carrierTypes` permits `sha256` and `url`, and `url` is load-bearing.** extract checks every decoded
78
84
  * reference against this list, so the discovery alias's url-typed hits would refuse
@@ -107,6 +113,10 @@ export const UCP_PLACEMENT: PlacementManifest = {
107
113
  "Terms of sale for this order, identified by a Legal Context Protocol reference. The reference identifies the exact terms document; it is not itself the terms.",
108
114
  },
109
115
  },
116
+ // `url` is declared on this very object by the host — "Optional link to the full policy document",
117
+ // format uri — and §C.3's illustration carries it beside `atrHash` in one entry. Verified at UCP HEAD
118
+ // (source/schemas/shopping/types/policy.json).
119
+ termsUrlField: "url",
110
120
  },
111
121
  field: "policies[type=com.integraledger.policy.legal_context]",
112
122
  readAlso: [
package/src/placement.ts CHANGED
@@ -34,12 +34,13 @@ export const ucpPlacement: ReferencePlacementAdapter = {
34
34
  extract(doc: unknown) {
35
35
  const out = base.extract(doc);
36
36
  if ("refused" in out) return out;
37
- if (out.value.type === "url" && !out.value.value.startsWith("https://"))
37
+ const { ref } = out.value;
38
+ if (ref.type === "url" && !ref.value.startsWith("https://"))
38
39
  return {
39
40
  refused: true,
40
41
  haltClass: "verification-failure",
41
42
  code: "ucp/insecure-terms-url",
42
- detail: `a terms link must be HTTPS — an http: reference is rewritable in transit: ${out.value.value}`,
43
+ detail: `a terms link must be HTTPS — an http: reference is rewritable in transit: ${ref.value}`,
43
44
  };
44
45
  return out;
45
46
  },