Repository navigation
v0.15.0
·
20 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
Minor Changes
- Fixed the direct server verification API accepting a credential on a route it was not paid for.
Mpp::verify_credentialandcompose_verifyonly check that the handler issued the challenge, so on a handler serving several prices a credential for the cheapest route unlocked all of them. AddedMpp::verify_chargeandMpp::stripe_verify_charge(plus_with_optionsvariants), which take the amount the route charges and reject credentials issued for anything else,Mpp::expected_charge_requestandMpp::stripe_expected_charge_requestfor the*_with_expected_requestmethods, andcompose_verify_with_expected_requests.Mpp::verify_credential,Mpp::verify_credential_with_bodyandcompose_verifyare deprecated. To migrate, replacempp.verify_credential(&credential)withmpp.verify_charge(&credential, amount), passing the amount the route gives tocharge(). (by @matthiasSeitz, #548) - Fixed
Mpp::createaccepting.fee_payer(true)with neither a fee payer signer nor a relay: every challenge advertisedfeePayer: trueand every sponsored credential was then rejected. This configuration now fails withInvalidConfig. 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 echoedopaqueuses the legacy object form sent by older mppx clients being rejected.ChallengeEchohas a newdescriptionfield thatPaymentChallenge::to_echofills in, and an object-shapedopaqueis normalized to its base64url string. Code that buildsChallengeEchowith a struct literal must adddescription: None(or useto_echo). (by @matthiasSeitz, #548) PaymentMiddlewarenow reports payment failures asHttpError, the errorFetchreturns, insidereqwest_middleware::Error::Middleware. They used to be ad-hocanyhowmessages that callers could not match on. Code that downcast the middleware error toMppErrorshould downcast toHttpErrorand matchHttpError::Payment. The messages, including theerrortext ofpayment.failedevents, now use theHttpErrorwording. (by @matthiasSeitz, #548)- Fixed weak HMAC secrets being accepted:
Mpp::createandMpp::create_stripenow returnInvalidConfigwhenMPP_SECRET_KEYor.secret_key(...)is shorter than 32 bytes, as mppx does. To migrate, generate a key withopenssl rand -base64 32and set it asMPP_SECRET_KEY; challenges issued under the old key no longer verify, so clients holding one are challenged again.Mpp::newand the challenge helpers cannot fail and do not check the length. (by @matthiasSeitz, #548) - Fixed
sse::serveandws_sessionrepeating 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.requiredCumulativenow also clears the server's minimum voucher delta, so a stream no longer stalls when that delta is larger than the tick cost.ServeOptionsandWsSessionOptionshave a new requiredmin_voucher_deltafield: set it to the session method'sSessionMethodConfig::min_voucher_delta(0if none is enforced). (by @matthiasSeitz, #548) - Fixed the problem types reported for payment failures. Malformed credentials now map to
malformed-credential(402) instead ofinternal-payment-errororinvalid-challenge, challenge-binding failures (unknown or tampered challenge id, missing or mismatched bound fields) map toinvalid-challengeinstead ofverification-failed, and RPC, store and upstream API failures during verification map tointernal-payment-error(500) instead ofverification-failed(402).internal-payment-errorproblems now carry a fixeddetailinstead of the underlying error text, andErrorCode::spec_code()returns the problem type that is actually reported. - Migration:
ErrorCodeis now#[non_exhaustive]and gainedInvalidChallengeandInternal, andMppErrorgainedInternalandInvalidReceipt, so exhaustive matches need a wildcard arm.Mpp::verify_*reports binding failures withErrorCode::InvalidChallengeinstead ofErrorCode::CredentialMismatch,parse_authorizationfails withMppError::MalformedCredential, andparse_receiptfails withMppError::InvalidReceiptinstead ofMppError::InvalidChallenge. (by @matthiasSeitz, #548) - Fixed the proxy's OpenAPI discovery document omitting the payment method.
proxy::PaidEndpointhas a newmethodfield, written asmethodin eachx-payment-infooffer (REQUIRED by the discovery spec) and in thepaymentobject of/services. - Migration:
PaidEndpointis now#[non_exhaustive], so it can no longer be built with a struct literal. UsePaidEndpoint::new(method, intent, amount)and thewith_decimals,with_currency,with_unit_typeandwith_descriptionsetters, 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::headerswas aHashMap, so the order changed from run to run, andbearer(..)followed byheader("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::headersis now aVec<(String, String)>. Replaceservice.headers.get(name)withservice.headers.iter().find(|(n, _)| n.eq_ignore_ascii_case(name)). (by @matthiasSeitz, #548) - Fixed
TempoSessionProviderenforcingmax_depositonly 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 bypayis now checked, so such a payment fails withexceeds local max_deposit; raisewith_max_depositto keep spending from that channel. (by @matthiasSeitz, #548) - Fixed Tempo session actions returning a base receipt that session clients such as mppx reject.
SessionMethodnow returns spec-shaped session receipts:intent,challengeId,channelId,acceptedCumulative,spent,unitsand, foropen,topUpandclose,txHashare carried as extension fields of theReceipt, so itsPayment-Receiptheader parses as aSessionReceipt.referenceis now the channel ID for every action; code that read the open or close transaction hash fromreferenceshould readtxHashinstead.SessionReceipt::to_base_receiptkeeps the session fields as well. (by @matthiasSeitz, #548) - Fixed the Stripe server method ignoring
externalId. A charge whose request carries anexternalIdnow requires the credential payload to echo the same value and rejects it otherwise before creating the PaymentIntent, and the receipt carries the request'sexternalId. Clients must echo a request-boundexternalId: mppx does, andStripeProviderdoes in releases after 0.14.0. PaymentIntent metadata values are now capped at Stripe's 500 characters, so a long client-suppliedsourcecan no longer make the request fail, and the method reuses one HTTP client instead of building one per verification. (by @matthiasSeitz, #548) - Fixed
TempoProvidersigning 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 cachedeth_chainIdrequest on the first payment), signs on that chain when the challenge omitschainId, and rejects any other chain withChainIdMismatch. Callwith_expected_chain_idto pin the chain without the lookup, e.g. when the RPC is not reachable while signing. Tempo charges also fail fast when a challenge'ssupportedModesdoes not listpullinstead of sending a transaction credential the server rejects, and the transaction'svalidBeforeis capped at the challengeexpires. (by @matthiasSeitz, #548) - Removed dependencies the library never used:
rand(pulled in byevmandutils),uuid(pulled in bytempo, only used in tests) andtokio-tungstenite(pulled in byws). Thesqlitefeature now enablesclient, where the SQLite channel store lives; before,--features sqlitealone compiledrusqlitewithout exposing the store. - Migration: the implicit
randanduuidcargo features are gone. If you enabledmpp/randormpp/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:intentmust match the intent grammar and is no longer lowercased by the header parser,requestmust be a base64url JSON object,opaquemust be base64url,digestmust besha-256=followed by base64 (optionally wrapped in colons), andexpiresmust be RFC 3339.PaymentChallenge'sDeserializealso rejects an emptyidand aheaderother thanPayment-Authorization. With these checks no bound field can contain the|that separates the fields in the challenge id input. - Migration:
format_www_authenticaterefuses the same values, so a server using an intent name with characters other than letters, digits,-and_, or arequestthat is not a JSON object, has to change it. (by @matthiasSeitz, #548)
Patch Changes
- Fixed
accept_payment::parsereadingAccept-Paymentweights loosely. Theqparameter name is now matched case-insensitively, soQ=0is an opt-out instead of being ignored and treated asq=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::putoverwriting 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_requestand the other expected-request verifiers now reject a credential whoseexternalIddiffers from the expected request, so a payment issued for one order cannot settle another order of the same price. Callers that issue challenges with anexternal_idmust pass the same value in the expected request. (by @matthiasSeitz, #548)- Fixed
PaymentBodyLayerreading credentials fromAuthorizationonly. It now reads the header selected by the verifier, so servers created withrequires_authcan verify body-bound payments sent inPayment-Authorization. (by @matthiasSeitz, #548) - Fixed body-bound payment front-ends buffering request bodies of any size before payment. The axum
MppChargeWithBodyextractor now honoursDefaultBodyLimit(2 MiB unless configured), and the towerPaymentBodyLayercaps the body at 2 MiB, configurable withmax_body_bytes. Oversized bodies are rejected with 413, and body read errors inMppChargeWithBodynow return 400 instead of 500. (by @matthiasSeitz, #548) - Parse challenge auth-param names case-insensitively, so
Realm=orID=are accepted and case-variant duplicates such asid=andID=are rejected. (by @matthiasSeitz, #548) - Fixed
Mpp::charge_challengeandMpp::charge_challenge_with_optionsissuing challenges withoutchainIdon handlers built withMpp::create, which the same handler then rejected withcredential chainId None does not match. They now add the handler's chain ID likecharge()does; achainIdalready present in the request is kept. (by @matthiasSeitz, #548) - Documented that
TempoChargeMethod::newconfigures no replay store, and updated the advanced API examples to add one withwith_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 theStripeProvider::newcallback, which could not be named before. Fixed the broken intra-doc links in the API docs. (by @matthiasSeitz, #548) - Fixed
PaymentMiddlewareandFetchanswering the same 402 differently. The middleware now calls the provider'sprepare_http_payment_challengehook, reopens a stale session once after410 Goneinstead of returning the 410, and returns a 402 that carries no challenge after a payment instead of failing.Fetchnow repeatsAccept-Paymenton retries and sends requests whose body cannot be cloned, failing withHttpError::CloneFailedonly when such a request is answered with a 402; a request builder that holds an error reports that error instead ofCloneFailed. 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 asmpp::mcp::client::PendingPayment, which is now a re-export, and gainednewandinvalidate. (by @matthiasSeitz, #548) - Challenge and credential headers are now read by one tokenizer, so
parse_www_authenticate,parse_www_authenticate_all,extract_payment_schemeandPaymentProtocol::detectagree on where aPaymentscheme starts. An auth-param namedPaymentno longer splits a challenge in the list parser,extract_payment_schemeignores 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
requestparameter 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_openapinow prefixes paths with the configuredbase_path, writes:paramroute segments as{param}templates with matching pathparameters, and emitsx-payment-infoin the multi-offer form ({"offers": [...]}) the discovery spec recommends instead of the flat single-offer shorthand. (by @matthiasSeitz, #548) - Fixed
parse_www_authenticatedropping 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_authenticateemitting 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 (emptyid, invalid method name,requestthat is not base64url JSON) are refused, andHttpTransport::respond_challengereturns a 500 response instead of panicking or sending a barePaymentchallenge 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
maxAmountInand 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_absentleaving 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 becauseprotocol::methodswas only compiled withserverortempo. The Stripe client types are now available withstripealone. (by @matthiasSeitz, #548) - Fixed
PaymentLayer::chargeandPaymentBodyLayer::chargebeing unusable from other crates: their return type named a private verifier type, and the layers were notClonefor non-Cloneverifiers, soaxum::Router::layerandroute_layerrejected them. (by @matthiasSeitz, #548) - Report errors that are not payment problems as the core spec's
internal-payment-errorwith status 500, instead of the undefinedinternal-errortype with status 402. (by @matthiasSeitz, #548) - Fixed clients dropping
WWW-Authenticatevalues 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 newparse_www_authenticate_all_bytes. (by @matthiasSeitz, #548) - Fixed MCP payment errors to match the JSON-RPC transport spec and mppx: challenges are serialized with
requestas a native JSON object instead of a base64url string, verification failures use-32043(whichis_payment_requirednow accepts), and one malformed challenge no longer discards the valid alternatives.attach_credentialandattach_receiptno longer panic on non-object input; the newtry_attach_credentialandtry_attach_receiptreport the failure. (by @matthiasSeitz, #548) - Fixed
PaymentMiddlewaresending 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, matchingFetch. (by @matthiasSeitz, #548) - Fixed
MultiProviderrejecting challenges for providers that send noAccept-Paymentheader, such as a Stripe-only offer failing withNoSupportedChallengenext to a Tempo session provider.TempoProvider,TempoSessionProvider,TempoAccountsProviderandStripeProvidernow advertise the methods they support, andMultiProvidermerges 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
0xprefix was verified but returned unchanged as the receiptreference; the reference is now always the lower-case0x-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
FetchandPaymentMiddlewarefailing withNoSupportedChallengewhen 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'ssessionProtocolmust match the escrow it is answered on. UseTempoSessionProvider::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-Authorizationcredential to the upstream service;scrub_request_headersnow strips it likeAuthorization. FixedProxyConfig::match_routeletting a:paramsegment match.,..(also%2e-encoded) or a segment containing a backslash, which reached other upstream paths once the forwarding client normalised the URL, andbase_pathmatching without a segment boundary (/apimatched/apiopenai/...). (by @matthiasSeitz, #548) - The prebuilt OpenAI proxy service now strips caller-supplied
OpenAI-OrganizationandOpenAI-Projectheaders, and the Stripe service stripsStripe-Contextin addition toStripe-Account. A caller can no longer pick the organization, project or account the operator's key acts on. Headers set withServiceBuilder::headerare still sent. (by @matthiasSeitz, #548) - Fixed
parse_www_authenticate_allsplitting challenges on, Paymenttext inside quoted parameter values, which broke valid challenges and let aPaymentchallenge be read out of another scheme's quoted parameter. Scheme detection now also accepts a tab afterPayment, auth-params accept whitespace around=, and a challenge's parameters end at the next auth scheme. (by @matthiasSeitz, #548) - Fixed realm auto-detection reading
HOSTandHOSTNAME. 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 onHOSTNAMEnow fall back to"MPP Payment"unless one of those is set; setMPP_REALMor 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-byter || s || vsignature withvof 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
headerparameter accepting any HTTP field name, which let a server steer the client's credential into headers such asCookieorProxy-Authorization. OnlyPayment-Authorizationis accepted now: challenges and credentials advertising any other field are rejected when parsed, clients no longer pay them, andformat_www_authenticaterefuses to emit them.PaymentChallenge::with_headerandwith_secret_key_fullignore 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
500and no fresh challenge instead of402with one, so auto-paying clients no longer pay twice. The axum extractors answer every rejected credential with anapplication/problem+jsonbody carrying the problem type and challenge id, returned as the newMppChargeRejection::Probleminstead ofVerificationFailed/VerificationFailedOffers. A challenge that cannot be sent as a header value is answered with500instead of an id-lessWWW-Authenticate: Payment, and500responses no longer contain internal error text. - Custom
ChargeChallengerandPaymentVerifierimplementations keep working and report every failure asverification-failed; implement the newChargeChallenger::verify_payment_for_routeandPaymentVerifier::verify_credentialto report precise problems. (by @matthiasSeitz, #548) - Fixed session challenges advertising
feePayer: trueand 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 newSessionMethod::supports_fee_payerandSessionMethod::supports_machine_tokenshooks (both default tofalse); charge challenges are unchanged. (by @matthiasSeitz, #548) - Fixed
sse::serveandws_sessionpolling 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
closesettling for less than was spent when units were deducted while the close was in flight. The close amount is now re-checked against the currentspentin the same atomic update that marks the channelclosing, and a close that no longer covers it is rejected. (by @matthiasSeitz, #548) - Fixed
TempoSessionProvider::closeforgetting the channel on any successful response. A2xxwithout aPayment-Receiptno 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
closereporting success without settling on-chain. A close whose transaction reverted is no longer recorded as finalized, and aSessionMethodwithout a close signer now rejectscloseinstead of finalizing the channel in the store only. A failed close also no longer leaves the channel stuck inclosing. (by @matthiasSeitz, #548) - Fixed session
closerejecting 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 at0, 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,5xxor429), which opened a second channel on the next request and stranded the first deposit.TempoSessionProvider::rollback_paymentnow only discards a channel whose open was never accepted. A channel is forgotten when the server answers410 Gone, reported through the newPaymentProvider::invalidate_paymenthook, which defaults torollback_payment. (by @matthiasSeitz, #548) - Fixed
TempoSessionProviderfailing to open a channel on a legacy escrow contract withnative 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
TempoSessionProviderresuming 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::serveandws_sessionpolling 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_voucherandclosefailing with402once 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 a402response 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: falseagain 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.finalizednow stays set and the recorded deposit only grows. (by @matthiasSeitz, #548) - Fixed
TempoSessionProvider::voucher_credential_with_top_upandvoucher_credential_with_top_up_for_challengesizing 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 withvoucher 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
openaccepting 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 challengeamount; an open below it is rejected withinsufficient-balancebefore the transaction is broadcast. (by @matthiasSeitz, #548) - Fixed a session
openfor 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::paywaiting 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 withPaymentExpired, and the example shows how to settle a credential before paying again. (by @matthiasSeitz, #548) - Fixed
TempoSessionProvider::send_voucherfailing withvoucher cumulative amount exceeds channel depositwhen apayment-need-voucherevent asked for more than the channel holds althoughmax_depositallowed a larger deposit. Withwith_max_depositset it now tops the channel up first, as the spec requires and asvoucher_credential_with_top_upalready did, sized bytop_up_amount, the server'ssuggestedDepositandmax_deposit. Withoutmax_depositthe channel deposit stays the limit and the error is unchanged. (by @matthiasSeitz, #548) - Serialize the session credential
settlementRoutefield in camelCase like every other payload field. The legacysettlement_routespelling is still accepted when parsing. (by @matthiasSeitz, #548) - Fixed the Tempo session method accepting a leading
+in thecumulativeAmountandadditionalDepositof session credentials. These amounts must be ASCII digits only, like the challenge amounts. (by @matthiasSeitz, #548) - Fixed
TempoSessionProvider::with_top_up_amountbeing ignored whenever the server'ssuggestedDepositwas 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 withinmax_deposit. (by @matthiasSeitz, #548) - Fixed Tempo session servers broadcasting client transactions before validating the credential. An
openwhose 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. AtopUptransaction is now decoded and must be a Tempo transaction callingtopUpon the escrow for the credential's channel andadditionalDeposit; previously any signed transaction was relayed. (by @matthiasSeitz, #548) - Fixed the
SignOptionsdocs, which described defaults the Tempo charge client does not use:nonce_keydefaults toU256::MAX(expiring nonce) rather thanU256::ZERO,nonceto0rather than a fetched pending nonce, the gas fees to static values rather than the latest base fee, andvalid_beforeapplies 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 publicrpc.tempo.xyzandrpc.moderato.tempo.xyzendpoints. The transaction's calls are now simulated witheth_callfrom the sender instead, so a transaction that would revert is rejected before the sponsor pays for it. (by @matthiasSeitz, #548) - Fixed SSE
messageevents for values containing line breaks: the value was truncated at the first newline while still being charged, and streamed content could injectpayment-need-voucherorpayment-receiptevents.format_message_eventnow emits onedata:line per line, andparse_eventaccepts CRLF/CR line endings anddata:fields without a space after the colon. (by @matthiasSeitz, #548) - Fixed amount parsing accepting forms the protocol does not allow, and the
u128andU256parsers disagreeing about them.ChargeRequest::parse_amountandSessionRequest::parse_amountaccepted a leading+, andevm::parse_amount(used byamount_u256,parse_amount_u256and split amounts) read0x10as 16,1_000as 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
externalIdin its credential, which made spec-compliant servers such as mppx reject the payment. The challenge'sexternalIdnow takes precedence over the one returned bycreate_token, and a challenge with a missing or emptymethodDetails.networkIdis 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) asverification-failed. It now fails with the newErrorCode::PaymentActionRequired, which is reported as thepayment-action-requiredproblem like in mppx. (by @matthiasSeitz, #548) - Fixed the Stripe
ChargeMethod::verifyignoring itsrequestargument. It created the PaymentIntent from the request echoed by the credential, so a caller that used the method directly, orMpp::verify/Mpp::broadcastwith its own request, charged whatever amount and currency the credential named. The PaymentIntent amount, currency, metadata andexternalIdbinding now come from the request passed in, as in mppx.Mpp::stripe_verify_chargeand 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.sourceon transaction (pull) credentials, soChargeValidation::sourcereported whatever payer the client claimed. Asourcethat is not adid:pkh:eip155DID for the challenge chain naming the recovered transaction sender is rejected now, before fee-payer co-signing and broadcast. Whensourceis omitted,ChargeValidation::sourcereports 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_modesvalues other than"pull"/"push"are rejected when the challenge is created. (by @matthiasSeitz, #548) - Fixed Tempo hash-credential verification counting the
TransferandTransferWithMemologs of a singletransferWithMemocall 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_sessionlooping onneedVoucherforever 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_voucherstakes a sink for the socket and answers every credential frame with areceiptframe, or with anerrorframe after which it returns the verification error.process_incoming_vouchersis 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.0by @tempo-github-sts[bot] in #442
New Contributors
- @kriss39 made their first contribution in #419
- @Bruce039 made their first contribution in #425
- @wodesiku made their first contribution in #430
- @kutluhaneth46 made their first contribution in #437
- @pucedoteth made their first contribution in #436
- @0xShadowX made their first contribution in #426
Full Changelog: v0.14.0...v0.15.0