Conversation
Parse WWW-Authenticate: Payment headers per IETF draft-ryan-httpauth-payment. Prefer L402 when server offers both; fall back to MPP when L402 unavailable. MppChallenge type, find_payment_challenge(), nullable macaroon in cache. 21 new tests, all 122 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds client-side support for MPP (Payment / Machine Payments Protocol) challenges alongside existing L402 handling, including parsing WWW-Authenticate: Payment headers, caching MPP credentials (no macaroon), and retrying requests with a Payment ... preimage=... Authorization header.
Changes:
- Introduces
MppChallenge,parse_mpp_challenge(), andfind_payment_challenge()(L402 preferred, MPP fallback); keepsfind_l402_challengeas an alias. - Updates the HTTP client to recognize and respond to MPP challenges, including Payment-scheme retries and cache writes.
- Extends the credential cache to support “macaroon = None” for MPP and to generate Payment authorization headers.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_challenge.py | Adds/updates unit tests for MPP parsing and combined challenge selection behavior. |
| src/l402_requests/credential_cache.py | Allows cached credentials without a macaroon and emits Payment ... Authorization headers. |
| src/l402_requests/client.py | Switches challenge detection to L402-or-MPP and retries with the appropriate auth scheme. |
| src/l402_requests/challenge.py | Adds MPP challenge model + parsing and combined finder; aliases find_l402_challenge. |
| src/l402_requests/init.py | Re-exports MPP parsing/finding APIs from the package top-level. |
Comments suppressed due to low confidence (2)
src/l402_requests/client.py:22
ChallengeParseErroris imported here but never used in this module, which will trigger unused-import linting in many setups. Remove the import or add the intended handling that uses it.
from l402_requests.exceptions import (
ChallengeParseError,
L402Error,
NoWalletError,
PaymentFailedError,
)
src/l402_requests/client.py:96
- MPP challenges can include an explicit
amounteven when the BOLT11 invoice is zero-amount. Right nowamount_satsis derived only fromchallenge.invoice, so budget checks and spending log recording will be skipped whenextract_amount_sats()returnsNoneeven ifMppChallenge.amountis present. Consider using the parsed MPP amount (and validatingcurrency, if applicable) as a fallback foramount_satswhen the invoice doesn’t encode an amount.
amount_sats = extract_amount_sats(challenge.invoice)
parsed_url = urlparse(url)
domain = parsed_url.hostname or ""
if self._budget and amount_sats is not None:
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Make find_l402_challenge L402-only (backward compat, never returns MppChallenge) - Accept str | None in parse_mpp_challenge signature - Make MPP regex order-insensitive (invoice before method works) - Generalize ChallengeParseError message for L402 + MPP - Remove unused ChallengeParseError import from client.py - Use MppChallenge.amount as fallback when invoice has no amount - Add end-to-end MPP client tests (sync + async + cache reuse) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Parse currency into MppChallenge and only use MPP amount as budget fallback when currency is "sat" (or absent); non-sat currencies like "usd" no longer cause incorrect budget enforcement - Change `if amount_sats:` to `if amount_sats is not None:` in both sync and async client paths so amount=0 still records spending logs and budget updates - Add tests for currency parsing, non-sat currency handling, and zero-amount recording Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Constrain MPP challenge parsing to a single Payment segment per RFC 7235, preventing cross-scheme leakage when multiple challenges share a header - Handle multiple WWW-Authenticate headers via get_list() instead of dict() which discards duplicates - Eliminate duplicated auth header formatting in retry paths by reusing the cached credential's authorization_header property Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| sorted_values = sorted( | ||
| www_auth_values, | ||
| key=lambda v: 0 | ||
| if v.lower().lstrip().startswith("l402") |
There was a problem hiding this comment.
The async client uses the same WWW-Authenticate sorting logic that only prioritizes values starting with "l402". Because parse_challenge() supports "LSAT" as well, a response with both Payment ... and LSAT ... could incorrectly select MPP first. Consider prioritizing both "l402" and "lsat" when ordering the header values so L402/LSAT is always preferred when present.
| if v.lower().lstrip().startswith("l402") | |
| if v.lower().lstrip().startswith(("l402", "lsat")) |
| cached = self._cache.put( | ||
| domain=domain, | ||
| path=parsed_url.path, |
There was a problem hiding this comment.
Async path: MPP credentials are cached with macaroon=None, but the cache key groups paths by a prefix (first two segments). For MPP/Payment, this can cause a preimage to be reused and sent on other endpoints under the same prefix, which may be unintended and can leak the preimage. Consider making MPP caching more specific (e.g., include token_type/invoice in the cache key, or avoid prefix grouping when macaroon is None).
| cached = self._cache.put( | |
| domain=domain, | |
| path=parsed_url.path, | |
| # For MPP challenges, include the invoice in the cache key so that | |
| # credentials (and their preimages) are not reused across different | |
| # endpoints that share the same path prefix. | |
| mpp_cache_path = f"{parsed_url.path}|{challenge.invoice}" | |
| cached = self._cache.put( | |
| domain=domain, | |
| path=mpp_cache_path, |
| cached = self._cache.put( | ||
| domain=domain, | ||
| path=parsed_url.path, |
There was a problem hiding this comment.
MPP credentials are cached with macaroon=None, but the credential cache keying groups paths by the first two segments (see CredentialCache._cache_key). That behavior makes sense for broad macaroons, but for MPP it can cause a preimage to be reused (and sent) on other endpoints under the same prefix, potentially leaking preimages and causing incorrect auth attempts. Consider adjusting cache keying for MPP (e.g., include scheme/token_type or invoice, or bypass path-prefix grouping when macaroon is None) so Payment preimages are only reused where intended.
| cached = self._cache.put( | |
| domain=domain, | |
| path=parsed_url.path, | |
| # For MPP, avoid caching under the real request path, because | |
| # CredentialCache groups by the first two path segments. Use a | |
| # synthetic path that includes the invoice so this credential | |
| # is not reused across different endpoints. | |
| mpp_path = f"/__mpp__/{challenge.invoice}{parsed_url.path}" | |
| cached = self._cache.put( | |
| domain=domain, | |
| path=mpp_path, |
Summary
WWW-Authenticate: Paymentheaders (IETF draft-ryan-httpauth-payment)MppChallengefrozen dataclass for MPP challenges (invoice only, no macaroon)find_payment_challenge()tries L402 first, falls back to MPPfind_l402_challengekept as backward-compatible aliasPayment method="lightning", preimage="<hex>"for MPPTest plan
🤖 Generated with Claude Code