import { VerifySettleClient } from './x402/server.js'; import { PaymentRequirements } from './x402/types.js'; /** * The signed settlement receipt issued by the facilitator's POST /settle. * Mirrors apps/facilitator/src/receipts/sign.ts. Every field listed in * `canonicalize` is covered by the Ed25519 signature — including `resource` * and `expiresAt`, which is what makes resource-binding and the replay window * enforceable HERE, at the consuming server, rather than trusting the * facilitator's `success` flag. */ interface X402ReceiptPayload { network: string; payer: string; amount: string; paidAtHeight: number; resource: string; merchantId: string; orderId: string; expiresAt: number; } interface X402SignedReceipt { payload: X402ReceiptPayload; signature: string; algorithm: "ed25519"; } interface VerifyX402ReceiptOptions { /** Facilitator's Ed25519 public key, 64 lowercase hex chars. */ publicKeyHex: string; /** The resource the server is actually about to serve. MUST match. */ expectedResource: string; /** Optional: assert the receipt is for this merchant/order. */ expectedMerchantId?: string; expectedOrderId?: string; /** * The minimum on-chain amount (atomic units, decimal string) this receipt * must attest to cover the price of what is being served. The receipt's * `amount` field is signed but was previously UNENFORCED at the consumer, so * a malicious/compromised facilitator could sign a receipt whose amount is * below the served tier's price and the consumer would accept it (O18). When * set, `signed.payload.amount` (BigInt) MUST be >= this value. */ expectedMinAmount?: string; /** Override clock for tests (ms). */ nowMs?: number; } type VerifyX402ReceiptResult = { ok: true; payload: X402ReceiptPayload; } | { ok: false; reason: string; }; /** * Verify a facilitator receipt at the consuming server. This is the check the * reference middleware previously SKIPPED — it trusted settle.success, so a * malicious/compromised facilitator, or a MITM on the plaintext * server<->facilitator hop, could return {success:true} with any/no receipt * and unlock the resource. Verifying the signature + resource binding here * makes the Ed25519 receipt load-bearing: * 1. signature valid under the configured facilitator key (forgery), * 2. receipt.resource === the resource being served (substitution), * 3. not expired (bounded replay window; pairs with the caller's * one-time-use guard for full replay defense). */ declare function verifyX402Receipt(signed: X402SignedReceipt | null | undefined, opts: VerifyX402ReceiptOptions): VerifyX402ReceiptResult; /** * Pluggable durable backend for the one-time-use ledger. Implement this over * Redis/Postgres/Durable Objects/etc. so replay defense survives across * serverless instances and cold starts. `reserve` MUST be atomic: it records * the key with a TTL and returns true ONLY on the first caller to claim it * (compare-and-set / SET NX PX). Any store whose reserve is not atomic * re-opens cross-instance replay and MUST NOT be used. */ interface ConsumedReceiptStore { /** Atomically claim `key` until `expiresAtMs`. True iff newly claimed. */ reserve(key: string, expiresAtMs: number): Promise; } /** * One-time-use ledger. A receipt's canonical identity is * (merchantId|orderId|payer|resource) — resource IS part of the identity so a * single order's receipt cannot be spent against a different served resource * even when the same static `resource` string is configured (see withX402's * per-request resource resolution). * * Default backend is in-memory, which is process-local: in a multi-instance or * cold-starting serverless deployment (the canonical Next.js target) an * in-memory ledger fails OPEN — a receipt consumed on instance A is unknown to * instance B, so the same payment unlocks again on every reachable instance * within its TTL. Pass a durable `ConsumedReceiptStore` (Redis SET NX PX, a * DB unique-insert, etc.) for any deployment that is not a single long-lived * process. The in-memory default exists only for local/single-process use and * logs a one-time warning so integrators cannot ship it unknowingly. */ declare class ConsumedReceiptLedger { private readonly store; constructor(store?: ConsumedReceiptStore); static key(p: X402ReceiptPayload): string; /** Returns true if this receipt was newly consumed; false if already used. */ consume(p: X402ReceiptPayload): Promise; } /** * Process-local store. NOT safe across instances/cold starts — see * ConsumedReceiptLedger. Exposed so tests and single-process deployments can * construct it explicitly (which suppresses the default-usage warning). */ declare class InMemoryConsumedReceiptStore implements ConsumedReceiptStore { private consumed; reserve(key: string, expiresAtMs: number): Promise; } /** * Stateless, server-authoritative order-id minting for x402. * * WHY THIS EXISTS (O8/O17/O19): the facilitator's order_mismatch guard only * has teeth if `pr.extra.orderId` is chosen by the SERVER, not by the caller. * When an integrator hard-codes a single static orderId (the obvious "static * accepts" shape), `pp.payload.orderId` and `pr.extra.orderId` become the same * fixed string for every request, the on-chain `paid_` slot for that * order is paid exactly ONCE (the contract PANICs on a second Pay), and from * then on the world-readable payment tuple replays for anyone within the * receipt TTL. Binding the orderId to a fresh per-request nonce closes that. * * This helper is STATELESS so it survives the multi-instance / serverless * target: the id is `.`, so any instance * can validate an id another instance minted using only the shared secret — * no cross-instance map, no liveness break on the paid retry. A client cannot * fabricate a "server-issued" id without the secret. Single-use is still * enforced downstream by the ConsumedReceiptLedger. * * `context` should bind the id to the merchant + resource it was issued for so * an id minted for one (merchant,resource) cannot be replayed as a valid * server-issued id for another. */ interface OrderIdMinter { /** Mint a fresh server-issued order id for the given binding context. */ mint(context: string): string; /** True iff `id` is a well-formed order id this deployment could have issued for `context`. */ isServerIssued(id: string, context: string): boolean; /** * Resolve the order id to use for a request: honor the caller's claimed id * ONLY when it validates as server-issued for this context; otherwise mint a * fresh one (which forces a 402 since no on-chain payment can exist for a * brand-new nonce). */ resolve(claimedOrderId: string | undefined, context: string): string; } declare function createOrderIdMinter(hmacSecret: string): OrderIdMinter; interface WithX402Options { facilitator: VerifySettleClient; accepts: PaymentRequirements[]; /** * The resource identity this handler protects. A bare string binds the * receipt to the whole handler URL — correct only when every request the * handler serves is the SAME paid unit. If the handler serves DIFFERENT * paid outputs per request (query string, path params, method, body), pass * a function that derives a distinct resource string PER REQUEST so a * receipt for `?symbol=BTC` cannot unlock `?symbol=ETH`. The resolved value * is what gets signature-checked (receipt.resource must equal it) AND what * keys the one-time-use ledger, so per-request granularity closes the * pay-per-handler collapse (arXiv:2605.11781 resource-binding). */ resource: string | ((req: Request) => string); /** * The facilitator's Ed25519 public key (64 hex). REQUIRED: without it the * server would be trusting the facilitator's `success` flag alone, so a * compromised facilitator or a MITM on the server<->facilitator hop could * unlock the resource with a forged/absent receipt. When set, every settled * receipt is signature-verified, resource-bound, and one-time-use here. */ facilitatorPublicKey: string; /** * OPT-IN strict one-time-use. When a ledger is supplied, a given payment's * receipt (identity = merchantId|orderId|payer) unlocks the resource exactly * ONCE within its TTL; a re-presentation — which any public-chain observer * can reconstruct — is rejected as receipt_replayed. * * Left UNSET (the default), a payment unlocks the resource for the whole TTL * window: this preserves the shipped "one payment, concurrent/again requests * for the same order share it" semantic. In BOTH modes the signed `expiresAt` * bounds replay from forever down to the TTL — the ledger only tightens that * to single-use. Choose strict for pay-per-call resources; leave it off for * pay-per-order/session resources. */ consumedLedger?: ConsumedReceiptLedger; /** * SERVER-AUTHORITATIVE order id (O19). The facilitator's order_mismatch guard * only has teeth if the orderId is chosen by the SERVER, not by the caller. * A static `accepts[].extra.orderId` reused for every request means one * on-chain payment (the contract PANICs on a second Pay to the same order) * replays world-readably for anyone within the receipt TTL. * * Supply an OrderIdMinter (createOrderIdMinter(secret)) to make each request * carry a fresh, HMAC-authenticated order id: the middleware honors the * X-PAYMENT header's claimed orderId ONLY when it validates for this * (merchant|resource) context, otherwise it mints a fresh one — which forces * a 402 since no on-chain payment can exist for a brand-new nonce. Stateless, * so it survives multi-instance/serverless without a shared map. * * Left UNSET, the static accepts[].extra.orderId is used verbatim — safe ONLY * when the server issues that orderId out-of-band per unit of work; a fixed * literal is a free-riding hazard on a public chain. */ orderIdMinter?: OrderIdMinter; } declare function withX402(opts: WithX402Options, handler: (req: Request) => Promise): (req: Request) => Promise; export { ConsumedReceiptLedger as C, InMemoryConsumedReceiptStore as I, type OrderIdMinter as O, type VerifyX402ReceiptOptions as V, type WithX402Options as W, type X402ReceiptPayload as X, type ConsumedReceiptStore as a, type VerifyX402ReceiptResult as b, type X402SignedReceipt as c, createOrderIdMinter as d, verifyX402Receipt as v, withX402 as w };