Repository navigation
v0.3.0
·
31 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
What's new
X402\Schemes\ReplayKeyExtractor— optional capability interface schemes opt into to surface their(from, nonce, expiresAt)triple for in-process nonce claiming. Built-inEvm\ExactScheme,Evm\Permit2Scheme, andUpto\UptoEvmSchemeimplement it; theirreplayKey()mirrorsverifyShape()exactly (usesJsonReader::string()'s numeric→string coercion, so a numeric JSONnonce: 123no longer silently skips the in-process claim while still settling).PaymentEnforcer::guardReplay()checksinstanceofand routes per-scheme, so a Permit2 caller cannot inject a forged top-levelauthorizationblock to redirect the claim onto attacker-chosen(from, nonce)— each scheme reads only the payload fields it itself validates.Erc7710Scheme, Stellar / SvmExactSchemedon't implement the interface and defer replay protection to the facilitator's on-chain nonce check, same as before.X402\Replay\CallbackNonceStore— closure-injected adapter for any Redis-compatible client that implementsSET key value NX EX ttlsemantics. Dep-free; works with phpredis, Predis, or hand-rolled clients. Fills the production gap left byPsr16NonceStore, which cannot satisfy theNonceStoreContractatomicity claim because PSR-16 has no add-if-absent primitive (see 0.2.1 notes for the underlying race).$redis = new \Redis(); $store = new CallbackNonceStore( static fn (string $key, int $ttl): bool => (bool) $redis->set($key, '1', ['NX', 'EX' => $ttl]), );
PaymentResponseCacheresponse-header allow-list + hard-block. Cached snapshots no longer includeSet-Cookie,Authorization,Proxy-Authorization,Www-Authenticate, orCookieregardless of caller-supplied allow-list — these would otherwise let a stolenX-PAYMENTheader replayer inherit the original buyer's session. Hard-block runs on the read path too, so any pre-0.3.0 snapshots in your persistent PSR-16 store get sanitised on first hit after upgrade. Default allow-list (Content-Type,Content-Language,Content-Length,Content-Disposition,Cache-Control,ETag,Last-Modified,Location,X-PAYMENT-RESPONSE,PAYMENT-RESPONSE) is overrideable via the newresponseHeadersAllowListconstructor arg; the hard-block list is enforced regardless.
Bug fixes
PaymentResponseCachewas EIP-3009-only on the cache-key path. It derived its key fromPaymentSignature::authorization()(which only readspayload.authorization), so Permit2 and Upto signatures had their nonces burned byPaymentEnforcerbut no matching cache entry — a dropped-response retry on those schemes would 402 as a replay. Same gap on numeric JSONnoncevalues for the EIP-3009 path becausestringOrNull()doesn't coerce. The cache now uses the sameReplayKeyExtractorpath as the enforcer, so Permit2 / Upto / numeric-nonce all key correctly.PaymentResponseCacheno longer caches variant-specific responses. Status206 Partial Contentand responses carryingContent-Encoding,Content-Range,Accept-Ranges, orVaryare skipped at write time. The cache key binds the signed authorization but not requestRange/Accept-Encoding/ negotiation inputs, so replaying a gzipped response to a non-gzip retry, or a partial-content snapshot to a full-body retry, would corrupt the dropped-response recovery guarantee.isValidSnapshot()rejects pre-0.3.0 entries carrying these headers/status on the read path too.
Notes
- BREAKING:
PaymentResponseCache::__construct()now takes a$schemes: array<string, SchemeContract>argument (same map you wire intoPaymentEnforcer). Pass the full enforcer map — non-ReplayKeyExtractorentries are filtered out internally so mixed deployments keep working without a separate map. Adopters using named arguments only need to addschemes:; positional callers need to re-order. SeeUPGRADING.mdfor diffs. - BC fallback retained for custom schemes. Custom
SchemeContractimplementations that haven't migrated toReplayKeyExtractorbut still validate an EIP-3009-shapedpayload.authorizationkeep their 0.2.x in-process replay protection via a server-controlled fallback gate (challengeassetTransferMethod=eip3009+eip155:*network). Schemes that genuinely defer to the facilitator are unaffected. - Drift signals at debug, not warning.
PaymentResponseCacheemitsdebug-level log lines when a payment header arrives for a scheme it can't extract a key for. The cache sits on the unauthenticated edge — warning-level would be public-traffic-spoofable and drown out the real drift signal. Operators detect cache/enforcer drift by enabling debug logging temporarily. - Twenty-nine new tests covering scheme
replayKeyextraction (numeric-nonce coercion, missing-field fail-closed, forged-payload-key isolation), non-EIP-3009 paths skipping cache,CallbackNonceStoresemantics, response-header allow-list + hard-block on both write and read, mixed scheme map acceptance, variant-response skip, stale snapshot sanitisation, BC fallback for legacy custom EIP-3009 schemes. docs/kms.mdclarified —SignatureVerifieraccepts{0, 1, 27, 28}and rejects EIP-155 chainId-offset values fast (no behavior change vs 0.2.1; doc was less precise).
Full Changelog: 0.2.1...0.3.0