@ar-agents/mercadopago 0.18.3 → 0.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`1a64552`](https://github.com/ar-agents/ar-agents/commit/1a6455234ea83a36cc51b595d449f907f47285f1), [`4e20dac`](https://github.com/ar-agents/ar-agents/commit/4e20dac9461ee81e28387cf799bc0a56867e986c), [`2670917`](https://github.com/ar-agents/ar-agents/commit/2670917a931df2093d0931c05023902cbcc63c3b)]:
8
+ - @ar-agents/core@0.3.0
9
+
10
+ ## 0.18.4
11
+
12
+ ### Patch Changes
13
+
14
+ - [`e73be19`](https://github.com/ar-agents/ar-agents/commit/e73be1945206b3f2ebce0eed13a74ada238b6c22) Thanks [@naza00000](https://github.com/naza00000)! - Make `VercelKVRateLimiter` token acquisition atomic (DeepSec MEDIUM, `rate-limit-bypass`).
15
+
16
+ `acquire()` / `tryAcquire()` previously did a non-atomic read → refill → check → decrement → write across separate KV calls, so concurrent callers could all observe the same lone token and each succeed — bypassing the global limit. Acquisition now runs a single server-side Upstash Lua `EVAL` script, so the refill-and-consume is atomic across all serverless instances and the limit holds exactly (no over-spend window). `learnFromHeaders` uses the same atomic primitive for its adaptive clamp. Behavior and the public API are unchanged.
17
+
3
18
  ## 0.18.3
4
19
 
5
20
  ### Patch Changes
@@ -21,7 +21,7 @@
21
21
  * as a Vercel Edge Function for sub-100ms global cold starts.
22
22
  */
23
23
 
24
- import { Experimental_Agent as Agent, stepCountIs } from "ai";
24
+ import { Experimental_Agent as Agent, isStepCount } from "ai";
25
25
  import {
26
26
  InMemoryStateAdapter,
27
27
  MercadoPagoClient,
@@ -50,7 +50,7 @@ Cuando el cliente quiere comprar:
50
50
  backUrl: "https://yourapp.com/payment-result",
51
51
  notificationUrl: "https://yourapp.com/api/mp/webhook",
52
52
  }),
53
- stopWhen: stepCountIs(5),
53
+ stopWhen: isStepCount(5),
54
54
  });
55
55
 
56
56
  // In a Next.js route handler:
@@ -51,7 +51,7 @@
51
51
  */
52
52
 
53
53
  import { trace, context as otelContext } from "@opentelemetry/api";
54
- import { Experimental_Agent as Agent, stepCountIs } from "ai";
54
+ import { Experimental_Agent as Agent, isStepCount } from "ai";
55
55
  import {
56
56
  MercadoPagoClient,
57
57
  mercadoPagoTools,
@@ -108,7 +108,7 @@ export const agent = new Agent({
108
108
  model: "anthropic/claude-sonnet-4-6",
109
109
  instructions: "You are a billing assistant for a SaaS in Argentina.",
110
110
  tools,
111
- stopWhen: stepCountIs(8),
111
+ stopWhen: isStepCount(8),
112
112
  });
113
113
 
114
114
  /**
@@ -22,7 +22,7 @@
22
22
  * ATTESTATION_HMAC_SECRET
23
23
  */
24
24
 
25
- import { Experimental_Agent as Agent, stepCountIs, type ToolSet } from "ai";
25
+ import { Experimental_Agent as Agent, isStepCount, type ToolSet } from "ai";
26
26
 
27
27
  // 1. Mercado Pago — always present, the headline package.
28
28
  import {
@@ -135,7 +135,7 @@ export async function buildBillingAgent() {
135
135
  "comprobante por WhatsApp con `send_text`. Respondé en castellano " +
136
136
  "rioplatense, breve, sin emojis.",
137
137
  tools,
138
- stopWhen: stepCountIs(15), // higher than usual — multi-package flows take steps
138
+ stopWhen: isStepCount(15), // higher than usual — multi-package flows take steps
139
139
  });
140
140
  }
141
141
 
@@ -59,7 +59,7 @@
59
59
  * # Sample agent loop (USA-LLC side, using Vercel AI SDK 6 + MCP client)
60
60
  */
61
61
 
62
- import { Experimental_Agent as Agent, stepCountIs } from "ai";
62
+ import { Experimental_Agent as Agent, isStepCount } from "ai";
63
63
  // In a USA agent's project, you'd use the MCP client from `ai` v6 (or `@modelcontextprotocol/sdk`)
64
64
  // to connect to the locally-spawned ar-agents MCP server. The agent then sees
65
65
  // every @ar-agents/* tool in its tool list.
@@ -86,7 +86,7 @@ async function exampleAgentLoop() {
86
86
  "yourself.",
87
87
  // tools, // injected from MCP client
88
88
  tools: {} as Record<string, unknown>,
89
- stopWhen: stepCountIs(10),
89
+ stopWhen: isStepCount(10),
90
90
  });
91
91
 
92
92
  // What the agent does behind this prompt:
@@ -125,7 +125,7 @@ export async function POST(req: Request) {
125
125
  try {
126
126
  const result = streamText({
127
127
  model: "anthropic/claude-sonnet-4-6",
128
- system: SYSTEM,
128
+ instructions: SYSTEM,
129
129
  messages: modelMessages,
130
130
  tools,
131
131
  stopWhen: ({ steps }) => steps.length >= 8,
@@ -66,6 +66,58 @@ var VercelKVOAuthTokenStore = class {
66
66
  }
67
67
  };
68
68
  var DEFAULT_RATELIMIT_PREFIX = "mp:rl:";
69
+ var RATELIMIT_TTL_SECONDS = 3600;
70
+ var LUA_CONSUME = `-- @op:consume
71
+ local raw = redis.call('GET', KEYS[1])
72
+ local capacity = tonumber(ARGV[1])
73
+ local refillPerSecond = tonumber(ARGV[2])
74
+ local now = tonumber(ARGV[3])
75
+ local cost = tonumber(ARGV[4])
76
+ local ttl = tonumber(ARGV[5])
77
+ local tokens = capacity
78
+ local lastRefill = now
79
+ if raw then
80
+ local ok, data = pcall(cjson.decode, raw)
81
+ if ok and type(data) == 'table' and type(data.tokens) == 'number' and type(data.lastRefillMs) == 'number' then
82
+ tokens = data.tokens
83
+ lastRefill = data.lastRefillMs
84
+ end
85
+ end
86
+ local elapsed = now - lastRefill
87
+ if elapsed < 0 then elapsed = 0 end
88
+ tokens = math.min(capacity, tokens + (elapsed / 1000.0) * refillPerSecond)
89
+ local allowed = 0
90
+ if tokens >= cost then
91
+ tokens = tokens - cost
92
+ allowed = 1
93
+ redis.call('SET', KEYS[1], cjson.encode({tokens = tokens, lastRefillMs = now}), 'EX', ttl)
94
+ end
95
+ return {allowed, tostring(tokens)}`;
96
+ var LUA_CLAMP = `-- @op:clamp
97
+ local raw = redis.call('GET', KEYS[1])
98
+ local capacity = tonumber(ARGV[1])
99
+ local refillPerSecond = tonumber(ARGV[2])
100
+ local now = tonumber(ARGV[3])
101
+ local remaining = tonumber(ARGV[4])
102
+ local ttl = tonumber(ARGV[5])
103
+ local tokens = capacity
104
+ local lastRefill = now
105
+ if raw then
106
+ local ok, data = pcall(cjson.decode, raw)
107
+ if ok and type(data) == 'table' and type(data.tokens) == 'number' and type(data.lastRefillMs) == 'number' then
108
+ tokens = data.tokens
109
+ lastRefill = data.lastRefillMs
110
+ end
111
+ end
112
+ local elapsed = now - lastRefill
113
+ if elapsed < 0 then elapsed = 0 end
114
+ tokens = math.min(capacity, tokens + (elapsed / 1000.0) * refillPerSecond)
115
+ if remaining < tokens then
116
+ if remaining < 0 then remaining = 0 end
117
+ tokens = remaining
118
+ end
119
+ redis.call('SET', KEYS[1], cjson.encode({tokens = tokens, lastRefillMs = now}), 'EX', ttl)
120
+ return tostring(tokens)`;
69
121
  var VercelKVRateLimiter = class {
70
122
  kv;
71
123
  prefix;
@@ -107,13 +159,31 @@ var VercelKVRateLimiter = class {
107
159
  return { tokens: refilled, lastRefillMs: nowMs };
108
160
  }
109
161
  async writeState(state) {
110
- await this.kv.set(this.fullKey(), state, { ex: 3600 });
162
+ await this.kv.set(this.fullKey(), state, { ex: RATELIMIT_TTL_SECONDS });
163
+ }
164
+ /**
165
+ * Atomically refill + conditionally consume `cost` tokens via a single
166
+ * server-side Lua script ({@link LUA_CONSUME}). This is the enforcement
167
+ * primitive: because Redis runs the script atomically, concurrent callers
168
+ * can never both consume the same token.
169
+ */
170
+ async consume(cost) {
171
+ const now = Date.now();
172
+ const res = await this.kv.eval(
173
+ LUA_CONSUME,
174
+ [this.fullKey()],
175
+ [this.capacity, this.refillPerSecond, now, cost, RATELIMIT_TTL_SECONDS]
176
+ );
177
+ return { allowed: Number(res[0]) === 1, tokens: Number(res[1]) };
111
178
  }
112
179
  /**
113
180
  * Acquire a token. Resolves immediately if the distributed bucket has
114
181
  * one available; otherwise waits until refilled. Throws if the wait
115
182
  * exceeds `acquireTimeoutMs` or if the retry cap is reached.
116
183
  *
184
+ * Each attempt consumes via the atomic {@link LUA_CONSUME} script, so the
185
+ * limit holds globally even under heavy concurrent contention.
186
+ *
117
187
  * Caps retries at 8 iterations so a misconfigured bucket (capacity too
118
188
  * low for traffic) fails fast for the agent layer to surface, instead
119
189
  * of silently burning serverless compute time.
@@ -123,18 +193,13 @@ var VercelKVRateLimiter = class {
123
193
  const MAX_RETRIES = 8;
124
194
  let attempt = 0;
125
195
  while (attempt < MAX_RETRIES) {
126
- const now = Date.now();
127
- const state = this.refill(await this.readState(), now);
128
- if (state.tokens >= 1) {
129
- state.tokens -= 1;
130
- await this.writeState(state);
131
- return;
132
- }
133
- const tokensNeeded = 1 - state.tokens;
196
+ const { allowed, tokens } = await this.consume(1);
197
+ if (allowed) return;
198
+ const tokensNeeded = 1 - tokens;
134
199
  const baseWaitMs = Math.ceil(tokensNeeded / this.refillPerSecond * 1e3);
135
200
  const jitterFactor = 0.7 + Math.random() * 0.6;
136
201
  const waitMs = Math.ceil(baseWaitMs * jitterFactor);
137
- const elapsed = now - start;
202
+ const elapsed = Date.now() - start;
138
203
  if (elapsed + waitMs > this.acquireTimeoutMs) {
139
204
  throw new Error(
140
205
  `VercelKVRateLimiter acquire timed out after ${elapsed + waitMs}ms (key=${this.key}).`
@@ -149,27 +214,29 @@ var VercelKVRateLimiter = class {
149
214
  }
150
215
  /** Best-effort acquire — returns true if a token was available, false otherwise. */
151
216
  async tryAcquire() {
152
- const state = this.refill(await this.readState(), Date.now());
153
- if (state.tokens >= 1) {
154
- state.tokens -= 1;
155
- await this.writeState(state);
156
- return true;
157
- }
158
- return false;
217
+ const { allowed } = await this.consume(1);
218
+ return allowed;
159
219
  }
160
220
  /**
161
221
  * Adaptive learning — call after each MP API response. If MP's stated
162
222
  * `x-rate-limit-remaining` is lower than our local count, trust MP and
163
- * drop the bucket to match (prevents over-spending).
223
+ * drop the bucket to match (prevents over-spending). Applied atomically via
224
+ * {@link LUA_CLAMP} so it can't race a concurrent acquire.
164
225
  */
165
226
  async learnFromHeaders(headers) {
166
227
  if (!this.adaptive) return;
167
228
  if (headers.remaining === null) return;
168
- const state = this.refill(await this.readState(), Date.now());
169
- if (headers.remaining < state.tokens) {
170
- state.tokens = Math.max(0, headers.remaining);
171
- await this.writeState(state);
172
- }
229
+ await this.kv.eval(
230
+ LUA_CLAMP,
231
+ [this.fullKey()],
232
+ [
233
+ this.capacity,
234
+ this.refillPerSecond,
235
+ Date.now(),
236
+ headers.remaining,
237
+ RATELIMIT_TTL_SECONDS
238
+ ]
239
+ );
173
240
  }
174
241
  /** Inspect bucket state. */
175
242
  async getStats() {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/vercel-kv.ts"],"names":["defaultKv"],"mappings":"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","file":"vercel-kv.cjs","sourcesContent":["/**\n * Vercel KV adapters — drop-in `SubscriptionStateAdapter`,\n * `OAuthTokenStore`, and `IdempotencyCache` implementations backed by\n * [Vercel KV](https://vercel.com/docs/storage/vercel-kv) (Upstash Redis).\n *\n * # Why a separate subpath?\n *\n * `@vercel/kv` is a peer dependency — only consumers who actually use Vercel\n * KV install it. Importing from `@ar-agents/mercadopago/vercel-kv` is\n * lazy: the main `@ar-agents/mercadopago` bundle stays tiny for callers who\n * use the in-memory adapters or a different store.\n *\n * # Setup\n *\n * 1. Create a KV store at https://vercel.com/dashboard/stores\n * 2. Connect it to your project — Vercel auto-injects `KV_*` env vars\n * 3. `pnpm add @vercel/kv`\n * 4. Wire the adapters:\n *\n * ```ts\n * import { mercadoPagoTools, MercadoPagoClient } from \"@ar-agents/mercadopago\";\n * import {\n * VercelKVSubscriptionStateAdapter,\n * VercelKVOAuthTokenStore,\n * } from \"@ar-agents/mercadopago/vercel-kv\";\n *\n * const tools = mercadoPagoTools(client, {\n * state: new VercelKVSubscriptionStateAdapter(),\n * backUrl: \"https://mysite.com/done\",\n * // ... oauth, webhookSecret, etc.\n * });\n *\n * // For marketplace flows, also wire the OAuth token store:\n * const oauthStore = new VercelKVOAuthTokenStore();\n * await oauthStore.set(token.user_id, {\n * user_id: token.user_id,\n * access_token: token.access_token,\n * refresh_token: token.refresh_token!,\n * expires_at: Date.now() + (token.expires_in ?? 21600) * 1000,\n * });\n * ```\n *\n * # Edge Runtime\n *\n * `@vercel/kv` works in Vercel Edge Runtime, Node.js, and any environment\n * with `fetch` (it's a thin REST client over Upstash). All adapters here\n * are async and Edge-safe.\n *\n * # Key namespacing\n *\n * Each adapter uses its own prefix so multiple adapters can share the same\n * KV store without collisions:\n * - Subscriptions: `mp:sub:{id}`\n * - OAuth tokens: `mp:oauth:{userId}`\n * - Idempotency: `mp:idem:{key}`\n *\n * Pass a custom prefix via the constructor if you need to share the store\n * with other apps.\n */\n\nimport { kv as defaultKv } from \"@vercel/kv\";\nimport type { VercelKV } from \"@vercel/kv\";\nimport type { AuditEntry, AuditLogAdapter, AuditOperation } from \"./audit\";\nimport type {\n IdempotencyCache,\n OAuthTokenRecord,\n OAuthTokenStore,\n SubscriptionStateAdapter,\n SubscriptionStateRecord,\n} from \"./state\";\n\nconst DEFAULT_SUBSCRIPTION_PREFIX = \"mp:sub:\";\nconst DEFAULT_OAUTH_PREFIX = \"mp:oauth:\";\nconst DEFAULT_IDEMPOTENCY_PREFIX = \"mp:idem:\";\nconst DEFAULT_AUDIT_PREFIX = \"mp:audit:\";\n\ninterface VercelKVAdapterOptions {\n /**\n * Custom KV client. If omitted, uses the default `kv` export from\n * `@vercel/kv` (which reads `KV_REST_API_URL` + `KV_REST_API_TOKEN` from\n * env — auto-injected when you connect a KV store to your Vercel project).\n */\n kv?: VercelKV;\n /** Override the key prefix. */\n prefix?: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// SubscriptionStateAdapter\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVSubscriptionStateAdapter\n implements SubscriptionStateAdapter\n{\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly indexKey: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_SUBSCRIPTION_PREFIX;\n this.indexKey = `${this.prefix}__index`;\n }\n\n private key(id: string): string {\n return `${this.prefix}${id}`;\n }\n\n async set(\n id: string,\n state: Partial<SubscriptionStateRecord>,\n ): Promise<void> {\n const existing = (await this.kv.get<SubscriptionStateRecord>(this.key(id))) ?? {};\n await this.kv.set(this.key(id), { ...existing, ...state });\n await this.kv.sadd(this.indexKey, id);\n }\n\n async get(id: string): Promise<SubscriptionStateRecord | null> {\n return (await this.kv.get<SubscriptionStateRecord>(this.key(id))) ?? null;\n }\n\n async list(): Promise<string[]> {\n const ids = await this.kv.smembers(this.indexKey);\n return ids.map(String);\n }\n\n /** Forget a subscription record. NOT part of the adapter interface. */\n async delete(id: string): Promise<void> {\n await this.kv.del(this.key(id));\n await this.kv.srem(this.indexKey, id);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// OAuthTokenStore (per-seller marketplace token persistence)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVOAuthTokenStore implements OAuthTokenStore {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly indexKey: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_OAUTH_PREFIX;\n this.indexKey = `${this.prefix}__index`;\n }\n\n private key(userId: string): string {\n return `${this.prefix}${userId}`;\n }\n\n async set(userId: string, token: OAuthTokenRecord): Promise<void> {\n await this.kv.set(this.key(userId), token);\n await this.kv.sadd(this.indexKey, userId);\n }\n\n async get(userId: string): Promise<OAuthTokenRecord | null> {\n return (await this.kv.get<OAuthTokenRecord>(this.key(userId))) ?? null;\n }\n\n async delete(userId: string): Promise<void> {\n await this.kv.del(this.key(userId));\n await this.kv.srem(this.indexKey, userId);\n }\n\n async list(): Promise<string[]> {\n const ids = await this.kv.smembers(this.indexKey);\n return ids.map(String);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Distributed Token Bucket Rate Limiter (KV-backed)\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst DEFAULT_RATELIMIT_PREFIX = \"mp:rl:\";\n\n/**\n * Distributed token bucket rate limiter backed by Vercel KV.\n *\n * # Why distributed\n *\n * The default in-memory `TokenBucketRateLimiter` is per-process. In\n * serverless (Vercel Functions, Lambda, Cloudflare Workers), each cold\n * start gets its own bucket — meaning N concurrent instances effectively\n * have N×capacity. For multi-region deployments or marketplace setups\n * with shared MP rate budget, that's a footgun.\n *\n * This adapter uses a single Vercel KV (Upstash Redis) bucket per `key`,\n * shared across all instances. Two instances acquiring at the same time\n * decrement the same counter atomically — the rate limit holds globally.\n *\n * # Algorithm\n *\n * Standard token bucket with lazy refill: every `acquire()` call:\n * 1. Reads `{ tokens, lastRefill }` from KV\n * 2. Computes refill since `lastRefill`\n * 3. If tokens >= 1: decrements and writes back\n * 4. Otherwise: computes wait time, sleeps, retries\n *\n * The read-modify-write isn't atomic per-call, so under heavy contention\n * a small over-spend window is possible (worst case: ~N concurrent\n * acquires can succeed when only 1 token was available). Acceptable for\n * MP rate limiting — the \"actual\" budget is much higher than what we\n * provision.\n *\n * # Usage — wire via `withRateLimit` middleware\n *\n * `MercadoPagoClient` does not accept a rate limiter directly. Apply the\n * limiter at the tool layer using `withRateLimit` from the middleware\n * module, which works for both the in-memory `TokenBucketRateLimiter` and\n * this distributed variant.\n *\n * ```ts\n * import {\n * MercadoPagoClient,\n * mercadoPagoTools,\n * InMemoryStateAdapter,\n * applyToAllTools,\n * withRateLimit,\n * } from \"@ar-agents/mercadopago\";\n * import { VercelKVRateLimiter } from \"@ar-agents/mercadopago/vercel-kv\";\n *\n * // ONE distributed bucket shared across all serverless instances of this app:\n * const limiter = new VercelKVRateLimiter({\n * key: \"mp-account-prod\",\n * capacity: 50,\n * refillPerSecond: 25,\n * });\n *\n * const client = new MercadoPagoClient({ accessToken: process.env.MP_ACCESS_TOKEN! });\n * const tools = applyToAllTools(\n * mercadoPagoTools(client, { state: new InMemoryStateAdapter(), backUrl: \"...\" }),\n * withRateLimit(limiter),\n * );\n * ```\n *\n * # Concurrency caveats\n *\n * Read-modify-write is NOT strictly atomic per `acquire()`. Under heavy\n * contention a small over-spend window is possible, acceptable for **API\n * call** rate limiting where the actual MP budget exceeds what we\n * provision. **Do NOT repurpose this limiter as a money/spend cap** —\n * the over-spend window means you could exceed a money budget by the\n * concurrent-instance count × per-call cost. For money budgets, use\n * Upstash's `EVAL`-based atomic Lua script (or a stricter primitive).\n *\n * The `acquire()` retry loop applies randomized jitter (±30%) to spread\n * concurrent acquirers across multiple refill windows, mitigating the\n * thundering-herd that would otherwise hit Upstash with N reads + N\n * writes the instant a bucket refills.\n *\n * # Marketplace setups (per-seller rate limit)\n *\n * Use the seller's MP user_id as part of the `key`:\n *\n * ```ts\n * function makeLimiter(sellerUserId: string) {\n * return new VercelKVRateLimiter({\n * key: `mp-seller-${sellerUserId}`,\n * capacity: 10,\n * refillPerSecond: 5,\n * });\n * }\n * ```\n *\n * Each seller now has their own globally-distributed bucket.\n */\nexport interface VercelKVRateLimiterOptions extends VercelKVAdapterOptions {\n /**\n * Unique key for this bucket. Use distinct keys per logical \"rate-limit\n * scope\" (per-environment, per-seller, per-region, etc.). Required.\n */\n key: string;\n /** Bucket capacity (max burst). Default 50. */\n capacity?: number;\n /** Refill rate in tokens per second. Default 25. */\n refillPerSecond?: number;\n /**\n * Hard cap on how long `acquire()` will wait. If the bucket can't\n * refill in this time, `acquire()` throws. Default 30s.\n */\n acquireTimeoutMs?: number;\n /**\n * If true, `learnFromHeaders` syncs the bucket with MP's stated\n * `x-rate-limit-remaining`. Default true.\n */\n adaptive?: boolean;\n}\n\ninterface BucketState {\n tokens: number;\n lastRefillMs: number;\n}\n\nexport class VercelKVRateLimiter {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly key: string;\n private readonly capacity: number;\n private readonly refillPerSecond: number;\n private readonly acquireTimeoutMs: number;\n private readonly adaptive: boolean;\n\n constructor(options: VercelKVRateLimiterOptions) {\n if (!options.key) {\n throw new Error(\n \"VercelKVRateLimiter requires a `key` (use distinct keys per rate-limit scope, e.g., per-environment or per-seller).\",\n );\n }\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_RATELIMIT_PREFIX;\n this.key = options.key;\n this.capacity = options.capacity ?? 50;\n this.refillPerSecond = options.refillPerSecond ?? 25;\n this.acquireTimeoutMs = options.acquireTimeoutMs ?? 30_000;\n this.adaptive = options.adaptive ?? true;\n }\n\n private fullKey(): string {\n return `${this.prefix}${this.key}`;\n }\n\n private async readState(): Promise<BucketState> {\n const stored = await this.kv.get<BucketState>(this.fullKey());\n if (stored && typeof stored.tokens === \"number\" && typeof stored.lastRefillMs === \"number\") {\n return stored;\n }\n return { tokens: this.capacity, lastRefillMs: Date.now() };\n }\n\n private refill(state: BucketState, nowMs: number): BucketState {\n const elapsedMs = Math.max(0, nowMs - state.lastRefillMs);\n const refilled = Math.min(\n this.capacity,\n state.tokens + (elapsedMs / 1000) * this.refillPerSecond,\n );\n return { tokens: refilled, lastRefillMs: nowMs };\n }\n\n private async writeState(state: BucketState): Promise<void> {\n // TTL = 1h. Long-idle buckets get garbage-collected, capacity rebuilds\n // from initial state on next acquire (which is fine — at the right rate).\n await this.kv.set(this.fullKey(), state, { ex: 3600 });\n }\n\n /**\n * Acquire a token. Resolves immediately if the distributed bucket has\n * one available; otherwise waits until refilled. Throws if the wait\n * exceeds `acquireTimeoutMs` or if the retry cap is reached.\n *\n * Caps retries at 8 iterations so a misconfigured bucket (capacity too\n * low for traffic) fails fast for the agent layer to surface, instead\n * of silently burning serverless compute time.\n */\n async acquire(): Promise<void> {\n const start = Date.now();\n const MAX_RETRIES = 8;\n let attempt = 0;\n while (attempt < MAX_RETRIES) {\n const now = Date.now();\n const state = this.refill(await this.readState(), now);\n\n if (state.tokens >= 1) {\n state.tokens -= 1;\n await this.writeState(state);\n return;\n }\n\n // Compute wait time until next token. Cap at remaining timeout budget.\n const tokensNeeded = 1 - state.tokens;\n const baseWaitMs = Math.ceil((tokensNeeded / this.refillPerSecond) * 1000);\n // Randomized jitter (±30%) prevents thundering herd: without it, all\n // concurrent acquirers compute identical waitMs, sleep identical\n // duration, and wake at the same wall-clock instant — then all hit\n // KV simultaneously. Jitter spreads them across the refill window.\n const jitterFactor = 0.7 + Math.random() * 0.6; // 0.7–1.3\n const waitMs = Math.ceil(baseWaitMs * jitterFactor);\n const elapsed = now - start;\n if (elapsed + waitMs > this.acquireTimeoutMs) {\n throw new Error(\n `VercelKVRateLimiter acquire timed out after ${elapsed + waitMs}ms (key=${this.key}).`,\n );\n }\n await new Promise((resolve) => setTimeout(resolve, waitMs));\n attempt += 1;\n }\n throw new Error(\n `VercelKVRateLimiter exhausted ${MAX_RETRIES} retries (key=${this.key}). ` +\n `Bucket likely undersized for traffic — increase capacity or refillPerSecond.`,\n );\n }\n\n /** Best-effort acquire — returns true if a token was available, false otherwise. */\n async tryAcquire(): Promise<boolean> {\n const state = this.refill(await this.readState(), Date.now());\n if (state.tokens >= 1) {\n state.tokens -= 1;\n await this.writeState(state);\n return true;\n }\n return false;\n }\n\n /**\n * Adaptive learning — call after each MP API response. If MP's stated\n * `x-rate-limit-remaining` is lower than our local count, trust MP and\n * drop the bucket to match (prevents over-spending).\n */\n async learnFromHeaders(headers: {\n remaining: number | null;\n resetSeconds: number | null;\n }): Promise<void> {\n if (!this.adaptive) return;\n if (headers.remaining === null) return;\n const state = this.refill(await this.readState(), Date.now());\n if (headers.remaining < state.tokens) {\n state.tokens = Math.max(0, headers.remaining);\n await this.writeState(state);\n }\n }\n\n /** Inspect bucket state. */\n async getStats(): Promise<{ tokens: number; capacity: number; refillPerSecond: number }> {\n const state = this.refill(await this.readState(), Date.now());\n return {\n tokens: state.tokens,\n capacity: this.capacity,\n refillPerSecond: this.refillPerSecond,\n };\n }\n\n /** Reset the bucket to full. Use sparingly (e.g., after a known-clean window). */\n async reset(): Promise<void> {\n await this.writeState({ tokens: this.capacity, lastRefillMs: Date.now() });\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// IdempotencyCache (KV-backed dedup of agent retries)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVIdempotencyCache implements IdempotencyCache {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_IDEMPOTENCY_PREFIX;\n }\n\n private key(k: string): string {\n return `${this.prefix}${k}`;\n }\n\n async get<T>(key: string): Promise<T | null> {\n return (await this.kv.get<T>(this.key(key))) ?? null;\n }\n\n async set<T>(key: string, value: T, ttlSeconds = 86_400): Promise<void> {\n // Vercel KV's `set` supports a TTL in seconds via the `ex` option.\n await this.kv.set(this.key(key), value, { ex: ttlSeconds });\n }\n\n async delete(key: string): Promise<void> {\n await this.kv.del(this.key(key));\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// AuditLogAdapter — production audit trail with daily-bucket indexing\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Vercel KV–backed audit log adapter. Stores each entry under\n * `mp:audit:entry:{id}` AND adds the id to a daily index sorted set\n * `mp:audit:day:{YYYY-MM-DD}` (score = timestamp ms). This gives O(log N)\n * time-range queries (\"all entries from May 1 to May 5\") without scanning\n * the entire log.\n *\n * # Storage layout\n *\n * - `mp:audit:entry:{id}` → the full entry JSON\n * - `mp:audit:day:{YYYY-MM-DD}` → ZSET of entry ids by timestamp (ms)\n * - `mp:audit:actor:{actor}` → ZSET of entry ids by timestamp (for \"all\n * entries by actor X\")\n * - `mp:audit:tenant:{tenantId}` → same, by tenant\n *\n * # Cost considerations\n *\n * Each `append()` does 1-3 KV writes (entry + 1-2 indexes). For high-traffic\n * deployments (>10/s sustained), batch via your own queue (e.g., Vercel\n * Queues with daily flush) and provide a custom adapter that batches.\n */\nexport class VercelKVAuditLog implements AuditLogAdapter {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_AUDIT_PREFIX;\n }\n\n async append(entry: AuditEntry): Promise<void> {\n const ts = new Date(entry.timestamp).getTime();\n const day = entry.timestamp.slice(0, 10); // YYYY-MM-DD\n await Promise.all([\n this.kv.set(`${this.prefix}entry:${entry.id}`, entry),\n this.kv.zadd(`${this.prefix}day:${day}`, { score: ts, member: entry.id }),\n this.kv.zadd(`${this.prefix}actor:${entry.actor}`, { score: ts, member: entry.id }),\n ...(entry.tenantId\n ? [\n this.kv.zadd(`${this.prefix}tenant:${entry.tenantId}`, {\n score: ts,\n member: entry.id,\n }),\n ]\n : []),\n ]);\n }\n\n async query(filter: {\n actor?: string;\n operation?: AuditOperation;\n tenantId?: string;\n from?: string;\n to?: string;\n limit?: number;\n }): Promise<AuditEntry[]> {\n const limit = filter.limit ?? 100;\n let ids: string[];\n\n // Pick the most selective index available\n if (filter.actor) {\n ids = await this.zrangeByScore(\n `${this.prefix}actor:${filter.actor}`,\n filter.from,\n filter.to,\n limit,\n );\n } else if (filter.tenantId) {\n ids = await this.zrangeByScore(\n `${this.prefix}tenant:${filter.tenantId}`,\n filter.from,\n filter.to,\n limit,\n );\n } else if (filter.from || filter.to) {\n // Walk daily buckets for the date range\n const fromDate = filter.from?.slice(0, 10) ?? \"0000-00-00\";\n const toDate = filter.to?.slice(0, 10) ?? \"9999-99-99\";\n ids = [];\n // Cap walk to ~1 year max to avoid runaway\n const fromTs = new Date(fromDate).getTime();\n const toTs = new Date(toDate).getTime();\n for (let d = fromTs; d <= toTs && ids.length < limit; d += 86_400_000) {\n const day = new Date(d).toISOString().slice(0, 10);\n const dayIds = await this.zrangeByScore(\n `${this.prefix}day:${day}`,\n filter.from,\n filter.to,\n limit - ids.length,\n );\n ids.push(...dayIds);\n }\n } else {\n // No filter — bail (full scan would be unbounded)\n return [];\n }\n\n // Load entries\n const entries: AuditEntry[] = [];\n for (const id of ids) {\n const entry = await this.kv.get<AuditEntry>(`${this.prefix}entry:${id}`);\n if (!entry) continue;\n if (filter.operation && entry.operation !== filter.operation) continue;\n entries.push(entry);\n }\n return entries;\n }\n\n private async zrangeByScore(\n key: string,\n from?: string,\n to?: string,\n limit?: number,\n ): Promise<string[]> {\n const min = from ? new Date(from).getTime() : 0;\n const max = to ? new Date(to).getTime() : Number.MAX_SAFE_INTEGER;\n const opts = {\n byScore: true as const,\n offset: 0,\n ...(limit !== undefined ? { count: limit } : { count: 100 }),\n };\n const ids = await this.kv.zrange(key, min, max, opts);\n return ids.map(String);\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/vercel-kv.ts"],"names":["defaultKv"],"mappings":"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","file":"vercel-kv.cjs","sourcesContent":["/**\n * Vercel KV adapters — drop-in `SubscriptionStateAdapter`,\n * `OAuthTokenStore`, and `IdempotencyCache` implementations backed by\n * [Vercel KV](https://vercel.com/docs/storage/vercel-kv) (Upstash Redis).\n *\n * # Why a separate subpath?\n *\n * `@vercel/kv` is a peer dependency — only consumers who actually use Vercel\n * KV install it. Importing from `@ar-agents/mercadopago/vercel-kv` is\n * lazy: the main `@ar-agents/mercadopago` bundle stays tiny for callers who\n * use the in-memory adapters or a different store.\n *\n * # Setup\n *\n * 1. Create a KV store at https://vercel.com/dashboard/stores\n * 2. Connect it to your project — Vercel auto-injects `KV_*` env vars\n * 3. `pnpm add @vercel/kv`\n * 4. Wire the adapters:\n *\n * ```ts\n * import { mercadoPagoTools, MercadoPagoClient } from \"@ar-agents/mercadopago\";\n * import {\n * VercelKVSubscriptionStateAdapter,\n * VercelKVOAuthTokenStore,\n * } from \"@ar-agents/mercadopago/vercel-kv\";\n *\n * const tools = mercadoPagoTools(client, {\n * state: new VercelKVSubscriptionStateAdapter(),\n * backUrl: \"https://mysite.com/done\",\n * // ... oauth, webhookSecret, etc.\n * });\n *\n * // For marketplace flows, also wire the OAuth token store:\n * const oauthStore = new VercelKVOAuthTokenStore();\n * await oauthStore.set(token.user_id, {\n * user_id: token.user_id,\n * access_token: token.access_token,\n * refresh_token: token.refresh_token!,\n * expires_at: Date.now() + (token.expires_in ?? 21600) * 1000,\n * });\n * ```\n *\n * # Edge Runtime\n *\n * `@vercel/kv` works in Vercel Edge Runtime, Node.js, and any environment\n * with `fetch` (it's a thin REST client over Upstash). All adapters here\n * are async and Edge-safe.\n *\n * # Key namespacing\n *\n * Each adapter uses its own prefix so multiple adapters can share the same\n * KV store without collisions:\n * - Subscriptions: `mp:sub:{id}`\n * - OAuth tokens: `mp:oauth:{userId}`\n * - Idempotency: `mp:idem:{key}`\n *\n * Pass a custom prefix via the constructor if you need to share the store\n * with other apps.\n */\n\nimport { kv as defaultKv } from \"@vercel/kv\";\nimport type { VercelKV } from \"@vercel/kv\";\nimport type { AuditEntry, AuditLogAdapter, AuditOperation } from \"./audit\";\nimport type {\n IdempotencyCache,\n OAuthTokenRecord,\n OAuthTokenStore,\n SubscriptionStateAdapter,\n SubscriptionStateRecord,\n} from \"./state\";\n\nconst DEFAULT_SUBSCRIPTION_PREFIX = \"mp:sub:\";\nconst DEFAULT_OAUTH_PREFIX = \"mp:oauth:\";\nconst DEFAULT_IDEMPOTENCY_PREFIX = \"mp:idem:\";\nconst DEFAULT_AUDIT_PREFIX = \"mp:audit:\";\n\ninterface VercelKVAdapterOptions {\n /**\n * Custom KV client. If omitted, uses the default `kv` export from\n * `@vercel/kv` (which reads `KV_REST_API_URL` + `KV_REST_API_TOKEN` from\n * env — auto-injected when you connect a KV store to your Vercel project).\n */\n kv?: VercelKV;\n /** Override the key prefix. */\n prefix?: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// SubscriptionStateAdapter\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVSubscriptionStateAdapter\n implements SubscriptionStateAdapter\n{\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly indexKey: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_SUBSCRIPTION_PREFIX;\n this.indexKey = `${this.prefix}__index`;\n }\n\n private key(id: string): string {\n return `${this.prefix}${id}`;\n }\n\n async set(\n id: string,\n state: Partial<SubscriptionStateRecord>,\n ): Promise<void> {\n const existing = (await this.kv.get<SubscriptionStateRecord>(this.key(id))) ?? {};\n await this.kv.set(this.key(id), { ...existing, ...state });\n await this.kv.sadd(this.indexKey, id);\n }\n\n async get(id: string): Promise<SubscriptionStateRecord | null> {\n return (await this.kv.get<SubscriptionStateRecord>(this.key(id))) ?? null;\n }\n\n async list(): Promise<string[]> {\n const ids = await this.kv.smembers(this.indexKey);\n return ids.map(String);\n }\n\n /** Forget a subscription record. NOT part of the adapter interface. */\n async delete(id: string): Promise<void> {\n await this.kv.del(this.key(id));\n await this.kv.srem(this.indexKey, id);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// OAuthTokenStore (per-seller marketplace token persistence)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVOAuthTokenStore implements OAuthTokenStore {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly indexKey: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_OAUTH_PREFIX;\n this.indexKey = `${this.prefix}__index`;\n }\n\n private key(userId: string): string {\n return `${this.prefix}${userId}`;\n }\n\n async set(userId: string, token: OAuthTokenRecord): Promise<void> {\n await this.kv.set(this.key(userId), token);\n await this.kv.sadd(this.indexKey, userId);\n }\n\n async get(userId: string): Promise<OAuthTokenRecord | null> {\n return (await this.kv.get<OAuthTokenRecord>(this.key(userId))) ?? null;\n }\n\n async delete(userId: string): Promise<void> {\n await this.kv.del(this.key(userId));\n await this.kv.srem(this.indexKey, userId);\n }\n\n async list(): Promise<string[]> {\n const ids = await this.kv.smembers(this.indexKey);\n return ids.map(String);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Distributed Token Bucket Rate Limiter (KV-backed)\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst DEFAULT_RATELIMIT_PREFIX = \"mp:rl:\";\n\n/**\n * Distributed token bucket rate limiter backed by Vercel KV.\n *\n * # Why distributed\n *\n * The default in-memory `TokenBucketRateLimiter` is per-process. In\n * serverless (Vercel Functions, Lambda, Cloudflare Workers), each cold\n * start gets its own bucket — meaning N concurrent instances effectively\n * have N×capacity. For multi-region deployments or marketplace setups\n * with shared MP rate budget, that's a footgun.\n *\n * This adapter uses a single Vercel KV (Upstash Redis) bucket per `key`,\n * shared across all instances. Two instances acquiring at the same time\n * decrement the same counter atomically — the rate limit holds globally.\n *\n * # Algorithm\n *\n * Standard token bucket with lazy refill. Each `acquire()` / `tryAcquire()`\n * runs a single server-side Lua script ({@link LUA_CONSUME}) that, in one\n * atomic Redis execution:\n * 1. Reads `{ tokens, lastRefill }` from KV\n * 2. Computes refill since `lastRefill`\n * 3. If tokens >= 1: decrements and writes back\n * 4. Otherwise: returns the refilled count so the caller can compute its wait\n *\n * Because the refill → check → decrement → write happens inside one Lua\n * script, the decrement is atomic across all instances: concurrent callers can\n * never both consume the same token, so the global limit holds exactly (no\n * over-spend window). `learnFromHeaders` uses the same atomic primitive.\n *\n * # Usage — wire via `withRateLimit` middleware\n *\n * `MercadoPagoClient` does not accept a rate limiter directly. Apply the\n * limiter at the tool layer using `withRateLimit` from the middleware\n * module, which works for both the in-memory `TokenBucketRateLimiter` and\n * this distributed variant.\n *\n * ```ts\n * import {\n * MercadoPagoClient,\n * mercadoPagoTools,\n * InMemoryStateAdapter,\n * applyToAllTools,\n * withRateLimit,\n * } from \"@ar-agents/mercadopago\";\n * import { VercelKVRateLimiter } from \"@ar-agents/mercadopago/vercel-kv\";\n *\n * // ONE distributed bucket shared across all serverless instances of this app:\n * const limiter = new VercelKVRateLimiter({\n * key: \"mp-account-prod\",\n * capacity: 50,\n * refillPerSecond: 25,\n * });\n *\n * const client = new MercadoPagoClient({ accessToken: process.env.MP_ACCESS_TOKEN! });\n * const tools = applyToAllTools(\n * mercadoPagoTools(client, { state: new InMemoryStateAdapter(), backUrl: \"...\" }),\n * withRateLimit(limiter),\n * );\n * ```\n *\n * # Concurrency\n *\n * Token consumption is atomic (one Upstash `EVAL` Lua script per acquire), so\n * the configured limit holds exactly across all serverless instances even\n * under heavy concurrent contention — there is no over-spend window. The\n * `acquire()` retry loop still applies randomized jitter (±30%) to spread\n * waiting acquirers across refill windows, mitigating the thundering-herd that\n * would otherwise hit Upstash the instant a bucket refills.\n *\n * Note this caps **count** of calls, not monetary spend. For a hard money\n * budget, enforce it at the payment layer (amount checks + idempotency), not\n * with a request-rate limiter.\n *\n * # Marketplace setups (per-seller rate limit)\n *\n * Use the seller's MP user_id as part of the `key`:\n *\n * ```ts\n * function makeLimiter(sellerUserId: string) {\n * return new VercelKVRateLimiter({\n * key: `mp-seller-${sellerUserId}`,\n * capacity: 10,\n * refillPerSecond: 5,\n * });\n * }\n * ```\n *\n * Each seller now has their own globally-distributed bucket.\n */\nexport interface VercelKVRateLimiterOptions extends VercelKVAdapterOptions {\n /**\n * Unique key for this bucket. Use distinct keys per logical \"rate-limit\n * scope\" (per-environment, per-seller, per-region, etc.). Required.\n */\n key: string;\n /** Bucket capacity (max burst). Default 50. */\n capacity?: number;\n /** Refill rate in tokens per second. Default 25. */\n refillPerSecond?: number;\n /**\n * Hard cap on how long `acquire()` will wait. If the bucket can't\n * refill in this time, `acquire()` throws. Default 30s.\n */\n acquireTimeoutMs?: number;\n /**\n * If true, `learnFromHeaders` syncs the bucket with MP's stated\n * `x-rate-limit-remaining`. Default true.\n */\n adaptive?: boolean;\n}\n\ninterface BucketState {\n tokens: number;\n lastRefillMs: number;\n}\n\n/** Bucket TTL (seconds). Long-idle buckets are GC'd; capacity rebuilds. */\nconst RATELIMIT_TTL_SECONDS = 3600;\n\n/**\n * Atomic token-bucket consume, executed server-side as a single Redis Lua\n * script so the read → refill → check → decrement → write sequence cannot\n * interleave across concurrent callers (the previous JS-side read-modify-write\n * let N concurrent acquirers all observe the same lone token and each succeed).\n *\n * KEYS[1] = bucket key.\n * ARGV = [capacity, refillPerSecond, nowMs, cost, ttlSeconds].\n * Returns [allowed (0|1), tokensRemaining (string)]. Persists ONLY on success\n * (a denied attempt leaves lastRefill untouched, so accrual stays continuous).\n *\n * State is a JSON string compatible with `@upstash/redis`'s auto (de)serializer\n * — `cjson` is available in Upstash's Lua runtime.\n */\nconst LUA_CONSUME = `-- @op:consume\nlocal raw = redis.call('GET', KEYS[1])\nlocal capacity = tonumber(ARGV[1])\nlocal refillPerSecond = tonumber(ARGV[2])\nlocal now = tonumber(ARGV[3])\nlocal cost = tonumber(ARGV[4])\nlocal ttl = tonumber(ARGV[5])\nlocal tokens = capacity\nlocal lastRefill = now\nif raw then\n local ok, data = pcall(cjson.decode, raw)\n if ok and type(data) == 'table' and type(data.tokens) == 'number' and type(data.lastRefillMs) == 'number' then\n tokens = data.tokens\n lastRefill = data.lastRefillMs\n end\nend\nlocal elapsed = now - lastRefill\nif elapsed < 0 then elapsed = 0 end\ntokens = math.min(capacity, tokens + (elapsed / 1000.0) * refillPerSecond)\nlocal allowed = 0\nif tokens >= cost then\n tokens = tokens - cost\n allowed = 1\n redis.call('SET', KEYS[1], cjson.encode({tokens = tokens, lastRefillMs = now}), 'EX', ttl)\nend\nreturn {allowed, tostring(tokens)}`;\n\n/**\n * Atomic adaptive clamp for `learnFromHeaders`: refill, then lower the bucket\n * to MP's stated remaining if that is smaller. Same atomicity guarantee as\n * {@link LUA_CONSUME} so a concurrent acquire can't race the clamp.\n *\n * KEYS[1] = bucket key.\n * ARGV = [capacity, refillPerSecond, nowMs, remaining, ttlSeconds].\n * Returns tokensRemaining (string).\n */\nconst LUA_CLAMP = `-- @op:clamp\nlocal raw = redis.call('GET', KEYS[1])\nlocal capacity = tonumber(ARGV[1])\nlocal refillPerSecond = tonumber(ARGV[2])\nlocal now = tonumber(ARGV[3])\nlocal remaining = tonumber(ARGV[4])\nlocal ttl = tonumber(ARGV[5])\nlocal tokens = capacity\nlocal lastRefill = now\nif raw then\n local ok, data = pcall(cjson.decode, raw)\n if ok and type(data) == 'table' and type(data.tokens) == 'number' and type(data.lastRefillMs) == 'number' then\n tokens = data.tokens\n lastRefill = data.lastRefillMs\n end\nend\nlocal elapsed = now - lastRefill\nif elapsed < 0 then elapsed = 0 end\ntokens = math.min(capacity, tokens + (elapsed / 1000.0) * refillPerSecond)\nif remaining < tokens then\n if remaining < 0 then remaining = 0 end\n tokens = remaining\nend\nredis.call('SET', KEYS[1], cjson.encode({tokens = tokens, lastRefillMs = now}), 'EX', ttl)\nreturn tostring(tokens)`;\n\nexport class VercelKVRateLimiter {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly key: string;\n private readonly capacity: number;\n private readonly refillPerSecond: number;\n private readonly acquireTimeoutMs: number;\n private readonly adaptive: boolean;\n\n constructor(options: VercelKVRateLimiterOptions) {\n if (!options.key) {\n throw new Error(\n \"VercelKVRateLimiter requires a `key` (use distinct keys per rate-limit scope, e.g., per-environment or per-seller).\",\n );\n }\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_RATELIMIT_PREFIX;\n this.key = options.key;\n this.capacity = options.capacity ?? 50;\n this.refillPerSecond = options.refillPerSecond ?? 25;\n this.acquireTimeoutMs = options.acquireTimeoutMs ?? 30_000;\n this.adaptive = options.adaptive ?? true;\n }\n\n private fullKey(): string {\n return `${this.prefix}${this.key}`;\n }\n\n private async readState(): Promise<BucketState> {\n const stored = await this.kv.get<BucketState>(this.fullKey());\n if (stored && typeof stored.tokens === \"number\" && typeof stored.lastRefillMs === \"number\") {\n return stored;\n }\n return { tokens: this.capacity, lastRefillMs: Date.now() };\n }\n\n private refill(state: BucketState, nowMs: number): BucketState {\n const elapsedMs = Math.max(0, nowMs - state.lastRefillMs);\n const refilled = Math.min(\n this.capacity,\n state.tokens + (elapsedMs / 1000) * this.refillPerSecond,\n );\n return { tokens: refilled, lastRefillMs: nowMs };\n }\n\n private async writeState(state: BucketState): Promise<void> {\n // TTL = 1h. Long-idle buckets get garbage-collected, capacity rebuilds\n // from initial state on next acquire (which is fine — at the right rate).\n await this.kv.set(this.fullKey(), state, { ex: RATELIMIT_TTL_SECONDS });\n }\n\n /**\n * Atomically refill + conditionally consume `cost` tokens via a single\n * server-side Lua script ({@link LUA_CONSUME}). This is the enforcement\n * primitive: because Redis runs the script atomically, concurrent callers\n * can never both consume the same token.\n */\n private async consume(\n cost: number,\n ): Promise<{ allowed: boolean; tokens: number }> {\n const now = Date.now();\n const res = (await this.kv.eval(\n LUA_CONSUME,\n [this.fullKey()],\n [this.capacity, this.refillPerSecond, now, cost, RATELIMIT_TTL_SECONDS],\n )) as [number | string, number | string];\n return { allowed: Number(res[0]) === 1, tokens: Number(res[1]) };\n }\n\n /**\n * Acquire a token. Resolves immediately if the distributed bucket has\n * one available; otherwise waits until refilled. Throws if the wait\n * exceeds `acquireTimeoutMs` or if the retry cap is reached.\n *\n * Each attempt consumes via the atomic {@link LUA_CONSUME} script, so the\n * limit holds globally even under heavy concurrent contention.\n *\n * Caps retries at 8 iterations so a misconfigured bucket (capacity too\n * low for traffic) fails fast for the agent layer to surface, instead\n * of silently burning serverless compute time.\n */\n async acquire(): Promise<void> {\n const start = Date.now();\n const MAX_RETRIES = 8;\n let attempt = 0;\n while (attempt < MAX_RETRIES) {\n const { allowed, tokens } = await this.consume(1);\n if (allowed) return;\n\n // Compute wait time until next token. Cap at remaining timeout budget.\n const tokensNeeded = 1 - tokens;\n const baseWaitMs = Math.ceil((tokensNeeded / this.refillPerSecond) * 1000);\n // Randomized jitter (±30%) prevents thundering herd: without it, all\n // concurrent acquirers compute identical waitMs, sleep identical\n // duration, and wake at the same wall-clock instant — then all hit\n // KV simultaneously. Jitter spreads them across the refill window.\n const jitterFactor = 0.7 + Math.random() * 0.6; // 0.7–1.3\n const waitMs = Math.ceil(baseWaitMs * jitterFactor);\n const elapsed = Date.now() - start;\n if (elapsed + waitMs > this.acquireTimeoutMs) {\n throw new Error(\n `VercelKVRateLimiter acquire timed out after ${elapsed + waitMs}ms (key=${this.key}).`,\n );\n }\n await new Promise((resolve) => setTimeout(resolve, waitMs));\n attempt += 1;\n }\n throw new Error(\n `VercelKVRateLimiter exhausted ${MAX_RETRIES} retries (key=${this.key}). ` +\n `Bucket likely undersized for traffic — increase capacity or refillPerSecond.`,\n );\n }\n\n /** Best-effort acquire — returns true if a token was available, false otherwise. */\n async tryAcquire(): Promise<boolean> {\n const { allowed } = await this.consume(1);\n return allowed;\n }\n\n /**\n * Adaptive learning — call after each MP API response. If MP's stated\n * `x-rate-limit-remaining` is lower than our local count, trust MP and\n * drop the bucket to match (prevents over-spending). Applied atomically via\n * {@link LUA_CLAMP} so it can't race a concurrent acquire.\n */\n async learnFromHeaders(headers: {\n remaining: number | null;\n resetSeconds: number | null;\n }): Promise<void> {\n if (!this.adaptive) return;\n if (headers.remaining === null) return;\n await this.kv.eval(\n LUA_CLAMP,\n [this.fullKey()],\n [\n this.capacity,\n this.refillPerSecond,\n Date.now(),\n headers.remaining,\n RATELIMIT_TTL_SECONDS,\n ],\n );\n }\n\n /** Inspect bucket state. */\n async getStats(): Promise<{ tokens: number; capacity: number; refillPerSecond: number }> {\n const state = this.refill(await this.readState(), Date.now());\n return {\n tokens: state.tokens,\n capacity: this.capacity,\n refillPerSecond: this.refillPerSecond,\n };\n }\n\n /** Reset the bucket to full. Use sparingly (e.g., after a known-clean window). */\n async reset(): Promise<void> {\n await this.writeState({ tokens: this.capacity, lastRefillMs: Date.now() });\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// IdempotencyCache (KV-backed dedup of agent retries)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVIdempotencyCache implements IdempotencyCache {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_IDEMPOTENCY_PREFIX;\n }\n\n private key(k: string): string {\n return `${this.prefix}${k}`;\n }\n\n async get<T>(key: string): Promise<T | null> {\n return (await this.kv.get<T>(this.key(key))) ?? null;\n }\n\n async set<T>(key: string, value: T, ttlSeconds = 86_400): Promise<void> {\n // Vercel KV's `set` supports a TTL in seconds via the `ex` option.\n await this.kv.set(this.key(key), value, { ex: ttlSeconds });\n }\n\n async delete(key: string): Promise<void> {\n await this.kv.del(this.key(key));\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// AuditLogAdapter — production audit trail with daily-bucket indexing\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Vercel KV–backed audit log adapter. Stores each entry under\n * `mp:audit:entry:{id}` AND adds the id to a daily index sorted set\n * `mp:audit:day:{YYYY-MM-DD}` (score = timestamp ms). This gives O(log N)\n * time-range queries (\"all entries from May 1 to May 5\") without scanning\n * the entire log.\n *\n * # Storage layout\n *\n * - `mp:audit:entry:{id}` → the full entry JSON\n * - `mp:audit:day:{YYYY-MM-DD}` → ZSET of entry ids by timestamp (ms)\n * - `mp:audit:actor:{actor}` → ZSET of entry ids by timestamp (for \"all\n * entries by actor X\")\n * - `mp:audit:tenant:{tenantId}` → same, by tenant\n *\n * # Cost considerations\n *\n * Each `append()` does 1-3 KV writes (entry + 1-2 indexes). For high-traffic\n * deployments (>10/s sustained), batch via your own queue (e.g., Vercel\n * Queues with daily flush) and provide a custom adapter that batches.\n */\nexport class VercelKVAuditLog implements AuditLogAdapter {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_AUDIT_PREFIX;\n }\n\n async append(entry: AuditEntry): Promise<void> {\n const ts = new Date(entry.timestamp).getTime();\n const day = entry.timestamp.slice(0, 10); // YYYY-MM-DD\n await Promise.all([\n this.kv.set(`${this.prefix}entry:${entry.id}`, entry),\n this.kv.zadd(`${this.prefix}day:${day}`, { score: ts, member: entry.id }),\n this.kv.zadd(`${this.prefix}actor:${entry.actor}`, { score: ts, member: entry.id }),\n ...(entry.tenantId\n ? [\n this.kv.zadd(`${this.prefix}tenant:${entry.tenantId}`, {\n score: ts,\n member: entry.id,\n }),\n ]\n : []),\n ]);\n }\n\n async query(filter: {\n actor?: string;\n operation?: AuditOperation;\n tenantId?: string;\n from?: string;\n to?: string;\n limit?: number;\n }): Promise<AuditEntry[]> {\n const limit = filter.limit ?? 100;\n let ids: string[];\n\n // Pick the most selective index available\n if (filter.actor) {\n ids = await this.zrangeByScore(\n `${this.prefix}actor:${filter.actor}`,\n filter.from,\n filter.to,\n limit,\n );\n } else if (filter.tenantId) {\n ids = await this.zrangeByScore(\n `${this.prefix}tenant:${filter.tenantId}`,\n filter.from,\n filter.to,\n limit,\n );\n } else if (filter.from || filter.to) {\n // Walk daily buckets for the date range\n const fromDate = filter.from?.slice(0, 10) ?? \"0000-00-00\";\n const toDate = filter.to?.slice(0, 10) ?? \"9999-99-99\";\n ids = [];\n // Cap walk to ~1 year max to avoid runaway\n const fromTs = new Date(fromDate).getTime();\n const toTs = new Date(toDate).getTime();\n for (let d = fromTs; d <= toTs && ids.length < limit; d += 86_400_000) {\n const day = new Date(d).toISOString().slice(0, 10);\n const dayIds = await this.zrangeByScore(\n `${this.prefix}day:${day}`,\n filter.from,\n filter.to,\n limit - ids.length,\n );\n ids.push(...dayIds);\n }\n } else {\n // No filter — bail (full scan would be unbounded)\n return [];\n }\n\n // Load entries\n const entries: AuditEntry[] = [];\n for (const id of ids) {\n const entry = await this.kv.get<AuditEntry>(`${this.prefix}entry:${id}`);\n if (!entry) continue;\n if (filter.operation && entry.operation !== filter.operation) continue;\n entries.push(entry);\n }\n return entries;\n }\n\n private async zrangeByScore(\n key: string,\n from?: string,\n to?: string,\n limit?: number,\n ): Promise<string[]> {\n const min = from ? new Date(from).getTime() : 0;\n const max = to ? new Date(to).getTime() : Number.MAX_SAFE_INTEGER;\n const opts = {\n byScore: true as const,\n offset: 0,\n ...(limit !== undefined ? { count: limit } : { count: 100 }),\n };\n const ids = await this.kv.zrange(key, min, max, opts);\n return ids.map(String);\n }\n}\n"]}
@@ -111,17 +111,18 @@ declare class VercelKVOAuthTokenStore implements OAuthTokenStore {
111
111
  *
112
112
  * # Algorithm
113
113
  *
114
- * Standard token bucket with lazy refill: every `acquire()` call:
114
+ * Standard token bucket with lazy refill. Each `acquire()` / `tryAcquire()`
115
+ * runs a single server-side Lua script ({@link LUA_CONSUME}) that, in one
116
+ * atomic Redis execution:
115
117
  * 1. Reads `{ tokens, lastRefill }` from KV
116
118
  * 2. Computes refill since `lastRefill`
117
119
  * 3. If tokens >= 1: decrements and writes back
118
- * 4. Otherwise: computes wait time, sleeps, retries
120
+ * 4. Otherwise: returns the refilled count so the caller can compute its wait
119
121
  *
120
- * The read-modify-write isn't atomic per-call, so under heavy contention
121
- * a small over-spend window is possible (worst case: ~N concurrent
122
- * acquires can succeed when only 1 token was available). Acceptable for
123
- * MP rate limiting the "actual" budget is much higher than what we
124
- * provision.
122
+ * Because the refill check decrement write happens inside one Lua
123
+ * script, the decrement is atomic across all instances: concurrent callers can
124
+ * never both consume the same token, so the global limit holds exactly (no
125
+ * over-spend window). `learnFromHeaders` uses the same atomic primitive.
125
126
  *
126
127
  * # Usage — wire via `withRateLimit` middleware
127
128
  *
@@ -154,20 +155,18 @@ declare class VercelKVOAuthTokenStore implements OAuthTokenStore {
154
155
  * );
155
156
  * ```
156
157
  *
157
- * # Concurrency caveats
158
+ * # Concurrency
158
159
  *
159
- * Read-modify-write is NOT strictly atomic per `acquire()`. Under heavy
160
- * contention a small over-spend window is possible, acceptable for **API
161
- * call** rate limiting where the actual MP budget exceeds what we
162
- * provision. **Do NOT repurpose this limiter as a money/spend cap** —
163
- * the over-spend window means you could exceed a money budget by the
164
- * concurrent-instance count × per-call cost. For money budgets, use
165
- * Upstash's `EVAL`-based atomic Lua script (or a stricter primitive).
160
+ * Token consumption is atomic (one Upstash `EVAL` Lua script per acquire), so
161
+ * the configured limit holds exactly across all serverless instances even
162
+ * under heavy concurrent contention there is no over-spend window. The
163
+ * `acquire()` retry loop still applies randomized jitter (±30%) to spread
164
+ * waiting acquirers across refill windows, mitigating the thundering-herd that
165
+ * would otherwise hit Upstash the instant a bucket refills.
166
166
  *
167
- * The `acquire()` retry loop applies randomized jitter (±30%) to spread
168
- * concurrent acquirers across multiple refill windows, mitigating the
169
- * thundering-herd that would otherwise hit Upstash with N reads + N
170
- * writes the instant a bucket refills.
167
+ * Note this caps **count** of calls, not monetary spend. For a hard money
168
+ * budget, enforce it at the payment layer (amount checks + idempotency), not
169
+ * with a request-rate limiter.
171
170
  *
172
171
  * # Marketplace setups (per-seller rate limit)
173
172
  *
@@ -219,11 +218,21 @@ declare class VercelKVRateLimiter {
219
218
  private readState;
220
219
  private refill;
221
220
  private writeState;
221
+ /**
222
+ * Atomically refill + conditionally consume `cost` tokens via a single
223
+ * server-side Lua script ({@link LUA_CONSUME}). This is the enforcement
224
+ * primitive: because Redis runs the script atomically, concurrent callers
225
+ * can never both consume the same token.
226
+ */
227
+ private consume;
222
228
  /**
223
229
  * Acquire a token. Resolves immediately if the distributed bucket has
224
230
  * one available; otherwise waits until refilled. Throws if the wait
225
231
  * exceeds `acquireTimeoutMs` or if the retry cap is reached.
226
232
  *
233
+ * Each attempt consumes via the atomic {@link LUA_CONSUME} script, so the
234
+ * limit holds globally even under heavy concurrent contention.
235
+ *
227
236
  * Caps retries at 8 iterations so a misconfigured bucket (capacity too
228
237
  * low for traffic) fails fast for the agent layer to surface, instead
229
238
  * of silently burning serverless compute time.
@@ -234,7 +243,8 @@ declare class VercelKVRateLimiter {
234
243
  /**
235
244
  * Adaptive learning — call after each MP API response. If MP's stated
236
245
  * `x-rate-limit-remaining` is lower than our local count, trust MP and
237
- * drop the bucket to match (prevents over-spending).
246
+ * drop the bucket to match (prevents over-spending). Applied atomically via
247
+ * {@link LUA_CLAMP} so it can't race a concurrent acquire.
238
248
  */
239
249
  learnFromHeaders(headers: {
240
250
  remaining: number | null;
@@ -111,17 +111,18 @@ declare class VercelKVOAuthTokenStore implements OAuthTokenStore {
111
111
  *
112
112
  * # Algorithm
113
113
  *
114
- * Standard token bucket with lazy refill: every `acquire()` call:
114
+ * Standard token bucket with lazy refill. Each `acquire()` / `tryAcquire()`
115
+ * runs a single server-side Lua script ({@link LUA_CONSUME}) that, in one
116
+ * atomic Redis execution:
115
117
  * 1. Reads `{ tokens, lastRefill }` from KV
116
118
  * 2. Computes refill since `lastRefill`
117
119
  * 3. If tokens >= 1: decrements and writes back
118
- * 4. Otherwise: computes wait time, sleeps, retries
120
+ * 4. Otherwise: returns the refilled count so the caller can compute its wait
119
121
  *
120
- * The read-modify-write isn't atomic per-call, so under heavy contention
121
- * a small over-spend window is possible (worst case: ~N concurrent
122
- * acquires can succeed when only 1 token was available). Acceptable for
123
- * MP rate limiting the "actual" budget is much higher than what we
124
- * provision.
122
+ * Because the refill check decrement write happens inside one Lua
123
+ * script, the decrement is atomic across all instances: concurrent callers can
124
+ * never both consume the same token, so the global limit holds exactly (no
125
+ * over-spend window). `learnFromHeaders` uses the same atomic primitive.
125
126
  *
126
127
  * # Usage — wire via `withRateLimit` middleware
127
128
  *
@@ -154,20 +155,18 @@ declare class VercelKVOAuthTokenStore implements OAuthTokenStore {
154
155
  * );
155
156
  * ```
156
157
  *
157
- * # Concurrency caveats
158
+ * # Concurrency
158
159
  *
159
- * Read-modify-write is NOT strictly atomic per `acquire()`. Under heavy
160
- * contention a small over-spend window is possible, acceptable for **API
161
- * call** rate limiting where the actual MP budget exceeds what we
162
- * provision. **Do NOT repurpose this limiter as a money/spend cap** —
163
- * the over-spend window means you could exceed a money budget by the
164
- * concurrent-instance count × per-call cost. For money budgets, use
165
- * Upstash's `EVAL`-based atomic Lua script (or a stricter primitive).
160
+ * Token consumption is atomic (one Upstash `EVAL` Lua script per acquire), so
161
+ * the configured limit holds exactly across all serverless instances even
162
+ * under heavy concurrent contention there is no over-spend window. The
163
+ * `acquire()` retry loop still applies randomized jitter (±30%) to spread
164
+ * waiting acquirers across refill windows, mitigating the thundering-herd that
165
+ * would otherwise hit Upstash the instant a bucket refills.
166
166
  *
167
- * The `acquire()` retry loop applies randomized jitter (±30%) to spread
168
- * concurrent acquirers across multiple refill windows, mitigating the
169
- * thundering-herd that would otherwise hit Upstash with N reads + N
170
- * writes the instant a bucket refills.
167
+ * Note this caps **count** of calls, not monetary spend. For a hard money
168
+ * budget, enforce it at the payment layer (amount checks + idempotency), not
169
+ * with a request-rate limiter.
171
170
  *
172
171
  * # Marketplace setups (per-seller rate limit)
173
172
  *
@@ -219,11 +218,21 @@ declare class VercelKVRateLimiter {
219
218
  private readState;
220
219
  private refill;
221
220
  private writeState;
221
+ /**
222
+ * Atomically refill + conditionally consume `cost` tokens via a single
223
+ * server-side Lua script ({@link LUA_CONSUME}). This is the enforcement
224
+ * primitive: because Redis runs the script atomically, concurrent callers
225
+ * can never both consume the same token.
226
+ */
227
+ private consume;
222
228
  /**
223
229
  * Acquire a token. Resolves immediately if the distributed bucket has
224
230
  * one available; otherwise waits until refilled. Throws if the wait
225
231
  * exceeds `acquireTimeoutMs` or if the retry cap is reached.
226
232
  *
233
+ * Each attempt consumes via the atomic {@link LUA_CONSUME} script, so the
234
+ * limit holds globally even under heavy concurrent contention.
235
+ *
227
236
  * Caps retries at 8 iterations so a misconfigured bucket (capacity too
228
237
  * low for traffic) fails fast for the agent layer to surface, instead
229
238
  * of silently burning serverless compute time.
@@ -234,7 +243,8 @@ declare class VercelKVRateLimiter {
234
243
  /**
235
244
  * Adaptive learning — call after each MP API response. If MP's stated
236
245
  * `x-rate-limit-remaining` is lower than our local count, trust MP and
237
- * drop the bucket to match (prevents over-spending).
246
+ * drop the bucket to match (prevents over-spending). Applied atomically via
247
+ * {@link LUA_CLAMP} so it can't race a concurrent acquire.
238
248
  */
239
249
  learnFromHeaders(headers: {
240
250
  remaining: number | null;
package/dist/vercel-kv.js CHANGED
@@ -64,6 +64,58 @@ var VercelKVOAuthTokenStore = class {
64
64
  }
65
65
  };
66
66
  var DEFAULT_RATELIMIT_PREFIX = "mp:rl:";
67
+ var RATELIMIT_TTL_SECONDS = 3600;
68
+ var LUA_CONSUME = `-- @op:consume
69
+ local raw = redis.call('GET', KEYS[1])
70
+ local capacity = tonumber(ARGV[1])
71
+ local refillPerSecond = tonumber(ARGV[2])
72
+ local now = tonumber(ARGV[3])
73
+ local cost = tonumber(ARGV[4])
74
+ local ttl = tonumber(ARGV[5])
75
+ local tokens = capacity
76
+ local lastRefill = now
77
+ if raw then
78
+ local ok, data = pcall(cjson.decode, raw)
79
+ if ok and type(data) == 'table' and type(data.tokens) == 'number' and type(data.lastRefillMs) == 'number' then
80
+ tokens = data.tokens
81
+ lastRefill = data.lastRefillMs
82
+ end
83
+ end
84
+ local elapsed = now - lastRefill
85
+ if elapsed < 0 then elapsed = 0 end
86
+ tokens = math.min(capacity, tokens + (elapsed / 1000.0) * refillPerSecond)
87
+ local allowed = 0
88
+ if tokens >= cost then
89
+ tokens = tokens - cost
90
+ allowed = 1
91
+ redis.call('SET', KEYS[1], cjson.encode({tokens = tokens, lastRefillMs = now}), 'EX', ttl)
92
+ end
93
+ return {allowed, tostring(tokens)}`;
94
+ var LUA_CLAMP = `-- @op:clamp
95
+ local raw = redis.call('GET', KEYS[1])
96
+ local capacity = tonumber(ARGV[1])
97
+ local refillPerSecond = tonumber(ARGV[2])
98
+ local now = tonumber(ARGV[3])
99
+ local remaining = tonumber(ARGV[4])
100
+ local ttl = tonumber(ARGV[5])
101
+ local tokens = capacity
102
+ local lastRefill = now
103
+ if raw then
104
+ local ok, data = pcall(cjson.decode, raw)
105
+ if ok and type(data) == 'table' and type(data.tokens) == 'number' and type(data.lastRefillMs) == 'number' then
106
+ tokens = data.tokens
107
+ lastRefill = data.lastRefillMs
108
+ end
109
+ end
110
+ local elapsed = now - lastRefill
111
+ if elapsed < 0 then elapsed = 0 end
112
+ tokens = math.min(capacity, tokens + (elapsed / 1000.0) * refillPerSecond)
113
+ if remaining < tokens then
114
+ if remaining < 0 then remaining = 0 end
115
+ tokens = remaining
116
+ end
117
+ redis.call('SET', KEYS[1], cjson.encode({tokens = tokens, lastRefillMs = now}), 'EX', ttl)
118
+ return tostring(tokens)`;
67
119
  var VercelKVRateLimiter = class {
68
120
  kv;
69
121
  prefix;
@@ -105,13 +157,31 @@ var VercelKVRateLimiter = class {
105
157
  return { tokens: refilled, lastRefillMs: nowMs };
106
158
  }
107
159
  async writeState(state) {
108
- await this.kv.set(this.fullKey(), state, { ex: 3600 });
160
+ await this.kv.set(this.fullKey(), state, { ex: RATELIMIT_TTL_SECONDS });
161
+ }
162
+ /**
163
+ * Atomically refill + conditionally consume `cost` tokens via a single
164
+ * server-side Lua script ({@link LUA_CONSUME}). This is the enforcement
165
+ * primitive: because Redis runs the script atomically, concurrent callers
166
+ * can never both consume the same token.
167
+ */
168
+ async consume(cost) {
169
+ const now = Date.now();
170
+ const res = await this.kv.eval(
171
+ LUA_CONSUME,
172
+ [this.fullKey()],
173
+ [this.capacity, this.refillPerSecond, now, cost, RATELIMIT_TTL_SECONDS]
174
+ );
175
+ return { allowed: Number(res[0]) === 1, tokens: Number(res[1]) };
109
176
  }
110
177
  /**
111
178
  * Acquire a token. Resolves immediately if the distributed bucket has
112
179
  * one available; otherwise waits until refilled. Throws if the wait
113
180
  * exceeds `acquireTimeoutMs` or if the retry cap is reached.
114
181
  *
182
+ * Each attempt consumes via the atomic {@link LUA_CONSUME} script, so the
183
+ * limit holds globally even under heavy concurrent contention.
184
+ *
115
185
  * Caps retries at 8 iterations so a misconfigured bucket (capacity too
116
186
  * low for traffic) fails fast for the agent layer to surface, instead
117
187
  * of silently burning serverless compute time.
@@ -121,18 +191,13 @@ var VercelKVRateLimiter = class {
121
191
  const MAX_RETRIES = 8;
122
192
  let attempt = 0;
123
193
  while (attempt < MAX_RETRIES) {
124
- const now = Date.now();
125
- const state = this.refill(await this.readState(), now);
126
- if (state.tokens >= 1) {
127
- state.tokens -= 1;
128
- await this.writeState(state);
129
- return;
130
- }
131
- const tokensNeeded = 1 - state.tokens;
194
+ const { allowed, tokens } = await this.consume(1);
195
+ if (allowed) return;
196
+ const tokensNeeded = 1 - tokens;
132
197
  const baseWaitMs = Math.ceil(tokensNeeded / this.refillPerSecond * 1e3);
133
198
  const jitterFactor = 0.7 + Math.random() * 0.6;
134
199
  const waitMs = Math.ceil(baseWaitMs * jitterFactor);
135
- const elapsed = now - start;
200
+ const elapsed = Date.now() - start;
136
201
  if (elapsed + waitMs > this.acquireTimeoutMs) {
137
202
  throw new Error(
138
203
  `VercelKVRateLimiter acquire timed out after ${elapsed + waitMs}ms (key=${this.key}).`
@@ -147,27 +212,29 @@ var VercelKVRateLimiter = class {
147
212
  }
148
213
  /** Best-effort acquire — returns true if a token was available, false otherwise. */
149
214
  async tryAcquire() {
150
- const state = this.refill(await this.readState(), Date.now());
151
- if (state.tokens >= 1) {
152
- state.tokens -= 1;
153
- await this.writeState(state);
154
- return true;
155
- }
156
- return false;
215
+ const { allowed } = await this.consume(1);
216
+ return allowed;
157
217
  }
158
218
  /**
159
219
  * Adaptive learning — call after each MP API response. If MP's stated
160
220
  * `x-rate-limit-remaining` is lower than our local count, trust MP and
161
- * drop the bucket to match (prevents over-spending).
221
+ * drop the bucket to match (prevents over-spending). Applied atomically via
222
+ * {@link LUA_CLAMP} so it can't race a concurrent acquire.
162
223
  */
163
224
  async learnFromHeaders(headers) {
164
225
  if (!this.adaptive) return;
165
226
  if (headers.remaining === null) return;
166
- const state = this.refill(await this.readState(), Date.now());
167
- if (headers.remaining < state.tokens) {
168
- state.tokens = Math.max(0, headers.remaining);
169
- await this.writeState(state);
170
- }
227
+ await this.kv.eval(
228
+ LUA_CLAMP,
229
+ [this.fullKey()],
230
+ [
231
+ this.capacity,
232
+ this.refillPerSecond,
233
+ Date.now(),
234
+ headers.remaining,
235
+ RATELIMIT_TTL_SECONDS
236
+ ]
237
+ );
171
238
  }
172
239
  /** Inspect bucket state. */
173
240
  async getStats() {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/vercel-kv.ts"],"names":["defaultKv"],"mappings":"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","file":"vercel-kv.js","sourcesContent":["/**\n * Vercel KV adapters — drop-in `SubscriptionStateAdapter`,\n * `OAuthTokenStore`, and `IdempotencyCache` implementations backed by\n * [Vercel KV](https://vercel.com/docs/storage/vercel-kv) (Upstash Redis).\n *\n * # Why a separate subpath?\n *\n * `@vercel/kv` is a peer dependency — only consumers who actually use Vercel\n * KV install it. Importing from `@ar-agents/mercadopago/vercel-kv` is\n * lazy: the main `@ar-agents/mercadopago` bundle stays tiny for callers who\n * use the in-memory adapters or a different store.\n *\n * # Setup\n *\n * 1. Create a KV store at https://vercel.com/dashboard/stores\n * 2. Connect it to your project — Vercel auto-injects `KV_*` env vars\n * 3. `pnpm add @vercel/kv`\n * 4. Wire the adapters:\n *\n * ```ts\n * import { mercadoPagoTools, MercadoPagoClient } from \"@ar-agents/mercadopago\";\n * import {\n * VercelKVSubscriptionStateAdapter,\n * VercelKVOAuthTokenStore,\n * } from \"@ar-agents/mercadopago/vercel-kv\";\n *\n * const tools = mercadoPagoTools(client, {\n * state: new VercelKVSubscriptionStateAdapter(),\n * backUrl: \"https://mysite.com/done\",\n * // ... oauth, webhookSecret, etc.\n * });\n *\n * // For marketplace flows, also wire the OAuth token store:\n * const oauthStore = new VercelKVOAuthTokenStore();\n * await oauthStore.set(token.user_id, {\n * user_id: token.user_id,\n * access_token: token.access_token,\n * refresh_token: token.refresh_token!,\n * expires_at: Date.now() + (token.expires_in ?? 21600) * 1000,\n * });\n * ```\n *\n * # Edge Runtime\n *\n * `@vercel/kv` works in Vercel Edge Runtime, Node.js, and any environment\n * with `fetch` (it's a thin REST client over Upstash). All adapters here\n * are async and Edge-safe.\n *\n * # Key namespacing\n *\n * Each adapter uses its own prefix so multiple adapters can share the same\n * KV store without collisions:\n * - Subscriptions: `mp:sub:{id}`\n * - OAuth tokens: `mp:oauth:{userId}`\n * - Idempotency: `mp:idem:{key}`\n *\n * Pass a custom prefix via the constructor if you need to share the store\n * with other apps.\n */\n\nimport { kv as defaultKv } from \"@vercel/kv\";\nimport type { VercelKV } from \"@vercel/kv\";\nimport type { AuditEntry, AuditLogAdapter, AuditOperation } from \"./audit\";\nimport type {\n IdempotencyCache,\n OAuthTokenRecord,\n OAuthTokenStore,\n SubscriptionStateAdapter,\n SubscriptionStateRecord,\n} from \"./state\";\n\nconst DEFAULT_SUBSCRIPTION_PREFIX = \"mp:sub:\";\nconst DEFAULT_OAUTH_PREFIX = \"mp:oauth:\";\nconst DEFAULT_IDEMPOTENCY_PREFIX = \"mp:idem:\";\nconst DEFAULT_AUDIT_PREFIX = \"mp:audit:\";\n\ninterface VercelKVAdapterOptions {\n /**\n * Custom KV client. If omitted, uses the default `kv` export from\n * `@vercel/kv` (which reads `KV_REST_API_URL` + `KV_REST_API_TOKEN` from\n * env — auto-injected when you connect a KV store to your Vercel project).\n */\n kv?: VercelKV;\n /** Override the key prefix. */\n prefix?: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// SubscriptionStateAdapter\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVSubscriptionStateAdapter\n implements SubscriptionStateAdapter\n{\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly indexKey: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_SUBSCRIPTION_PREFIX;\n this.indexKey = `${this.prefix}__index`;\n }\n\n private key(id: string): string {\n return `${this.prefix}${id}`;\n }\n\n async set(\n id: string,\n state: Partial<SubscriptionStateRecord>,\n ): Promise<void> {\n const existing = (await this.kv.get<SubscriptionStateRecord>(this.key(id))) ?? {};\n await this.kv.set(this.key(id), { ...existing, ...state });\n await this.kv.sadd(this.indexKey, id);\n }\n\n async get(id: string): Promise<SubscriptionStateRecord | null> {\n return (await this.kv.get<SubscriptionStateRecord>(this.key(id))) ?? null;\n }\n\n async list(): Promise<string[]> {\n const ids = await this.kv.smembers(this.indexKey);\n return ids.map(String);\n }\n\n /** Forget a subscription record. NOT part of the adapter interface. */\n async delete(id: string): Promise<void> {\n await this.kv.del(this.key(id));\n await this.kv.srem(this.indexKey, id);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// OAuthTokenStore (per-seller marketplace token persistence)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVOAuthTokenStore implements OAuthTokenStore {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly indexKey: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_OAUTH_PREFIX;\n this.indexKey = `${this.prefix}__index`;\n }\n\n private key(userId: string): string {\n return `${this.prefix}${userId}`;\n }\n\n async set(userId: string, token: OAuthTokenRecord): Promise<void> {\n await this.kv.set(this.key(userId), token);\n await this.kv.sadd(this.indexKey, userId);\n }\n\n async get(userId: string): Promise<OAuthTokenRecord | null> {\n return (await this.kv.get<OAuthTokenRecord>(this.key(userId))) ?? null;\n }\n\n async delete(userId: string): Promise<void> {\n await this.kv.del(this.key(userId));\n await this.kv.srem(this.indexKey, userId);\n }\n\n async list(): Promise<string[]> {\n const ids = await this.kv.smembers(this.indexKey);\n return ids.map(String);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Distributed Token Bucket Rate Limiter (KV-backed)\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst DEFAULT_RATELIMIT_PREFIX = \"mp:rl:\";\n\n/**\n * Distributed token bucket rate limiter backed by Vercel KV.\n *\n * # Why distributed\n *\n * The default in-memory `TokenBucketRateLimiter` is per-process. In\n * serverless (Vercel Functions, Lambda, Cloudflare Workers), each cold\n * start gets its own bucket — meaning N concurrent instances effectively\n * have N×capacity. For multi-region deployments or marketplace setups\n * with shared MP rate budget, that's a footgun.\n *\n * This adapter uses a single Vercel KV (Upstash Redis) bucket per `key`,\n * shared across all instances. Two instances acquiring at the same time\n * decrement the same counter atomically — the rate limit holds globally.\n *\n * # Algorithm\n *\n * Standard token bucket with lazy refill: every `acquire()` call:\n * 1. Reads `{ tokens, lastRefill }` from KV\n * 2. Computes refill since `lastRefill`\n * 3. If tokens >= 1: decrements and writes back\n * 4. Otherwise: computes wait time, sleeps, retries\n *\n * The read-modify-write isn't atomic per-call, so under heavy contention\n * a small over-spend window is possible (worst case: ~N concurrent\n * acquires can succeed when only 1 token was available). Acceptable for\n * MP rate limiting — the \"actual\" budget is much higher than what we\n * provision.\n *\n * # Usage — wire via `withRateLimit` middleware\n *\n * `MercadoPagoClient` does not accept a rate limiter directly. Apply the\n * limiter at the tool layer using `withRateLimit` from the middleware\n * module, which works for both the in-memory `TokenBucketRateLimiter` and\n * this distributed variant.\n *\n * ```ts\n * import {\n * MercadoPagoClient,\n * mercadoPagoTools,\n * InMemoryStateAdapter,\n * applyToAllTools,\n * withRateLimit,\n * } from \"@ar-agents/mercadopago\";\n * import { VercelKVRateLimiter } from \"@ar-agents/mercadopago/vercel-kv\";\n *\n * // ONE distributed bucket shared across all serverless instances of this app:\n * const limiter = new VercelKVRateLimiter({\n * key: \"mp-account-prod\",\n * capacity: 50,\n * refillPerSecond: 25,\n * });\n *\n * const client = new MercadoPagoClient({ accessToken: process.env.MP_ACCESS_TOKEN! });\n * const tools = applyToAllTools(\n * mercadoPagoTools(client, { state: new InMemoryStateAdapter(), backUrl: \"...\" }),\n * withRateLimit(limiter),\n * );\n * ```\n *\n * # Concurrency caveats\n *\n * Read-modify-write is NOT strictly atomic per `acquire()`. Under heavy\n * contention a small over-spend window is possible, acceptable for **API\n * call** rate limiting where the actual MP budget exceeds what we\n * provision. **Do NOT repurpose this limiter as a money/spend cap** —\n * the over-spend window means you could exceed a money budget by the\n * concurrent-instance count × per-call cost. For money budgets, use\n * Upstash's `EVAL`-based atomic Lua script (or a stricter primitive).\n *\n * The `acquire()` retry loop applies randomized jitter (±30%) to spread\n * concurrent acquirers across multiple refill windows, mitigating the\n * thundering-herd that would otherwise hit Upstash with N reads + N\n * writes the instant a bucket refills.\n *\n * # Marketplace setups (per-seller rate limit)\n *\n * Use the seller's MP user_id as part of the `key`:\n *\n * ```ts\n * function makeLimiter(sellerUserId: string) {\n * return new VercelKVRateLimiter({\n * key: `mp-seller-${sellerUserId}`,\n * capacity: 10,\n * refillPerSecond: 5,\n * });\n * }\n * ```\n *\n * Each seller now has their own globally-distributed bucket.\n */\nexport interface VercelKVRateLimiterOptions extends VercelKVAdapterOptions {\n /**\n * Unique key for this bucket. Use distinct keys per logical \"rate-limit\n * scope\" (per-environment, per-seller, per-region, etc.). Required.\n */\n key: string;\n /** Bucket capacity (max burst). Default 50. */\n capacity?: number;\n /** Refill rate in tokens per second. Default 25. */\n refillPerSecond?: number;\n /**\n * Hard cap on how long `acquire()` will wait. If the bucket can't\n * refill in this time, `acquire()` throws. Default 30s.\n */\n acquireTimeoutMs?: number;\n /**\n * If true, `learnFromHeaders` syncs the bucket with MP's stated\n * `x-rate-limit-remaining`. Default true.\n */\n adaptive?: boolean;\n}\n\ninterface BucketState {\n tokens: number;\n lastRefillMs: number;\n}\n\nexport class VercelKVRateLimiter {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly key: string;\n private readonly capacity: number;\n private readonly refillPerSecond: number;\n private readonly acquireTimeoutMs: number;\n private readonly adaptive: boolean;\n\n constructor(options: VercelKVRateLimiterOptions) {\n if (!options.key) {\n throw new Error(\n \"VercelKVRateLimiter requires a `key` (use distinct keys per rate-limit scope, e.g., per-environment or per-seller).\",\n );\n }\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_RATELIMIT_PREFIX;\n this.key = options.key;\n this.capacity = options.capacity ?? 50;\n this.refillPerSecond = options.refillPerSecond ?? 25;\n this.acquireTimeoutMs = options.acquireTimeoutMs ?? 30_000;\n this.adaptive = options.adaptive ?? true;\n }\n\n private fullKey(): string {\n return `${this.prefix}${this.key}`;\n }\n\n private async readState(): Promise<BucketState> {\n const stored = await this.kv.get<BucketState>(this.fullKey());\n if (stored && typeof stored.tokens === \"number\" && typeof stored.lastRefillMs === \"number\") {\n return stored;\n }\n return { tokens: this.capacity, lastRefillMs: Date.now() };\n }\n\n private refill(state: BucketState, nowMs: number): BucketState {\n const elapsedMs = Math.max(0, nowMs - state.lastRefillMs);\n const refilled = Math.min(\n this.capacity,\n state.tokens + (elapsedMs / 1000) * this.refillPerSecond,\n );\n return { tokens: refilled, lastRefillMs: nowMs };\n }\n\n private async writeState(state: BucketState): Promise<void> {\n // TTL = 1h. Long-idle buckets get garbage-collected, capacity rebuilds\n // from initial state on next acquire (which is fine — at the right rate).\n await this.kv.set(this.fullKey(), state, { ex: 3600 });\n }\n\n /**\n * Acquire a token. Resolves immediately if the distributed bucket has\n * one available; otherwise waits until refilled. Throws if the wait\n * exceeds `acquireTimeoutMs` or if the retry cap is reached.\n *\n * Caps retries at 8 iterations so a misconfigured bucket (capacity too\n * low for traffic) fails fast for the agent layer to surface, instead\n * of silently burning serverless compute time.\n */\n async acquire(): Promise<void> {\n const start = Date.now();\n const MAX_RETRIES = 8;\n let attempt = 0;\n while (attempt < MAX_RETRIES) {\n const now = Date.now();\n const state = this.refill(await this.readState(), now);\n\n if (state.tokens >= 1) {\n state.tokens -= 1;\n await this.writeState(state);\n return;\n }\n\n // Compute wait time until next token. Cap at remaining timeout budget.\n const tokensNeeded = 1 - state.tokens;\n const baseWaitMs = Math.ceil((tokensNeeded / this.refillPerSecond) * 1000);\n // Randomized jitter (±30%) prevents thundering herd: without it, all\n // concurrent acquirers compute identical waitMs, sleep identical\n // duration, and wake at the same wall-clock instant — then all hit\n // KV simultaneously. Jitter spreads them across the refill window.\n const jitterFactor = 0.7 + Math.random() * 0.6; // 0.7–1.3\n const waitMs = Math.ceil(baseWaitMs * jitterFactor);\n const elapsed = now - start;\n if (elapsed + waitMs > this.acquireTimeoutMs) {\n throw new Error(\n `VercelKVRateLimiter acquire timed out after ${elapsed + waitMs}ms (key=${this.key}).`,\n );\n }\n await new Promise((resolve) => setTimeout(resolve, waitMs));\n attempt += 1;\n }\n throw new Error(\n `VercelKVRateLimiter exhausted ${MAX_RETRIES} retries (key=${this.key}). ` +\n `Bucket likely undersized for traffic — increase capacity or refillPerSecond.`,\n );\n }\n\n /** Best-effort acquire — returns true if a token was available, false otherwise. */\n async tryAcquire(): Promise<boolean> {\n const state = this.refill(await this.readState(), Date.now());\n if (state.tokens >= 1) {\n state.tokens -= 1;\n await this.writeState(state);\n return true;\n }\n return false;\n }\n\n /**\n * Adaptive learning — call after each MP API response. If MP's stated\n * `x-rate-limit-remaining` is lower than our local count, trust MP and\n * drop the bucket to match (prevents over-spending).\n */\n async learnFromHeaders(headers: {\n remaining: number | null;\n resetSeconds: number | null;\n }): Promise<void> {\n if (!this.adaptive) return;\n if (headers.remaining === null) return;\n const state = this.refill(await this.readState(), Date.now());\n if (headers.remaining < state.tokens) {\n state.tokens = Math.max(0, headers.remaining);\n await this.writeState(state);\n }\n }\n\n /** Inspect bucket state. */\n async getStats(): Promise<{ tokens: number; capacity: number; refillPerSecond: number }> {\n const state = this.refill(await this.readState(), Date.now());\n return {\n tokens: state.tokens,\n capacity: this.capacity,\n refillPerSecond: this.refillPerSecond,\n };\n }\n\n /** Reset the bucket to full. Use sparingly (e.g., after a known-clean window). */\n async reset(): Promise<void> {\n await this.writeState({ tokens: this.capacity, lastRefillMs: Date.now() });\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// IdempotencyCache (KV-backed dedup of agent retries)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVIdempotencyCache implements IdempotencyCache {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_IDEMPOTENCY_PREFIX;\n }\n\n private key(k: string): string {\n return `${this.prefix}${k}`;\n }\n\n async get<T>(key: string): Promise<T | null> {\n return (await this.kv.get<T>(this.key(key))) ?? null;\n }\n\n async set<T>(key: string, value: T, ttlSeconds = 86_400): Promise<void> {\n // Vercel KV's `set` supports a TTL in seconds via the `ex` option.\n await this.kv.set(this.key(key), value, { ex: ttlSeconds });\n }\n\n async delete(key: string): Promise<void> {\n await this.kv.del(this.key(key));\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// AuditLogAdapter — production audit trail with daily-bucket indexing\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Vercel KV–backed audit log adapter. Stores each entry under\n * `mp:audit:entry:{id}` AND adds the id to a daily index sorted set\n * `mp:audit:day:{YYYY-MM-DD}` (score = timestamp ms). This gives O(log N)\n * time-range queries (\"all entries from May 1 to May 5\") without scanning\n * the entire log.\n *\n * # Storage layout\n *\n * - `mp:audit:entry:{id}` → the full entry JSON\n * - `mp:audit:day:{YYYY-MM-DD}` → ZSET of entry ids by timestamp (ms)\n * - `mp:audit:actor:{actor}` → ZSET of entry ids by timestamp (for \"all\n * entries by actor X\")\n * - `mp:audit:tenant:{tenantId}` → same, by tenant\n *\n * # Cost considerations\n *\n * Each `append()` does 1-3 KV writes (entry + 1-2 indexes). For high-traffic\n * deployments (>10/s sustained), batch via your own queue (e.g., Vercel\n * Queues with daily flush) and provide a custom adapter that batches.\n */\nexport class VercelKVAuditLog implements AuditLogAdapter {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_AUDIT_PREFIX;\n }\n\n async append(entry: AuditEntry): Promise<void> {\n const ts = new Date(entry.timestamp).getTime();\n const day = entry.timestamp.slice(0, 10); // YYYY-MM-DD\n await Promise.all([\n this.kv.set(`${this.prefix}entry:${entry.id}`, entry),\n this.kv.zadd(`${this.prefix}day:${day}`, { score: ts, member: entry.id }),\n this.kv.zadd(`${this.prefix}actor:${entry.actor}`, { score: ts, member: entry.id }),\n ...(entry.tenantId\n ? [\n this.kv.zadd(`${this.prefix}tenant:${entry.tenantId}`, {\n score: ts,\n member: entry.id,\n }),\n ]\n : []),\n ]);\n }\n\n async query(filter: {\n actor?: string;\n operation?: AuditOperation;\n tenantId?: string;\n from?: string;\n to?: string;\n limit?: number;\n }): Promise<AuditEntry[]> {\n const limit = filter.limit ?? 100;\n let ids: string[];\n\n // Pick the most selective index available\n if (filter.actor) {\n ids = await this.zrangeByScore(\n `${this.prefix}actor:${filter.actor}`,\n filter.from,\n filter.to,\n limit,\n );\n } else if (filter.tenantId) {\n ids = await this.zrangeByScore(\n `${this.prefix}tenant:${filter.tenantId}`,\n filter.from,\n filter.to,\n limit,\n );\n } else if (filter.from || filter.to) {\n // Walk daily buckets for the date range\n const fromDate = filter.from?.slice(0, 10) ?? \"0000-00-00\";\n const toDate = filter.to?.slice(0, 10) ?? \"9999-99-99\";\n ids = [];\n // Cap walk to ~1 year max to avoid runaway\n const fromTs = new Date(fromDate).getTime();\n const toTs = new Date(toDate).getTime();\n for (let d = fromTs; d <= toTs && ids.length < limit; d += 86_400_000) {\n const day = new Date(d).toISOString().slice(0, 10);\n const dayIds = await this.zrangeByScore(\n `${this.prefix}day:${day}`,\n filter.from,\n filter.to,\n limit - ids.length,\n );\n ids.push(...dayIds);\n }\n } else {\n // No filter — bail (full scan would be unbounded)\n return [];\n }\n\n // Load entries\n const entries: AuditEntry[] = [];\n for (const id of ids) {\n const entry = await this.kv.get<AuditEntry>(`${this.prefix}entry:${id}`);\n if (!entry) continue;\n if (filter.operation && entry.operation !== filter.operation) continue;\n entries.push(entry);\n }\n return entries;\n }\n\n private async zrangeByScore(\n key: string,\n from?: string,\n to?: string,\n limit?: number,\n ): Promise<string[]> {\n const min = from ? new Date(from).getTime() : 0;\n const max = to ? new Date(to).getTime() : Number.MAX_SAFE_INTEGER;\n const opts = {\n byScore: true as const,\n offset: 0,\n ...(limit !== undefined ? { count: limit } : { count: 100 }),\n };\n const ids = await this.kv.zrange(key, min, max, opts);\n return ids.map(String);\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/vercel-kv.ts"],"names":["defaultKv"],"mappings":"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","file":"vercel-kv.js","sourcesContent":["/**\n * Vercel KV adapters — drop-in `SubscriptionStateAdapter`,\n * `OAuthTokenStore`, and `IdempotencyCache` implementations backed by\n * [Vercel KV](https://vercel.com/docs/storage/vercel-kv) (Upstash Redis).\n *\n * # Why a separate subpath?\n *\n * `@vercel/kv` is a peer dependency — only consumers who actually use Vercel\n * KV install it. Importing from `@ar-agents/mercadopago/vercel-kv` is\n * lazy: the main `@ar-agents/mercadopago` bundle stays tiny for callers who\n * use the in-memory adapters or a different store.\n *\n * # Setup\n *\n * 1. Create a KV store at https://vercel.com/dashboard/stores\n * 2. Connect it to your project — Vercel auto-injects `KV_*` env vars\n * 3. `pnpm add @vercel/kv`\n * 4. Wire the adapters:\n *\n * ```ts\n * import { mercadoPagoTools, MercadoPagoClient } from \"@ar-agents/mercadopago\";\n * import {\n * VercelKVSubscriptionStateAdapter,\n * VercelKVOAuthTokenStore,\n * } from \"@ar-agents/mercadopago/vercel-kv\";\n *\n * const tools = mercadoPagoTools(client, {\n * state: new VercelKVSubscriptionStateAdapter(),\n * backUrl: \"https://mysite.com/done\",\n * // ... oauth, webhookSecret, etc.\n * });\n *\n * // For marketplace flows, also wire the OAuth token store:\n * const oauthStore = new VercelKVOAuthTokenStore();\n * await oauthStore.set(token.user_id, {\n * user_id: token.user_id,\n * access_token: token.access_token,\n * refresh_token: token.refresh_token!,\n * expires_at: Date.now() + (token.expires_in ?? 21600) * 1000,\n * });\n * ```\n *\n * # Edge Runtime\n *\n * `@vercel/kv` works in Vercel Edge Runtime, Node.js, and any environment\n * with `fetch` (it's a thin REST client over Upstash). All adapters here\n * are async and Edge-safe.\n *\n * # Key namespacing\n *\n * Each adapter uses its own prefix so multiple adapters can share the same\n * KV store without collisions:\n * - Subscriptions: `mp:sub:{id}`\n * - OAuth tokens: `mp:oauth:{userId}`\n * - Idempotency: `mp:idem:{key}`\n *\n * Pass a custom prefix via the constructor if you need to share the store\n * with other apps.\n */\n\nimport { kv as defaultKv } from \"@vercel/kv\";\nimport type { VercelKV } from \"@vercel/kv\";\nimport type { AuditEntry, AuditLogAdapter, AuditOperation } from \"./audit\";\nimport type {\n IdempotencyCache,\n OAuthTokenRecord,\n OAuthTokenStore,\n SubscriptionStateAdapter,\n SubscriptionStateRecord,\n} from \"./state\";\n\nconst DEFAULT_SUBSCRIPTION_PREFIX = \"mp:sub:\";\nconst DEFAULT_OAUTH_PREFIX = \"mp:oauth:\";\nconst DEFAULT_IDEMPOTENCY_PREFIX = \"mp:idem:\";\nconst DEFAULT_AUDIT_PREFIX = \"mp:audit:\";\n\ninterface VercelKVAdapterOptions {\n /**\n * Custom KV client. If omitted, uses the default `kv` export from\n * `@vercel/kv` (which reads `KV_REST_API_URL` + `KV_REST_API_TOKEN` from\n * env — auto-injected when you connect a KV store to your Vercel project).\n */\n kv?: VercelKV;\n /** Override the key prefix. */\n prefix?: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// SubscriptionStateAdapter\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVSubscriptionStateAdapter\n implements SubscriptionStateAdapter\n{\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly indexKey: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_SUBSCRIPTION_PREFIX;\n this.indexKey = `${this.prefix}__index`;\n }\n\n private key(id: string): string {\n return `${this.prefix}${id}`;\n }\n\n async set(\n id: string,\n state: Partial<SubscriptionStateRecord>,\n ): Promise<void> {\n const existing = (await this.kv.get<SubscriptionStateRecord>(this.key(id))) ?? {};\n await this.kv.set(this.key(id), { ...existing, ...state });\n await this.kv.sadd(this.indexKey, id);\n }\n\n async get(id: string): Promise<SubscriptionStateRecord | null> {\n return (await this.kv.get<SubscriptionStateRecord>(this.key(id))) ?? null;\n }\n\n async list(): Promise<string[]> {\n const ids = await this.kv.smembers(this.indexKey);\n return ids.map(String);\n }\n\n /** Forget a subscription record. NOT part of the adapter interface. */\n async delete(id: string): Promise<void> {\n await this.kv.del(this.key(id));\n await this.kv.srem(this.indexKey, id);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// OAuthTokenStore (per-seller marketplace token persistence)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVOAuthTokenStore implements OAuthTokenStore {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly indexKey: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_OAUTH_PREFIX;\n this.indexKey = `${this.prefix}__index`;\n }\n\n private key(userId: string): string {\n return `${this.prefix}${userId}`;\n }\n\n async set(userId: string, token: OAuthTokenRecord): Promise<void> {\n await this.kv.set(this.key(userId), token);\n await this.kv.sadd(this.indexKey, userId);\n }\n\n async get(userId: string): Promise<OAuthTokenRecord | null> {\n return (await this.kv.get<OAuthTokenRecord>(this.key(userId))) ?? null;\n }\n\n async delete(userId: string): Promise<void> {\n await this.kv.del(this.key(userId));\n await this.kv.srem(this.indexKey, userId);\n }\n\n async list(): Promise<string[]> {\n const ids = await this.kv.smembers(this.indexKey);\n return ids.map(String);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Distributed Token Bucket Rate Limiter (KV-backed)\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst DEFAULT_RATELIMIT_PREFIX = \"mp:rl:\";\n\n/**\n * Distributed token bucket rate limiter backed by Vercel KV.\n *\n * # Why distributed\n *\n * The default in-memory `TokenBucketRateLimiter` is per-process. In\n * serverless (Vercel Functions, Lambda, Cloudflare Workers), each cold\n * start gets its own bucket — meaning N concurrent instances effectively\n * have N×capacity. For multi-region deployments or marketplace setups\n * with shared MP rate budget, that's a footgun.\n *\n * This adapter uses a single Vercel KV (Upstash Redis) bucket per `key`,\n * shared across all instances. Two instances acquiring at the same time\n * decrement the same counter atomically — the rate limit holds globally.\n *\n * # Algorithm\n *\n * Standard token bucket with lazy refill. Each `acquire()` / `tryAcquire()`\n * runs a single server-side Lua script ({@link LUA_CONSUME}) that, in one\n * atomic Redis execution:\n * 1. Reads `{ tokens, lastRefill }` from KV\n * 2. Computes refill since `lastRefill`\n * 3. If tokens >= 1: decrements and writes back\n * 4. Otherwise: returns the refilled count so the caller can compute its wait\n *\n * Because the refill → check → decrement → write happens inside one Lua\n * script, the decrement is atomic across all instances: concurrent callers can\n * never both consume the same token, so the global limit holds exactly (no\n * over-spend window). `learnFromHeaders` uses the same atomic primitive.\n *\n * # Usage — wire via `withRateLimit` middleware\n *\n * `MercadoPagoClient` does not accept a rate limiter directly. Apply the\n * limiter at the tool layer using `withRateLimit` from the middleware\n * module, which works for both the in-memory `TokenBucketRateLimiter` and\n * this distributed variant.\n *\n * ```ts\n * import {\n * MercadoPagoClient,\n * mercadoPagoTools,\n * InMemoryStateAdapter,\n * applyToAllTools,\n * withRateLimit,\n * } from \"@ar-agents/mercadopago\";\n * import { VercelKVRateLimiter } from \"@ar-agents/mercadopago/vercel-kv\";\n *\n * // ONE distributed bucket shared across all serverless instances of this app:\n * const limiter = new VercelKVRateLimiter({\n * key: \"mp-account-prod\",\n * capacity: 50,\n * refillPerSecond: 25,\n * });\n *\n * const client = new MercadoPagoClient({ accessToken: process.env.MP_ACCESS_TOKEN! });\n * const tools = applyToAllTools(\n * mercadoPagoTools(client, { state: new InMemoryStateAdapter(), backUrl: \"...\" }),\n * withRateLimit(limiter),\n * );\n * ```\n *\n * # Concurrency\n *\n * Token consumption is atomic (one Upstash `EVAL` Lua script per acquire), so\n * the configured limit holds exactly across all serverless instances even\n * under heavy concurrent contention — there is no over-spend window. The\n * `acquire()` retry loop still applies randomized jitter (±30%) to spread\n * waiting acquirers across refill windows, mitigating the thundering-herd that\n * would otherwise hit Upstash the instant a bucket refills.\n *\n * Note this caps **count** of calls, not monetary spend. For a hard money\n * budget, enforce it at the payment layer (amount checks + idempotency), not\n * with a request-rate limiter.\n *\n * # Marketplace setups (per-seller rate limit)\n *\n * Use the seller's MP user_id as part of the `key`:\n *\n * ```ts\n * function makeLimiter(sellerUserId: string) {\n * return new VercelKVRateLimiter({\n * key: `mp-seller-${sellerUserId}`,\n * capacity: 10,\n * refillPerSecond: 5,\n * });\n * }\n * ```\n *\n * Each seller now has their own globally-distributed bucket.\n */\nexport interface VercelKVRateLimiterOptions extends VercelKVAdapterOptions {\n /**\n * Unique key for this bucket. Use distinct keys per logical \"rate-limit\n * scope\" (per-environment, per-seller, per-region, etc.). Required.\n */\n key: string;\n /** Bucket capacity (max burst). Default 50. */\n capacity?: number;\n /** Refill rate in tokens per second. Default 25. */\n refillPerSecond?: number;\n /**\n * Hard cap on how long `acquire()` will wait. If the bucket can't\n * refill in this time, `acquire()` throws. Default 30s.\n */\n acquireTimeoutMs?: number;\n /**\n * If true, `learnFromHeaders` syncs the bucket with MP's stated\n * `x-rate-limit-remaining`. Default true.\n */\n adaptive?: boolean;\n}\n\ninterface BucketState {\n tokens: number;\n lastRefillMs: number;\n}\n\n/** Bucket TTL (seconds). Long-idle buckets are GC'd; capacity rebuilds. */\nconst RATELIMIT_TTL_SECONDS = 3600;\n\n/**\n * Atomic token-bucket consume, executed server-side as a single Redis Lua\n * script so the read → refill → check → decrement → write sequence cannot\n * interleave across concurrent callers (the previous JS-side read-modify-write\n * let N concurrent acquirers all observe the same lone token and each succeed).\n *\n * KEYS[1] = bucket key.\n * ARGV = [capacity, refillPerSecond, nowMs, cost, ttlSeconds].\n * Returns [allowed (0|1), tokensRemaining (string)]. Persists ONLY on success\n * (a denied attempt leaves lastRefill untouched, so accrual stays continuous).\n *\n * State is a JSON string compatible with `@upstash/redis`'s auto (de)serializer\n * — `cjson` is available in Upstash's Lua runtime.\n */\nconst LUA_CONSUME = `-- @op:consume\nlocal raw = redis.call('GET', KEYS[1])\nlocal capacity = tonumber(ARGV[1])\nlocal refillPerSecond = tonumber(ARGV[2])\nlocal now = tonumber(ARGV[3])\nlocal cost = tonumber(ARGV[4])\nlocal ttl = tonumber(ARGV[5])\nlocal tokens = capacity\nlocal lastRefill = now\nif raw then\n local ok, data = pcall(cjson.decode, raw)\n if ok and type(data) == 'table' and type(data.tokens) == 'number' and type(data.lastRefillMs) == 'number' then\n tokens = data.tokens\n lastRefill = data.lastRefillMs\n end\nend\nlocal elapsed = now - lastRefill\nif elapsed < 0 then elapsed = 0 end\ntokens = math.min(capacity, tokens + (elapsed / 1000.0) * refillPerSecond)\nlocal allowed = 0\nif tokens >= cost then\n tokens = tokens - cost\n allowed = 1\n redis.call('SET', KEYS[1], cjson.encode({tokens = tokens, lastRefillMs = now}), 'EX', ttl)\nend\nreturn {allowed, tostring(tokens)}`;\n\n/**\n * Atomic adaptive clamp for `learnFromHeaders`: refill, then lower the bucket\n * to MP's stated remaining if that is smaller. Same atomicity guarantee as\n * {@link LUA_CONSUME} so a concurrent acquire can't race the clamp.\n *\n * KEYS[1] = bucket key.\n * ARGV = [capacity, refillPerSecond, nowMs, remaining, ttlSeconds].\n * Returns tokensRemaining (string).\n */\nconst LUA_CLAMP = `-- @op:clamp\nlocal raw = redis.call('GET', KEYS[1])\nlocal capacity = tonumber(ARGV[1])\nlocal refillPerSecond = tonumber(ARGV[2])\nlocal now = tonumber(ARGV[3])\nlocal remaining = tonumber(ARGV[4])\nlocal ttl = tonumber(ARGV[5])\nlocal tokens = capacity\nlocal lastRefill = now\nif raw then\n local ok, data = pcall(cjson.decode, raw)\n if ok and type(data) == 'table' and type(data.tokens) == 'number' and type(data.lastRefillMs) == 'number' then\n tokens = data.tokens\n lastRefill = data.lastRefillMs\n end\nend\nlocal elapsed = now - lastRefill\nif elapsed < 0 then elapsed = 0 end\ntokens = math.min(capacity, tokens + (elapsed / 1000.0) * refillPerSecond)\nif remaining < tokens then\n if remaining < 0 then remaining = 0 end\n tokens = remaining\nend\nredis.call('SET', KEYS[1], cjson.encode({tokens = tokens, lastRefillMs = now}), 'EX', ttl)\nreturn tostring(tokens)`;\n\nexport class VercelKVRateLimiter {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly key: string;\n private readonly capacity: number;\n private readonly refillPerSecond: number;\n private readonly acquireTimeoutMs: number;\n private readonly adaptive: boolean;\n\n constructor(options: VercelKVRateLimiterOptions) {\n if (!options.key) {\n throw new Error(\n \"VercelKVRateLimiter requires a `key` (use distinct keys per rate-limit scope, e.g., per-environment or per-seller).\",\n );\n }\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_RATELIMIT_PREFIX;\n this.key = options.key;\n this.capacity = options.capacity ?? 50;\n this.refillPerSecond = options.refillPerSecond ?? 25;\n this.acquireTimeoutMs = options.acquireTimeoutMs ?? 30_000;\n this.adaptive = options.adaptive ?? true;\n }\n\n private fullKey(): string {\n return `${this.prefix}${this.key}`;\n }\n\n private async readState(): Promise<BucketState> {\n const stored = await this.kv.get<BucketState>(this.fullKey());\n if (stored && typeof stored.tokens === \"number\" && typeof stored.lastRefillMs === \"number\") {\n return stored;\n }\n return { tokens: this.capacity, lastRefillMs: Date.now() };\n }\n\n private refill(state: BucketState, nowMs: number): BucketState {\n const elapsedMs = Math.max(0, nowMs - state.lastRefillMs);\n const refilled = Math.min(\n this.capacity,\n state.tokens + (elapsedMs / 1000) * this.refillPerSecond,\n );\n return { tokens: refilled, lastRefillMs: nowMs };\n }\n\n private async writeState(state: BucketState): Promise<void> {\n // TTL = 1h. Long-idle buckets get garbage-collected, capacity rebuilds\n // from initial state on next acquire (which is fine — at the right rate).\n await this.kv.set(this.fullKey(), state, { ex: RATELIMIT_TTL_SECONDS });\n }\n\n /**\n * Atomically refill + conditionally consume `cost` tokens via a single\n * server-side Lua script ({@link LUA_CONSUME}). This is the enforcement\n * primitive: because Redis runs the script atomically, concurrent callers\n * can never both consume the same token.\n */\n private async consume(\n cost: number,\n ): Promise<{ allowed: boolean; tokens: number }> {\n const now = Date.now();\n const res = (await this.kv.eval(\n LUA_CONSUME,\n [this.fullKey()],\n [this.capacity, this.refillPerSecond, now, cost, RATELIMIT_TTL_SECONDS],\n )) as [number | string, number | string];\n return { allowed: Number(res[0]) === 1, tokens: Number(res[1]) };\n }\n\n /**\n * Acquire a token. Resolves immediately if the distributed bucket has\n * one available; otherwise waits until refilled. Throws if the wait\n * exceeds `acquireTimeoutMs` or if the retry cap is reached.\n *\n * Each attempt consumes via the atomic {@link LUA_CONSUME} script, so the\n * limit holds globally even under heavy concurrent contention.\n *\n * Caps retries at 8 iterations so a misconfigured bucket (capacity too\n * low for traffic) fails fast for the agent layer to surface, instead\n * of silently burning serverless compute time.\n */\n async acquire(): Promise<void> {\n const start = Date.now();\n const MAX_RETRIES = 8;\n let attempt = 0;\n while (attempt < MAX_RETRIES) {\n const { allowed, tokens } = await this.consume(1);\n if (allowed) return;\n\n // Compute wait time until next token. Cap at remaining timeout budget.\n const tokensNeeded = 1 - tokens;\n const baseWaitMs = Math.ceil((tokensNeeded / this.refillPerSecond) * 1000);\n // Randomized jitter (±30%) prevents thundering herd: without it, all\n // concurrent acquirers compute identical waitMs, sleep identical\n // duration, and wake at the same wall-clock instant — then all hit\n // KV simultaneously. Jitter spreads them across the refill window.\n const jitterFactor = 0.7 + Math.random() * 0.6; // 0.7–1.3\n const waitMs = Math.ceil(baseWaitMs * jitterFactor);\n const elapsed = Date.now() - start;\n if (elapsed + waitMs > this.acquireTimeoutMs) {\n throw new Error(\n `VercelKVRateLimiter acquire timed out after ${elapsed + waitMs}ms (key=${this.key}).`,\n );\n }\n await new Promise((resolve) => setTimeout(resolve, waitMs));\n attempt += 1;\n }\n throw new Error(\n `VercelKVRateLimiter exhausted ${MAX_RETRIES} retries (key=${this.key}). ` +\n `Bucket likely undersized for traffic — increase capacity or refillPerSecond.`,\n );\n }\n\n /** Best-effort acquire — returns true if a token was available, false otherwise. */\n async tryAcquire(): Promise<boolean> {\n const { allowed } = await this.consume(1);\n return allowed;\n }\n\n /**\n * Adaptive learning — call after each MP API response. If MP's stated\n * `x-rate-limit-remaining` is lower than our local count, trust MP and\n * drop the bucket to match (prevents over-spending). Applied atomically via\n * {@link LUA_CLAMP} so it can't race a concurrent acquire.\n */\n async learnFromHeaders(headers: {\n remaining: number | null;\n resetSeconds: number | null;\n }): Promise<void> {\n if (!this.adaptive) return;\n if (headers.remaining === null) return;\n await this.kv.eval(\n LUA_CLAMP,\n [this.fullKey()],\n [\n this.capacity,\n this.refillPerSecond,\n Date.now(),\n headers.remaining,\n RATELIMIT_TTL_SECONDS,\n ],\n );\n }\n\n /** Inspect bucket state. */\n async getStats(): Promise<{ tokens: number; capacity: number; refillPerSecond: number }> {\n const state = this.refill(await this.readState(), Date.now());\n return {\n tokens: state.tokens,\n capacity: this.capacity,\n refillPerSecond: this.refillPerSecond,\n };\n }\n\n /** Reset the bucket to full. Use sparingly (e.g., after a known-clean window). */\n async reset(): Promise<void> {\n await this.writeState({ tokens: this.capacity, lastRefillMs: Date.now() });\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// IdempotencyCache (KV-backed dedup of agent retries)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVIdempotencyCache implements IdempotencyCache {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_IDEMPOTENCY_PREFIX;\n }\n\n private key(k: string): string {\n return `${this.prefix}${k}`;\n }\n\n async get<T>(key: string): Promise<T | null> {\n return (await this.kv.get<T>(this.key(key))) ?? null;\n }\n\n async set<T>(key: string, value: T, ttlSeconds = 86_400): Promise<void> {\n // Vercel KV's `set` supports a TTL in seconds via the `ex` option.\n await this.kv.set(this.key(key), value, { ex: ttlSeconds });\n }\n\n async delete(key: string): Promise<void> {\n await this.kv.del(this.key(key));\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// AuditLogAdapter — production audit trail with daily-bucket indexing\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Vercel KV–backed audit log adapter. Stores each entry under\n * `mp:audit:entry:{id}` AND adds the id to a daily index sorted set\n * `mp:audit:day:{YYYY-MM-DD}` (score = timestamp ms). This gives O(log N)\n * time-range queries (\"all entries from May 1 to May 5\") without scanning\n * the entire log.\n *\n * # Storage layout\n *\n * - `mp:audit:entry:{id}` → the full entry JSON\n * - `mp:audit:day:{YYYY-MM-DD}` → ZSET of entry ids by timestamp (ms)\n * - `mp:audit:actor:{actor}` → ZSET of entry ids by timestamp (for \"all\n * entries by actor X\")\n * - `mp:audit:tenant:{tenantId}` → same, by tenant\n *\n * # Cost considerations\n *\n * Each `append()` does 1-3 KV writes (entry + 1-2 indexes). For high-traffic\n * deployments (>10/s sustained), batch via your own queue (e.g., Vercel\n * Queues with daily flush) and provide a custom adapter that batches.\n */\nexport class VercelKVAuditLog implements AuditLogAdapter {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_AUDIT_PREFIX;\n }\n\n async append(entry: AuditEntry): Promise<void> {\n const ts = new Date(entry.timestamp).getTime();\n const day = entry.timestamp.slice(0, 10); // YYYY-MM-DD\n await Promise.all([\n this.kv.set(`${this.prefix}entry:${entry.id}`, entry),\n this.kv.zadd(`${this.prefix}day:${day}`, { score: ts, member: entry.id }),\n this.kv.zadd(`${this.prefix}actor:${entry.actor}`, { score: ts, member: entry.id }),\n ...(entry.tenantId\n ? [\n this.kv.zadd(`${this.prefix}tenant:${entry.tenantId}`, {\n score: ts,\n member: entry.id,\n }),\n ]\n : []),\n ]);\n }\n\n async query(filter: {\n actor?: string;\n operation?: AuditOperation;\n tenantId?: string;\n from?: string;\n to?: string;\n limit?: number;\n }): Promise<AuditEntry[]> {\n const limit = filter.limit ?? 100;\n let ids: string[];\n\n // Pick the most selective index available\n if (filter.actor) {\n ids = await this.zrangeByScore(\n `${this.prefix}actor:${filter.actor}`,\n filter.from,\n filter.to,\n limit,\n );\n } else if (filter.tenantId) {\n ids = await this.zrangeByScore(\n `${this.prefix}tenant:${filter.tenantId}`,\n filter.from,\n filter.to,\n limit,\n );\n } else if (filter.from || filter.to) {\n // Walk daily buckets for the date range\n const fromDate = filter.from?.slice(0, 10) ?? \"0000-00-00\";\n const toDate = filter.to?.slice(0, 10) ?? \"9999-99-99\";\n ids = [];\n // Cap walk to ~1 year max to avoid runaway\n const fromTs = new Date(fromDate).getTime();\n const toTs = new Date(toDate).getTime();\n for (let d = fromTs; d <= toTs && ids.length < limit; d += 86_400_000) {\n const day = new Date(d).toISOString().slice(0, 10);\n const dayIds = await this.zrangeByScore(\n `${this.prefix}day:${day}`,\n filter.from,\n filter.to,\n limit - ids.length,\n );\n ids.push(...dayIds);\n }\n } else {\n // No filter — bail (full scan would be unbounded)\n return [];\n }\n\n // Load entries\n const entries: AuditEntry[] = [];\n for (const id of ids) {\n const entry = await this.kv.get<AuditEntry>(`${this.prefix}entry:${id}`);\n if (!entry) continue;\n if (filter.operation && entry.operation !== filter.operation) continue;\n entries.push(entry);\n }\n return entries;\n }\n\n private async zrangeByScore(\n key: string,\n from?: string,\n to?: string,\n limit?: number,\n ): Promise<string[]> {\n const min = from ? new Date(from).getTime() : 0;\n const max = to ? new Date(to).getTime() : Number.MAX_SAFE_INTEGER;\n const opts = {\n byScore: true as const,\n offset: 0,\n ...(limit !== undefined ? { count: limit } : { count: 100 }),\n };\n const ids = await this.kv.zrange(key, min, max, opts);\n return ids.map(String);\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ar-agents/mercadopago",
3
- "version": "0.18.3",
3
+ "version": "0.18.5",
4
4
  "description": "Mercado Pago Agent Toolkit for the Vercel AI SDK 6. 89 typed tools across the agent-relevant Mercado Pago API surface: Subscriptions, Payments, Checkout Pro, Marketplace OAuth, Order Management, Customers, Cards, Cuotas, QR, 3DS, Point devices, Webhooks, Stores+POS, Account/Balance/Settlements, Disputes, Lookups, Bank Accounts. Edge Runtime. Vercel KV adapters. OpenTelemetry. Deterministic idempotency. Programmatic HITL on irreversible ops.",
5
5
  "keywords": [
6
6
  "mercadopago",
@@ -162,7 +162,7 @@
162
162
  "@types/node": "^20.19.39",
163
163
  "@types/qrcode": "^1.5.6",
164
164
  "@vercel/kv": "^3.0.0",
165
- "ai": "^6.0.175",
165
+ "ai": "^7.0.0",
166
166
  "fast-check": "^4.7.0",
167
167
  "msw": "^2.6.0",
168
168
  "tsup": "^8.3.5",
@@ -179,7 +179,7 @@
179
179
  },
180
180
  "dependencies": {
181
181
  "qrcode": "^1.5.4",
182
- "@ar-agents/core": "^0.2.0"
182
+ "@ar-agents/core": "^0.3.0"
183
183
  },
184
184
  "scripts": {
185
185
  "build": "tsup",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://github.com/ar-agents/ar-agents/blob/main/tools-manifest.schema.json",
3
3
  "package": "@ar-agents/mercadopago",
4
- "version": "0.18.3",
4
+ "version": "0.18.5",
5
5
  "factory": "mercadoPagoTools",
6
6
  "tools": [
7
7
  {