Skip to content

feat: Add MPP client-side support#2

Merged
refined-element merged 4 commits intomainfrom
feat/mpp-client-support
Mar 21, 2026
Merged

feat: Add MPP client-side support#2
refined-element merged 4 commits intomainfrom
feat/mpp-client-support

Conversation

@refined-element
Copy link
Owner

Summary

  • Parse WWW-Authenticate: Payment headers (IETF draft-ryan-httpauth-payment)
  • MppChallenge frozen dataclass for MPP challenges (invoice only, no macaroon)
  • find_payment_challenge() tries L402 first, falls back to MPP
  • find_l402_challenge kept as backward-compatible alias
  • Credential cache supports null macaroon (MPP mode)
  • Auth header: Payment method="lightning", preimage="<hex>" for MPP

Test plan

  • All 122 tests pass (21 new MPP tests)
  • Verify L402-only servers still work (regression)

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings March 21, 2026 05:31
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(), and find_payment_challenge() (L402 preferred, MPP fallback); keeps find_l402_challenge as 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

  • ChallengeParseError is 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 amount even when the BOLT11 invoice is zero-amount. Right now amount_sats is derived only from challenge.invoice, so budget checks and spending log recording will be skipped when extract_amount_sats() returns None even if MppChallenge.amount is present. Consider using the parsed MPP amount (and validating currency, if applicable) as a fallback for amount_sats when 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>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@refined-element refined-element requested a review from Copilot March 21, 2026 06:05
@refined-element refined-element merged commit 1cdb2b2 into main Mar 21, 2026
4 checks passed
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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")
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if v.lower().lstrip().startswith("l402")
if v.lower().lstrip().startswith(("l402", "lsat"))

Copilot uses AI. Check for mistakes.
Comment on lines +336 to +338
cached = self._cache.put(
domain=domain,
path=parsed_url.path,
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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,

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +163
cached = self._cache.put(
domain=domain,
path=parsed_url.path,
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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,

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants