portage-ucp 0.13.0 → 0.15.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.
Files changed (154) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/lib/generators/portage/ucp/install/templates/portage_ucp.rb +2 -2
  4. data/lib/portage/ucp/adapter.rb +1 -1
  5. data/lib/portage/ucp/capabilities/cart.rb +1 -0
  6. data/lib/portage/ucp/capabilities/catalog.rb +1 -0
  7. data/lib/portage/ucp/capabilities/checkout.rb +1 -0
  8. data/lib/portage/ucp/capabilities/discount.rb +1 -0
  9. data/lib/portage/ucp/capabilities/fulfillment.rb +1 -0
  10. data/lib/portage/ucp/capabilities/order.rb +1 -0
  11. data/lib/portage/ucp/capability.rb +7 -2
  12. data/lib/portage/ucp/configuration.rb +1 -1
  13. data/lib/portage/ucp/manifest.rb +31 -9
  14. data/lib/portage/ucp/schema_validator.rb +3 -3
  15. data/lib/portage/ucp/security/signature.rb +1 -1
  16. data/lib/portage/ucp/value_objects.rb +31 -13
  17. data/lib/portage/ucp/version.rb +1 -1
  18. data/lib/portage/ucp/wire_envelope.rb +1 -1
  19. data/schemas/{2026-04-08 → 2026-08-25}/schemas/capability.json +11 -8
  20. data/schemas/2026-08-25/schemas/common/identity_linking.json +133 -0
  21. data/schemas/2026-08-25/schemas/common/location_lookup.json +108 -0
  22. data/schemas/2026-08-25/schemas/common/location_search.json +72 -0
  23. data/schemas/2026-08-25/schemas/common/loyalty.json +294 -0
  24. data/schemas/2026-08-25/schemas/common/payment_ap2_mandate.json +91 -0
  25. data/schemas/2026-08-25/schemas/common/payment_authentication.json +90 -0
  26. data/schemas/2026-08-25/schemas/common/payment_split_payments.json +82 -0
  27. data/schemas/2026-08-25/schemas/common/payment_terms.json +92 -0
  28. data/schemas/2026-08-25/schemas/common/types/actions.json +42 -0
  29. data/schemas/2026-08-25/schemas/common/types/amenity_type.json +13 -0
  30. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/amount.json +4 -3
  31. data/schemas/2026-08-25/schemas/common/types/available_payment_instrument.json +20 -0
  32. data/schemas/2026-08-25/schemas/common/types/binding.json +22 -0
  33. data/schemas/2026-08-25/schemas/common/types/business_split_payments_config.json +25 -0
  34. data/schemas/2026-08-25/schemas/common/types/card_credential.json +78 -0
  35. data/schemas/2026-08-25/schemas/common/types/card_payment_instrument.json +77 -0
  36. data/schemas/2026-08-25/schemas/common/types/constraint_expression.json +71 -0
  37. data/schemas/2026-08-25/schemas/common/types/context.json +66 -0
  38. data/schemas/2026-08-25/schemas/common/types/daily_hour.json +36 -0
  39. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/description.json +2 -2
  40. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/error_code.json +3 -3
  41. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/error_response.json +4 -4
  42. data/schemas/2026-08-25/schemas/common/types/exception_hour.json +38 -0
  43. data/schemas/2026-08-25/schemas/common/types/geo.json +25 -0
  44. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/info_code.json +3 -3
  45. data/schemas/2026-08-25/schemas/common/types/instrument_group.json +32 -0
  46. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/link.json +3 -3
  47. data/schemas/2026-08-25/schemas/common/types/locality.json +22 -0
  48. data/schemas/2026-08-25/schemas/common/types/location.json +93 -0
  49. data/schemas/2026-08-25/schemas/common/types/location_distance.json +23 -0
  50. data/schemas/2026-08-25/schemas/common/types/location_filter.json +43 -0
  51. data/schemas/2026-08-25/schemas/common/types/location_serves.json +80 -0
  52. data/schemas/2026-08-25/schemas/common/types/location_summary.json +29 -0
  53. data/schemas/2026-08-25/schemas/common/types/measure.json +25 -0
  54. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/media.json +3 -3
  55. data/schemas/2026-08-25/schemas/common/types/message.json +18 -0
  56. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/message_error.json +5 -5
  57. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/message_info.json +4 -4
  58. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/message_warning.json +4 -4
  59. data/schemas/2026-08-25/schemas/common/types/network_token_credential.json +68 -0
  60. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/pagination.json +3 -4
  61. data/schemas/2026-08-25/schemas/common/types/pan_credential.json +54 -0
  62. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common/types}/payment.json +3 -3
  63. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/payment_credential.json +2 -2
  64. data/schemas/2026-08-25/schemas/common/types/payment_identity.json +16 -0
  65. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/payment_instrument.json +5 -5
  66. data/schemas/2026-08-25/schemas/common/types/payment_schedule.json +37 -0
  67. data/schemas/2026-08-25/schemas/common/types/payment_term.json +35 -0
  68. data/schemas/2026-08-25/schemas/common/types/policy.json +34 -0
  69. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/postal_address.json +2 -2
  70. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/price.json +3 -3
  71. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/price_filter.json +4 -4
  72. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/price_range.json +5 -5
  73. data/schemas/2026-08-25/schemas/common/types/quantity_unit.json +22 -0
  74. data/schemas/2026-08-25/schemas/common/types/request_constraints.json +47 -0
  75. data/schemas/2026-08-25/schemas/common/types/reverse_domain_name.json +17 -0
  76. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/signals.json +3 -3
  77. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/signed_amount.json +5 -3
  78. data/schemas/2026-08-25/schemas/common/types/time_interval.json +29 -0
  79. data/schemas/2026-08-25/schemas/common/types/token_credential.json +29 -0
  80. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/total.json +3 -3
  81. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/totals.json +4 -4
  82. data/schemas/2026-08-25/schemas/common/types/unit.json +49 -0
  83. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/warning_code.json +4 -3
  84. data/schemas/{2026-04-08 → 2026-08-25}/schemas/payment_handler.json +4 -4
  85. data/schemas/2026-08-25/schemas/profile.json +266 -0
  86. data/schemas/{2026-04-08 → 2026-08-25}/schemas/service.json +6 -6
  87. data/schemas/2026-08-25/schemas/shopping/buyer_consent.json +167 -0
  88. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/cart.json +32 -37
  89. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/catalog_lookup.json +46 -22
  90. data/schemas/2026-08-25/schemas/shopping/catalog_search.json +76 -0
  91. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/checkout.json +29 -16
  92. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/discount.json +9 -9
  93. data/schemas/2026-08-25/schemas/shopping/fulfillment.json +440 -0
  94. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/order.json +19 -11
  95. data/schemas/2026-08-25/schemas/shopping/permalink.json +68 -0
  96. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/adjustment.json +10 -4
  97. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/attribution.json +2 -2
  98. data/schemas/2026-08-25/schemas/shopping/types/availability.json +17 -0
  99. data/schemas/2026-08-25/schemas/shopping/types/business_fulfillment_config.json +37 -0
  100. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/buyer.json +2 -2
  101. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/category.json +2 -2
  102. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/detail_option_value.json +3 -3
  103. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/expectation.json +6 -10
  104. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/fulfillment.json +4 -5
  105. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/fulfillment_available_method.json +3 -8
  106. data/schemas/2026-08-25/schemas/shopping/types/fulfillment_destination.json +55 -0
  107. data/schemas/2026-08-25/schemas/shopping/types/fulfillment_destination_filter.json +23 -0
  108. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/fulfillment_event.json +4 -3
  109. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/fulfillment_group.json +3 -3
  110. data/schemas/2026-08-25/schemas/shopping/types/fulfillment_method.json +118 -0
  111. data/schemas/2026-08-25/schemas/shopping/types/fulfillment_option.json +46 -0
  112. data/schemas/2026-08-25/schemas/shopping/types/fulfillment_option_base.json +29 -0
  113. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/input_correlation.json +2 -2
  114. data/schemas/2026-08-25/schemas/shopping/types/item.json +43 -0
  115. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/line_item.json +7 -6
  116. data/schemas/2026-08-25/schemas/shopping/types/location_destination.json +26 -0
  117. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/option_value.json +2 -2
  118. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/order_confirmation.json +2 -2
  119. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/order_line_item.json +12 -9
  120. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/platform_fulfillment_config.json +2 -2
  121. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/product.json +10 -10
  122. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/product_option.json +3 -3
  123. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/rating.json +2 -2
  124. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/search_filters.json +3 -3
  125. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/selected_option.json +2 -2
  126. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/shipping_destination.json +11 -5
  127. data/schemas/2026-08-25/schemas/shopping/types/unit_price.json +56 -0
  128. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/variant.json +19 -78
  129. data/schemas/2026-08-25/schemas/transports/a2a_message.json +165 -0
  130. data/schemas/{2026-04-08 → 2026-08-25}/schemas/transports/embedded_config.json +2 -2
  131. data/schemas/2026-08-25/schemas/transports/embedded_message.json +69 -0
  132. data/schemas/2026-08-25/schemas/transports/jsonrpc.json +136 -0
  133. data/schemas/2026-08-25/schemas/transports/mcp_tool_call.json +143 -0
  134. data/schemas/{2026-04-08 → 2026-08-25}/schemas/ucp.json +73 -23
  135. data/schemas/2026-08-25/services/common/mcp.openrpc.json +105 -0
  136. data/schemas/2026-08-25/services/common/rest.openapi.json +328 -0
  137. data/schemas/2026-08-25/services/payment-actions/embedded.openrpc.json +89 -0
  138. data/schemas/2026-08-25/services/shopping/embedded.openrpc.json +634 -0
  139. data/schemas/{2026-04-08 → 2026-08-25}/services/shopping/mcp.openrpc.json +21 -20
  140. data/schemas/2026-08-25/services/shopping/permalink.openapi.json +136 -0
  141. data/schemas/2026-08-25/services/shopping/rest.openapi.json +1350 -0
  142. metadata +153 -71
  143. data/schemas/2026-04-08/schemas/shopping/catalog_search.json +0 -64
  144. data/schemas/2026-04-08/schemas/shopping/fulfillment.json +0 -154
  145. data/schemas/2026-04-08/schemas/shopping/types/available_payment_instrument.json +0 -22
  146. data/schemas/2026-04-08/schemas/shopping/types/business_fulfillment_config.json +0 -38
  147. data/schemas/2026-04-08/schemas/shopping/types/context.json +0 -42
  148. data/schemas/2026-04-08/schemas/shopping/types/fulfillment_destination.json +0 -16
  149. data/schemas/2026-04-08/schemas/shopping/types/fulfillment_method.json +0 -67
  150. data/schemas/2026-04-08/schemas/shopping/types/fulfillment_option.json +0 -56
  151. data/schemas/2026-04-08/schemas/shopping/types/item.json +0 -33
  152. data/schemas/2026-04-08/schemas/shopping/types/message.json +0 -18
  153. data/schemas/2026-04-08/schemas/shopping/types/retail_location.json +0 -28
  154. data/schemas/2026-04-08/schemas/shopping/types/reverse_domain_name.json +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e47e8018ba5966b67205cb9046efdf0e8f0edbe9ab58eef1e7613232a5b6b27
4
- data.tar.gz: c5eaa266f8d88a7fda16a822e0eb0b6aad82465b1e6adbf36abe08e7d2c859c4
3
+ metadata.gz: 3030cd7b510d8e0e829878dd48ab11fd18f70e0d0e5cac5107852ead26a0077c
4
+ data.tar.gz: 7e6abed9cd7ea40da3ee5f8681ec69f80a6f8c852e8956901f4fce1b383816da
5
5
  SHA512:
6
- metadata.gz: '053845855d29f395fb001b78d13622798aadf54c8dc087c88bdf32a943e65e992e7d6d79a3f9a5c86232ebfb8f65f36eb1137888bd9a806e2ceeeef045a57789'
7
- data.tar.gz: 6e1cec328b2d07567d4441340fe177367db3bbe7e094aa93388446cb062ea948e600abeb353dcf31b3833f7f76d2ccc189c275a44ab4c5ea546b94e4f3c152d7
6
+ metadata.gz: 4c4dddb8b6aaeaa674d270400287420f3bcbbfc028c8d38b9482fe53fe2cb47b493711cd7fffe50051b892b49f720f4332ba0e3af89fe595df9d46cdf80c5306
7
+ data.tar.gz: 5b1c1ee5a21fae8d7c8821c43820bcdcca15f54001e9d6e25af3b4916965da69a1bedb144faadb06ec222c11a59d3cd7e60c753579a4d8931924f51e62f06b78
data/CHANGELOG.md CHANGED
@@ -6,6 +6,20 @@ pre-1.0, so APIs may still shift between minor versions.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.15.0] - 2026-10-13
10
+
11
+ - **Added: optional `policies:` on `Cart`, `Checkout`, `Order`, `Product`, `CatalogSearchResult` and `ProductDetail`** (UCP 2026-08-25 `policies[]`). An array of `policy.json` hashes (`type` and `description` required; `applies_to` and `url` optional), default `[]`, omitted from the wire payload when empty. `portage-ucp-shopify` 0.7.0 supplies it (design-log §60).
12
+
13
+ ## [0.14.0] - 2026-10-09
14
+
15
+ Moves the gem to UCP 2026-08-25 (design-log §56-58). Minor-level for a pre-1.0 gem, with breaking config changes below.
16
+
17
+ - **Breaking: `config.payment_handlers` is now a Hash of `reverse-domain-key => [entry, ...]`, each entry carrying an `id`.** Default is `{}` (was an Array). The manifest emits it as UCP 2026-08-25's keyed `payment_handlers` object with date `version`s; an Array no longer works. `config.services` arrays still work: they are keyed by service name and each entry gets a `version`. A services Hash already keyed by name passes through, keeping an entry's own `version`.
18
+ - **Breaking: the manifest emits `keys` (a JWK Set) at the document root; `ucp.signing_keys` is gone.** Anything reading signing keys from `ucp.signing_keys` must read `keys`. Each key needs a `kid`.
19
+ - **Changed: UCP version is 2026-08-25** (manifest, `WireEnvelope`, `SchemaValidator` default). Capabilities are keyed by reverse-domain name with a date `version` and `schema` URL, and the whole manifest validates against `profile.json#/$defs/business_schema`.
20
+ - **Changed: `FulfillmentOption` `description` is sent as a `description.json` object** rather than a bare string, as 2026-08-25 requires.
21
+ - **Removed: the vendored `schemas/2026-04-08/` tree.** `SchemaValidator.new(version: "2026-04-08")` no longer loads; only `schemas/2026-08-25/` ships. Nothing else in the workspace validated against it (`portage-ucp-client` reads remote profiles without vendored schemas, so stores still serving 2026-04-08 keep working).
22
+
9
23
  ## [0.13.0] - 2026-10-07
10
24
 
11
25
  - **Added: `Support::CheckoutState` can ask the platform about a checkout this process doesn't track.** An adapter may define a private `platform_checkout_order(checkout_id)` that returns the `Order` its platform placed from that checkout, or nil. `#checkout_status` then reports `completed` for an id with no in-process record, the new `#checkout_order` returns its `OrderConfirmation`, and the new `#checkout_from_platform_order` rebuilds a completed `Checkout` from the order when the platform has already dropped the cart. A found order is cached through `#record_checkout_status`/`#record_order_checkout`; no order, no hook, or a lookup error keeps today's `incomplete`. This lets `portage orders reconcile`, which runs in a new process, see a checkout the shopper paid for in the browser (design-log §55). The `Adapter` contract is unchanged. Minor-level.
@@ -14,8 +14,8 @@ Portage::Ucp.configure do |config|
14
14
 
15
15
  # config.signer = MySigner.new
16
16
  # config.signing_keys = []
17
- # config.payment_handlers = []
18
- # config.services = []
17
+ # config.payment_handlers = {} # { "com.example.pay" => [{ id: "..." }] }
18
+ # config.services = {} # { "dev.ucp.shopping" => [{ transport: "mcp", endpoint: "..." }] }
19
19
  # config.mandate_trusted_keys = []
20
20
  # config.require_mandate_signature = false
21
21
  end
@@ -105,7 +105,7 @@ module Portage
105
105
 
106
106
  # --- Reorder (app.portage-ucp.reorder — Portage extension, not part of
107
107
  # the UCP spec: dev.ucp.dev has no reorder/cart-hydration capability as
108
- # of the 2026-04-08 spec) ---
108
+ # of the 2026-08-25 spec) ---
109
109
  # Hydrates a cart from a previous order's line items so a caller doesn't
110
110
  # have to re-walk get_order + create_cart itself. Re-checks each item's
111
111
  # current price/availability rather than replaying the order's snapshot
@@ -4,6 +4,7 @@ module Portage
4
4
  CART = Portage::Ucp::Capability.new(
5
5
  name: "dev.ucp.shopping.cart",
6
6
  version: "1",
7
+ schema: "shopping/cart.json",
7
8
  actions: {
8
9
  "create_cart" => :create_cart,
9
10
  "get_cart" => :get_cart,
@@ -4,6 +4,7 @@ module Portage
4
4
  CATALOG = Portage::Ucp::Capability.new(
5
5
  name: "dev.ucp.shopping.catalog",
6
6
  version: "1",
7
+ schema: "shopping/catalog_search.json",
7
8
  actions: { "search_catalog" => :search_catalog, "get_product" => :get_product,
8
9
  "lookup_catalog" => :lookup_catalog }
9
10
  )
@@ -4,6 +4,7 @@ module Portage
4
4
  CHECKOUT = Portage::Ucp::Capability.new(
5
5
  name: "dev.ucp.shopping.checkout",
6
6
  version: "1",
7
+ schema: "shopping/checkout.json",
7
8
  actions: {
8
9
  "create_checkout" => :create_checkout,
9
10
  "get_checkout" => :get_checkout,
@@ -8,6 +8,7 @@ module Portage
8
8
  DISCOUNT = Portage::Ucp::Capability.new(
9
9
  name: "dev.ucp.shopping.discount",
10
10
  version: "1",
11
+ schema: "shopping/discount.json",
11
12
  actions: {},
12
13
  predicate: :discount_codes_supported?
13
14
  )
@@ -8,6 +8,7 @@ module Portage
8
8
  FULFILLMENT = Portage::Ucp::Capability.new(
9
9
  name: "dev.ucp.shopping.fulfillment",
10
10
  version: "1",
11
+ schema: "shopping/fulfillment.json",
11
12
  actions: {},
12
13
  predicate: :fulfillment_supported?
13
14
  )
@@ -4,6 +4,7 @@ module Portage
4
4
  ORDER = Portage::Ucp::Capability.new(
5
5
  name: "dev.ucp.shopping.order",
6
6
  version: "1",
7
+ schema: "shopping/order.json",
7
8
  actions: {
8
9
  "get_order" => :get_order,
9
10
  "cancel_order" => :cancel_order,
@@ -3,7 +3,7 @@ module Portage
3
3
  # A named, versioned UCP capability (e.g. "dev.ucp.shopping.catalog") mapping
4
4
  # UCP action names to the Adapter methods that back them.
5
5
  class Capability
6
- attr_reader :name, :version, :actions
6
+ attr_reader :name, :version, :actions, :schema
7
7
 
8
8
  # `predicate:` is for extension capabilities like
9
9
  # dev.ucp.shopping.discount and dev.ucp.shopping.fulfillment that add a
@@ -11,8 +11,13 @@ module Portage
11
11
  # there's no dedicated method whose override signals support, so the
12
12
  # adapter exposes a boolean method instead and the capability asks it
13
13
  # directly rather than inspecting `actions`.
14
- def initialize(name:, version:, actions:, predicate: nil)
14
+ #
15
+ # `schema:` is the capability's schema path under a UCP revision's
16
+ # `schemas/` (e.g. "shopping/checkout.json"); the manifest turns it into
17
+ # the `schema` URL 2026-08-25 business profiles require.
18
+ def initialize(name:, version:, actions:, predicate: nil, schema: nil)
15
19
  @name = name
20
+ @schema = schema
16
21
  @version = version
17
22
  @actions = actions
18
23
  @predicate = predicate
@@ -51,7 +51,7 @@ module Portage
51
51
  @authenticator = UnconfiguredAuthenticator.new
52
52
  @rate_limiter = NullRateLimiter.new
53
53
  @logger = Logger.new($stdout)
54
- @payment_handlers = []
54
+ @payment_handlers = {}
55
55
  @signing_keys = []
56
56
  @services = []
57
57
  @require_mandate_signature = false
@@ -6,11 +6,12 @@ module Portage
6
6
  # Builds the /.well-known/ucp discovery document: protocol version, the
7
7
  # business's advertised capabilities (only those the Adapter overrides —
8
8
  # see Capability#advertised_for?), payment handlers, signing keys, and the
9
- # services array (transport + endpoint, e.g. {transport: "mcp", endpoint:
9
+ # services (transport + endpoint, e.g. {transport: "mcp", endpoint:
10
10
  # "https://..."}) a client needs to find where to actually connect.
11
11
  # Signing keys are never generated here — see §9, consumer-provided only.
12
12
  class Manifest
13
- UCP_VERSION = "2026-04-08".freeze
13
+ UCP_VERSION = "2026-08-25".freeze
14
+ SERVICE_KEY = "dev.ucp.shopping".freeze
14
15
 
15
16
  # @param signer [#kid, #sign] optional. Consumer-provided — the gem never
16
17
  # generates or stores keys itself (§9). `sign(canonical_json_string)`
@@ -35,26 +36,47 @@ module Portage
35
36
  # capability name. That is the shape live Shopify UCP rollouts (Casper,
36
37
  # Allbirds, Glossier, and 34+ others) serve under their own manifest
37
38
  # "version": "2026-08-25", and the shape portage-ucp-client's
38
- # Client.discover reads. Only the shape is borrowed: `version` here is
39
- # still UCP_VERSION ("2026-04-08"), the spec revision this gem targets.
39
+ # Client.discover reads. As of 2026-08-25 the JWK Set is `keys` beside
40
+ # `ucp` (not `ucp.signing_keys`), and `services`, `capabilities` and
41
+ # `payment_handlers` are objects keyed by reverse-domain name whose
42
+ # entries carry a date `version` (capabilities also a `schema` URL,
43
+ # handlers an `id`). `services` still accepts the old bare array of
44
+ # entries, filed under SERVICE_KEY; `payment_handlers` must already be
45
+ # a Hash of key => [entry, ...].
40
46
  def to_h
41
47
  ucp = {
42
48
  version: UCP_VERSION,
43
49
  business: @business,
44
- services: @services,
50
+ services: keyed(service_hash),
45
51
  capabilities: capability_hash,
46
- payment_handlers: @payment_handlers,
47
- signing_keys: @signing_keys
52
+ payment_handlers: keyed(@payment_handlers)
48
53
  }
49
54
  ucp = ucp.merge(signature: sign(ucp)) if @signer
50
55
 
51
- { ucp: ucp }
56
+ { ucp: ucp, keys: @signing_keys }
52
57
  end
53
58
 
54
59
  private
55
60
 
56
61
  def capability_hash
57
- @registry.advertised(@adapter).to_h { |capability| [capability.name, [{ version: capability.version }]] }
62
+ @registry.advertised(@adapter).to_h do |capability|
63
+ entry = { version: UCP_VERSION }
64
+ entry[:schema] = "https://ucp.dev/#{UCP_VERSION}/schemas/#{capability.schema}" if capability.schema
65
+ [capability.name, [entry]]
66
+ end
67
+ end
68
+
69
+ def service_hash
70
+ return @services if @services.is_a?(Hash)
71
+
72
+ @services.empty? ? {} : { SERVICE_KEY => @services }
73
+ end
74
+
75
+ # Every entry needs a version; the caller's own wins.
76
+ def keyed(entries_by_key)
77
+ entries_by_key.to_h do |key, entries|
78
+ [key, [entries].flatten(1).map { |entry| { version: UCP_VERSION }.merge(entry) }]
79
+ end
58
80
  end
59
81
 
60
82
  def sign(payload)
@@ -9,12 +9,12 @@ module Portage
9
9
  # is the CI-safe equivalent).
10
10
  #
11
11
  # The vendored tree mirrors https://ucp.dev/<version>/... path-for-path
12
- # (schemas/2026-04-08/schemas/shopping/cart.json <->
13
- # https://ucp.dev/2026-04-08/schemas/shopping/cart.json), so every $ref
12
+ # (schemas/2026-08-25/schemas/shopping/cart.json <->
13
+ # https://ucp.dev/2026-08-25/schemas/shopping/cart.json), so every $ref
14
14
  # inside a vendored document — however deeply nested — resolves to another
15
15
  # vendored file with a single prefix rewrite, no network access needed.
16
16
  class SchemaValidator
17
- def initialize(version: "2026-04-08", root: File.join(__dir__, "..", "..", "..", "schemas"))
17
+ def initialize(version: "2026-08-25", root: File.join(__dir__, "..", "..", "..", "schemas"))
18
18
  @base_url = "https://ucp.dev/#{version}/"
19
19
  @base_dir = File.expand_path(File.join(root, version))
20
20
  end
@@ -7,7 +7,7 @@ module Portage
7
7
  module Security
8
8
  # Verifies that an inbound agent request carries a valid RFC 9421 HTTP
9
9
  # Message Signature, per UCP's own signature spec
10
- # (ucp.dev/2026-04-08/specification/signatures/, pinned 2026-09-15 —
10
+ # (ucp.dev/2026-08-25/specification/signatures/, pinned 2026-09-15 —
11
11
  # this is the piece §22 of the design log flagged as unresearched
12
12
  # before writing code, now checked against the live spec rather than
13
13
  # guessed). This is a distinct concern from three other signing
@@ -115,9 +115,10 @@ module Portage
115
115
  # (Shopify/Wix metafields etc.) — see Portage::Ucp::Shopify.configure's
116
116
  # metadata_field.
117
117
  Product = Data.define(:id, :title, :description, :price_range, :variants, :handle, :url, :categories,
118
- :list_price_range, :media, :options, :tags, :metadata, :rating) do
118
+ :list_price_range, :media, :options, :tags, :metadata, :rating, :policies) do
119
119
  def initialize(id:, title:, description:, price_range:, variants:, handle: nil, url: nil, categories: [],
120
- list_price_range: nil, media: [], options: [], tags: [], metadata: nil, rating: nil)
120
+ list_price_range: nil, media: [], options: [], tags: [], metadata: nil, rating: nil,
121
+ policies: [])
121
122
  super
122
123
  end
123
124
 
@@ -135,6 +136,7 @@ module Portage
135
136
  h["media"] = media.map(&:to_wire_h) unless media.empty?
136
137
  h["options"] = options.map(&:to_wire_h) unless options.empty?
137
138
  h["tags"] = tags unless tags.empty?
139
+ h["policies"] = policies unless policies.empty?
138
140
  h
139
141
  end
140
142
  end
@@ -144,12 +146,13 @@ module Portage
144
146
  # isn't schema-conformant (search_response requires `ucp` + `products` at
145
147
  # the top level, not a bare array). `messages` entries are left as plain
146
148
  # wire-shaped hashes, same rationale as Variant#barcodes above.
147
- CatalogSearchResult = Data.define(:products, :messages) do
148
- def initialize(products:, messages: []) = super
149
+ CatalogSearchResult = Data.define(:products, :messages, :policies) do
150
+ def initialize(products:, messages: [], policies: []) = super
149
151
 
150
152
  def to_wire_h
151
153
  h = { "products" => products.map(&:to_wire_h) }
152
154
  h["messages"] = messages unless messages.empty?
155
+ h["policies"] = policies unless policies.empty?
153
156
  h
154
157
  end
155
158
  end
@@ -161,8 +164,14 @@ module Portage
161
164
  # extension (interactive variant narrowing) — Adapter#get_product's
162
165
  # signature has no `selected:`/`preferences:` params to source it from
163
166
  # yet; unresearched, same status as design-log §16's other backlog items.
164
- ProductDetail = Data.define(:product) do
165
- def to_wire_h = { "product" => product.to_wire_h }
167
+ ProductDetail = Data.define(:product, :policies) do
168
+ def initialize(product:, policies: []) = super
169
+
170
+ def to_wire_h
171
+ h = { "product" => product.to_wire_h }
172
+ h["policies"] = policies unless policies.empty?
173
+ h
174
+ end
166
175
  end
167
176
 
168
177
  # One cost-breakdown entry (schemas/shopping/types/total.json). `amount` is a
@@ -198,6 +207,10 @@ module Portage
198
207
  end
199
208
  end
200
209
 
210
+ # `policies` (cart/checkout/order/catalog responses and products) is the
211
+ # 2026-08-25 policy.json array (return/warranty terms), optional and omitted
212
+ # when empty. Entries stay wire-shaped hashes (`type` and `description` are
213
+ # required, `applies_to`/`url` optional), same posture as `messages`.
201
214
  # schemas/shopping/types/link.json
202
215
  Link = Data.define(:type, :url, :title) do
203
216
  def initialize(type:, url:, title: nil) = super
@@ -272,6 +285,8 @@ module Portage
272
285
 
273
286
  # schemas/shopping/types/fulfillment_option.json — a single priced choice
274
287
  # within a FulfillmentGroup (e.g. "Standard Shipping $5", "Express $15").
288
+ # `description` is a common/types/description.json object
289
+ # ({"plain"/"html"/"markdown" => ...}); a bare String is sent as `plain`.
275
290
  FulfillmentOption = Data.define(:id, :title, :totals, :description, :carrier, :earliest_fulfillment_time,
276
291
  :latest_fulfillment_time) do
277
292
  def initialize(id:, title:, totals:, description: nil, carrier: nil, earliest_fulfillment_time: nil,
@@ -281,7 +296,7 @@ module Portage
281
296
 
282
297
  def to_wire_h
283
298
  h = { "id" => id, "title" => title, "totals" => totals.map(&:to_wire_h) }
284
- h["description"] = description if description
299
+ h["description"] = description.is_a?(String) ? { "plain" => description } : description if description
285
300
  h["carrier"] = carrier if carrier
286
301
  h["earliest_fulfillment_time"] = earliest_fulfillment_time if earliest_fulfillment_time
287
302
  h["latest_fulfillment_time"] = latest_fulfillment_time if latest_fulfillment_time
@@ -378,13 +393,14 @@ module Portage
378
393
  # `discounts` is the dev.ucp.shopping.discount extension (optional on both
379
394
  # cart.json and its own schema) — omitted from the wire payload when empty,
380
395
  # same as Order#adjustments.
381
- Cart = Data.define(:id, :line_items, :currency, :totals, :discounts) do
382
- def initialize(id:, line_items:, currency:, totals:, discounts: Discounts.new) = super
396
+ Cart = Data.define(:id, :line_items, :currency, :totals, :discounts, :policies) do
397
+ def initialize(id:, line_items:, currency:, totals:, discounts: Discounts.new, policies: []) = super
383
398
 
384
399
  def to_wire_h
385
400
  h = { "id" => id, "line_items" => line_items.map(&:to_wire_h), "currency" => currency,
386
401
  "totals" => totals.map(&:to_wire_h) }
387
402
  h["discounts"] = discounts.to_wire_h unless discounts.empty?
403
+ h["policies"] = policies unless policies.empty?
388
404
  h
389
405
  end
390
406
  end
@@ -413,9 +429,9 @@ module Portage
413
429
  # omitted from the wire payload when empty), same posture as
414
430
  # Order#adjustments.
415
431
  Checkout = Data.define(:id, :status, :line_items, :currency, :totals, :links, :order, :discounts,
416
- :fulfillment) do
432
+ :fulfillment, :policies) do
417
433
  def initialize(id:, status:, line_items:, currency:, totals:, links:, order: nil, discounts: Discounts.new,
418
- fulfillment: CheckoutFulfillment.new)
434
+ fulfillment: CheckoutFulfillment.new, policies: [])
419
435
  super
420
436
  end
421
437
 
@@ -425,6 +441,7 @@ module Portage
425
441
  h["order"] = order.to_wire_h if order
426
442
  h["discounts"] = discounts.to_wire_h unless discounts.empty?
427
443
  h["fulfillment"] = fulfillment.to_wire_h unless fulfillment.empty?
444
+ h["policies"] = policies unless policies.empty?
428
445
  h
429
446
  end
430
447
  end
@@ -506,9 +523,9 @@ module Portage
506
523
  # line_items/fulfillment/currency/totals. `adjustments` is optional —
507
524
  # omitted from the wire payload when empty.
508
525
  Order = Data.define(:id, :checkout_id, :permalink_url, :line_items, :fulfillment, :currency, :totals,
509
- :adjustments) do
526
+ :adjustments, :policies) do
510
527
  def initialize(id:, checkout_id:, permalink_url:, line_items:, fulfillment:, currency:, totals:,
511
- adjustments: [])
528
+ adjustments: [], policies: [])
512
529
  super
513
530
  end
514
531
 
@@ -517,6 +534,7 @@ module Portage
517
534
  "line_items" => line_items.map(&:to_wire_h), "fulfillment" => fulfillment.to_wire_h,
518
535
  "currency" => currency, "totals" => totals.map(&:to_wire_h) }
519
536
  h["adjustments"] = adjustments.map(&:to_wire_h) unless adjustments.empty?
537
+ h["policies"] = policies unless policies.empty?
520
538
  h
521
539
  end
522
540
  end
@@ -1,5 +1,5 @@
1
1
  module Portage
2
2
  module Ucp
3
- VERSION = "0.13.0".freeze
3
+ VERSION = "0.15.0".freeze
4
4
  end
5
5
  end
@@ -6,7 +6,7 @@ module Portage
6
6
  # capability's minimum envelope shape live in one place, not duplicated
7
7
  # across value objects.
8
8
  module WireEnvelope
9
- SPEC_VERSION = "2026-04-08".freeze
9
+ SPEC_VERSION = "2026-08-25".freeze
10
10
 
11
11
  ENVELOPES = {
12
12
  "dev.ucp.shopping.cart" => -> { { "version" => SPEC_VERSION } },
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://ucp.dev/2026-04-08/schemas/capability.json",
3
+ "$id": "https://ucp.dev/2026-08-25/schemas/capability.json",
4
4
  "title": "UCP Capability",
5
5
  "description": "Schema for UCP capabilities and extensions. Extensions are capabilities with an 'extends' field. Uses reverse-domain naming for governance.",
6
6
  "$defs": {
7
7
  "base": {
8
8
  "allOf": [
9
9
  {
10
- "$ref": "https://ucp.dev/2026-04-08/schemas/ucp.json#/$defs/entity"
10
+ "$ref": "https://ucp.dev/2026-08-25/schemas/ucp.json#/$defs/entity"
11
11
  },
12
12
  {
13
13
  "type": "object",
@@ -15,14 +15,12 @@
15
15
  "extends": {
16
16
  "oneOf": [
17
17
  {
18
- "type": "string",
19
- "pattern": "^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$"
18
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/reverse_domain_name.json"
20
19
  },
21
20
  {
22
21
  "type": "array",
23
22
  "items": {
24
- "type": "string",
25
- "pattern": "^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$"
23
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/reverse_domain_name.json"
26
24
  },
27
25
  "minItems": 1
28
26
  }
@@ -50,10 +48,15 @@
50
48
  },
51
49
  "business_schema": {
52
50
  "title": "Capability (Business Schema)",
53
- "description": "Capability configuration for business/merchant level. May include business-specific config overrides.",
51
+ "description": "Capability declaration for business/merchant discovery. Requires the `schema` URL so platforms can fetch and compose it during negotiation; may also include business-specific config overrides.",
54
52
  "allOf": [
55
53
  {
56
54
  "$ref": "#/$defs/base"
55
+ },
56
+ {
57
+ "required": [
58
+ "schema"
59
+ ]
57
60
  }
58
61
  ]
59
62
  },
@@ -67,4 +70,4 @@
67
70
  ]
68
71
  }
69
72
  }
70
- }
73
+ }
@@ -0,0 +1,133 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ucp.dev/2026-08-25/schemas/common/identity_linking.json",
4
+ "name": "dev.ucp.common.identity_linking",
5
+ "title": "Identity Linking",
6
+ "description": "Capability schema for identity linking. Businesses declare the user-authenticated scopes they offer in a flat 'scopes' map. Each key is the OAuth scope string as it appears on the wire ('{capability}:{scope}', e.g. 'dev.ucp.shopping.order:read'). Scope presence implies that the corresponding operations require user authentication. Operations not gated by any listed scope operate at whatever access level the business permits; UCP does not prescribe a default.",
7
+ "$defs": {
8
+ "scope_policy": {
9
+ "type": "object",
10
+ "title": "Scope Policy",
11
+ "description": "Per-scope policy and metadata — auth constraints (e.g. min_acr, max_token_age), declarative metadata (e.g. claims produced, consent descriptions), or any other scope-specific configuration. An empty object means user authentication is required with no additional policy. Open for non-breaking extension.",
12
+ "properties": {
13
+ "description": {
14
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/description.json",
15
+ "description": "Optional human-readable description of the scope that platforms can use to present and explain context (requirement and value) to the user."
16
+ }
17
+ },
18
+ "additionalProperties": true
19
+ },
20
+ "scope_token": {
21
+ "type": "string",
22
+ "description": "OAuth scope string formed by joining a capability name and a scope name with a colon: '{capability}:{scope}', e.g. 'dev.ucp.shopping.order:read'. Capability names use reverse-DNS naming; scope names denote the permission granted, defined by each capability's spec (e.g. 'read', 'manage', 'create'). Platforms request these strings verbatim in OAuth 'scope' parameters; issued tokens carry them in the 'scope' claim.",
23
+ "pattern": "^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+:[a-z][a-z0-9_]*$"
24
+ },
25
+ "provider": {
26
+ "type": "object",
27
+ "title": "Identity Provider",
28
+ "description": "A trusted identity provider for delegated authentication, keyed by the 'type' discriminator. 'oauth2' denotes an OAuth 2.0 / OIDC authorization server. Future versions MAY define additional types (e.g. wallet attestation) as non-breaking extensions; platforms MUST treat entries whose 'type' they do not support as filtered out (see Provider Selection).",
29
+ "required": [
30
+ "type"
31
+ ],
32
+ "properties": {
33
+ "type": {
34
+ "type": "string",
35
+ "description": "Provider mechanism discriminator. 'oauth2' for OAuth 2.0 / OIDC authorization servers. Additional values MAY be defined by future versions; the value is an open string, not a closed enum, so unrecognized types remain valid and are filtered at runtime."
36
+ }
37
+ },
38
+ "allOf": [
39
+ {
40
+ "if": {
41
+ "properties": {
42
+ "type": {
43
+ "const": "oauth2"
44
+ }
45
+ }
46
+ },
47
+ "then": {
48
+ "required": [
49
+ "auth_url"
50
+ ],
51
+ "properties": {
52
+ "auth_url": {
53
+ "type": "string",
54
+ "format": "uri",
55
+ "description": "Base URL for authorization server metadata discovery. Platforms resolve metadata via RFC 8414 (/.well-known/oauth-authorization-server) with OIDC fallback (/.well-known/openid-configuration) on 404."
56
+ },
57
+ "required_claims": {
58
+ "type": "array",
59
+ "items": {
60
+ "type": "string"
61
+ },
62
+ "uniqueItems": true,
63
+ "description": "OIDC Core §5.1 claim names the business requires in the JWT authorization grant from this provider (e.g. 'email'). Optional pre-filter hint: platforms SHOULD skip this provider during selection when their upstream identity lacks any of the listed claim names. Reactive enforcement at the token endpoint remains mandatory regardless; this field expresses claim presence only, not value constraints."
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ],
69
+ "additionalProperties": true
70
+ },
71
+ "dev.ucp.common.identity_linking": {
72
+ "platform_schema": {
73
+ "title": "Identity Linking (Platform)",
74
+ "description": "Platform-level identity linking capability declaration. Platforms advertise support for identity linking; no auth-specific config is required.",
75
+ "allOf": [
76
+ {
77
+ "$ref": "https://ucp.dev/2026-08-25/schemas/capability.json#/$defs/platform_schema"
78
+ }
79
+ ]
80
+ },
81
+ "business_schema": {
82
+ "title": "Identity Linking (Business)",
83
+ "description": "Business-level identity linking configuration. Businesses declare the user-authenticated scopes they offer in 'config.scopes'.",
84
+ "allOf": [
85
+ {
86
+ "$ref": "https://ucp.dev/2026-08-25/schemas/capability.json#/$defs/business_schema"
87
+ },
88
+ {
89
+ "type": "object",
90
+ "required": [
91
+ "config"
92
+ ],
93
+ "properties": {
94
+ "config": {
95
+ "type": "object",
96
+ "required": [
97
+ "scopes"
98
+ ],
99
+ "properties": {
100
+ "providers": {
101
+ "type": "object",
102
+ "description": "Map of trusted external identity providers keyed by reverse-domain identifier. Each key maps to an array of mechanism entries — an IdP namespace MAY offer multiple token acquisition mechanisms. Declares which upstream IdPs the business will accept JWT bearer assertions from for the Accelerated IdP Flow (chaining via RFC 8693 + RFC 7523). This field is additive: direct OAuth against the business domain via RFC 8414 discovery is always available regardless of 'providers' content. Businesses MUST NOT list their own authorization server here — chaining-to-self is degenerate, and direct OAuth covers that path. When absent, empty, or when no listed mechanism is supported by the platform, platforms run direct OAuth on the business domain.",
103
+ "propertyNames": {
104
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/reverse_domain_name.json"
105
+ },
106
+ "additionalProperties": {
107
+ "type": "array",
108
+ "items": {
109
+ "$ref": "#/$defs/provider"
110
+ }
111
+ }
112
+ },
113
+ "scopes": {
114
+ "type": "object",
115
+ "description": "Map of user-authenticated scopes offered by this business. Each key is an OAuth scope string formed as '{capability}:{scope}' (e.g. 'dev.ucp.shopping.order:read'). Scope presence in this map declares that the corresponding operations require a user identity token. Operations not gated by any listed scope operate at whatever access level the business permits; UCP does not prescribe a default. Each value is a per-scope policy object (empty object means user auth required with no additional policy).",
116
+ "propertyNames": {
117
+ "$ref": "#/$defs/scope_token"
118
+ },
119
+ "additionalProperties": {
120
+ "$ref": "#/$defs/scope_policy"
121
+ }
122
+ }
123
+ },
124
+ "additionalProperties": true
125
+ }
126
+ }
127
+ }
128
+ ]
129
+ }
130
+ }
131
+ },
132
+ "version": "2026-08-25"
133
+ }