portage-ucp 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +2 -2
- data/lib/portage/ucp/authenticator.rb +5 -4
- data/lib/portage/ucp/check.rb +24 -2
- data/lib/portage/ucp/manifest.rb +5 -4
- data/lib/portage/ucp/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 37b246e100783b94c0de9a89bdf4093123a45ae87b397202d63fa845847366d1
|
|
4
|
+
data.tar.gz: bfb928d1fd3e5cebff958ecd38cc5c3076ead011b0931cfd497fb2bea70fb64e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fce92b2376dbbce4849a2bda592a3162096f5d472f5d4b222dfe5f8449bdea62bd6ed32ff32db451864080b883c5c50fb7c5165e524dbfe10a58ee04c17e33c5
|
|
7
|
+
data.tar.gz: 9fea9246766b51b2819564732f3af0b4ac0a933a2797c97ad5fe75af88122e7eaeb95681f47631cc1f71d9528876b5c68224fce6ee055474633238c48c0a97d4
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to this project are documented here. Format loosely follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/); this project is
|
|
5
5
|
pre-1.0, so APIs may still shift between minor versions.
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.11.0] - 2026-09-29
|
|
10
|
+
|
|
11
|
+
- `Portage::Ucp::Check` (and `portage-ucp-check`) now follows a `<link rel="ucp" href="...">`
|
|
12
|
+
manifest pointer in the homepage when `/.well-known/ucp` is missing, the same fallback
|
|
13
|
+
`portage buy` uses. The report adds `manifest_url` when the manifest came from that link.
|
|
14
|
+
|
|
15
|
+
- Documentation only, no code change. Fixes two wrong comments. `Authenticator`
|
|
16
|
+
pointed at a `mutating_only:` option that `Mcp::Server.build` doesn't have:
|
|
17
|
+
it only authenticates tools whose adapter method takes `idempotency_key:`,
|
|
18
|
+
and no option changes that. `Manifest#to_h` read as if this manifest reports
|
|
19
|
+
`"version": "2026-08-25"`. That is what live Shopify stores serve. This
|
|
20
|
+
manifest reports `UCP_VERSION`, `"2026-04-08"`.
|
|
21
|
+
|
|
7
22
|
## [0.10.0] - 2026-09-25
|
|
8
23
|
|
|
9
24
|
- **Proxy support** (`docs/plans/proxy-support.md` Phases 1 and 3).
|
data/README.md
CHANGED
|
@@ -19,7 +19,7 @@ in this gem.
|
|
|
19
19
|
| `Portage::Ucp::Adapter` | The contract your backend implements — override only the catalog/cart/checkout/order/identity methods you support; the rest stay unadvertised. |
|
|
20
20
|
| `Portage::Ucp::CapabilityRegistry` | Figures out which capabilities an `Adapter` actually backs. |
|
|
21
21
|
| `Portage::Ucp::Dispatcher` | Routes a capability+action call to the right `Adapter` method. |
|
|
22
|
-
| `Portage::Ucp::Mcp::Server` | Wraps an `Adapter` as an MCP server — one `MCP::Tool` per advertised action
|
|
22
|
+
| `Portage::Ucp::Mcp::Server` | Wraps an `Adapter` as an MCP server — one `MCP::Tool` per advertised action; serve it over stdio or Streamable HTTP with the `mcp` gem's transports. |
|
|
23
23
|
| `Portage::Ucp::Manifest` | Builds the signed `/.well-known/ucp` discovery document. |
|
|
24
24
|
| `Portage::Ucp::Rack::ManifestEndpoint` | Serves that manifest over Rack. |
|
|
25
25
|
| `Portage::Ucp::Rack::WebhookEndpoint` | HMAC-verified inbound order-lifecycle webhooks. |
|
|
@@ -32,7 +32,7 @@ in this gem.
|
|
|
32
32
|
| `Portage::Ucp::PolicyGuard` / `Portage::Ucp::Policy` | Per-transaction/rolling/velocity caps and a merchant allowlist, checked before `complete_checkout` dispatch; configured via `portage-cli`'s `portage policy show/set`. |
|
|
33
33
|
| `Portage::Ucp::Support::OfferRanking` / `Portage::Ucp::Support::Escalation` | The offer-ranking rule (buyable, then priced, then cheapest, ties stable) and the escalation rule (`requires_escalation`, then a mismatch). `portage-ucp-decision` wraps both as typed verdicts; `portage-cli` calls them directly. |
|
|
34
34
|
| `Portage::Ucp::PaymentEnrollmentGuard` | Validates every `create_payment_enrollment`/`get_payment_enrollment` result an `Adapter` returns — `status` must be `"pending"` (with a `setup_url`, no `payment_token`) or `"complete"` (with a `payment_token`, no `setup_url`). Runs automatically in `Dispatcher#call`. |
|
|
35
|
-
| `Portage::Ucp::Ap2::PaymentMandate` / `Portage::Ucp::Ap2::MandateGuard` | A typed shape for an AP2 payment mandate, and
|
|
35
|
+
| `Portage::Ucp::Ap2::PaymentMandate` / `Portage::Ucp::Ap2::MandateGuard` | A typed shape for an AP2 payment mandate, and validation (required fields + expiry, plus signature verification when `mandate_trusted_keys` is configured) run automatically on any `mandate:` argument passed through `Dispatcher#call`. |
|
|
36
36
|
|
|
37
37
|
Security defaults are all locked down, not permissive-by-omission —
|
|
38
38
|
`UnconfiguredAuthenticator` rejects every mutating call until you configure a real
|
|
@@ -4,10 +4,11 @@ module Portage
|
|
|
4
4
|
# tool call, return an auth context (any truthy value) or raise
|
|
5
5
|
# Portage::Ucp::AuthenticationError. There is deliberately no default that
|
|
6
6
|
# allows anonymous mutation — an unconfigured server rejects every
|
|
7
|
-
# mutating capability call (see
|
|
8
|
-
# calls
|
|
9
|
-
#
|
|
10
|
-
#
|
|
7
|
+
# mutating capability call (see UnconfiguredAuthenticator below).
|
|
8
|
+
# Mcp::Server.build calls it only for mutating tools: those whose
|
|
9
|
+
# adapter method takes `idempotency_key:`. Every other tool (catalog
|
|
10
|
+
# search, product, cart, checkout and order lookups) never reaches it,
|
|
11
|
+
# and Server.build has no option to change that.
|
|
11
12
|
class Authenticator
|
|
12
13
|
def call(_server_context)
|
|
13
14
|
raise NotImplementedError, "#{self.class} must implement #call"
|
data/lib/portage/ucp/check.rb
CHANGED
|
@@ -8,7 +8,9 @@ module Portage
|
|
|
8
8
|
# `portage-ucp-check <url>` — point it at any storefront and find out what
|
|
9
9
|
# portage-ucp can do with it. Checks for a native `/.well-known/ucp`
|
|
10
10
|
# manifest first (the store may already speak UCP without this gem, see
|
|
11
|
-
# README's "Why /.well-known/ucp?")
|
|
11
|
+
# README's "Why /.well-known/ucp?"), then a `<link rel="ucp" href=...>`
|
|
12
|
+
# manifest pointer in the homepage (the same fallback `portage buy`
|
|
13
|
+
# follows); if there isn't one, detects the
|
|
12
14
|
# commerce platform from the page itself (see Resolver) and names
|
|
13
15
|
# the matching portage-ucp-<adapter> gem — probing it live when that
|
|
14
16
|
# adapter's env vars are already set, so "best match" means a
|
|
@@ -30,6 +32,10 @@ module Portage
|
|
|
30
32
|
return { url: @uri.to_s, native_ucp: manifest } if manifest
|
|
31
33
|
|
|
32
34
|
body, headers = fetch_homepage
|
|
35
|
+
linked = manifest_link(body)
|
|
36
|
+
manifest = linked && fetch_json(linked)
|
|
37
|
+
return { url: @uri.to_s, native_ucp: manifest, manifest_url: linked.to_s } if manifest
|
|
38
|
+
|
|
33
39
|
platform = Resolver.detect_platform(body, headers)
|
|
34
40
|
|
|
35
41
|
report = { url: @uri.to_s, native_ucp: nil, platform: platform&.name, recommended_gem: platform&.gem }
|
|
@@ -42,7 +48,11 @@ module Portage
|
|
|
42
48
|
def fetch_manifest
|
|
43
49
|
manifest_uri = @uri.dup
|
|
44
50
|
manifest_uri.path = MANIFEST_PATH
|
|
45
|
-
|
|
51
|
+
fetch_json(manifest_uri)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def fetch_json(uri)
|
|
55
|
+
response = get(uri)
|
|
46
56
|
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
47
57
|
|
|
48
58
|
JSON.parse(response.body)
|
|
@@ -59,6 +69,18 @@ module Portage
|
|
|
59
69
|
[nil, {}]
|
|
60
70
|
end
|
|
61
71
|
|
|
72
|
+
# `<link rel="ucp" href="...">` in either attribute order, resolved
|
|
73
|
+
# against the store URL.
|
|
74
|
+
def manifest_link(body)
|
|
75
|
+
return nil unless body
|
|
76
|
+
|
|
77
|
+
match = body.match(/<link[^>]+rel=["']ucp["'][^>]+href=["']([^"']+)["']/i) ||
|
|
78
|
+
body.match(/<link[^>]+href=["']([^"']+)["'][^>]+rel=["']ucp["']/i)
|
|
79
|
+
match && URI.join(@uri, match[1])
|
|
80
|
+
rescue URI::Error
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
62
84
|
def probe(platform)
|
|
63
85
|
env = Resolver.env_for(platform)
|
|
64
86
|
missing = Resolver.missing_env(platform, env)
|
data/lib/portage/ucp/manifest.rb
CHANGED
|
@@ -32,10 +32,11 @@ module Portage
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# Nests everything under a "ucp" envelope, and keys `capabilities` by
|
|
35
|
-
# capability name
|
|
36
|
-
# Allbirds, Glossier, and 34+ others
|
|
37
|
-
# "version": "2026-08-25"
|
|
38
|
-
#
|
|
35
|
+
# capability name. That is the shape live Shopify UCP rollouts (Casper,
|
|
36
|
+
# Allbirds, Glossier, and 34+ others) serve under their own manifest
|
|
37
|
+
# "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
40
|
def to_h
|
|
40
41
|
ucp = {
|
|
41
42
|
version: UCP_VERSION,
|
data/lib/portage/ucp/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: portage-ucp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tom Whitbread
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|