From 52ab223762202a5c472c3597acd6cdd61748e6c5 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 17 Jul 2026 21:41:54 -0700 Subject: [PATCH 1/2] fix(x402): derive WWW-Authenticate realm from the host, not the full endpoint URL The Checkout auto-derived the MPP challenge realm from the full endpoint `url` (e.g. https://agents.example.com/purchase), so the emitted WWW-Authenticate realm carried the path. The Node SDK passes `new URL(APP_URL).host`; validators expect the realm to be the bare protection-space host. `_realm_from_url` normalizes the derived realm to the host (agents.example.com), restoring parity with the Node SDK. Falls back to the input unchanged when it has no parseable host. Bump to 2.5.11. Co-Authored-By: Claude Opus 4.8 --- agentscore_commerce/checkout.py | 16 +++++++++++++++- pyproject.toml | 2 +- tests/test_checkout.py | 12 ++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/agentscore_commerce/checkout.py b/agentscore_commerce/checkout.py index f8fa17a..10fe9e6 100644 --- a/agentscore_commerce/checkout.py +++ b/agentscore_commerce/checkout.py @@ -149,6 +149,20 @@ def _spec_method_name(spec: CheckoutRailSpec) -> str: return "stripe/spt" # StripeRailSpec is the only remaining variant in CheckoutRailSpec. +def _realm_from_url(url: str) -> str: + """Derive the WWW-Authenticate ``realm`` from the checkout endpoint URL. + + The realm identifies the protection space and, by convention (and to match the Node + SDK, which passes ``new URL(APP_URL).host``), is the bare host, not the full endpoint + URL. ``Checkout(url="https://agents.example.com/purchase")`` yields realm + ``agents.example.com``. Falls back to the input unchanged when it has no parseable host + (e.g. already a bare host, or a relative path). + """ + from urllib.parse import urlparse + + return urlparse(url).netloc or url + + @dataclass class CheckoutRequest: """Framework-neutral HTTP request input to :meth:`Checkout.handle`. @@ -907,7 +921,7 @@ def __init__( getter = lazy_mppx_server( rails=mpp_rails, secret_key=mppx_secret_key, - realm=url, + realm=_realm_from_url(url), ) compose_mppx = make_mppx_compose_hook(server_getter=getter) diff --git a/pyproject.toml b/pyproject.toml index 13d727a..b93dc04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agentscore-commerce" -version = "2.5.10" +version = "2.5.11" description = "Agent commerce SDK for Python — identity middleware (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI) + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for AgentScore-powered agent commerce." readme = "README.md" license = "MIT" diff --git a/tests/test_checkout.py b/tests/test_checkout.py index 8feccc7..047072e 100644 --- a/tests/test_checkout.py +++ b/tests/test_checkout.py @@ -985,3 +985,15 @@ def test_rails_key_for_mppx_method_returns_none_when_rail_absent() -> None: assert checkout._rails_key_for_mppx_method("solana") is None assert checkout._rails_key_for_mppx_method("stripe") is None assert checkout._rails_key_for_mppx_method("tempo") == "tempo" + + +def test_realm_from_url_derives_bare_host(): + from agentscore_commerce.checkout import _realm_from_url + + # Full endpoint URL -> bare host (matches the Node SDK's new URL(APP_URL).host), + # so the WWW-Authenticate realm is the protection space, not the full path. + assert _realm_from_url("https://agents.agentscore.com/purchase") == "agents.agentscore.com" + assert _realm_from_url("https://agents.example.com:8443/x/y") == "agents.example.com:8443" + # No parseable host: pass through unchanged. + assert _realm_from_url("agents.example.com") == "agents.example.com" + assert _realm_from_url("/purchase") == "/purchase" From eb628845fa9cf6f68d218cc60ad317789c8a8044 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 17 Jul 2026 21:42:28 -0700 Subject: [PATCH 2/2] chore: sync uv.lock to 2.5.11 --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 1165035..68590a5 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ constraints = [{ name = "fastapi", specifier = "!=0.136.3" }] [[package]] name = "agentscore-commerce" -version = "2.5.10" +version = "2.5.11" source = { editable = "." } dependencies = [ { name = "agentscore-py" },