import type { RequestHandler } from "express"; /** Default short-term window for gateway-level payment proof replay protection. */ export declare const DEFAULT_DEDUPE_TTL_MS: number; export declare const DEFAULT_DEDUPE_MAX_ENTRIES = 10000; /** Local / facilitator verify budget (ms). Settle uses a longer separate timeout. */ export declare const DEFAULT_VERIFY_TIMEOUT_MS = 15000; /** * Facilitator / on-chain settle wait (ms). Base congestion can take minutes — * do not treat HTTP abort as "payment failed" for the buyer. */ export declare const DEFAULT_SETTLE_TIMEOUT_MS = 180000; export type PaymentDedupeStatus = "pending" | "settled" | "consumed"; export interface PaymentDedupeRecord { status: PaymentDedupeStatus; expiresAt: number; /** Optional facilitator / demo receipt (PAYMENT-RESPONSE header value, etc.). */ receipt?: string; updatedAt: number; } /** * Durable-ish payment proof store (set-if-absent / state transitions). * Default is in-process memory; swap for Redis by implementing this interface. */ export interface PaymentDedupeStore { get(idempotencyKey: string): PaymentDedupeRecord | undefined | Promise; /** * Create `pending` only if absent (or expired). Returns the record after the attempt * and whether this caller created the pending reservation. */ tryBeginPending(idempotencyKey: string): { record: PaymentDedupeRecord; created: boolean; } | Promise<{ record: PaymentDedupeRecord; created: boolean; }>; /** Transition pending → settled after facilitator settle success (before upstream). */ markSettled(idempotencyKey: string, receipt?: string): PaymentDedupeRecord | undefined | Promise; /** Transition settled → consumed when upstream is about to run (anti double-proxy). */ markConsumed(idempotencyKey: string): PaymentDedupeRecord | undefined | Promise; /** * Drop a pending reservation so honest retries are not bricked after verify/settle failure. * No-op if status is already settled/consumed. */ releasePending(idempotencyKey: string): void | Promise; } export interface MemoryPaymentDedupeStoreOptions { ttlMs?: number; maxEntries?: number; now?: () => number; } /** * In-memory LRU + TTL `PaymentDedupeStore` (single Node process). * Structure is Redis-ready: keys are opaque idempotency hashes. */ export declare class MemoryPaymentDedupeStore implements PaymentDedupeStore { private readonly ttlMs; private readonly maxEntries; private readonly now; private readonly map; constructor(options?: MemoryPaymentDedupeStoreOptions); get size(): number; clear(): void; get(idempotencyKey: string): PaymentDedupeRecord | undefined; tryBeginPending(idempotencyKey: string): { record: PaymentDedupeRecord; created: boolean; }; markSettled(idempotencyKey: string, receipt?: string): PaymentDedupeRecord | undefined; markConsumed(idempotencyKey: string): PaymentDedupeRecord | undefined; releasePending(idempotencyKey: string): void; private touch; private evictExpired; private evictOverflow; } /** * @deprecated Prefer `MemoryPaymentDedupeStore`. Thin compatibility wrapper that * fingerprints raw PAYMENT-SIGNATURE strings. */ export declare class SignatureDedupeCache { private readonly store; constructor(options?: MemoryPaymentDedupeStoreOptions); fingerprint(signature: string): string; get size(): number; has(signature: string): boolean; add(signature: string): void; clear(): void; /** Expose underlying store for the idempotency middleware. */ getStore(): MemoryPaymentDedupeStore; } /** Stable idempotency key from payment proof (PAYMENT-SIGNATURE header body). */ export declare function paymentIdempotencyKey(signature: string): string; export declare function readPaymentSignature(req: { header(name: string): string | undefined; }): string; /** In-process per-key mutex (cluster-less). Replace with Redis lock for multi-instance. */ export declare class KeyMutex { private readonly chains; acquire(key: string): Promise<() => void>; } export interface PaymentSignatureDedupeOptions { /** @deprecated Use `store`. */ cache?: SignatureDedupeCache; store?: PaymentDedupeStore; mutex?: KeyMutex; ttlMs?: number; maxEntries?: number; gatedPrefix: string; /** * Max time to wait for payment middleware to call `next()` after settle. * On expiry → `202 payment_pending` + `retry_with_same_proof` (settle may still confirm). */ settleTimeoutMs?: number; /** Reserved for verify-phase budgets / docs; settle uses `settleTimeoutMs`. */ verifyTimeoutMs?: number; /** Injectable clock for tests. */ now?: () => number; /** Injectable timer (tests). */ delay?: (ms: number, signal?: AbortSignal) => Promise; } /** * Payment proof idempotency + settle-latency guard for gated paths. * * Policy: * 1. Idempotency key = SHA-256(PAYMENT-SIGNATURE). * 2. Acquire in-process mutex for that key, then consult `PaymentDedupeStore`. * 3. `consumed` → 409 payment_already_used (proof already delivered upstream). * 4. `pending` (another request settling) → 202 payment_pending / retry_with_same_proof. * 5. `settled` (paid but upstream not delivered — e.g. prior HTTP timeout) → skip re-settle, proxy once, mark consumed. * 6. Otherwise begin `pending`, run verify+settle; **mark settled only after settle success**. * 7. If settle exceeds `settleTimeoutMs`, respond 202 payment_pending but leave reservation * pending/settled so a retry resumes instead of charging again. Never tell the buyer the * payment "failed" solely because HTTP timed out while settle may still confirm. * 8. Verify/settle hard failure → `releasePending` so honest retries are not bricked. */ export declare function withPaymentSignatureDedupe(middleware: RequestHandler, options: PaymentSignatureDedupeOptions): RequestHandler;