import { VercelKV } from '@vercel/kv'; import { c as AuditLogAdapter, b as AuditEntry, a as AuditOperation, I as IdempotencyCache, h as OAuthTokenStore, O as OAuthTokenRecord, S as SubscriptionStateAdapter, i as SubscriptionStateRecord } from './audit-B9Nhj3PH.js'; /** * Vercel KV adapters — drop-in `SubscriptionStateAdapter`, * `OAuthTokenStore`, and `IdempotencyCache` implementations backed by * [Vercel KV](https://vercel.com/docs/storage/vercel-kv) (Upstash Redis). * * # Why a separate subpath? * * `@vercel/kv` is a peer dependency — only consumers who actually use Vercel * KV install it. Importing from `@ar-agents/mercadopago/vercel-kv` is * lazy: the main `@ar-agents/mercadopago` bundle stays tiny for callers who * use the in-memory adapters or a different store. * * # Setup * * 1. Create a KV store at https://vercel.com/dashboard/stores * 2. Connect it to your project — Vercel auto-injects `KV_*` env vars * 3. `pnpm add @vercel/kv` * 4. Wire the adapters: * * ```ts * import { mercadoPagoTools, MercadoPagoClient } from "@ar-agents/mercadopago"; * import { * VercelKVSubscriptionStateAdapter, * VercelKVOAuthTokenStore, * } from "@ar-agents/mercadopago/vercel-kv"; * * const tools = mercadoPagoTools(client, { * state: new VercelKVSubscriptionStateAdapter(), * backUrl: "https://mysite.com/done", * // ... oauth, webhookSecret, etc. * }); * * // For marketplace flows, also wire the OAuth token store: * const oauthStore = new VercelKVOAuthTokenStore(); * await oauthStore.set(token.user_id, { * user_id: token.user_id, * access_token: token.access_token, * refresh_token: token.refresh_token!, * expires_at: Date.now() + (token.expires_in ?? 21600) * 1000, * }); * ``` * * # Edge Runtime * * `@vercel/kv` works in Vercel Edge Runtime, Node.js, and any environment * with `fetch` (it's a thin REST client over Upstash). All adapters here * are async and Edge-safe. * * # Key namespacing * * Each adapter uses its own prefix so multiple adapters can share the same * KV store without collisions: * - Subscriptions: `mp:sub:{id}` * - OAuth tokens: `mp:oauth:{userId}` * - Idempotency: `mp:idem:{key}` * * Pass a custom prefix via the constructor if you need to share the store * with other apps. */ interface VercelKVAdapterOptions { /** * Custom KV client. If omitted, uses the default `kv` export from * `@vercel/kv` (which reads `KV_REST_API_URL` + `KV_REST_API_TOKEN` from * env — auto-injected when you connect a KV store to your Vercel project). */ kv?: VercelKV; /** Override the key prefix. */ prefix?: string; } declare class VercelKVSubscriptionStateAdapter implements SubscriptionStateAdapter { private readonly kv; private readonly prefix; private readonly indexKey; constructor(options?: VercelKVAdapterOptions); private key; set(id: string, state: Partial): Promise; get(id: string): Promise; list(): Promise; /** Forget a subscription record. NOT part of the adapter interface. */ delete(id: string): Promise; } declare class VercelKVOAuthTokenStore implements OAuthTokenStore { private readonly kv; private readonly prefix; private readonly indexKey; constructor(options?: VercelKVAdapterOptions); private key; set(userId: string, token: OAuthTokenRecord): Promise; get(userId: string): Promise; delete(userId: string): Promise; list(): Promise; } /** * Distributed token bucket rate limiter backed by Vercel KV. * * # Why distributed * * The default in-memory `TokenBucketRateLimiter` is per-process. In * serverless (Vercel Functions, Lambda, Cloudflare Workers), each cold * start gets its own bucket — meaning N concurrent instances effectively * have N×capacity. For multi-region deployments or marketplace setups * with shared MP rate budget, that's a footgun. * * This adapter uses a single Vercel KV (Upstash Redis) bucket per `key`, * shared across all instances. Two instances acquiring at the same time * decrement the same counter atomically — the rate limit holds globally. * * # Algorithm * * Standard token bucket with lazy refill. Each `acquire()` / `tryAcquire()` * runs a single server-side Lua script ({@link LUA_CONSUME}) that, in one * atomic Redis execution: * 1. Reads `{ tokens, lastRefill }` from KV * 2. Computes refill since `lastRefill` * 3. If tokens >= 1: decrements and writes back * 4. Otherwise: returns the refilled count so the caller can compute its wait * * Because the refill → check → decrement → write happens inside one Lua * script, the decrement is atomic across all instances: concurrent callers can * never both consume the same token, so the global limit holds exactly (no * over-spend window). `learnFromHeaders` uses the same atomic primitive. * * # Usage — wire via `withRateLimit` middleware * * `MercadoPagoClient` does not accept a rate limiter directly. Apply the * limiter at the tool layer using `withRateLimit` from the middleware * module, which works for both the in-memory `TokenBucketRateLimiter` and * this distributed variant. * * ```ts * import { * MercadoPagoClient, * mercadoPagoTools, * InMemoryStateAdapter, * applyToAllTools, * withRateLimit, * } from "@ar-agents/mercadopago"; * import { VercelKVRateLimiter } from "@ar-agents/mercadopago/vercel-kv"; * * // ONE distributed bucket shared across all serverless instances of this app: * const limiter = new VercelKVRateLimiter({ * key: "mp-account-prod", * capacity: 50, * refillPerSecond: 25, * }); * * const client = new MercadoPagoClient({ accessToken: process.env.MP_ACCESS_TOKEN! }); * const tools = applyToAllTools( * mercadoPagoTools(client, { state: new InMemoryStateAdapter(), backUrl: "..." }), * withRateLimit(limiter), * ); * ``` * * # Concurrency * * Token consumption is atomic (one Upstash `EVAL` Lua script per acquire), so * the configured limit holds exactly across all serverless instances even * under heavy concurrent contention — there is no over-spend window. The * `acquire()` retry loop still applies randomized jitter (±30%) to spread * waiting acquirers across refill windows, mitigating the thundering-herd that * would otherwise hit Upstash the instant a bucket refills. * * Note this caps **count** of calls, not monetary spend. For a hard money * budget, enforce it at the payment layer (amount checks + idempotency), not * with a request-rate limiter. * * # Marketplace setups (per-seller rate limit) * * Use the seller's MP user_id as part of the `key`: * * ```ts * function makeLimiter(sellerUserId: string) { * return new VercelKVRateLimiter({ * key: `mp-seller-${sellerUserId}`, * capacity: 10, * refillPerSecond: 5, * }); * } * ``` * * Each seller now has their own globally-distributed bucket. */ interface VercelKVRateLimiterOptions extends VercelKVAdapterOptions { /** * Unique key for this bucket. Use distinct keys per logical "rate-limit * scope" (per-environment, per-seller, per-region, etc.). Required. */ key: string; /** Bucket capacity (max burst). Default 50. */ capacity?: number; /** Refill rate in tokens per second. Default 25. */ refillPerSecond?: number; /** * Hard cap on how long `acquire()` will wait. If the bucket can't * refill in this time, `acquire()` throws. Default 30s. */ acquireTimeoutMs?: number; /** * If true, `learnFromHeaders` syncs the bucket with MP's stated * `x-rate-limit-remaining`. Default true. */ adaptive?: boolean; } declare class VercelKVRateLimiter { private readonly kv; private readonly prefix; private readonly key; private readonly capacity; private readonly refillPerSecond; private readonly acquireTimeoutMs; private readonly adaptive; constructor(options: VercelKVRateLimiterOptions); private fullKey; private readState; private refill; private writeState; /** * Atomically refill + conditionally consume `cost` tokens via a single * server-side Lua script ({@link LUA_CONSUME}). This is the enforcement * primitive: because Redis runs the script atomically, concurrent callers * can never both consume the same token. */ private consume; /** * Acquire a token. Resolves immediately if the distributed bucket has * one available; otherwise waits until refilled. Throws if the wait * exceeds `acquireTimeoutMs` or if the retry cap is reached. * * Each attempt consumes via the atomic {@link LUA_CONSUME} script, so the * limit holds globally even under heavy concurrent contention. * * Caps retries at 8 iterations so a misconfigured bucket (capacity too * low for traffic) fails fast for the agent layer to surface, instead * of silently burning serverless compute time. */ acquire(): Promise; /** Best-effort acquire — returns true if a token was available, false otherwise. */ tryAcquire(): Promise; /** * Adaptive learning — call after each MP API response. If MP's stated * `x-rate-limit-remaining` is lower than our local count, trust MP and * drop the bucket to match (prevents over-spending). Applied atomically via * {@link LUA_CLAMP} so it can't race a concurrent acquire. */ learnFromHeaders(headers: { remaining: number | null; resetSeconds: number | null; }): Promise; /** Inspect bucket state. */ getStats(): Promise<{ tokens: number; capacity: number; refillPerSecond: number; }>; /** Reset the bucket to full. Use sparingly (e.g., after a known-clean window). */ reset(): Promise; } declare class VercelKVIdempotencyCache implements IdempotencyCache { private readonly kv; private readonly prefix; constructor(options?: VercelKVAdapterOptions); private key; get(key: string): Promise; set(key: string, value: T, ttlSeconds?: number): Promise; delete(key: string): Promise; } /** * Vercel KV–backed audit log adapter. Stores each entry under * `mp:audit:entry:{id}` AND adds the id to a daily index sorted set * `mp:audit:day:{YYYY-MM-DD}` (score = timestamp ms). This gives O(log N) * time-range queries ("all entries from May 1 to May 5") without scanning * the entire log. * * # Storage layout * * - `mp:audit:entry:{id}` → the full entry JSON * - `mp:audit:day:{YYYY-MM-DD}` → ZSET of entry ids by timestamp (ms) * - `mp:audit:actor:{actor}` → ZSET of entry ids by timestamp (for "all * entries by actor X") * - `mp:audit:tenant:{tenantId}` → same, by tenant * * # Cost considerations * * Each `append()` does 1-3 KV writes (entry + 1-2 indexes). For high-traffic * deployments (>10/s sustained), batch via your own queue (e.g., Vercel * Queues with daily flush) and provide a custom adapter that batches. */ declare class VercelKVAuditLog implements AuditLogAdapter { private readonly kv; private readonly prefix; constructor(options?: VercelKVAdapterOptions); append(entry: AuditEntry): Promise; query(filter: { actor?: string; operation?: AuditOperation; tenantId?: string; from?: string; to?: string; limit?: number; }): Promise; private zrangeByScore; } export { VercelKVAuditLog, VercelKVIdempotencyCache, VercelKVOAuthTokenStore, VercelKVRateLimiter, type VercelKVRateLimiterOptions, VercelKVSubscriptionStateAdapter };