portage-ucp 0.13.0 → 0.14.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/lib/generators/portage/ucp/install/templates/portage_ucp.rb +2 -2
  4. data/lib/portage/ucp/capabilities/cart.rb +1 -0
  5. data/lib/portage/ucp/capabilities/catalog.rb +1 -0
  6. data/lib/portage/ucp/capabilities/checkout.rb +1 -0
  7. data/lib/portage/ucp/capabilities/discount.rb +1 -0
  8. data/lib/portage/ucp/capabilities/fulfillment.rb +1 -0
  9. data/lib/portage/ucp/capabilities/order.rb +1 -0
  10. data/lib/portage/ucp/capability.rb +7 -2
  11. data/lib/portage/ucp/configuration.rb +1 -1
  12. data/lib/portage/ucp/manifest.rb +31 -9
  13. data/lib/portage/ucp/schema_validator.rb +3 -3
  14. data/lib/portage/ucp/value_objects.rb +3 -1
  15. data/lib/portage/ucp/version.rb +1 -1
  16. data/lib/portage/ucp/wire_envelope.rb +1 -1
  17. data/schemas/{2026-04-08 → 2026-08-25}/schemas/capability.json +11 -8
  18. data/schemas/2026-08-25/schemas/common/identity_linking.json +133 -0
  19. data/schemas/2026-08-25/schemas/common/location_lookup.json +108 -0
  20. data/schemas/2026-08-25/schemas/common/location_search.json +72 -0
  21. data/schemas/2026-08-25/schemas/common/loyalty.json +294 -0
  22. data/schemas/2026-08-25/schemas/common/payment_ap2_mandate.json +91 -0
  23. data/schemas/2026-08-25/schemas/common/payment_authentication.json +90 -0
  24. data/schemas/2026-08-25/schemas/common/payment_split_payments.json +82 -0
  25. data/schemas/2026-08-25/schemas/common/payment_terms.json +92 -0
  26. data/schemas/2026-08-25/schemas/common/types/actions.json +42 -0
  27. data/schemas/2026-08-25/schemas/common/types/amenity_type.json +13 -0
  28. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/amount.json +4 -3
  29. data/schemas/2026-08-25/schemas/common/types/available_payment_instrument.json +20 -0
  30. data/schemas/2026-08-25/schemas/common/types/binding.json +22 -0
  31. data/schemas/2026-08-25/schemas/common/types/business_split_payments_config.json +25 -0
  32. data/schemas/2026-08-25/schemas/common/types/card_credential.json +78 -0
  33. data/schemas/2026-08-25/schemas/common/types/card_payment_instrument.json +77 -0
  34. data/schemas/2026-08-25/schemas/common/types/constraint_expression.json +71 -0
  35. data/schemas/2026-08-25/schemas/common/types/context.json +66 -0
  36. data/schemas/2026-08-25/schemas/common/types/daily_hour.json +36 -0
  37. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/description.json +2 -2
  38. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/error_code.json +3 -3
  39. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/error_response.json +4 -4
  40. data/schemas/2026-08-25/schemas/common/types/exception_hour.json +38 -0
  41. data/schemas/2026-08-25/schemas/common/types/geo.json +25 -0
  42. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/info_code.json +3 -3
  43. data/schemas/2026-08-25/schemas/common/types/instrument_group.json +32 -0
  44. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/link.json +3 -3
  45. data/schemas/2026-08-25/schemas/common/types/locality.json +22 -0
  46. data/schemas/2026-08-25/schemas/common/types/location.json +93 -0
  47. data/schemas/2026-08-25/schemas/common/types/location_distance.json +23 -0
  48. data/schemas/2026-08-25/schemas/common/types/location_filter.json +43 -0
  49. data/schemas/2026-08-25/schemas/common/types/location_serves.json +80 -0
  50. data/schemas/2026-08-25/schemas/common/types/location_summary.json +29 -0
  51. data/schemas/2026-08-25/schemas/common/types/measure.json +25 -0
  52. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/media.json +3 -3
  53. data/schemas/2026-08-25/schemas/common/types/message.json +18 -0
  54. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/message_error.json +5 -5
  55. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/message_info.json +4 -4
  56. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/message_warning.json +4 -4
  57. data/schemas/2026-08-25/schemas/common/types/network_token_credential.json +68 -0
  58. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/pagination.json +3 -4
  59. data/schemas/2026-08-25/schemas/common/types/pan_credential.json +54 -0
  60. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common/types}/payment.json +3 -3
  61. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/payment_credential.json +2 -2
  62. data/schemas/2026-08-25/schemas/common/types/payment_identity.json +16 -0
  63. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/payment_instrument.json +5 -5
  64. data/schemas/2026-08-25/schemas/common/types/payment_schedule.json +37 -0
  65. data/schemas/2026-08-25/schemas/common/types/payment_term.json +35 -0
  66. data/schemas/2026-08-25/schemas/common/types/policy.json +34 -0
  67. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/postal_address.json +2 -2
  68. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/price.json +3 -3
  69. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/price_filter.json +4 -4
  70. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/price_range.json +5 -5
  71. data/schemas/2026-08-25/schemas/common/types/quantity_unit.json +22 -0
  72. data/schemas/2026-08-25/schemas/common/types/request_constraints.json +47 -0
  73. data/schemas/2026-08-25/schemas/common/types/reverse_domain_name.json +17 -0
  74. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/signals.json +3 -3
  75. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/signed_amount.json +5 -3
  76. data/schemas/2026-08-25/schemas/common/types/time_interval.json +29 -0
  77. data/schemas/2026-08-25/schemas/common/types/token_credential.json +29 -0
  78. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/total.json +3 -3
  79. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/totals.json +4 -4
  80. data/schemas/2026-08-25/schemas/common/types/unit.json +49 -0
  81. data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/warning_code.json +4 -3
  82. data/schemas/{2026-04-08 → 2026-08-25}/schemas/payment_handler.json +4 -4
  83. data/schemas/2026-08-25/schemas/profile.json +266 -0
  84. data/schemas/{2026-04-08 → 2026-08-25}/schemas/service.json +6 -6
  85. data/schemas/2026-08-25/schemas/shopping/buyer_consent.json +167 -0
  86. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/cart.json +32 -37
  87. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/catalog_lookup.json +46 -22
  88. data/schemas/2026-08-25/schemas/shopping/catalog_search.json +76 -0
  89. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/checkout.json +29 -16
  90. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/discount.json +9 -9
  91. data/schemas/2026-08-25/schemas/shopping/fulfillment.json +440 -0
  92. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/order.json +19 -11
  93. data/schemas/2026-08-25/schemas/shopping/permalink.json +68 -0
  94. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/adjustment.json +10 -4
  95. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/attribution.json +2 -2
  96. data/schemas/2026-08-25/schemas/shopping/types/availability.json +17 -0
  97. data/schemas/2026-08-25/schemas/shopping/types/business_fulfillment_config.json +37 -0
  98. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/buyer.json +2 -2
  99. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/category.json +2 -2
  100. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/detail_option_value.json +3 -3
  101. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/expectation.json +6 -10
  102. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/fulfillment.json +4 -5
  103. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/fulfillment_available_method.json +3 -8
  104. data/schemas/2026-08-25/schemas/shopping/types/fulfillment_destination.json +55 -0
  105. data/schemas/2026-08-25/schemas/shopping/types/fulfillment_destination_filter.json +23 -0
  106. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/fulfillment_event.json +4 -3
  107. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/fulfillment_group.json +3 -3
  108. data/schemas/2026-08-25/schemas/shopping/types/fulfillment_method.json +118 -0
  109. data/schemas/2026-08-25/schemas/shopping/types/fulfillment_option.json +46 -0
  110. data/schemas/2026-08-25/schemas/shopping/types/fulfillment_option_base.json +29 -0
  111. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/input_correlation.json +2 -2
  112. data/schemas/2026-08-25/schemas/shopping/types/item.json +43 -0
  113. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/line_item.json +7 -6
  114. data/schemas/2026-08-25/schemas/shopping/types/location_destination.json +26 -0
  115. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/option_value.json +2 -2
  116. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/order_confirmation.json +2 -2
  117. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/order_line_item.json +12 -9
  118. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/platform_fulfillment_config.json +2 -2
  119. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/product.json +10 -10
  120. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/product_option.json +3 -3
  121. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/rating.json +2 -2
  122. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/search_filters.json +3 -3
  123. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/selected_option.json +2 -2
  124. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/shipping_destination.json +11 -5
  125. data/schemas/2026-08-25/schemas/shopping/types/unit_price.json +56 -0
  126. data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/variant.json +19 -78
  127. data/schemas/2026-08-25/schemas/transports/a2a_message.json +165 -0
  128. data/schemas/{2026-04-08 → 2026-08-25}/schemas/transports/embedded_config.json +2 -2
  129. data/schemas/2026-08-25/schemas/transports/embedded_message.json +69 -0
  130. data/schemas/2026-08-25/schemas/transports/jsonrpc.json +136 -0
  131. data/schemas/2026-08-25/schemas/transports/mcp_tool_call.json +143 -0
  132. data/schemas/{2026-04-08 → 2026-08-25}/schemas/ucp.json +73 -23
  133. data/schemas/2026-08-25/services/common/mcp.openrpc.json +105 -0
  134. data/schemas/2026-08-25/services/common/rest.openapi.json +328 -0
  135. data/schemas/2026-08-25/services/payment-actions/embedded.openrpc.json +89 -0
  136. data/schemas/2026-08-25/services/shopping/embedded.openrpc.json +634 -0
  137. data/schemas/{2026-04-08 → 2026-08-25}/services/shopping/mcp.openrpc.json +21 -20
  138. data/schemas/2026-08-25/services/shopping/permalink.openapi.json +136 -0
  139. data/schemas/2026-08-25/services/shopping/rest.openapi.json +1350 -0
  140. metadata +125 -71
  141. data/schemas/2026-04-08/schemas/shopping/catalog_search.json +0 -64
  142. data/schemas/2026-04-08/schemas/shopping/fulfillment.json +0 -154
  143. data/schemas/2026-04-08/schemas/shopping/types/available_payment_instrument.json +0 -22
  144. data/schemas/2026-04-08/schemas/shopping/types/business_fulfillment_config.json +0 -38
  145. data/schemas/2026-04-08/schemas/shopping/types/context.json +0 -42
  146. data/schemas/2026-04-08/schemas/shopping/types/fulfillment_destination.json +0 -16
  147. data/schemas/2026-04-08/schemas/shopping/types/fulfillment_method.json +0 -67
  148. data/schemas/2026-04-08/schemas/shopping/types/fulfillment_option.json +0 -56
  149. data/schemas/2026-04-08/schemas/shopping/types/item.json +0 -33
  150. data/schemas/2026-04-08/schemas/shopping/types/message.json +0 -18
  151. data/schemas/2026-04-08/schemas/shopping/types/retail_location.json +0 -28
  152. 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: a245e863596f643972f10c9be20792055d6bf1d2925b90373571966896ee429a
4
+ data.tar.gz: ad0008e190d5c865e613b616653a7d5dbd5f6b19166f8f71723e57ae70ef7ce0
5
5
  SHA512:
6
- metadata.gz: '053845855d29f395fb001b78d13622798aadf54c8dc087c88bdf32a943e65e992e7d6d79a3f9a5c86232ebfb8f65f36eb1137888bd9a806e2ceeeef045a57789'
7
- data.tar.gz: 6e1cec328b2d07567d4441340fe177367db3bbe7e094aa93388446cb062ea948e600abeb353dcf31b3833f7f76d2ccc189c275a44ab4c5ea546b94e4f3c152d7
6
+ metadata.gz: d9a93b39705265b1095da290e5615ddbfcf3d3ea529d9b5863308ad99819c70b11ed77bc9805444a003b78c61bb418d4faa756617148d74b964b88beb90b5330
7
+ data.tar.gz: c2c95f6b3217ea2c2dc6b86318eaa845677b45c753b707ad2eb17f2ae7599f92fe457f7f82a34d46cfea26f1b5bfa8bc17ff4ee46459c94967c6de5ed6286611
data/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ pre-1.0, so APIs may still shift between minor versions.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.14.0] - 2026-10-09
10
+
11
+ 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.
12
+
13
+ - **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`.
14
+ - **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`.
15
+ - **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`.
16
+ - **Changed: `FulfillmentOption` `description` is sent as a `description.json` object** rather than a bare string, as 2026-08-25 requires.
17
+ - **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).
18
+
9
19
  ## [0.13.0] - 2026-10-07
10
20
 
11
21
  - **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
@@ -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
@@ -272,6 +272,8 @@ module Portage
272
272
 
273
273
  # schemas/shopping/types/fulfillment_option.json — a single priced choice
274
274
  # within a FulfillmentGroup (e.g. "Standard Shipping $5", "Express $15").
275
+ # `description` is a common/types/description.json object
276
+ # ({"plain"/"html"/"markdown" => ...}); a bare String is sent as `plain`.
275
277
  FulfillmentOption = Data.define(:id, :title, :totals, :description, :carrier, :earliest_fulfillment_time,
276
278
  :latest_fulfillment_time) do
277
279
  def initialize(id:, title:, totals:, description: nil, carrier: nil, earliest_fulfillment_time: nil,
@@ -281,7 +283,7 @@ module Portage
281
283
 
282
284
  def to_wire_h
283
285
  h = { "id" => id, "title" => title, "totals" => totals.map(&:to_wire_h) }
284
- h["description"] = description if description
286
+ h["description"] = description.is_a?(String) ? { "plain" => description } : description if description
285
287
  h["carrier"] = carrier if carrier
286
288
  h["earliest_fulfillment_time"] = earliest_fulfillment_time if earliest_fulfillment_time
287
289
  h["latest_fulfillment_time"] = latest_fulfillment_time if latest_fulfillment_time
@@ -1,5 +1,5 @@
1
1
  module Portage
2
2
  module Ucp
3
- VERSION = "0.13.0".freeze
3
+ VERSION = "0.14.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
+ }
@@ -0,0 +1,108 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ucp.dev/2026-08-25/schemas/common/location_lookup.json",
4
+ "name": "dev.ucp.common.location.lookup",
5
+ "title": "Location Lookup",
6
+ "description": "Location lookup by identifiers. Supports batch retrieval and single-location detail.",
7
+ "type": "object",
8
+ "$defs": {
9
+ "lookup_location": {
10
+ "description": "Location with required correlation metadata for lookup responses.",
11
+ "allOf": [
12
+ {
13
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/location.json"
14
+ },
15
+ {
16
+ "type": "object",
17
+ "required": [
18
+ "inputs"
19
+ ],
20
+ "properties": {
21
+ "inputs": {
22
+ "type": "array",
23
+ "items": {
24
+ "type": "object",
25
+ "required": [
26
+ "id"
27
+ ],
28
+ "properties": {
29
+ "id": {
30
+ "type": "string",
31
+ "description": "The identifier exactly as supplied in the lookup request.",
32
+ "ucp_request": "omit"
33
+ }
34
+ }
35
+ },
36
+ "minItems": 1,
37
+ "description": "Which request identifiers resolved to this Location. Each entry preserves one identifier exactly as supplied in the request.",
38
+ "ucp_request": "omit"
39
+ }
40
+ }
41
+ }
42
+ ]
43
+ },
44
+ "lookup_request": {
45
+ "type": "object",
46
+ "description": "Request body for batch location lookup. The Business resolves and deduplicates `ids` before applying `distance`, `serves`, and every supplied `filters` predicate; all structured predicates combine with AND.",
47
+ "required": [
48
+ "ids"
49
+ ],
50
+ "properties": {
51
+ "ids": {
52
+ "type": "array",
53
+ "items": {
54
+ "type": "string"
55
+ },
56
+ "minItems": 1,
57
+ "description": "Identifiers of the Locations to look up. The Business MUST support canonical `Location.id` values and MAY support secondary or alias identifiers."
58
+ },
59
+ "distance": {
60
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/location_distance.json",
61
+ "description": "Optional explicit-center radius predicate applied after ID resolution. It combines with `serves` and every supplied `filters` predicate using AND.",
62
+ "ucp_request": "optional"
63
+ },
64
+ "serves": {
65
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/location_serves.json",
66
+ "description": "Optional authoritative service-target predicate applied after ID resolution. It combines with `distance` and every supplied `filters` predicate using AND.",
67
+ "ucp_request": "optional"
68
+ },
69
+ "filters": {
70
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/location_filter.json"
71
+ },
72
+ "context": {
73
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/context.json"
74
+ },
75
+ "signals": {
76
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/signals.json"
77
+ }
78
+ }
79
+ },
80
+ "lookup_response": {
81
+ "type": "object",
82
+ "required": [
83
+ "ucp",
84
+ "locations"
85
+ ],
86
+ "properties": {
87
+ "ucp": {
88
+ "$ref": "https://ucp.dev/2026-08-25/schemas/ucp.json#/$defs/response_location_schema"
89
+ },
90
+ "locations": {
91
+ "type": "array",
92
+ "items": {
93
+ "$ref": "#/$defs/lookup_location"
94
+ },
95
+ "description": "Locations matching the requested identifiers and refinements. May contain fewer Locations if some identifiers do not resolve or their resolved Locations are filtered out, or more if one identifier resolves to multiple Locations. When multiple identifiers resolve to the same Location, one returned Location carries all corresponding `inputs` entries."
96
+ },
97
+ "messages": {
98
+ "type": "array",
99
+ "items": {
100
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/message.json"
101
+ },
102
+ "description": "Errors, warnings, or informational messages about the requested Locations, including `batch_limit_applied` when the Business processes only its configured maximum number of identifiers."
103
+ }
104
+ }
105
+ }
106
+ },
107
+ "version": "2026-08-25"
108
+ }
@@ -0,0 +1,72 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ucp.dev/2026-08-25/schemas/common/location_search.json",
4
+ "name": "dev.ucp.common.location.search",
5
+ "title": "Location Search",
6
+ "description": "Location search capability. Supports natural language queries, distance and serviceability relations, structured filtering including current item availability, and pagination.",
7
+ "type": "object",
8
+ "$defs": {
9
+ "search_request": {
10
+ "type": "object",
11
+ "description": "Request body for location search. The `distance` and `serves` relations and every supplied `filters` predicate combine with AND; `query` does not relax them.",
12
+ "properties": {
13
+ "query": {
14
+ "type": "string",
15
+ "description": "Free-text search query for natural language location search (e.g., 'restaurants near me that deliver', 'hotels with pool')."
16
+ },
17
+ "context": {
18
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/context.json"
19
+ },
20
+ "signals": {
21
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/signals.json"
22
+ },
23
+ "distance": {
24
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/location_distance.json",
25
+ "description": "Optional explicit-center radius predicate. When present, it combines with `serves` and every supplied `filters` predicate using AND.",
26
+ "ucp_request": "optional"
27
+ },
28
+ "serves": {
29
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/location_serves.json",
30
+ "description": "Optional authoritative service-target predicate. When present, it combines with `distance` and every supplied `filters` predicate using AND.",
31
+ "ucp_request": "optional"
32
+ },
33
+ "filters": {
34
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/location_filter.json"
35
+ },
36
+ "pagination": {
37
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/pagination.json#/$defs/request"
38
+ }
39
+ }
40
+ },
41
+ "search_response": {
42
+ "type": "object",
43
+ "required": [
44
+ "ucp",
45
+ "locations"
46
+ ],
47
+ "properties": {
48
+ "ucp": {
49
+ "$ref": "https://ucp.dev/2026-08-25/schemas/ucp.json#/$defs/response_location_schema"
50
+ },
51
+ "locations": {
52
+ "type": "array",
53
+ "items": {
54
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/location.json"
55
+ },
56
+ "description": "Locations matching the search criteria."
57
+ },
58
+ "pagination": {
59
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/pagination.json#/$defs/response"
60
+ },
61
+ "messages": {
62
+ "type": "array",
63
+ "items": {
64
+ "$ref": "https://ucp.dev/2026-08-25/schemas/common/types/message.json"
65
+ },
66
+ "description": "Errors, warnings, or informational messages about the search results."
67
+ }
68
+ }
69
+ }
70
+ },
71
+ "version": "2026-08-25"
72
+ }