@integraledger/lcp-placement-ucp 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/CHANGELOG.md CHANGED
@@ -1,5 +1,124 @@
1
1
  # @integraledger/lcp-placement-ucp
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b2ffecc: Report the class a record actually supports, place a terms URL on UCP, and stop refusing a conformant UCP
8
+ profile — the remediation of the 2026-08-19 conformance re-audit.
9
+
10
+ **`verify` now computes `supportedClass` instead of echoing the claim.** It was `anyFailed ? "TC-0" :
11
+ claimedClass`, so a record proving nothing — no settlement, no acceptance, no authority chain — reported
12
+ whatever class the caller named, while the field's own published docblock promised "what the record
13
+ honestly supports, not what the caller asked for". It is now the highest class every one of whose required
14
+ steps is `proved`, `TC-0` on any failure, computed from the steps alone: neither capped by the claim (rungs
15
+ that reach TC-3 read TC-3 where the caller claimed TC-2) nor lifted by it. White paper #4 §5 defines the
16
+ class of a transaction as "the highest class whose criteria it fully meets", and this is that.
17
+
18
+ The claim is not discarded — the report gains **`claimedClass`**, a required member, because `verified`
19
+ answers "did the record reach the class it claimed?" and cannot be read without it. The two fields are the
20
+ report's two halves: an input echoed, and a finding computed. Where they differ, the record did not reach
21
+ its own shape. An out-of-taxonomy claim now lands only in the echo and can no longer masquerade as a
22
+ finding.
23
+
24
+ **UCP can advertise a terms URL.** Its policy object declares `url` — "Optional link to the full policy
25
+ document", `format: uri` — on the very entry this placement writes, and §C.3's illustration carries `url`
26
+ and `atrHash` side by side there. The manifest previously said the protocol had no slot, citing `links[]`,
27
+ which §C.3 separates as "a standing page, not a per-transaction record". The obstacle was mechanical:
28
+ `termsUrlFields` addresses document paths, and a tagged-array entry's index is chosen at write time. The
29
+ `tagged-array` container therefore gains `termsUrlField`, written onto the same entry in the same write, and
30
+ read back through the same first-match rule. UCP was the last shipped protocol that refused an
31
+ advertisement carrying its own locator (integra-protocol#8).
32
+
33
+ **`readUcpProfile` no longer refuses a conformant business profile.** `requireHttps` mapped an ABSENT `spec`
34
+ to the same branch as a malformed one, and the live host requires `spec` only of a platform declaration —
35
+ as this repository's own README already said. It is now `requireHttpsIfDeclared`: absence is absence, and a
36
+ declared value is still held to the host's https MUST.
37
+
38
+ Also: `requireWritten` replaces an unchecked cast in the x402 override, so a broken postcondition throws
39
+ instead of returning a success carrying no document; six x402 citations move to the revision that actually
40
+ touches the file they name, and a new gate refuses any `owner/repo@sha` in source that `spec-pins.json` does
41
+ not record; four spec citations move from line anchors to section anchors; the escrow binding states why it
42
+ declares no §8.3.1 off-canonical variant, and asserts it; and §C.3's `policies[]` illustration is recorded as
43
+ invalid against the live UCP schema, which shows `description` as a bare string where the host requires an
44
+ object — owed upstream, not a defect here.
45
+
46
+ Corpus 844 → 847, root `ec4ad1b02a81538b…`.
47
+
48
+ - 822190a: Give the terms URL the write path the published set never had, and certify the composition that broke
49
+ without it (integra-protocol#8).
50
+
51
+ A third party assembling a seller from published parts emitted a 402 the published buyer refuses: every
52
+ published reader demanded `legalContextUrl` and no published writer placed it, and the schema
53
+ `placement-x402` inlined onto the wire (`required: ["type","value"]`, closed) contradicted the authority
54
+ document integraledger.com serves (`required: ["type","value","legalContextUrl"]`, closed) — two
55
+ definitions of one `info`, no document valid against both, each package self-consistent. Three structural
56
+ gaps let it ship: the manifest's `termsUrlField` was singular and read-only (declared, hygiene-checked,
57
+ never written — and x402's wire carries the URL in two slots, so one path could not even name the shape),
58
+ nothing compared the inlined schema to the authority document, and the corpus certified `place` and
59
+ `extract` separately but never fed one to the other.
60
+
61
+ `binding-core` — the placement seam now moves an ADVERTISEMENT, not a bare reference. `place` takes
62
+ `{ ref, termsUrl? }` and writes the URL at every slot the manifest's new `termsUrlFields` (plural,
63
+ replacing `termsUrlField`) declares; it REFUSES an integrity-bearing advertisement with no URL where slots
64
+ are declared (a hash no counterparty can resolve is unverifiable by construction), a URL where no slot
65
+ exists (silent dropping is fail-open), and a non-https URL on either side of the seam. `extract` returns
66
+ `{ ref, termsUrl }` with absence as a typed value — `no-field-declared` is a fact about the protocol,
67
+ `declared-fields-empty` a fact about the document, and the gate decides what an absence means — while two
68
+ slots that disagree, or a malformed value in either, refuse. The object-path writer learned to descend
69
+ into an EXISTING array element (never minting one, never extending a list, refusing an index segment it
70
+ would have to create), which is what lets x402's `accepts[0].extra` mirrors land.
71
+
72
+ `placement-x402` — the inlined wire schema now IS the authority document minus `$id` and `$defs`
73
+ (Bazaar forbids both on the wire), drift-gated in `lcp-conformance` where the two packages meet.
74
+ `termsUrlFields` declares both slots the wire carries; the bare-hash alias is written (`extra` stopped
75
+ being wholly scheme-private when x402 §6.1 reserved names inside it, and LCP v1.38 §C.4's own Tier A
76
+ illustration carries the pair there); the `url` carrier admission is withdrawn (`carrierTypes` is
77
+ `sha256` alone — the schema on the wire is `const: "sha256"`, and no shipped reader ever accepted a url
78
+ in this slot). The `place` override shrinks to composition: the kit performs the whole placement and the
79
+ override adds only the `{info, schema}` wrapper.
80
+
81
+ `placement-mpp` / `placement-acp` — the singular member becomes the one-entry `termsUrlFields`; the kit
82
+ now writes the slot their buyer parsers always demanded and refuses first at the seller.
83
+
84
+ `lcp-conformance` — the corpus grows 812 → 844: a `roundtrip` op (place then extract in one case, the
85
+ composition certification whose absence let two separately-conformant halves ship jointly broken),
86
+ advertisement-rule refusals for every manifest, and the authority↔wire drift gate. Extract expectations
87
+ across every placement area become the extracted advertisement.
88
+
89
+ `lcp-verify` — `referencePlacementStep` reads the advertisement (`extracted.ref.value`) and deliberately
90
+ ignores `termsUrl`: where the terms live is the gate's fetch concern, not a fact the record can
91
+ contradict.
92
+
93
+ `lcp-discovery` — the x402 authority document restates the atrHash pattern inline in both definitions
94
+ (no `$defs` indirection the wire copy would have to rewrite) and moves the two-definitions rationale into
95
+ `$defs.receipt`, so the challenge-time root is byte-derivable for the wire.
96
+
97
+ ### Patch Changes
98
+
99
+ - Updated dependencies [b2ffecc]
100
+ - Updated dependencies [822190a]
101
+ - @integraledger/lcp-binding-core@0.11.0
102
+
103
+ ## 0.10.1
104
+
105
+ **0.10.0 was staged and withdrawn before approval; this is that release, re-cut.** The conformance corpus
106
+ was re-sealed after 0.10.0 was staged — its root moved `32fa90a6…` → `28bbf4ef…` when the vector tree was
107
+ brought inside the prose gates — so the staged `lcp-conformance` tarball carried a seal that no longer
108
+ matched the repository. The seal is what proves corpus authenticity to an independent implementer, and a
109
+ published version cannot be replaced, so the whole set was rejected and re-cut rather than shipping one
110
+ package that disagreed with its own source. No version 0.10.0 exists on the registry.
111
+
112
+ Released as part of a flat `0.10.1` across the whole suite. One number describes the set that was built,
113
+ tested and sealed together, so a consumer never has to work out which combination of versions was verified.
114
+
115
+ This package has no source change of its own in this release. What moved across the suite: three exported
116
+ names were corrected before anyone depends on them — `USDC_DECIMALS` became rail-qualified after it was
117
+ found to mean 6 on three rails and 7 on Stellar under one name, `VISA_TAP_PLACEMENT_TIER_A` lost the tier
118
+ it had baked into an identifier, and the memo codecs were named for the carrier they encode. Two wire
119
+ identities were respelled to match the vocabulary they are written into. Five gates were added or widened,
120
+ and a large number of documentation claims were corrected against the host specifications at HEAD.
121
+
3
122
  ## 0.9.0
4
123
 
5
124
  First public release.
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
 
@@ -47,15 +47,19 @@ Both members are total: a refusal is a returned value, never a thrown exception.
47
47
 
48
48
  Read against the live spec at ucp.dev, version `2026-04-08` (`overview#capabilities`,
49
49
  `overview#namespace-governance`, `overview#intersection-algorithm`, and the checkout capability page), the
50
- gate falsified the completion plan's P1 twice:
50
+ gate falsified the design this package was specced from, twice:
51
51
 
52
- 1. **Vendor capabilities are Tier A.** The plan specced a links-only placement on the premise that UCP's
52
+ 1. **Vendor capabilities are Tier A.** It specced a links-only placement on the premise that UCP's
53
53
  strict schema rejects unregistered keys, making the integrity path Tier B. The live spec says the
54
54
  opposite: "Vendors MUST use their own reverse-domain namespace for custom capabilities" — no central
55
- registry, no maintainer approval. What replaces registration is **authority binding**: "the origin of
56
- these URLs MUST match the namespace authority" and "Platform MUST validate this binding and SHOULD
57
- reject capabilities where the spec origin does not match." Built as planned, this package would have
58
- shipped a URL and no hash while a hash-bearing carrier was available the whole time.
55
+ registry, no maintainer approval. What replaces registration is **authority binding**, and it binds the
56
+ `schema` URL alone: "a declared `schema` URL's origin MUST match the namespace authority in its name",
57
+ with a platform obliged to "validate each business-declared `schema` URL before fetching it". The `spec`
58
+ URL is expressly outside the trust path. Built as planned, this package would have shipped a URL and no
59
+ hash while a hash-bearing carrier was available the whole time. (Verified verbatim at UCP HEAD
60
+ 2026-08-11. An earlier release quoted the host as also saying platforms "SHOULD reject capabilities where
61
+ the spec origin does not match" — that sentence is nowhere in UCP, and it inverts the rule above;
62
+ `discovery/src/capability-identity.ts` records the search that established it.)
59
63
  2. **`links[].type` is an OPEN set** ("Businesses MAY define custom types"), not the closed enum the plan
60
64
  claimed — that enum is ACP's; an earlier reading conflated the two protocols. `links[]` is REQUIRED on checkout
61
65
  responses; entries are `{type (req), url (req), title (opt)}`; `terms_of_service` is a recommended
@@ -104,7 +108,7 @@ alone. Both arms are pinned as vectors.
104
108
 
105
109
  ## Why the links alias has no `write` flag
106
110
 
107
- 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)`
108
112
  holds one reference, not two data: writing `ref.value` into `links[].url` would put a bare hash where every
109
113
  UCP client expects a URL. Publishing the links entry is the deployment's act (UCP already makes `links[]`
110
114
  required on checkout responses), and this placement **reads** it: `extract` falls to the alias when no policy entry is
@@ -112,7 +116,9 @@ present, the `url` carrier type itself signalling discovery-not-integrity.
112
116
 
113
117
  ## Provenance
114
118
 
115
- Cut against UCP `2026-04-08` (ucp.dev) and reconciled against LCP v1.37 §C.3. A deployment advertises under
119
+ Cut against UCP `2026-04-08` (ucp.dev) and reconciled against LCP v1.37 §C.3; re-read against **v1.38 §C.3**
120
+ on 2026-08-12, which now records `policies[]` as a Tier A carrier in the negotiated baseline — the carrier
121
+ this package moved to. A deployment advertises under
116
122
  its own reverse-domain namespace; `org.legalcontextprotocol.*` is reserved for a TSC-ratified capability.
117
123
 
118
124
  ---
@@ -66,11 +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 S7 fixed for `field`: a declared property that is not the declared thing. 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.
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).
74
81
  *
75
82
  * **`carrierTypes` permits `sha256` and `url`, and `url` is load-bearing.** extract checks every decoded
76
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoFG;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,11 +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 S7 fixed for `field`: a declared property that is not the declared thing. The discovery
71
- * alias states the same fact WITH its machine-readable container, and the `PlacementManifest` contract says
72
- * 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).
73
80
  *
74
81
  * **`carrierTypes` permits `sha256` and `url`, and `url` is load-bearing.** extract checks every decoded
75
82
  * reference against this list, so the discovery alias's url-typed hits would refuse
@@ -103,6 +110,10 @@ export const UCP_PLACEMENT = {
103
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.",
104
111
  },
105
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",
106
117
  },
107
118
  field: "policies[type=com.integraledger.policy.legal_context]",
108
119
  readAlso: [
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoFG;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"}
@@ -2,14 +2,17 @@ import { type ReferencePlacementAdapter } from "@integraledger/lcp-binding-core"
2
2
  /**
3
3
  * The UCP reference placement — the kit plus ONE protocol rule the kit cannot know.
4
4
  *
5
- * `makePlacement(UCP_PLACEMENT)` supplies everything structural: the dotted-key capability write (the
6
- * container's `segments` make `com.integraledger.legal-context` ONE key, not four), the tagged-array read of
7
- * the `links` discovery alias, strength-ordered extraction, purity, and every refusal the vectors pin. This
5
+ * `makePlacement(UCP_PLACEMENT)` supplies everything structural: the tagged-array write into `policies[]`,
6
+ * the tagged-array read of the `links` discovery alias, strength-ordered extraction, purity, and every
7
+ * refusal the vectors pin. (An earlier revision described a dotted-key capability write through the
8
+ * container's `segments`; that carrier was retired when UCP turned out to define no `extensions` map, and
9
+ * `segments` now has no shipped declarer.) This
8
10
  * file adds only the rule that is UCP's semantics rather than any container's mechanics: **a terms link must
9
11
  * be HTTPS.** An `http:` URL is rewritable in transit, so accepting one would put an unauthenticated document
10
12
  * behind a reference the record cites.
11
13
  *
12
- * This is the composition S7 anticipates — generic mechanics, protocol-specific semantics layered on top —
14
+ * This is the composition the kit is shaped for — generic mechanics, protocol-specific semantics layered on
15
+ * top —
13
16
  * and it wraps `extract`, not `place`: `place` never writes the links alias (the terms URL is the
14
17
  * DEPLOYMENT's datum, not the reference — see the manifest), so the write side has no URL to police.
15
18
  *
@@ -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;;;;;;;;;;;;;;;;;;;GAmBG;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
@@ -4,14 +4,17 @@ const base = makePlacement(UCP_PLACEMENT);
4
4
  /**
5
5
  * The UCP reference placement — the kit plus ONE protocol rule the kit cannot know.
6
6
  *
7
- * `makePlacement(UCP_PLACEMENT)` supplies everything structural: the dotted-key capability write (the
8
- * container's `segments` make `com.integraledger.legal-context` ONE key, not four), the tagged-array read of
9
- * the `links` discovery alias, strength-ordered extraction, purity, and every refusal the vectors pin. This
7
+ * `makePlacement(UCP_PLACEMENT)` supplies everything structural: the tagged-array write into `policies[]`,
8
+ * the tagged-array read of the `links` discovery alias, strength-ordered extraction, purity, and every
9
+ * refusal the vectors pin. (An earlier revision described a dotted-key capability write through the
10
+ * container's `segments`; that carrier was retired when UCP turned out to define no `extensions` map, and
11
+ * `segments` now has no shipped declarer.) This
10
12
  * file adds only the rule that is UCP's semantics rather than any container's mechanics: **a terms link must
11
13
  * be HTTPS.** An `http:` URL is rewritable in transit, so accepting one would put an unauthenticated document
12
14
  * behind a reference the record cites.
13
15
  *
14
- * This is the composition S7 anticipates — generic mechanics, protocol-specific semantics layered on top —
16
+ * This is the composition the kit is shaped for — generic mechanics, protocol-specific semantics layered on
17
+ * top —
15
18
  * and it wraps `extract`, not `place`: `place` never writes the links alias (the terms URL is the
16
19
  * DEPLOYMENT's datum, not the reference — see the manifest), so the write side has no URL to police.
17
20
  *
@@ -27,12 +30,13 @@ export const ucpPlacement = {
27
30
  const out = base.extract(doc);
28
31
  if ("refused" in out)
29
32
  return out;
30
- if (out.value.type === "url" && !out.value.value.startsWith("https://"))
33
+ const { ref } = out.value;
34
+ if (ref.type === "url" && !ref.value.startsWith("https://"))
31
35
  return {
32
36
  refused: true,
33
37
  haltClass: "verification-failure",
34
38
  code: "ucp/insecure-terms-url",
35
- 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}`,
36
40
  };
37
41
  return out;
38
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;;;;;;;;;;;;;;;;;;;GAmBG;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.9.0",
3
+ "version": "0.11.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.9.0"
43
+ "@integraledger/lcp-binding-core": "0.11.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@cfworker/json-schema": "4.1.1",
package/src/manifest.ts CHANGED
@@ -67,11 +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 S7 fixed for `field`: a declared property that is not the declared thing. 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.
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).
75
82
  *
76
83
  * **`carrierTypes` permits `sha256` and `url`, and `url` is load-bearing.** extract checks every decoded
77
84
  * reference against this list, so the discovery alias's url-typed hits would refuse
@@ -106,6 +113,10 @@ export const UCP_PLACEMENT: PlacementManifest = {
106
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.",
107
114
  },
108
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",
109
120
  },
110
121
  field: "policies[type=com.integraledger.policy.legal_context]",
111
122
  readAlso: [
package/src/placement.ts CHANGED
@@ -9,14 +9,17 @@ const base = makePlacement(UCP_PLACEMENT);
9
9
  /**
10
10
  * The UCP reference placement — the kit plus ONE protocol rule the kit cannot know.
11
11
  *
12
- * `makePlacement(UCP_PLACEMENT)` supplies everything structural: the dotted-key capability write (the
13
- * container's `segments` make `com.integraledger.legal-context` ONE key, not four), the tagged-array read of
14
- * the `links` discovery alias, strength-ordered extraction, purity, and every refusal the vectors pin. This
12
+ * `makePlacement(UCP_PLACEMENT)` supplies everything structural: the tagged-array write into `policies[]`,
13
+ * the tagged-array read of the `links` discovery alias, strength-ordered extraction, purity, and every
14
+ * refusal the vectors pin. (An earlier revision described a dotted-key capability write through the
15
+ * container's `segments`; that carrier was retired when UCP turned out to define no `extensions` map, and
16
+ * `segments` now has no shipped declarer.) This
15
17
  * file adds only the rule that is UCP's semantics rather than any container's mechanics: **a terms link must
16
18
  * be HTTPS.** An `http:` URL is rewritable in transit, so accepting one would put an unauthenticated document
17
19
  * behind a reference the record cites.
18
20
  *
19
- * This is the composition S7 anticipates — generic mechanics, protocol-specific semantics layered on top —
21
+ * This is the composition the kit is shaped for — generic mechanics, protocol-specific semantics layered on
22
+ * top —
20
23
  * and it wraps `extract`, not `place`: `place` never writes the links alias (the terms URL is the
21
24
  * DEPLOYMENT's datum, not the reference — see the manifest), so the write side has no URL to police.
22
25
  *
@@ -31,12 +34,13 @@ export const ucpPlacement: ReferencePlacementAdapter = {
31
34
  extract(doc: unknown) {
32
35
  const out = base.extract(doc);
33
36
  if ("refused" in out) return out;
34
- if (out.value.type === "url" && !out.value.value.startsWith("https://"))
37
+ const { ref } = out.value;
38
+ if (ref.type === "url" && !ref.value.startsWith("https://"))
35
39
  return {
36
40
  refused: true,
37
41
  haltClass: "verification-failure",
38
42
  code: "ucp/insecure-terms-url",
39
- 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}`,
40
44
  };
41
45
  return out;
42
46
  },