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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7888232b409966ef29fae287d17d6af418dd676e12ad53a7ef7d30d7178f6ae6
4
- data.tar.gz: e967bbb103d6911ca4ed8d4fb1204687cd8a2c7a51e3ab7675db5420b7eb0623
3
+ metadata.gz: 37b246e100783b94c0de9a89bdf4093123a45ae87b397202d63fa845847366d1
4
+ data.tar.gz: bfb928d1fd3e5cebff958ecd38cc5c3076ead011b0931cfd497fb2bea70fb64e
5
5
  SHA512:
6
- metadata.gz: 3f78f566ac2ee41da063ae9a96a658707bf481f8f9ee791e288fc2d78ce0325ed147c2423a137b06350b3ddb5f64f09c17a5ba6967894e7795ef72c90d6f1384
7
- data.tar.gz: 61a0ec7f01125fb8da2e0eb74fdc03b50d4fc94e38a3863e8366b1932cc871ab3787253d3199845a4a18a1902245fb7b2d3ca336b2bd896a82c4ce011d187f5b
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, stdio or Streamable HTTP. |
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 shape-only validation (required fields + expiry — not cryptographic verification) run automatically on any `mandate:` argument passed through `Dispatcher#call`. |
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 UNCONFIGURED below). Read-only catalog
8
- # calls MAY be left open at the consumer's explicit choice by not
9
- # requiring authentication for those specific actions (see
10
- # Mcp::Server.build's `mutating_only:`).
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"
@@ -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?"); if there isn't one, detects the
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
- response = get(manifest_uri)
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)
@@ -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 (matching what live Shopify UCP rollouts — Casper,
36
- # Allbirds, Glossier, and 34+ others — actually serve as of manifest
37
- # "version": "2026-08-25"; see docs/well-known-ucp.md and
38
- # portage-ucp-client's Client.discover, which this shape now matches).
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,
@@ -1,5 +1,5 @@
1
1
  module Portage
2
2
  module Ucp
3
- VERSION = "0.10.0".freeze
3
+ VERSION = "0.11.0".freeze
4
4
  end
5
5
  end
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.10.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-25 00:00:00.000000000 Z
11
+ date: 2026-09-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64