@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 +15 -0
- package/cookbook/01-checkout-pro-basic.ts +2 -2
- package/cookbook/09-otel-wired.ts +2 -2
- package/cookbook/10-cross-package-billing.ts +2 -2
- package/cookbook/17-usa-llc-companion.ts +2 -2
- package/cookbook/23-astro-arg-reference-customer.ts +1 -1
- package/dist/vercel-kv.cjs +90 -23
- package/dist/vercel-kv.cjs.map +1 -1
- package/dist/vercel-kv.d.cts +30 -20
- package/dist/vercel-kv.d.ts +30 -20
- package/dist/vercel-kv.js +90 -23
- package/dist/vercel-kv.js.map +1 -1
- package/package.json +3 -3
- package/tools.manifest.json +1 -1
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,
|
|
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:
|
|
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,
|
|
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:
|
|
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,
|
|
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:
|
|
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,
|
|
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:
|
|
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
|
-
|
|
128
|
+
instructions: SYSTEM,
|
|
129
129
|
messages: modelMessages,
|
|
130
130
|
tools,
|
|
131
131
|
stopWhen: ({ steps }) => steps.length >= 8,
|
package/dist/vercel-kv.cjs
CHANGED
|
@@ -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:
|
|
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
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
153
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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() {
|
package/dist/vercel-kv.cjs.map
CHANGED
|
@@ -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"]}
|
package/dist/vercel-kv.d.cts
CHANGED
|
@@ -111,17 +111,18 @@ declare class VercelKVOAuthTokenStore implements OAuthTokenStore {
|
|
|
111
111
|
*
|
|
112
112
|
* # Algorithm
|
|
113
113
|
*
|
|
114
|
-
* Standard token bucket with lazy refill
|
|
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:
|
|
120
|
+
* 4. Otherwise: returns the refilled count so the caller can compute its wait
|
|
119
121
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
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
|
|
158
|
+
* # Concurrency
|
|
158
159
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
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
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
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.d.ts
CHANGED
|
@@ -111,17 +111,18 @@ declare class VercelKVOAuthTokenStore implements OAuthTokenStore {
|
|
|
111
111
|
*
|
|
112
112
|
* # Algorithm
|
|
113
113
|
*
|
|
114
|
-
* Standard token bucket with lazy refill
|
|
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:
|
|
120
|
+
* 4. Otherwise: returns the refilled count so the caller can compute its wait
|
|
119
121
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
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
|
|
158
|
+
* # Concurrency
|
|
158
159
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
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
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
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:
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
151
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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() {
|
package/dist/vercel-kv.js.map
CHANGED
|
@@ -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":";;;AAuEA,IAAM,2BAAA,GAA8B,SAAA;AACpC,IAAM,oBAAA,GAAuB,WAAA;AAC7B,IAAM,0BAAA,GAA6B,UAAA;AACnC,IAAM,oBAAA,GAAuB,WAAA;AAiBtB,IAAM,mCAAN,MAEP;AAAA,EACmB,EAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EAEjB,WAAA,CAAY,OAAA,GAAkC,EAAC,EAAG;AAChD,IAAA,IAAA,CAAK,EAAA,GAAK,QAAQ,EAAA,IAAMA,EAAA;AACxB,IAAA,IAAA,CAAK,MAAA,GAAS,QAAQ,MAAA,IAAU,2BAAA;AAChC,IAAA,IAAA,CAAK,QAAA,GAAW,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,OAAA,CAAA;AAAA,EAChC;AAAA,EAEQ,IAAI,EAAA,EAAoB;AAC9B,IAAA,OAAO,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,EAAG,EAAE,CAAA,CAAA;AAAA,EAC5B;AAAA,EAEA,MAAM,GAAA,CACJ,EAAA,EACA,KAAA,EACe;AACf,IAAA,MAAM,QAAA,GAAY,MAAM,IAAA,CAAK,EAAA,CAAG,GAAA,CAA6B,KAAK,GAAA,CAAI,EAAE,CAAC,CAAA,IAAM,EAAC;AAChF,IAAA,MAAM,IAAA,CAAK,EAAA,CAAG,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,EAAE,CAAA,EAAG,EAAE,GAAG,QAAA,EAAU,GAAG,KAAA,EAAO,CAAA;AACzD,IAAA,MAAM,IAAA,CAAK,EAAA,CAAG,IAAA,CAAK,IAAA,CAAK,UAAU,EAAE,CAAA;AAAA,EACtC;AAAA,EAEA,MAAM,IAAI,EAAA,EAAqD;AAC7D,IAAA,OAAQ,MAAM,KAAK,EAAA,CAAG,GAAA,CAA6B,KAAK,GAAA,CAAI,EAAE,CAAC,CAAA,IAAM,IAAA;AAAA,EACvE;AAAA,EAEA,MAAM,IAAA,GAA0B;AAC9B,IAAA,MAAM,MAAM,MAAM,IAAA,CAAK,EAAA,CAAG,QAAA,CAAS,KAAK,QAAQ,CAAA;AAChD,IAAA,OAAO,GAAA,CAAI,IAAI,MAAM,CAAA;AAAA,EACvB;AAAA;AAAA,EAGA,MAAM,OAAO,EAAA,EAA2B;AACtC,IAAA,MAAM,KAAK,EAAA,CAAG,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,EAAE,CAAC,CAAA;AAC9B,IAAA,MAAM,IAAA,CAAK,EAAA,CAAG,IAAA,CAAK,IAAA,CAAK,UAAU,EAAE,CAAA;AAAA,EACtC;AACF;AAMO,IAAM,0BAAN,MAAyD;AAAA,EAC7C,EAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EAEjB,WAAA,CAAY,OAAA,GAAkC,EAAC,EAAG;AAChD,IAAA,IAAA,CAAK,EAAA,GAAK,QAAQ,EAAA,IAAMA,EAAA;AACxB,IAAA,IAAA,CAAK,MAAA,GAAS,QAAQ,MAAA,IAAU,oBAAA;AAChC,IAAA,IAAA,CAAK,QAAA,GAAW,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,OAAA,CAAA;AAAA,EAChC;AAAA,EAEQ,IAAI,MAAA,EAAwB;AAClC,IAAA,OAAO,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,EAAG,MAAM,CAAA,CAAA;AAAA,EAChC;AAAA,EAEA,MAAM,GAAA,CAAI,MAAA,EAAgB,KAAA,EAAwC;AAChE,IAAA,MAAM,KAAK,EAAA,CAAG,GAAA,CAAI,KAAK,GAAA,CAAI,MAAM,GAAG,KAAK,CAAA;AACzC,IAAA,MAAM,IAAA,CAAK,EAAA,CAAG,IAAA,CAAK,IAAA,CAAK,UAAU,MAAM,CAAA;AAAA,EAC1C;AAAA,EAEA,MAAM,IAAI,MAAA,EAAkD;AAC1D,IAAA,OAAQ,MAAM,KAAK,EAAA,CAAG,GAAA,CAAsB,KAAK,GAAA,CAAI,MAAM,CAAC,CAAA,IAAM,IAAA;AAAA,EACpE;AAAA,EAEA,MAAM,OAAO,MAAA,EAA+B;AAC1C,IAAA,MAAM,KAAK,EAAA,CAAG,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,MAAM,CAAC,CAAA;AAClC,IAAA,MAAM,IAAA,CAAK,EAAA,CAAG,IAAA,CAAK,IAAA,CAAK,UAAU,MAAM,CAAA;AAAA,EAC1C;AAAA,EAEA,MAAM,IAAA,GAA0B;AAC9B,IAAA,MAAM,MAAM,MAAM,IAAA,CAAK,EAAA,CAAG,QAAA,CAAS,KAAK,QAAQ,CAAA;AAChD,IAAA,OAAO,GAAA,CAAI,IAAI,MAAM,CAAA;AAAA,EACvB;AACF;AAMA,IAAM,wBAAA,GAA2B,QAAA;AAwHjC,IAAM,qBAAA,GAAwB,IAAA;AAgB9B,IAAM,WAAA,GAAc,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kCAAA,CAAA;AAoCpB,IAAM,SAAA,GAAY,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAAA,CAAA;AA0BX,IAAM,sBAAN,MAA0B;AAAA,EACd,EAAA;AAAA,EACA,MAAA;AAAA,EACA,GAAA;AAAA,EACA,QAAA;AAAA,EACA,eAAA;AAAA,EACA,gBAAA;AAAA,EACA,QAAA;AAAA,EAEjB,YAAY,OAAA,EAAqC;AAC/C,IAAA,IAAI,CAAC,QAAQ,GAAA,EAAK;AAChB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AACA,IAAA,IAAA,CAAK,EAAA,GAAK,QAAQ,EAAA,IAAMA,EAAA;AACxB,IAAA,IAAA,CAAK,MAAA,GAAS,QAAQ,MAAA,IAAU,wBAAA;AAChC,IAAA,IAAA,CAAK,MAAM,OAAA,CAAQ,GAAA;AACnB,IAAA,IAAA,CAAK,QAAA,GAAW,QAAQ,QAAA,IAAY,EAAA;AACpC,IAAA,IAAA,CAAK,eAAA,GAAkB,QAAQ,eAAA,IAAmB,EAAA;AAClD,IAAA,IAAA,CAAK,gBAAA,GAAmB,QAAQ,gBAAA,IAAoB,GAAA;AACpD,IAAA,IAAA,CAAK,QAAA,GAAW,QAAQ,QAAA,IAAY,IAAA;AAAA,EACtC;AAAA,EAEQ,OAAA,GAAkB;AACxB,IAAA,OAAO,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,EAAG,KAAK,GAAG,CAAA,CAAA;AAAA,EAClC;AAAA,EAEA,MAAc,SAAA,GAAkC;AAC9C,IAAA,MAAM,SAAS,MAAM,IAAA,CAAK,GAAG,GAAA,CAAiB,IAAA,CAAK,SAAS,CAAA;AAC5D,IAAA,IAAI,MAAA,IAAU,OAAO,MAAA,CAAO,MAAA,KAAW,YAAY,OAAO,MAAA,CAAO,iBAAiB,QAAA,EAAU;AAC1F,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,OAAO,EAAE,MAAA,EAAQ,IAAA,CAAK,UAAU,YAAA,EAAc,IAAA,CAAK,KAAI,EAAE;AAAA,EAC3D;AAAA,EAEQ,MAAA,CAAO,OAAoB,KAAA,EAA4B;AAC7D,IAAA,MAAM,YAAY,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAA,GAAQ,MAAM,YAAY,CAAA;AACxD,IAAA,MAAM,WAAW,IAAA,CAAK,GAAA;AAAA,MACpB,IAAA,CAAK,QAAA;AAAA,MACL,KAAA,CAAM,MAAA,GAAU,SAAA,GAAY,GAAA,GAAQ,IAAA,CAAK;AAAA,KAC3C;AACA,IAAA,OAAO,EAAE,MAAA,EAAQ,QAAA,EAAU,YAAA,EAAc,KAAA,EAAM;AAAA,EACjD;AAAA,EAEA,MAAc,WAAW,KAAA,EAAmC;AAG1D,IAAA,MAAM,IAAA,CAAK,EAAA,CAAG,GAAA,CAAI,IAAA,CAAK,OAAA,IAAW,KAAA,EAAO,EAAE,EAAA,EAAI,qBAAA,EAAuB,CAAA;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,QACZ,IAAA,EAC+C;AAC/C,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,MAAM,GAAA,GAAO,MAAM,IAAA,CAAK,EAAA,CAAG,IAAA;AAAA,MACzB,WAAA;AAAA,MACA,CAAC,IAAA,CAAK,OAAA,EAAS,CAAA;AAAA,MACf,CAAC,IAAA,CAAK,QAAA,EAAU,KAAK,eAAA,EAAiB,GAAA,EAAK,MAAM,qBAAqB;AAAA,KACxE;AACA,IAAA,OAAO,EAAE,OAAA,EAAS,MAAA,CAAO,GAAA,CAAI,CAAC,CAAC,CAAA,KAAM,CAAA,EAAG,MAAA,EAAQ,MAAA,CAAO,GAAA,CAAI,CAAC,CAAC,CAAA,EAAE;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAM,OAAA,GAAyB;AAC7B,IAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,EAAI;AACvB,IAAA,MAAM,WAAA,GAAc,CAAA;AACpB,IAAA,IAAI,OAAA,GAAU,CAAA;AACd,IAAA,OAAO,UAAU,WAAA,EAAa;AAC5B,MAAA,MAAM,EAAE,OAAA,EAAS,MAAA,KAAW,MAAM,IAAA,CAAK,QAAQ,CAAC,CAAA;AAChD,MAAA,IAAI,OAAA,EAAS;AAGb,MAAA,MAAM,eAAe,CAAA,GAAI,MAAA;AACzB,MAAA,MAAM,aAAa,IAAA,CAAK,IAAA,CAAM,YAAA,GAAe,IAAA,CAAK,kBAAmB,GAAI,CAAA;AAKzE,MAAA,MAAM,YAAA,GAAe,GAAA,GAAM,IAAA,CAAK,MAAA,EAAO,GAAI,GAAA;AAC3C,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,IAAA,CAAK,UAAA,GAAa,YAAY,CAAA;AAClD,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA;AAC7B,MAAA,IAAI,OAAA,GAAU,MAAA,GAAS,IAAA,CAAK,gBAAA,EAAkB;AAC5C,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAA,4CAAA,EAA+C,OAAA,GAAU,MAAM,CAAA,QAAA,EAAW,KAAK,GAAG,CAAA,EAAA;AAAA,SACpF;AAAA,MACF;AACA,MAAA,MAAM,IAAI,OAAA,CAAQ,CAAC,YAAY,UAAA,CAAW,OAAA,EAAS,MAAM,CAAC,CAAA;AAC1D,MAAA,OAAA,IAAW,CAAA;AAAA,IACb;AACA,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,8BAAA,EAAiC,WAAW,CAAA,cAAA,EAAiB,IAAA,CAAK,GAAG,CAAA,oFAAA;AAAA,KAEvE;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,UAAA,GAA+B;AACnC,IAAA,MAAM,EAAE,OAAA,EAAQ,GAAI,MAAM,IAAA,CAAK,QAAQ,CAAC,CAAA;AACxC,IAAA,OAAO,OAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,iBAAiB,OAAA,EAGL;AAChB,IAAA,IAAI,CAAC,KAAK,QAAA,EAAU;AACpB,IAAA,IAAI,OAAA,CAAQ,cAAc,IAAA,EAAM;AAChC,IAAA,MAAM,KAAK,EAAA,CAAG,IAAA;AAAA,MACZ,SAAA;AAAA,MACA,CAAC,IAAA,CAAK,OAAA,EAAS,CAAA;AAAA,MACf;AAAA,QACE,IAAA,CAAK,QAAA;AAAA,QACL,IAAA,CAAK,eAAA;AAAA,QACL,KAAK,GAAA,EAAI;AAAA,QACT,OAAA,CAAQ,SAAA;AAAA,QACR;AAAA;AACF,KACF;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,QAAA,GAAmF;AACvF,IAAA,MAAM,KAAA,GAAQ,KAAK,MAAA,CAAO,MAAM,KAAK,SAAA,EAAU,EAAG,IAAA,CAAK,GAAA,EAAK,CAAA;AAC5D,IAAA,OAAO;AAAA,MACL,QAAQ,KAAA,CAAM,MAAA;AAAA,MACd,UAAU,IAAA,CAAK,QAAA;AAAA,MACf,iBAAiB,IAAA,CAAK;AAAA,KACxB;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,KAAA,GAAuB;AAC3B,IAAA,MAAM,IAAA,CAAK,UAAA,CAAW,EAAE,MAAA,EAAQ,IAAA,CAAK,UAAU,YAAA,EAAc,IAAA,CAAK,GAAA,EAAI,EAAG,CAAA;AAAA,EAC3E;AACF;AAMO,IAAM,2BAAN,MAA2D;AAAA,EAC/C,EAAA;AAAA,EACA,MAAA;AAAA,EAEjB,WAAA,CAAY,OAAA,GAAkC,EAAC,EAAG;AAChD,IAAA,IAAA,CAAK,EAAA,GAAK,QAAQ,EAAA,IAAMA,EAAA;AACxB,IAAA,IAAA,CAAK,MAAA,GAAS,QAAQ,MAAA,IAAU,0BAAA;AAAA,EAClC;AAAA,EAEQ,IAAI,CAAA,EAAmB;AAC7B,IAAA,OAAO,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,EAAG,CAAC,CAAA,CAAA;AAAA,EAC3B;AAAA,EAEA,MAAM,IAAO,GAAA,EAAgC;AAC3C,IAAA,OAAQ,MAAM,KAAK,EAAA,CAAG,GAAA,CAAO,KAAK,GAAA,CAAI,GAAG,CAAC,CAAA,IAAM,IAAA;AAAA,EAClD;AAAA,EAEA,MAAM,GAAA,CAAO,GAAA,EAAa,KAAA,EAAU,aAAa,KAAA,EAAuB;AAEtE,IAAA,MAAM,IAAA,CAAK,EAAA,CAAG,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA,EAAG,KAAA,EAAO,EAAE,EAAA,EAAI,UAAA,EAAY,CAAA;AAAA,EAC5D;AAAA,EAEA,MAAM,OAAO,GAAA,EAA4B;AACvC,IAAA,MAAM,KAAK,EAAA,CAAG,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,GAAG,CAAC,CAAA;AAAA,EACjC;AACF;AA2BO,IAAM,mBAAN,MAAkD;AAAA,EACtC,EAAA;AAAA,EACA,MAAA;AAAA,EAEjB,WAAA,CAAY,OAAA,GAAkC,EAAC,EAAG;AAChD,IAAA,IAAA,CAAK,EAAA,GAAK,QAAQ,EAAA,IAAMA,EAAA;AACxB,IAAA,IAAA,CAAK,MAAA,GAAS,QAAQ,MAAA,IAAU,oBAAA;AAAA,EAClC;AAAA,EAEA,MAAM,OAAO,KAAA,EAAkC;AAC7C,IAAA,MAAM,KAAK,IAAI,IAAA,CAAK,KAAA,CAAM,SAAS,EAAE,OAAA,EAAQ;AAC7C,IAAA,MAAM,GAAA,GAAM,KAAA,CAAM,SAAA,CAAU,KAAA,CAAM,GAAG,EAAE,CAAA;AACvC,IAAA,MAAM,QAAQ,GAAA,CAAI;AAAA,MAChB,IAAA,CAAK,EAAA,CAAG,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,MAAA,EAAS,KAAA,CAAM,EAAE,CAAA,CAAA,EAAI,KAAK,CAAA;AAAA,MACpD,IAAA,CAAK,EAAA,CAAG,IAAA,CAAK,CAAA,EAAG,KAAK,MAAM,CAAA,IAAA,EAAO,GAAG,CAAA,CAAA,EAAI,EAAE,KAAA,EAAO,EAAA,EAAI,MAAA,EAAQ,KAAA,CAAM,IAAI,CAAA;AAAA,MACxE,KAAK,EAAA,CAAG,IAAA,CAAK,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,MAAA,EAAS,KAAA,CAAM,KAAK,CAAA,CAAA,EAAI,EAAE,KAAA,EAAO,EAAA,EAAI,MAAA,EAAQ,KAAA,CAAM,IAAI,CAAA;AAAA,MAClF,GAAI,MAAM,QAAA,GACN;AAAA,QACE,IAAA,CAAK,GAAG,IAAA,CAAK,CAAA,EAAG,KAAK,MAAM,CAAA,OAAA,EAAU,KAAA,CAAM,QAAQ,CAAA,CAAA,EAAI;AAAA,UACrD,KAAA,EAAO,EAAA;AAAA,UACP,QAAQ,KAAA,CAAM;AAAA,SACf;AAAA,UAEH;AAAC,KACN,CAAA;AAAA,EACH;AAAA,EAEA,MAAM,MAAM,MAAA,EAOc;AACxB,IAAA,MAAM,KAAA,GAAQ,OAAO,KAAA,IAAS,GAAA;AAC9B,IAAA,IAAI,GAAA;AAGJ,IAAA,IAAI,OAAO,KAAA,EAAO;AAChB,MAAA,GAAA,GAAM,MAAM,IAAA,CAAK,aAAA;AAAA,QACf,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,MAAA,EAAS,OAAO,KAAK,CAAA,CAAA;AAAA,QACnC,MAAA,CAAO,IAAA;AAAA,QACP,MAAA,CAAO,EAAA;AAAA,QACP;AAAA,OACF;AAAA,IACF,CAAA,MAAA,IAAW,OAAO,QAAA,EAAU;AAC1B,MAAA,GAAA,GAAM,MAAM,IAAA,CAAK,aAAA;AAAA,QACf,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,OAAA,EAAU,OAAO,QAAQ,CAAA,CAAA;AAAA,QACvC,MAAA,CAAO,IAAA;AAAA,QACP,MAAA,CAAO,EAAA;AAAA,QACP;AAAA,OACF;AAAA,IACF,CAAA,MAAA,IAAW,MAAA,CAAO,IAAA,IAAQ,MAAA,CAAO,EAAA,EAAI;AAEnC,MAAA,MAAM,WAAW,MAAA,CAAO,IAAA,EAAM,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,IAAK,YAAA;AAC9C,MAAA,MAAM,SAAS,MAAA,CAAO,EAAA,EAAI,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,IAAK,YAAA;AAC1C,MAAA,GAAA,GAAM,EAAC;AAEP,MAAA,MAAM,MAAA,GAAS,IAAI,IAAA,CAAK,QAAQ,EAAE,OAAA,EAAQ;AAC1C,MAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,MAAM,EAAE,OAAA,EAAQ;AACtC,MAAA,KAAA,IAAS,CAAA,GAAI,QAAQ,CAAA,IAAK,IAAA,IAAQ,IAAI,MAAA,GAAS,KAAA,EAAO,KAAK,KAAA,EAAY;AACrE,QAAA,MAAM,GAAA,GAAM,IAAI,IAAA,CAAK,CAAC,EAAE,WAAA,EAAY,CAAE,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AACjD,QAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,aAAA;AAAA,UACxB,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,IAAA,EAAO,GAAG,CAAA,CAAA;AAAA,UACxB,MAAA,CAAO,IAAA;AAAA,UACP,MAAA,CAAO,EAAA;AAAA,UACP,QAAQ,GAAA,CAAI;AAAA,SACd;AACA,QAAA,GAAA,CAAI,IAAA,CAAK,GAAG,MAAM,CAAA;AAAA,MACpB;AAAA,IACF,CAAA,MAAO;AAEL,MAAA,OAAO,EAAC;AAAA,IACV;AAGA,IAAA,MAAM,UAAwB,EAAC;AAC/B,IAAA,KAAA,MAAW,MAAM,GAAA,EAAK;AACpB,MAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,EAAA,CAAG,GAAA,CAAgB,GAAG,IAAA,CAAK,MAAM,CAAA,MAAA,EAAS,EAAE,CAAA,CAAE,CAAA;AACvE,MAAA,IAAI,CAAC,KAAA,EAAO;AACZ,MAAA,IAAI,MAAA,CAAO,SAAA,IAAa,KAAA,CAAM,SAAA,KAAc,OAAO,SAAA,EAAW;AAC9D,MAAA,OAAA,CAAQ,KAAK,KAAK,CAAA;AAAA,IACpB;AACA,IAAA,OAAO,OAAA;AAAA,EACT;AAAA,EAEA,MAAc,aAAA,CACZ,GAAA,EACA,IAAA,EACA,IACA,KAAA,EACmB;AACnB,IAAA,MAAM,MAAM,IAAA,GAAO,IAAI,KAAK,IAAI,CAAA,CAAE,SAAQ,GAAI,CAAA;AAC9C,IAAA,MAAM,GAAA,GAAM,KAAK,IAAI,IAAA,CAAK,EAAE,CAAA,CAAE,OAAA,KAAY,MAAA,CAAO,gBAAA;AACjD,IAAA,MAAM,IAAA,GAAO;AAAA,MACX,OAAA,EAAS,IAAA;AAAA,MACT,MAAA,EAAQ,CAAA;AAAA,MACR,GAAI,UAAU,MAAA,GAAY,EAAE,OAAO,KAAA,EAAM,GAAI,EAAE,KAAA,EAAO,GAAA;AAAI,KAC5D;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,EAAA,CAAG,OAAO,GAAA,EAAK,GAAA,EAAK,KAAK,IAAI,CAAA;AACpD,IAAA,OAAO,GAAA,CAAI,IAAI,MAAM,CAAA;AAAA,EACvB;AACF","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
|
+
"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": "^
|
|
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.
|
|
182
|
+
"@ar-agents/core": "^0.3.0"
|
|
183
183
|
},
|
|
184
184
|
"scripts": {
|
|
185
185
|
"build": "tsup",
|