Skip to content

v0.15.0

Choose a tag to compare

@tempo-github-sts tempo-github-sts released this 06 Oct 15:14
· 20 commits to main since this release
Immutable release. Only release title and notes can be modified.
6e500ad

Minor Changes

  • Fixed the direct server verification API accepting a credential on a route it was not paid for. Mpp::verify_credential and compose_verify only check that the handler issued the challenge, so on a handler serving several prices a credential for the cheapest route unlocked all of them. Added Mpp::verify_charge and Mpp::stripe_verify_charge (plus _with_options variants), which take the amount the route charges and reject credentials issued for anything else, Mpp::expected_charge_request and Mpp::stripe_expected_charge_request for the *_with_expected_request methods, and compose_verify_with_expected_requests. Mpp::verify_credential, Mpp::verify_credential_with_body and compose_verify are deprecated. To migrate, replace mpp.verify_credential(&credential) with mpp.verify_charge(&credential, amount), passing the amount the route gives to charge(). (by @matthiasSeitz, #548)
  • Fixed Mpp::create accepting .fee_payer(true) with neither a fee payer signer nor a relay: every challenge advertised feePayer: true and every sponsored credential was then rejected. This configuration now fails with InvalidConfig. To migrate, add .fee_payer_signer(...) or .relay(...), or drop .fee_payer(true). (by @matthiasSeitz, #548)
  • Fixed the credential's challenge echo dropping description, and credentials whose echoed opaque uses the legacy object form sent by older mppx clients being rejected. ChallengeEcho has a new description field that PaymentChallenge::to_echo fills in, and an object-shaped opaque is normalized to its base64url string. Code that builds ChallengeEcho with a struct literal must add description: None (or use to_echo). (by @matthiasSeitz, #548)
  • PaymentMiddleware now reports payment failures as HttpError, the error Fetch returns, inside reqwest_middleware::Error::Middleware. They used to be ad-hoc anyhow messages that callers could not match on. Code that downcast the middleware error to MppError should downcast to HttpError and match HttpError::Payment. The messages, including the error text of payment.failed events, now use the HttpError wording. (by @matthiasSeitz, #548)
  • Fixed weak HMAC secrets being accepted: Mpp::create and Mpp::create_stripe now return InvalidConfig when MPP_SECRET_KEY or .secret_key(...) is shorter than 32 bytes, as mppx does. To migrate, generate a key with openssl rand -base64 32 and set it as MPP_SECRET_KEY; challenges issued under the old key no longer verify, so clients holding one are challenged again. Mpp::new and the challenge helpers cannot fail and do not check the length. (by @matthiasSeitz, #548)
  • Fixed sse::serve and ws_session repeating the need-voucher event on every poll tick and channel write while the balance was exhausted. It is now sent once per exhaustion, as mppx does, so clients no longer answer with duplicate vouchers. requiredCumulative now also clears the server's minimum voucher delta, so a stream no longer stalls when that delta is larger than the tick cost. ServeOptions and WsSessionOptions have a new required min_voucher_delta field: set it to the session method's SessionMethodConfig::min_voucher_delta (0 if none is enforced). (by @matthiasSeitz, #548)
  • Fixed the problem types reported for payment failures. Malformed credentials now map to malformed-credential (402) instead of internal-payment-error or invalid-challenge, challenge-binding failures (unknown or tampered challenge id, missing or mismatched bound fields) map to invalid-challenge instead of verification-failed, and RPC, store and upstream API failures during verification map to internal-payment-error (500) instead of verification-failed (402). internal-payment-error problems now carry a fixed detail instead of the underlying error text, and ErrorCode::spec_code() returns the problem type that is actually reported.
  • Migration: ErrorCode is now #[non_exhaustive] and gained InvalidChallenge and Internal, and MppError gained Internal and InvalidReceipt, so exhaustive matches need a wildcard arm. Mpp::verify_* reports binding failures with ErrorCode::InvalidChallenge instead of ErrorCode::CredentialMismatch, parse_authorization fails with MppError::MalformedCredential, and parse_receipt fails with MppError::InvalidReceipt instead of MppError::InvalidChallenge. (by @matthiasSeitz, #548)
  • Fixed the proxy's OpenAPI discovery document omitting the payment method. proxy::PaidEndpoint has a new method field, written as method in each x-payment-info offer (REQUIRED by the discovery spec) and in the payment object of /services.
  • Migration: PaidEndpoint is now #[non_exhaustive], so it can no longer be built with a struct literal. Use PaidEndpoint::new(method, intent, amount) and the with_decimals, with_currency, with_unit_type and with_description setters, e.g. PaidEndpoint::new("tempo", "charge", "50000").with_decimals(6). (by @matthiasSeitz, #548)
  • Fixed the proxy injecting upstream headers in a random order. proxy::Service::headers was a HashMap, so the order changed from run to run, and bearer(..) followed by header("authorization", ..) kept both entries and let either one win. Headers are now injected in the order they were added, and adding a name again (in any letter case) replaces the earlier value.
  • Migration: Service::headers is now a Vec<(String, String)>. Replace service.headers.get(name) with service.headers.iter().find(|(n, _)| n.eq_ignore_ascii_case(name)). (by @matthiasSeitz, #548)
  • Fixed TempoSessionProvider enforcing max_deposit only when a voucher needed a top-up. A channel whose deposit is above the limit, e.g. one opened with a higher limit, restored from the channel store or recovered from the server, was spent up to its full deposit. Every voucher signed by pay is now checked, so such a payment fails with exceeds local max_deposit; raise with_max_deposit to keep spending from that channel. (by @matthiasSeitz, #548)
  • Fixed Tempo session actions returning a base receipt that session clients such as mppx reject. SessionMethod now returns spec-shaped session receipts: intent, challengeId, channelId, acceptedCumulative, spent, units and, for open, topUp and close, txHash are carried as extension fields of the Receipt, so its Payment-Receipt header parses as a SessionReceipt. reference is now the channel ID for every action; code that read the open or close transaction hash from reference should read txHash instead. SessionReceipt::to_base_receipt keeps the session fields as well. (by @matthiasSeitz, #548)
  • Fixed the Stripe server method ignoring externalId. A charge whose request carries an externalId now requires the credential payload to echo the same value and rejects it otherwise before creating the PaymentIntent, and the receipt carries the request's externalId. Clients must echo a request-bound externalId: mppx does, and StripeProvider does in releases after 0.14.0. PaymentIntent metadata values are now capped at Stripe's 500 characters, so a long client-supplied source can no longer make the request fail, and the method reuses one HTTP client instead of building one per verification. (by @matthiasSeitz, #548)
  • Fixed TempoProvider signing for whatever chain a challenge named, which let a server move a client configured with a testnet RPC onto mainnet funds. An unpinned provider now pays only on the chain its RPC reports (one cached eth_chainId request on the first payment), signs on that chain when the challenge omits chainId, and rejects any other chain with ChainIdMismatch. Call with_expected_chain_id to pin the chain without the lookup, e.g. when the RPC is not reachable while signing. Tempo charges also fail fast when a challenge's supportedModes does not list pull instead of sending a transaction credential the server rejects, and the transaction's validBefore is capped at the challenge expires. (by @matthiasSeitz, #548)
  • Removed dependencies the library never used: rand (pulled in by evm and utils), uuid (pulled in by tempo, only used in tests) and tokio-tungstenite (pulled in by ws). The sqlite feature now enables client, where the SQLite channel store lives; before, --features sqlite alone compiled rusqlite without exposing the store.
  • Migration: the implicit rand and uuid cargo features are gone. If you enabled mpp/rand or mpp/uuid, drop them; they did not change the public API. (by @matthiasSeitz, #548)
  • Fixed challenge fields being validated differently depending on how they arrived. Challenges parsed from WWW-Authenticate, challenges deserialized from JSON (MCP) and the challenge echoed in a credential now go through one validator: intent must match the intent grammar and is no longer lowercased by the header parser, request must be a base64url JSON object, opaque must be base64url, digest must be sha-256= followed by base64 (optionally wrapped in colons), and expires must be RFC 3339. PaymentChallenge's Deserialize also rejects an empty id and a header other than Payment-Authorization. With these checks no bound field can contain the | that separates the fields in the challenge id input.
  • Migration: format_www_authenticate refuses the same values, so a server using an intent name with characters other than letters, digits, - and _, or a request that is not a JSON object, has to change it. (by @matthiasSeitz, #548)

Patch Changes

  • Fixed accept_payment::parse reading Accept-Payment weights loosely. The q parameter name is now matched case-insensitively, so Q=0 is an opt-out instead of being ignored and treated as q=1. Values that are not HTTP qvalues (.5, 1e-1, +0.5) and parameters without a value (;q) are rejected, and method tokens may contain the : and _ that method names allow. (by @matthiasSeitz, #548)
  • Fixed FileStore::put overwriting the key file in place, which let concurrent readers observe an empty or truncated value and could leave a corrupt file behind after a crash. The value is now written to a temp file and renamed over the key, so readers always see a complete value. (by @matthiasSeitz, #548)
  • verify_credential_with_expected_request and the other expected-request verifiers now reject a credential whose externalId differs from the expected request, so a payment issued for one order cannot settle another order of the same price. Callers that issue challenges with an external_id must pass the same value in the expected request. (by @matthiasSeitz, #548)
  • Fixed PaymentBodyLayer reading credentials from Authorization only. It now reads the header selected by the verifier, so servers created with requires_auth can verify body-bound payments sent in Payment-Authorization. (by @matthiasSeitz, #548)
  • Fixed body-bound payment front-ends buffering request bodies of any size before payment. The axum MppChargeWithBody extractor now honours DefaultBodyLimit (2 MiB unless configured), and the tower PaymentBodyLayer caps the body at 2 MiB, configurable with max_body_bytes. Oversized bodies are rejected with 413, and body read errors in MppChargeWithBody now return 400 instead of 500. (by @matthiasSeitz, #548)
  • Parse challenge auth-param names case-insensitively, so Realm= or ID= are accepted and case-variant duplicates such as id= and ID= are rejected. (by @matthiasSeitz, #548)
  • Fixed Mpp::charge_challenge and Mpp::charge_challenge_with_options issuing challenges without chainId on handlers built with Mpp::create, which the same handler then rejected with credential chainId None does not match. They now add the handler's chain ID like charge() does; a chainId already present in the request is kept. (by @matthiasSeitz, #548)
  • Documented that TempoChargeMethod::new configures no replay store, and updated the advanced API examples to add one with with_store. Without a store, a hash or proof credential is accepted again until its challenge expires. (by @matthiasSeitz, #548)
  • Exported client::stripe::CreateTokenParams, the argument of the StripeProvider::new callback, which could not be named before. Fixed the broken intra-doc links in the API docs. (by @matthiasSeitz, #548)
  • Fixed PaymentMiddleware and Fetch answering the same 402 differently. The middleware now calls the provider's prepare_http_payment_challenge hook, reopens a stale session once after 410 Gone instead of returning the 410, and returns a 402 that carries no challenge after a payment instead of failing. Fetch now repeats Accept-Payment on retries and sends requests whose body cannot be cloned, failing with HttpError::CloneFailed only when such a request is answered with a 402; a request builder that holds an error reports that error instead of CloneFailed. A session that is still gone after the retry is invalidated again rather than rolled back. (by @matthiasSeitz, #548)
  • Added mpp::client::PendingPayment, the guard that commits, rolls back or abandons a created credential. It was only available as mpp::mcp::client::PendingPayment, which is now a re-export, and gained new and invalidate. (by @matthiasSeitz, #548)
  • Challenge and credential headers are now read by one tokenizer, so parse_www_authenticate, parse_www_authenticate_all, extract_payment_scheme and PaymentProtocol::detect agree on where a Payment scheme starts. An auth-param named Payment no longer splits a challenge in the list parser, extract_payment_scheme ignores text inside quoted-strings, empty list elements before a challenge are skipped by every parser, and only ASCII whitespace is skipped around the scheme. (by @matthiasSeitz, #548)
  • Fixed the challenge parser rejecting a quoted request parameter of exactly 16384 bytes as too long. The limit is now the same for quoted and unquoted values, and the same as in mppx. (by @matthiasSeitz, #548)
  • Fixed the proxy's OpenAPI discovery document describing paths clients cannot call. generate_openapi now prefixes paths with the configured base_path, writes :param route segments as {param} templates with matching path parameters, and emits x-payment-info in the multi-offer form ({"offers": [...]}) the discovery spec recommends instead of the flat single-offer shorthand. (by @matthiasSeitz, #548)
  • Fixed parse_www_authenticate dropping an empty unquoted auth-param at the end of a header (description=). It is now kept as an empty value, as it already was when followed by a comma, and as mppx does. (by @matthiasSeitz, #548)
  • Fixed format_www_authenticate emitting Latin-1 and control characters raw, which produced header values that clients could not read or that were not valid header values at all. Everything outside printable ASCII is now escaped as \uXXXX. Challenges that the parser would reject (empty id, invalid method name, request that is not base64url JSON) are refused, and HttpTransport::respond_challenge returns a 500 response instead of panicking or sending a bare Payment challenge when a challenge cannot be formatted. (by @matthiasSeitz, #548)
  • Fixed the Tempo fee payer co-signing sponsored transactions that spend its fee budget on work other than the charge. A sponsored transaction with an authorization list is now rejected, and an approve/swap prefix must approve exactly the swap's maxAmountIn and buy exactly the payment amount of the payment currency, matching mppx. Key authorizations stay sponsored by default; ChargeMethod::with_fee_payer_allow_key_authorization(false) rejects them. (by @matthiasSeitz, #548)
  • Fixed FileStore::put_if_absent leaving an empty or truncated file behind when the write failed, which made the key look used forever. The value is now written to a temp file and hard-linked into place, so the key only exists once its contents are complete. (by @matthiasSeitz, #548)
  • Fixed the build with --no-default-features --features client,stripe, which failed because protocol::methods was only compiled with server or tempo. The Stripe client types are now available with stripe alone. (by @matthiasSeitz, #548)
  • Fixed PaymentLayer::charge and PaymentBodyLayer::charge being unusable from other crates: their return type named a private verifier type, and the layers were not Clone for non-Clone verifiers, so axum::Router::layer and route_layer rejected them. (by @matthiasSeitz, #548)
  • Report errors that are not payment problems as the core spec's internal-payment-error with status 500, instead of the undefined internal-error type with status 402. (by @matthiasSeitz, #548)
  • Fixed clients dropping WWW-Authenticate values that contain non-ASCII bytes, so challenges from servers that send Latin-1 text raw (as mppx does) could not be paid. Received challenge headers are now decoded as ISO-8859-1 through the new parse_www_authenticate_all_bytes. (by @matthiasSeitz, #548)
  • Fixed MCP payment errors to match the JSON-RPC transport spec and mppx: challenges are serialized with request as a native JSON object instead of a base64url string, verification failures use -32043 (which is_payment_required now accepts), and one malformed challenge no longer discards the valid alternatives. attach_credential and attach_receipt no longer panic on non-object input; the new try_attach_credential and try_attach_receipt report the failure. (by @matthiasSeitz, #548)
  • Fixed PaymentMiddleware sending the paid retry to the original request URL after a same-origin redirect. The credential now goes to the final URL that issued the challenge, matching Fetch. (by @matthiasSeitz, #548)
  • Fixed MultiProvider rejecting challenges for providers that send no Accept-Payment header, such as a Stripe-only offer failing with NoSupportedChallenge next to a Tempo session provider. TempoProvider, TempoSessionProvider, TempoAccountsProvider and StripeProvider now advertise the methods they support, and MultiProvider merges its children's headers, sending none if a child does not advertise. (by @matthiasSeitz, #548)
  • Fixed Tempo charge receipts for hash credentials echoing the transaction hash as the client spelled it. A hash sent in upper case or without the 0x prefix was verified but returned unchanged as the receipt reference; the reference is now always the lower-case 0x-prefixed hash, as it already was for transaction credentials. (by @matthiasSeitz, #548)
  • Fixed session channel IDs being matched as raw strings, so a credential that spelled the channel ID with uppercase hex was rejected with channel-not-found. The session method, the built-in channel stores and the SSE/WebSocket session helpers now key channels by the lowercase ID. (by @matthiasSeitz, #548)
  • Fixed Fetch and PaymentMiddleware failing with NoSupportedChallenge when a paid request was answered with a 402 whose challenges the provider cannot pay. That response is now returned like a 402 without a challenge, as mppx does, so the caller can read its problem details. An unpayable first 402 is still an error. (by @matthiasSeitz, #548)
  • Fixed the Tempo session client trusting any escrow contract advertised by the server, which let a malicious server have the client approve and deposit into a contract of its choosing. Only the canonical escrow for the chain (or the one set with with_escrow_contract) is accepted now, and a challenge's sessionProtocol must match the escrow it is answered on. Use TempoSessionProvider::with_allow_custom_escrow(true) to accept server-chosen escrows, e.g. for local deployments. (by @matthiasSeitz, #548)
  • Fixed the proxy helpers forwarding the client's Payment-Authorization credential to the upstream service; scrub_request_headers now strips it like Authorization. Fixed ProxyConfig::match_route letting a :param segment match ., .. (also %2e-encoded) or a segment containing a backslash, which reached other upstream paths once the forwarding client normalised the URL, and base_path matching without a segment boundary (/api matched /apiopenai/...). (by @matthiasSeitz, #548)
  • The prebuilt OpenAI proxy service now strips caller-supplied OpenAI-Organization and OpenAI-Project headers, and the Stripe service strips Stripe-Context in addition to Stripe-Account. A caller can no longer pick the organization, project or account the operator's key acts on. Headers set with ServiceBuilder::header are still sent. (by @matthiasSeitz, #548)
  • Fixed parse_www_authenticate_all splitting challenges on , Payment text inside quoted parameter values, which broke valid challenges and let a Payment challenge be read out of another scheme's quoted parameter. Scheme detection now also accepts a tab after Payment, auth-params accept whitespace around =, and a challenge's parameters end at the next auth scheme. (by @matthiasSeitz, #548)
  • Fixed realm auto-detection reading HOST and HOSTNAME. Container runtimes set these per replica, so each replica signed challenges with its own realm and rejected credentials for challenges issued by the others. The variable list now matches mppx (MPP_REALM, FLY_APP_NAME, HEROKU_APP_NAME, RAILWAY_PUBLIC_DOMAIN, RENDER_EXTERNAL_HOSTNAME, VERCEL_URL, WEBSITE_HOSTNAME). Deployments that relied on HOSTNAME now fall back to "MPP Payment" unless one of those is set; set MPP_REALM or call .realm(...) to keep the previous value. (by @matthiasSeitz, #548)
  • Fixed the session server accepting voucher signatures the escrow contract cannot settle: high-s signatures and signatures with trailing bytes (such as the Tempo envelope magic trailer) are now rejected with invalid-signature. Vouchers must be a 65-byte r || s || v signature with v of 27 or 28, or a 64-byte EIP-2098 compact signature, and are stored in the 65-byte form. (by @matthiasSeitz, #548)
  • Fixed the challenge header parameter accepting any HTTP field name, which let a server steer the client's credential into headers such as Cookie or Proxy-Authorization. Only Payment-Authorization is accepted now: challenges and credentials advertising any other field are rejected when parsed, clients no longer pay them, and format_www_authenticate refuses to emit them. PaymentChallenge::with_header and with_secret_key_full ignore other values. (by @matthiasSeitz, #548)
  • Fixed the axum extractors and tower payment layers discarding why a credential was rejected. A credential that fails because of a server-side fault (RPC, store, upstream API) is now answered with 500 and no fresh challenge instead of 402 with one, so auto-paying clients no longer pay twice. The axum extractors answer every rejected credential with an application/problem+json body carrying the problem type and challenge id, returned as the new MppChargeRejection::Problem instead of VerificationFailed/VerificationFailedOffers. A challenge that cannot be sent as a header value is answered with 500 instead of an id-less WWW-Authenticate: Payment, and 500 responses no longer contain internal error text.
  • Custom ChargeChallenger and PaymentVerifier implementations keep working and report every failure as verification-failed; implement the new ChargeChallenger::verify_payment_for_route and PaymentVerifier::verify_credential to report precise problems. (by @matthiasSeitz, #548)
  • Fixed session challenges advertising feePayer: true and a machine-token settlement route whenever the handler had fee sponsorship or machine tokens enabled for charges. The Tempo session method can honour neither, so every session open or top-up from a client that followed the advertisement failed. Session challenges now only advertise them when the session method opts in through the new SessionMethod::supports_fee_payer and SessionMethod::supports_machine_tokens hooks (both default to false); charge challenges are unchanged. (by @matthiasSeitz, #548)
  • Fixed sse::serve and ws_session polling the application generator before looking at the channel, which started the first unit of work for channels that were exhausted, closed or missing. The generator is now first polled once the channel can pay for one tick: until then the stream asks for a voucher and waits, and it ends without polling the generator when the channel is closed or missing. (by @matthiasSeitz, #548)
  • Fixed session close settling for less than was spent when units were deducted while the close was in flight. The close amount is now re-checked against the current spent in the same atomic update that marks the channel closing, and a close that no longer covers it is rejected. (by @matthiasSeitz, #548)
  • Fixed TempoSessionProvider::close forgetting the channel on any successful response. A 2xx without a Payment-Receipt no longer removes the channel from memory and the channel store, so a close the server did not confirm can be retried instead of leaving a funded channel untracked. (by @matthiasSeitz, #548)
  • Fixed session close reporting success without settling on-chain. A close whose transaction reverted is no longer recorded as finalized, and a SessionMethod without a close signer now rejects close instead of finalizing the channel in the store only. A failed close also no longer leaves the channel stuck in closing. (by @matthiasSeitz, #548)
  • Fixed session close rejecting a zero-amount close of a channel that was opened but never used, which left the deposit locked until a forced close. A funded channel with nothing spent and nothing settled on-chain can now be closed at 0, refunding the payer. A close at or below a non-zero settled amount, or above the deposit, is still rejected. (by @matthiasSeitz, #548)
  • Fixed the Tempo session client forgetting an established channel whenever a voucher was answered without a receipt (any 402, 5xx or 429), which opened a second channel on the next request and stranded the first deposit. TempoSessionProvider::rollback_payment now only discards a channel whose open was never accepted. A channel is forgotten when the server answers 410 Gone, reported through the new PaymentProvider::invalidate_payment hook, which defaults to rollback_payment. (by @matthiasSeitz, #548)
  • Fixed TempoSessionProvider failing to open a channel on a legacy escrow contract with native MPP channel is missing its descriptor, a regression from 0.12.0. The failed open also left the channel tracked as open although it was never sent, so the next payment signed a voucher for a channel that does not exist. A failed open no longer leaves anything behind, and rolling back or invalidating a legacy channel no longer fails either. (by @matthiasSeitz, #548)
  • Fixed TempoSessionProvider resuming a legacy channel suggested by the server without checking that the remaining deposit covers the request, which signed a voucher above the deposit, and panicking in debug builds when the settled amount plus the request amount overflowed. Such a channel is no longer resumed and a new one is opened instead. (by @matthiasSeitz, #548)
  • Fixed sse::serve and ws_session polling forever when the channel is missing or the store fails. Only an insufficient balance now waits for a voucher; any other deduction error emits the final session receipt (if the channel is still readable) and stops. (by @matthiasSeitz, #548)
  • Fixed TempoSessionProvider::send_voucher and close failing with 402 once the challenge of the last payment had expired, which is five minutes by default and shorter than many streams. Both now answer the fresh session challenge of a 402 response once, as top-ups already did. A challenge for a different payee, token, escrow or chain is refused. (by @matthiasSeitz, #548)
  • Fixed the Tempo session method overwriting its stored channel with an older on-chain read. A voucher that awaited the chain while the channel was closed stored finalized: false again and was accepted, and a lagging node or two top-ups finishing out of order lowered the recorded deposit so vouchers within the real deposit were rejected. finalized now stays set and the recorded deposit only grows. (by @matthiasSeitz, #548)
  • Fixed TempoSessionProvider::voucher_credential_with_top_up and voucher_credential_with_top_up_for_challenge sizing the top-up from the deposit the server reports while checking the voucher against the provider's own, possibly older, deposit. After a top-up the provider did not make itself, a voucher within the real deposit failed with voucher cumulative amount exceeds channel deposit, and a needed top-up was too small. The larger of the two deposits is now used for both, and the local deposit is raised to it. (by @matthiasSeitz, #548)
  • Fixed session open accepting a channel that cannot pay for a single unit. The open transaction's deposit, and the channel's available balance once it is on-chain, must now cover the challenge amount; an open below it is rejected with insufficient-balance before the transaction is broadcast. (by @matthiasSeitz, #548)
  • Fixed a session open for a channel the server already records lowering the recorded deposit when its on-chain read was older than the record, e.g. from a lagging node right after a top-up. The deposit only grows, as it already did for vouchers and top-ups. (by @matthiasSeitz, #548)
  • Fixed TempoSessionProvider::pay waiting forever when an earlier payment was never committed, rolled back or abandoned, as in the documented example. The wait for the payment lock now ends when the challenge expires and fails with PaymentExpired, and the example shows how to settle a credential before paying again. (by @matthiasSeitz, #548)
  • Fixed TempoSessionProvider::send_voucher failing with voucher cumulative amount exceeds channel deposit when a payment-need-voucher event asked for more than the channel holds although max_deposit allowed a larger deposit. With with_max_deposit set it now tops the channel up first, as the spec requires and as voucher_credential_with_top_up already did, sized by top_up_amount, the server's suggestedDeposit and max_deposit. Without max_deposit the channel deposit stays the limit and the error is unchanged. (by @matthiasSeitz, #548)
  • Serialize the session credential settlementRoute field in camelCase like every other payload field. The legacy settlement_route spelling is still accepted when parsing. (by @matthiasSeitz, #548)
  • Fixed the Tempo session method accepting a leading + in the cumulativeAmount and additionalDeposit of session credentials. These amounts must be ASCII digits only, like the challenge amounts. (by @matthiasSeitz, #548)
  • Fixed TempoSessionProvider::with_top_up_amount being ignored whenever the server's suggestedDeposit was larger, so an automatic top-up deposited the server's amount instead of the configured one. The configured amount now takes precedence; a top-up still covers at least the shortfall and stays within max_deposit. (by @matthiasSeitz, #548)
  • Fixed Tempo session servers broadcasting client transactions before validating the credential. An open whose voucher has an invalid signature or exceeds the deposit is now rejected before its transaction is sent, instead of leaving a funded channel the server never recorded. A topUp transaction is now decoded and must be a Tempo transaction calling topUp on the escrow for the credential's channel and additionalDeposit; previously any signed transaction was relayed. (by @matthiasSeitz, #548)
  • Fixed the SignOptions docs, which described defaults the Tempo charge client does not use: nonce_key defaults to U256::MAX (expiring nonce) rather than U256::ZERO, nonce to 0 rather than a fetched pending nonce, the gas fees to static values rather than the latest base fee, and valid_before applies to every charge. (by @matthiasSeitz, #548)
  • Fixed the Tempo fee payer broadcasting sponsored transactions without any pre-broadcast check when the node does not expose tempo_simulateV1, which includes the public rpc.tempo.xyz and rpc.moderato.tempo.xyz endpoints. The transaction's calls are now simulated with eth_call from the sender instead, so a transaction that would revert is rejected before the sponsor pays for it. (by @matthiasSeitz, #548)
  • Fixed SSE message events for values containing line breaks: the value was truncated at the first newline while still being charged, and streamed content could inject payment-need-voucher or payment-receipt events. format_message_event now emits one data: line per line, and parse_event accepts CRLF/CR line endings and data: fields without a space after the colon. (by @matthiasSeitz, #548)
  • Fixed amount parsing accepting forms the protocol does not allow, and the u128 and U256 parsers disagreeing about them. ChargeRequest::parse_amount and SessionRequest::parse_amount accepted a leading +, and evm::parse_amount (used by amount_u256, parse_amount_u256 and split amounts) read 0x10 as 16, 1_000 as 1000 and the empty string as 0. All of them now accept ASCII digits only. (by @matthiasSeitz, #548)
  • Fixed the Stripe client not echoing a request-bound externalId in its credential, which made spec-compliant servers such as mppx reject the payment. The challenge's externalId now takes precedence over the one returned by create_token, and a challenge with a missing or empty methodDetails.networkId is rejected instead of passing an empty network ID to the callback. (by @matthiasSeitz, #548)
  • Fixed the Stripe charge method reporting a PaymentIntent in requires_action (e.g. 3D Secure) as verification-failed. It now fails with the new ErrorCode::PaymentActionRequired, which is reported as the payment-action-required problem like in mppx. (by @matthiasSeitz, #548)
  • Fixed the Stripe ChargeMethod::verify ignoring its request argument. It created the PaymentIntent from the request echoed by the credential, so a caller that used the method directly, or Mpp::verify/Mpp::broadcast with its own request, charged whatever amount and currency the credential named. The PaymentIntent amount, currency, metadata and externalId binding now come from the request passed in, as in mppx. Mpp::stripe_verify_charge and the other credential entry points pass the echoed request after checking it and behave as before. (by @matthiasSeitz, #548)
  • Fixed the Tempo charge method never checking credential.source on transaction (pull) credentials, so ChargeValidation::source reported whatever payer the client claimed. A source that is not a did:pkh:eip155 DID for the challenge chain naming the recovered transaction sender is rejected now, before fee-payer co-signing and broadcast. When source is omitted, ChargeValidation::source reports the recovered sender. (by @matthiasSeitz, #548)
  • Fixed the Tempo charge server ignoring methodDetails.supportedModes: a credential whose mode the challenge does not advertise (a hash credential for a pull-only challenge, or a transaction credential for a push-only one) is now rejected. ChargeOptions::supported_modes values other than "pull"/"push" are rejected when the challenge is created. (by @matthiasSeitz, #548)
  • Fixed Tempo hash-credential verification counting the Transfer and TransferWithMemo logs of a single transferWithMemo call as two transfers, which let one transfer satisfy two expected transfers of a split payment. The pair is now matched as a single transfer. (by @matthiasSeitz, #548)
  • Fixed zero-amount Tempo proof challenges not being single-use: the replay marker was keyed by the proof signature, so a second account or a second signature could reuse the same challenge. The marker is now keyed by challenge id (mpp:charge:proof:{id}), as in mppx. (by @matthiasSeitz, #548)
  • Fixed Tempo charge verification accepting a payment when any matched transfer carried a memo bound to the challenge, even if another matched transfer carried an MPP attribution memo for a different challenge or server. Such conflicting attribution is now rejected, so one transaction cannot be credited to two challenges. (by @matthiasSeitz, #548)
  • Fixed ws_session looping on needVoucher forever once the channel is finalized or closing. It now emits the final session receipt and returns, matching the SSE session loop. (by @matthiasSeitz, #548)
  • Fixed in-band WebSocket vouchers being verified without telling the client the result, so a rejected voucher left the session waiting forever. The new ws_session::process_vouchers takes a sink for the socket and answers every credential frame with a receipt frame, or with an error frame after which it returns the verification error. process_incoming_vouchers is deprecated in its favour. (by @matthiasSeitz, #548)
What's Changed
  • fix(middleware): read body-layer credentials from the verifier header by @kriss39 in #419
  • fix(server): stop ws_session on closed channels instead of looping on needVoucher by @Bruce039 in #425
  • fix(server): stop session loops on non-balance deduction errors by @mattsse in #443
  • fix: report non-payment errors as internal-payment-error by @wodesiku in #430
  • fix(store): avoid poisoning FileStore keys on partial write by @kutluhaneth46 in #437
  • fix: parse challenge auth-param names case-insensitively by @pucedoteth in #436
  • fix(tempo): serialize session settlementRoute in camelCase by @0xShadowX in #426
  • fix(alloy): settle app-ws payment on handshake timeout or cancel by @mattsse in #444
  • fix(core): make challenge splitting quote-aware by @mattsse in #445
  • fix(sse): frame multi-line message events safely by @mattsse in #446
  • fix(mcp): emit spec-shaped JSON-RPC payment errors by @mattsse in #447
  • fix(tempo): merge Transfer/TransferWithMemo twin logs by @mattsse in #448
  • fix(alloy): bound payments per MppWsConnect connection by @mattsse in #449
  • fix(core): escape non-ASCII and control chars in challenges by @mattsse in #450
  • fix(server): make tower payment layers usable downstream by @mattsse in #451
  • fix(tempo): reject non-canonical voucher signatures by @mattsse in #452
  • fix(core): only accept Payment-Authorization as credential header by @mattsse in #453
  • fix(client): advertise every MultiProvider child in Accept-Payment by @mattsse in #454
  • fix(tempo): enforce supportedModes on charge credentials by @mattsse in #455
  • fix(tempo): reject conflicting MPP attribution memos by @mattsse in #456
  • fix(store): make FileStore::put atomic by @mattsse in #458
  • fix(tempo): make zero-amount proof challenges single-use by @mattsse in #459
  • fix(tempo): require on-chain settlement for session close by @mattsse in #460
  • fix(tempo): pin session escrow on the client by @mattsse in #461
  • fix(build): compile client,stripe without server or tempo by @mattsse in #462
  • fix(stripe): echo request externalId and require networkId by @mattsse in #463
  • fix(tempo): normalise session channel ids by @mattsse in #464
  • fix(tempo): gate session challenge capabilities on the method by @mattsse in #465
  • fix(server): bound body reads in body-bound payment front-ends by @mattsse in #457
  • test(fuzz): invariant-based fuzz targets and a CI smoke job by @mattsse in #502
  • fix(tempo): return session receipts from session actions by @mattsse in #495
  • fix: accept only ASCII digits in protocol amounts by @mattsse in #485
  • fix(ws): report in-band voucher results to the client by @mattsse in #480
  • fix(stripe): bind externalId on the server by @mattsse in #483
  • fix(tempo): return the canonical hash as receipt reference by @mattsse in #484
  • fix(core): parse Accept-Payment weights as HTTP qvalues by @mattsse in #488
  • fix(server): require a 32-byte HMAC secret key by @mattsse in #491
  • fix(server): pin chainId in Mpp::charge_challenge by @mattsse in #492
  • fix(core): echo description and accept legacy object opaque by @mattsse in #490
  • fix(proxy): scrub Payment-Authorization and harden route matching by @mattsse in #481
  • ci: ignore the PR's npm config in the changelog job by @mattsse in #474
  • fix(tempo): keep session channel after a rejected voucher by @mattsse in #499
  • fix(proxy): emit callable paths and offers in OpenAPI discovery by @mattsse in #482
  • test: fix test build after merging #483, #490 and #491 by @mattsse in #503
  • fix(alloy): bound MppHttpTransport response bodies by @mattsse in #478
  • fix(server): reject fee_payer(true) without a sponsor by @mattsse in #493
  • fix(tempo): check chain, modes and expiry before signing charges by @mattsse in #470
  • fix(tempo): keep session channel when close has no receipt by @mattsse in #486
  • fix(session): emit need-voucher once and honour min voucher delta by @mattsse in #479
  • fix(error): correct problem-type mapping for payment failures by @mattsse in #496
  • test(fuzz): follow the Accept-Payment and amount grammar changes by @mattsse in #504
  • fix(client): decode WWW-Authenticate as Latin-1 by @mattsse in #489
  • fix(tempo): bind sponsored swap prefix and reject authorization lists by @mattsse in #473
  • fix(server): stop reading HOST/HOSTNAME for realm detection by @mattsse in #466
  • docs(tempo): fix SignOptions defaults by @mattsse in #471
  • fix(tempo): simulate sponsored txs with eth_call without simulateV1 by @mattsse in #475
  • fix(tempo): bind transaction credential source to sender by @mattsse in #477
  • fix(tempo): validate session open and topUp before broadcast by @mattsse in #494
  • fix(client): retry paid middleware requests on the final URL by @mattsse in #467
  • fix(proxy): strip tenant headers in OpenAI and Stripe services by @mattsse in #468
  • docs(tempo): add a replay store to the advanced API examples by @mattsse in #472
  • fix(tempo): re-check close amount against spent under the lock by @mattsse in #476
  • fix(tempo): stop pay() waiting forever on an unsettled payment by @mattsse in #500
  • fix(server): answer rejected credentials per spec in axum and tower by @mattsse in #497
  • fix(session): check the channel before first polling the generator by @mattsse in #501
  • fix(server): bind direct credential verification to the route by @mattsse in #487
  • fix(server): bind externalId in expected-request verification by @mattsse in #469
  • fix(core): keep an empty auth-param at the end of a challenge by @mattsse in #505
  • test(fuzz): generate well-formed challenges in the structured targets by @mattsse in #506
  • fix(tempo): allow closing an untouched channel at zero by @mattsse in #507
  • fix(tempo): keep finalized and deposit when refreshing from chain by @mattsse in #510
  • refactor(tempo): turn method.rs into a module with tests in submodules by @mattsse in #520
  • fix(tempo): require a session open to cover the challenge amount by @mattsse in #509
  • fix(stripe): charge the request passed to verify by @mattsse in #515
  • feat(proxy)!: advertise the payment method in discovery offers by @mattsse in #516
  • fix(tempo): reject signed amounts in session credentials by @mattsse in #511
  • chore(deps): trim unused dependencies and tighten features by @mattsse in #512
  • ci: run sqlite tests and fail docs on warnings by @mattsse in #513
  • fix(stripe): report requires_action as payment-action-required by @mattsse in #514
  • fix(proxy)!: inject service headers in insertion order by @mattsse in #517
  • refactor(server): move mpp and axum tests into submodules by @mattsse in #518
  • refactor(client): move fetch and middleware tests into submodules by @mattsse in #519
  • refactor(core): split challenge.rs and headers.rs by @mattsse in #521
  • fix(tempo): open legacy session channels again by @mattsse in #524
  • refactor(server): split mpp.rs into issue/verify/session modules by @mattsse in #522
  • refactor(client): single PendingPayment guard by @mattsse in #531
  • refactor(tempo): split the charge verifier by credential flow by @mattsse in #536
  • fix(core): accept a quoted request of exactly 16 KiB by @mattsse in #540
  • refactor(server): share Mpp state behind Arcs by @mattsse in #523
  • fix(tempo): enforce max_deposit on every session voucher by @mattsse in #525
  • fix(client): return HttpError from the payment middleware by @mattsse in #532
  • refactor(tempo): decode receipt logs with alloy instead of JSON by @mattsse in #537
  • fix(tempo): let top_up_amount take precedence over suggestedDeposit by @mattsse in #526
  • refactor(client): shared payment flow for fetch and middleware by @mattsse in #533
  • refactor(tempo): parse the call shape of a charge transaction once by @mattsse in #538
  • refactor(core): parse auth headers with one tokenizer by @mattsse in #541
  • fix(tempo): plan need-voucher top-ups against the known deposit by @mattsse in #527
  • fix(client): middleware parity with fetch by @mattsse in #534
  • refactor(tempo): match transfer calls and logs with one matcher by @mattsse in #539
  • refactor(tempo): turn session_method.rs into a module by @mattsse in #543
  • fix(tempo): answer a refreshed challenge in send_voucher and close by @mattsse in #528
  • fix(client): return a paid 402 that offers no payable challenge by @mattsse in #535
  • refactor(tempo): split the session verifier by action by @mattsse in #544
  • fix(tempo): top up in send_voucher when max_deposit allows it by @mattsse in #529
  • refactor(tempo): extract the channel state transitions by @mattsse in #545
  • fix(tempo): do not resume a legacy channel that cannot cover the request by @mattsse in #530
  • refactor(tempo): borrow in verify_session and trim voucher arguments by @mattsse in #546
  • refactor(tempo): read the settlement route from the channel entry by @mattsse in #542
  • refactor(session): share the metering loop between SSE and WS by @mattsse in #547
  • test(tempo): cover the sponsor's eth_call simulation fallback by @mattsse in #549
  • test(tempo): property-test the session channel state transitions by @mattsse in #550
  • ci: gate merges on the fuzz smoke run and fuzz nightly by @mattsse in #551
  • fix(tempo): keep the recorded deposit when an open reads an older one by @mattsse in #552
  • fix(examples): answer session management POSTs without a stream by @mattsse in #553
  • test(session): add end-to-end session tests on the localnet by @mattsse in #548
  • chore: release v0.15.0 by @tempo-github-sts[bot] in #442

New Contributors

Full Changelog: v0.14.0...v0.15.0