Skip to content

tempo request: session-first routing dead-ends on sessionProtocol-v1 session challenges instead of falling back to charge #82

Description

@jake-bp

Summary

When a server's WWW-Authenticate header includes a tempo/session challenge, tempo request unconditionally routes into a session-only payment path, even if the same response also offers tempo/charge challenges the client fully supports. If the session challenge is TIP-1034-legacy (protocol v1 — i.e. methodDetails carries no sessionProtocol marker), the currently-registered session method rejects it and the request fails outright with E_PAYMENT, despite compatible charge challenges being right there in the same header. There's no fallback from the session-only path back to the generic method set.

Environment

  • wallet-cli (tempo launcher) 1.x, tempo-wallet 0.7.0, tempo-request 0.7.0
  • Linux x86_64 (WSL2)
  • Tempo mainnet, chainId 4217
  • mppx 0.8.9 (the payment method library wallet-cli builds on)

Reproduction

Target: a live MPP server on Tempo mainnet that emits 2 tempo/charge challenges plus 1 tempo/session challenge in the same 402, where the session challenge is protocol v1 (no sessionProtocol marker in methodDetails):

curl -s -D - -o /dev/null "https://api.onesource.io/api/chain/block-number?network=ethereum"
HTTP/1.1 402 Payment Required
Www-Authenticate: Payment id="...", realm="api.onesource.io", method="tempo", intent="charge", request="<base64>", expires="...", description="api.onesource.io API call"
Www-Authenticate: Payment id="...", realm="api.onesource.io", method="tempo", intent="charge", request="<base64>", expires="...", description="api.onesource.io API call"
Www-Authenticate: Payment id="...", realm="api.onesource.io", method="tempo", intent="session", request="<base64>", expires="...", description="api.onesource.io API call"

Decoding the session challenge's request field (base64url JSON, no secrets):

{
  "amount": "1000",
  "currency": "0x20C000000000000000000000b9537d11c60E8b50",
  "description": "api.onesource.io API call",
  "methodDetails": {
    "chainId": 4217,
    "escrowContract": "0x33b901018174DDabE4841042ab76ba85D4e24f25",
    "minVoucherDelta": "1000"
  },
  "recipient": "0x19B8e99079A5558ff4460357b0a78e14a7F600B7",
  "suggestedDeposit": "10000"
}

Note there is no sessionProtocol key under methodDetails — this is a v1-style session challenge.

Three probes against this endpoint, all on 0.7.0:

Identity Command Result
Default passkey wallet (P-256 access key, funded 0.72 USDC.e) tempo request <url> Fails: E_PAYMENT: No method found for challenges: tempo.charge, tempo.charge, tempo.session. Available: tempo.session
Funded secp256k1 EOA via --private-key tempo request <url> --private-key <key> --max-spend 0.02 Same failure — identity type is irrelevant to the bug
Fresh, unfunded key via --private-key tempo request <url> --private-key <key> Fails earlier, at balance-check: Insufficient balance for session deposit: available=0 required=0.01 — confirms the client parses the v1 challenge and computes the deposit from suggestedDeposit fine; only the method-matching step rejects it

For comparison, the same wallet against the same endpoint on the last pre-regression release, tempo-wallet/tempo-request 0.4.4, pays successfully via the charge challenge. So the regression window is the 0.6.0 rewrite onward.

Root cause analysis

(All file/line references below are against wallet-cli HEAD 9a94aab, package.json version 0.7.0, and mppx@0.8.9.)

  1. src/commands/request.ts:502-506 — if any session challenge is present in the header, payAndRetryRequest short-circuits into the session-only path before the generic path is ever reached:

    const sessionChallenge = sessionChallengeFromHeader(header);
    if (sessionChallenge)
      return withSessionLock(request.url, () =>
        paySessionAndRetryRequest(paymentRequiredResponse, request, options, sessionChallenge),
      );
  2. src/commands/request.ts:559-567, inside paySessionAndRetryRequest, the Mppx instance is built with only the session method registered:

    const payment = Mppx.create({
      methods: [
        tempoSession({
          ...identity.methodOptions,
          ...(options.maxSpend ? { maxDeposit: options.maxSpend } : {}),
        }),
      ],
      polyfill: false,
    });

    The generic path at src/commands/request.ts:518-521, which registers tempo(methodOptions) (charge-capable) alongside tempo.subscription(...), is never reached once a session challenge is present.

  3. mppx@0.8.9 src/tempo/session/client/Session.ts:56-63 — the modern session method that wallet-cli imports (session as tempoSession from mppx/client) only accepts protocol-v2 challenges:

    canHandleChallenge({ challenge }) {
      return (
        Constants.getMethodDetail(
          challenge.request.methodDetails,
          Constants.MethodDetailKeys.sessionProtocol,
        ) === Constants.SessionProtocols.v2
      )
    },

    mppx also ships a legacy session method (src/tempo/legacy/client/Session.ts:340-350) whose canHandleChallenge accepts undefined or v1 — but wallet-cli never registers it.

  4. mppx@0.8.9 dist/internal/AcceptPayment.js:93 (also dist/client/Mppx.js / dist/client/internal/Fetch.js, same filter) is where challenge-to-method matching happens:

    .filter((method) => method.canHandleChallenge?.({ challenge }) ?? true)

    With only the modern session method registered and a v1-shaped challenge, this filter yields zero candidates for tempo.session, and — because paySessionAndRetryRequest never registers a charge method at all — zero candidates for tempo.charge too. Mppx.createCredential then throws:

    No method found for challenges: tempo.charge, tempo.charge, tempo.session. Available: tempo.session
    

Net effect: the CLI's own routing decision (step 1) removes the charge methods from consideration before mppx's method matcher (step 4) ever gets a chance to fall back to them.

Expected behavior

When the session-only path finds no method able to handle the session challenge (or, more directly, whenever a session challenge is present alongside charge challenges), the CLI should fall back to the generic method set so the charge challenges — which it already supports — get honored instead. Failing that, at minimum it should surface a clear, actionable error (e.g. "server offers only a session protocol v1 challenge, which this client version doesn't support — no compatible payment method found") rather than the current message, which reads as a total capability mismatch even when two compatible charge challenges were on offer.

Impact

Any server still on session protocol v1 that also advertises charge (the expected state during a v1→v2 migration window) hard-fails every tempo request call, with no workaround short of pinning to tempo-wallet/tempo-request 0.4.4. Because the CLI auto-updates, this affects all current users hitting such servers, not just ones who opt in to a new version.

We're also adding session protocol v2 support server-side on our end, so this report is about the missing graceful degradation for the transition period, not a request to keep v1 alive indefinitely.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions