/** * Idempotency-Key cache for mutating ACP endpoints (#66). * * Why: ACP agents can retry on network failures. Without an idempotency * cache the same `Idempotency-Key` could double-book, double-charge, or * double-refund. The ACP/Stripe spec (mirroring stripe.com/docs/idempotency) * defines the contract: * - Same key + same request body → return the cached prior response. * - Same key + different body → 409 Conflict. * - New key → execute, cache the response. * * Backend: Upstash Redis REST (same backend the rate-limiter uses). We * piggy-back on the existing UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN * (or the Vercel KV-integration names UPSTASH_REDIS_KV_REST_API_*). * * Fail-open policy: if Upstash isn't configured (local dev, preview without * secrets), the cache silently degrades to a non-cache — every request * executes. This matches the rate-limiter's fail-open policy: a misconfigured * sidecar must NOT block production traffic. The risk window is small * because Vercel preview deploys are not customer-facing. * * TTL: 24h. Stripe uses 24h. Matches typical retry windows. * * Body hash: SHA-256 over a stable JSON serialisation (sorted keys) so * `{a:1,b:2}` and `{b:2,a:1}` produce the same hash. We keep the hash, not * the body, in Redis — bodies can contain PII (guest name/email/phone in * ACP create) and Redis should never log them. * * Response storage: status + JSON body, both small. We cap the cached body * at 64KB to keep Redis memory bounded; if a response is larger, the cache * entry is dropped on write (the request still executes, just not cached). */ export type IdempotencyOutcome = { kind: "miss"; } | { kind: "hit"; status: number; body: unknown; } | { kind: "conflict"; existingFingerprint: string; }; export interface IdempotencyDeps { fetch?: typeof fetch; env?: NodeJS.ProcessEnv; } export declare function fingerprint(body: unknown): string; /** * Reject Idempotency-Key values that aren't safe to use as a Redis key. * The ACP/Stripe convention is a UUID-ish opaque string; we permit * `[A-Za-z0-9._:-]` up to 200 chars. Returns null on invalid. */ export declare function normaliseIdempotencyKey(raw: unknown): string | null; /** * Look up an idempotency key. Returns: * - { kind: "miss" } → caller should execute and call `record`. * - { kind: "hit", … } → caller should return cached response verbatim. * - { kind: "conflict" } → caller should return 409 (same key, different body). * * Fail-open: any Upstash error (network, auth, JSON shape) → "miss". */ export declare function lookup(key: string, bodyFingerprint: string, deps?: IdempotencyDeps): Promise; /** * Store the response for a successful execution. Best-effort: Upstash errors * are swallowed (the original response is what matters; cache is an * optimisation for retries). */ export declare function record(key: string, bodyFingerprint: string, status: number, body: unknown, deps?: IdempotencyDeps): Promise;