portage-ucp 0.12.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/lib/generators/portage/ucp/install/templates/portage_ucp.rb +2 -2
- data/lib/portage/ucp/capabilities/cart.rb +1 -0
- data/lib/portage/ucp/capabilities/catalog.rb +1 -0
- data/lib/portage/ucp/capabilities/checkout.rb +1 -0
- data/lib/portage/ucp/capabilities/discount.rb +1 -0
- data/lib/portage/ucp/capabilities/fulfillment.rb +1 -0
- data/lib/portage/ucp/capabilities/order.rb +1 -0
- data/lib/portage/ucp/capability.rb +7 -2
- data/lib/portage/ucp/configuration.rb +1 -1
- data/lib/portage/ucp/manifest.rb +31 -9
- data/lib/portage/ucp/schema_validator.rb +3 -3
- data/lib/portage/ucp/support/checkout_state.rb +73 -2
- data/lib/portage/ucp/value_objects.rb +3 -1
- data/lib/portage/ucp/version.rb +1 -1
- data/lib/portage/ucp/wire_envelope.rb +1 -1
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/capability.json +11 -8
- data/schemas/2026-08-25/schemas/common/identity_linking.json +133 -0
- data/schemas/2026-08-25/schemas/common/location_lookup.json +108 -0
- data/schemas/2026-08-25/schemas/common/location_search.json +72 -0
- data/schemas/2026-08-25/schemas/common/loyalty.json +294 -0
- data/schemas/2026-08-25/schemas/common/payment_ap2_mandate.json +91 -0
- data/schemas/2026-08-25/schemas/common/payment_authentication.json +90 -0
- data/schemas/2026-08-25/schemas/common/payment_split_payments.json +82 -0
- data/schemas/2026-08-25/schemas/common/payment_terms.json +92 -0
- data/schemas/2026-08-25/schemas/common/types/actions.json +42 -0
- data/schemas/2026-08-25/schemas/common/types/amenity_type.json +13 -0
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/amount.json +4 -3
- data/schemas/2026-08-25/schemas/common/types/available_payment_instrument.json +20 -0
- data/schemas/2026-08-25/schemas/common/types/binding.json +22 -0
- data/schemas/2026-08-25/schemas/common/types/business_split_payments_config.json +25 -0
- data/schemas/2026-08-25/schemas/common/types/card_credential.json +78 -0
- data/schemas/2026-08-25/schemas/common/types/card_payment_instrument.json +77 -0
- data/schemas/2026-08-25/schemas/common/types/constraint_expression.json +71 -0
- data/schemas/2026-08-25/schemas/common/types/context.json +66 -0
- data/schemas/2026-08-25/schemas/common/types/daily_hour.json +36 -0
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/description.json +2 -2
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/error_code.json +3 -3
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/error_response.json +4 -4
- data/schemas/2026-08-25/schemas/common/types/exception_hour.json +38 -0
- data/schemas/2026-08-25/schemas/common/types/geo.json +25 -0
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/info_code.json +3 -3
- data/schemas/2026-08-25/schemas/common/types/instrument_group.json +32 -0
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/link.json +3 -3
- data/schemas/2026-08-25/schemas/common/types/locality.json +22 -0
- data/schemas/2026-08-25/schemas/common/types/location.json +93 -0
- data/schemas/2026-08-25/schemas/common/types/location_distance.json +23 -0
- data/schemas/2026-08-25/schemas/common/types/location_filter.json +43 -0
- data/schemas/2026-08-25/schemas/common/types/location_serves.json +80 -0
- data/schemas/2026-08-25/schemas/common/types/location_summary.json +29 -0
- data/schemas/2026-08-25/schemas/common/types/measure.json +25 -0
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/media.json +3 -3
- data/schemas/2026-08-25/schemas/common/types/message.json +18 -0
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/message_error.json +5 -5
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/message_info.json +4 -4
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/message_warning.json +4 -4
- data/schemas/2026-08-25/schemas/common/types/network_token_credential.json +68 -0
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/pagination.json +3 -4
- data/schemas/2026-08-25/schemas/common/types/pan_credential.json +54 -0
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common/types}/payment.json +3 -3
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/payment_credential.json +2 -2
- data/schemas/2026-08-25/schemas/common/types/payment_identity.json +16 -0
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/payment_instrument.json +5 -5
- data/schemas/2026-08-25/schemas/common/types/payment_schedule.json +37 -0
- data/schemas/2026-08-25/schemas/common/types/payment_term.json +35 -0
- data/schemas/2026-08-25/schemas/common/types/policy.json +34 -0
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/postal_address.json +2 -2
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/price.json +3 -3
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/price_filter.json +4 -4
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/price_range.json +5 -5
- data/schemas/2026-08-25/schemas/common/types/quantity_unit.json +22 -0
- data/schemas/2026-08-25/schemas/common/types/request_constraints.json +47 -0
- data/schemas/2026-08-25/schemas/common/types/reverse_domain_name.json +17 -0
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/signals.json +3 -3
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/signed_amount.json +5 -3
- data/schemas/2026-08-25/schemas/common/types/time_interval.json +29 -0
- data/schemas/2026-08-25/schemas/common/types/token_credential.json +29 -0
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/total.json +3 -3
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/totals.json +4 -4
- data/schemas/2026-08-25/schemas/common/types/unit.json +49 -0
- data/schemas/{2026-04-08/schemas/shopping → 2026-08-25/schemas/common}/types/warning_code.json +4 -3
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/payment_handler.json +4 -4
- data/schemas/2026-08-25/schemas/profile.json +266 -0
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/service.json +6 -6
- data/schemas/2026-08-25/schemas/shopping/buyer_consent.json +167 -0
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/cart.json +32 -37
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/catalog_lookup.json +46 -22
- data/schemas/2026-08-25/schemas/shopping/catalog_search.json +76 -0
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/checkout.json +29 -16
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/discount.json +9 -9
- data/schemas/2026-08-25/schemas/shopping/fulfillment.json +440 -0
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/order.json +19 -11
- data/schemas/2026-08-25/schemas/shopping/permalink.json +68 -0
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/adjustment.json +10 -4
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/attribution.json +2 -2
- data/schemas/2026-08-25/schemas/shopping/types/availability.json +17 -0
- data/schemas/2026-08-25/schemas/shopping/types/business_fulfillment_config.json +37 -0
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/buyer.json +2 -2
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/category.json +2 -2
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/detail_option_value.json +3 -3
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/expectation.json +6 -10
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/fulfillment.json +4 -5
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/fulfillment_available_method.json +3 -8
- data/schemas/2026-08-25/schemas/shopping/types/fulfillment_destination.json +55 -0
- data/schemas/2026-08-25/schemas/shopping/types/fulfillment_destination_filter.json +23 -0
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/fulfillment_event.json +4 -3
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/fulfillment_group.json +3 -3
- data/schemas/2026-08-25/schemas/shopping/types/fulfillment_method.json +118 -0
- data/schemas/2026-08-25/schemas/shopping/types/fulfillment_option.json +46 -0
- data/schemas/2026-08-25/schemas/shopping/types/fulfillment_option_base.json +29 -0
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/input_correlation.json +2 -2
- data/schemas/2026-08-25/schemas/shopping/types/item.json +43 -0
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/line_item.json +7 -6
- data/schemas/2026-08-25/schemas/shopping/types/location_destination.json +26 -0
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/option_value.json +2 -2
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/order_confirmation.json +2 -2
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/order_line_item.json +12 -9
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/platform_fulfillment_config.json +2 -2
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/product.json +10 -10
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/product_option.json +3 -3
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/rating.json +2 -2
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/search_filters.json +3 -3
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/selected_option.json +2 -2
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/shipping_destination.json +11 -5
- data/schemas/2026-08-25/schemas/shopping/types/unit_price.json +56 -0
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/shopping/types/variant.json +19 -78
- data/schemas/2026-08-25/schemas/transports/a2a_message.json +165 -0
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/transports/embedded_config.json +2 -2
- data/schemas/2026-08-25/schemas/transports/embedded_message.json +69 -0
- data/schemas/2026-08-25/schemas/transports/jsonrpc.json +136 -0
- data/schemas/2026-08-25/schemas/transports/mcp_tool_call.json +143 -0
- data/schemas/{2026-04-08 → 2026-08-25}/schemas/ucp.json +73 -23
- data/schemas/2026-08-25/services/common/mcp.openrpc.json +105 -0
- data/schemas/2026-08-25/services/common/rest.openapi.json +328 -0
- data/schemas/2026-08-25/services/payment-actions/embedded.openrpc.json +89 -0
- data/schemas/2026-08-25/services/shopping/embedded.openrpc.json +634 -0
- data/schemas/{2026-04-08 → 2026-08-25}/services/shopping/mcp.openrpc.json +21 -20
- data/schemas/2026-08-25/services/shopping/permalink.openapi.json +136 -0
- data/schemas/2026-08-25/services/shopping/rest.openapi.json +1350 -0
- metadata +125 -71
- data/schemas/2026-04-08/schemas/shopping/catalog_search.json +0 -64
- data/schemas/2026-04-08/schemas/shopping/fulfillment.json +0 -154
- data/schemas/2026-04-08/schemas/shopping/types/available_payment_instrument.json +0 -22
- data/schemas/2026-04-08/schemas/shopping/types/business_fulfillment_config.json +0 -38
- data/schemas/2026-04-08/schemas/shopping/types/context.json +0 -42
- data/schemas/2026-04-08/schemas/shopping/types/fulfillment_destination.json +0 -16
- data/schemas/2026-04-08/schemas/shopping/types/fulfillment_method.json +0 -67
- data/schemas/2026-04-08/schemas/shopping/types/fulfillment_option.json +0 -56
- data/schemas/2026-04-08/schemas/shopping/types/item.json +0 -33
- data/schemas/2026-04-08/schemas/shopping/types/message.json +0 -18
- data/schemas/2026-04-08/schemas/shopping/types/retail_location.json +0 -28
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a245e863596f643972f10c9be20792055d6bf1d2925b90373571966896ee429a
|
|
4
|
+
data.tar.gz: ad0008e190d5c865e613b616653a7d5dbd5f6b19166f8f71723e57ae70ef7ce0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9a93b39705265b1095da290e5615ddbfcf3d3ea529d9b5863308ad99819c70b11ed77bc9805444a003b78c61bb418d4faa756617148d74b964b88beb90b5330
|
|
7
|
+
data.tar.gz: c2c95f6b3217ea2c2dc6b86318eaa845677b45c753b707ad2eb17f2ae7599f92fe457f7f82a34d46cfea26f1b5bfa8bc17ff4ee46459c94967c6de5ed6286611
|
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.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
|
+
|
|
19
|
+
## [0.13.0] - 2026-10-07
|
|
20
|
+
|
|
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.
|
|
22
|
+
|
|
9
23
|
## [0.12.0] - 2026-10-05
|
|
10
24
|
|
|
11
25
|
- **Removed the unused `services/shopping/embedded.openrpc.json` schema.** Nothing loaded or listed it (`SchemaValidator` only reads `mcp.openrpc.json`); it just shipped in the gem. Patch-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
|
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
|
)
|
|
@@ -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
|
-
|
|
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
|
data/lib/portage/ucp/manifest.rb
CHANGED
|
@@ -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
|
|
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-
|
|
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.
|
|
39
|
-
#
|
|
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:
|
|
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
|
|
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-
|
|
13
|
-
# https://ucp.dev/2026-
|
|
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-
|
|
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
|
|
@@ -14,7 +14,9 @@ module Portage
|
|
|
14
14
|
#
|
|
15
15
|
# Both default rather than raise on an unknown id ("incomplete" and "",
|
|
16
16
|
# respectively): a checkout this process didn't create is still
|
|
17
|
-
# schema-valid to report, just not information-complete.
|
|
17
|
+
# schema-valid to report, just not information-complete. An adapter
|
|
18
|
+
# can add a platform lookup for the unknown-id case (see
|
|
19
|
+
# #platform_order).
|
|
18
20
|
module CheckoutState
|
|
19
21
|
# Dispatcher wraps each adapter call in .with_observability rather
|
|
20
22
|
# than writing [logger, correlation_id] onto an instance variable on
|
|
@@ -49,8 +51,77 @@ module Portage
|
|
|
49
51
|
|
|
50
52
|
private
|
|
51
53
|
|
|
54
|
+
# A checkout this process has no record of falls back to the
|
|
55
|
+
# platform: `portage orders reconcile` runs in a new process, after
|
|
56
|
+
# the shopper paid in their browser, and the in-process hash alone
|
|
57
|
+
# would always say "incomplete" there (design-log §55). See
|
|
58
|
+
# #platform_order.
|
|
52
59
|
def checkout_status(checkout_id)
|
|
53
|
-
(@checkout_status ||= {})
|
|
60
|
+
recorded = (@checkout_status ||= {})[checkout_id]
|
|
61
|
+
return recorded if recorded
|
|
62
|
+
|
|
63
|
+
platform_order(checkout_id) ? "completed" : "incomplete"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The order_confirmation for a checkout #platform_order found an order
|
|
67
|
+
# for, nil otherwise (an in-process #complete_checkout hands its own
|
|
68
|
+
# back directly).
|
|
69
|
+
def checkout_order(checkout_id)
|
|
70
|
+
order = (@platform_orders ||= {})[checkout_id]
|
|
71
|
+
order && Portage::Ucp::OrderConfirmation.new(id: order.id, permalink_url: order.permalink_url)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# For a #get_checkout whose cart read came back not-found: most
|
|
75
|
+
# platforms drop the cart once it becomes an order (BigCommerce
|
|
76
|
+
# deletes it on payment, Magento deactivates the quote), so a paid
|
|
77
|
+
# checkout would otherwise only ever read as not-found. This builds
|
|
78
|
+
# the completed Checkout from the order the platform reports for it.
|
|
79
|
+
# No order means nil: a vanished cart on its own is never read as a
|
|
80
|
+
# completion (docs/plans/handoff-reconcile.md, "Settle only on a
|
|
81
|
+
# `completed` status the store actually reports").
|
|
82
|
+
def checkout_from_platform_order(checkout_id)
|
|
83
|
+
order = platform_order(checkout_id)
|
|
84
|
+
return nil unless order
|
|
85
|
+
|
|
86
|
+
line_items = order.line_items.map do |li|
|
|
87
|
+
Portage::Ucp::LineItem.new(id: li.id, item: li.item, quantity: li.quantity[:total], totals: li.totals)
|
|
88
|
+
end
|
|
89
|
+
Portage::Ucp::Checkout.new(id: checkout_id, status: "completed", line_items: line_items,
|
|
90
|
+
currency: order.currency, totals: order.totals, links: [],
|
|
91
|
+
order: checkout_order(checkout_id))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The optional per-adapter hook: an adapter that can ask its platform
|
|
95
|
+
# "did this checkout become a placed order?" defines a private
|
|
96
|
+
# `platform_checkout_order(checkout_id)` returning that Portage::Ucp::
|
|
97
|
+
# Order, or nil. Without the hook (or when this process already
|
|
98
|
+
# tracks the checkout) nothing changes. A found order is cached
|
|
99
|
+
# through #record_checkout_status/#record_order_checkout so the rest
|
|
100
|
+
# of this process agrees with it. A lookup error is swallowed: the
|
|
101
|
+
# caller gets today's in-process answer, which reconcile reads as
|
|
102
|
+
# "still pending", never as a settle. The hook is a private method
|
|
103
|
+
# rather than a new Adapter method or kwarg for the same §9/§23
|
|
104
|
+
# reason as .with_observability above.
|
|
105
|
+
def platform_order(checkout_id)
|
|
106
|
+
orders = (@platform_orders ||= {})
|
|
107
|
+
return orders[checkout_id] if orders.key?(checkout_id)
|
|
108
|
+
|
|
109
|
+
order = ask_platform(checkout_id)
|
|
110
|
+
return nil unless order
|
|
111
|
+
|
|
112
|
+
orders[checkout_id] = order
|
|
113
|
+
record_order_checkout(order.id, checkout_id)
|
|
114
|
+
record_checkout_status(checkout_id, "completed")
|
|
115
|
+
order
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def ask_platform(checkout_id)
|
|
119
|
+
return nil if (@checkout_status ||= {}).key?(checkout_id)
|
|
120
|
+
return nil unless respond_to?(:platform_checkout_order, true)
|
|
121
|
+
|
|
122
|
+
platform_checkout_order(checkout_id)
|
|
123
|
+
rescue StandardError
|
|
124
|
+
nil
|
|
54
125
|
end
|
|
55
126
|
|
|
56
127
|
def record_checkout_status(checkout_id, status)
|
|
@@ -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
|
data/lib/portage/ucp/version.rb
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
+
}
|