Skip to content

Commit 8f30d45

Browse files
vvillait88claude
andauthored
fix(x402): derive WWW-Authenticate realm from host, not full endpoint URL (#93)
Surfaced by `npx mppx@latest validate` against store: the challenge realm was `https://agents.agentscore.com/purchase` (full URL with path) while martin/sayer (Node) emit the bare host. Root cause: the Python Checkout auto-derived the MPP realm from the full endpoint `url`. The Node SDK passes `new URL(APP_URL).host`; the WWW-Authenticate realm should be the bare protection-space host. `_realm_from_url` normalizes the derived realm to the host, restoring parity. Falls back to the input unchanged when it has no parseable host (already-bare host, relative path). Advisory-only (the challenge was fully valid and settles); this clears the validator warning and aligns the two SDKs. Unit test covers full-URL, host:port, bare-host, and relative-path cases. Full suite green (1841 passed, 95.36% coverage), ruff + ty clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent eef3dfe commit 8f30d45

4 files changed

Lines changed: 29 additions & 3 deletions

File tree

agentscore_commerce/checkout.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,20 @@ def _spec_method_name(spec: CheckoutRailSpec) -> str:
149149
return "stripe/spt" # StripeRailSpec is the only remaining variant in CheckoutRailSpec.
150150

151151

152+
def _realm_from_url(url: str) -> str:
153+
"""Derive the WWW-Authenticate ``realm`` from the checkout endpoint URL.
154+
155+
The realm identifies the protection space and, by convention (and to match the Node
156+
SDK, which passes ``new URL(APP_URL).host``), is the bare host, not the full endpoint
157+
URL. ``Checkout(url="https://agents.example.com/purchase")`` yields realm
158+
``agents.example.com``. Falls back to the input unchanged when it has no parseable host
159+
(e.g. already a bare host, or a relative path).
160+
"""
161+
from urllib.parse import urlparse
162+
163+
return urlparse(url).netloc or url
164+
165+
152166
@dataclass
153167
class CheckoutRequest:
154168
"""Framework-neutral HTTP request input to :meth:`Checkout.handle`.
@@ -907,7 +921,7 @@ def __init__(
907921
getter = lazy_mppx_server(
908922
rails=mpp_rails,
909923
secret_key=mppx_secret_key,
910-
realm=url,
924+
realm=_realm_from_url(url),
911925
)
912926
compose_mppx = make_mppx_compose_hook(server_getter=getter)
913927

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "agentscore-commerce"
7-
version = "2.5.10"
7+
version = "2.5.11"
88
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."
99
readme = "README.md"
1010
license = "MIT"

tests/test_checkout.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,3 +985,15 @@ def test_rails_key_for_mppx_method_returns_none_when_rail_absent() -> None:
985985
assert checkout._rails_key_for_mppx_method("solana") is None
986986
assert checkout._rails_key_for_mppx_method("stripe") is None
987987
assert checkout._rails_key_for_mppx_method("tempo") == "tempo"
988+
989+
990+
def test_realm_from_url_derives_bare_host():
991+
from agentscore_commerce.checkout import _realm_from_url
992+
993+
# Full endpoint URL -> bare host (matches the Node SDK's new URL(APP_URL).host),
994+
# so the WWW-Authenticate realm is the protection space, not the full path.
995+
assert _realm_from_url("https://agents.agentscore.com/purchase") == "agents.agentscore.com"
996+
assert _realm_from_url("https://agents.example.com:8443/x/y") == "agents.example.com:8443"
997+
# No parseable host: pass through unchanged.
998+
assert _realm_from_url("agents.example.com") == "agents.example.com"
999+
assert _realm_from_url("/purchase") == "/purchase"

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)