@ar-agents/mercadopago 0.12.0 → 0.13.0
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 +48 -0
- package/dist/vercel-kv.cjs +109 -0
- package/dist/vercel-kv.cjs.map +1 -1
- package/dist/vercel-kv.d.cts +125 -1
- package/dist/vercel-kv.d.ts +125 -1
- package/dist/vercel-kv.js +109 -1
- package/dist/vercel-kv.js.map +1 -1
- package/package.json +28 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes — Distributed rate limiter via Vercel KV
|
|
6
|
+
|
|
7
|
+
`VercelKVRateLimiter` — drop-in distributed token bucket backed by Vercel
|
|
8
|
+
KV (Upstash Redis). The default `TokenBucketRateLimiter` is per-process,
|
|
9
|
+
which is fine for single-instance deploys but breaks down in serverless:
|
|
10
|
+
each cold start gets its own bucket, so N concurrent instances effectively
|
|
11
|
+
have N×capacity. For multi-region deployments or marketplace setups with
|
|
12
|
+
shared MP rate budget, that's a footgun.
|
|
13
|
+
|
|
14
|
+
`VercelKVRateLimiter` shares one logical bucket across all instances via
|
|
15
|
+
KV. Same `acquire` / `tryAcquire` / `learnFromHeaders` interface as the
|
|
16
|
+
in-memory version — drop-in replacement.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { MercadoPagoClient } from "@ar-agents/mercadopago";
|
|
20
|
+
import { VercelKVRateLimiter } from "@ar-agents/mercadopago/vercel-kv";
|
|
21
|
+
|
|
22
|
+
// One global bucket shared across all serverless instances
|
|
23
|
+
const limiter = new VercelKVRateLimiter({
|
|
24
|
+
key: "mp-account-prod",
|
|
25
|
+
capacity: 50,
|
|
26
|
+
refillPerSecond: 25,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Marketplace: one bucket per seller
|
|
30
|
+
function makeSellerLimiter(sellerUserId: string) {
|
|
31
|
+
return new VercelKVRateLimiter({
|
|
32
|
+
key: `mp-seller-${sellerUserId}`,
|
|
33
|
+
capacity: 10,
|
|
34
|
+
refillPerSecond: 5,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Storage: `mp:rl:{key}` → `{ tokens: number, lastRefillMs: number }` with
|
|
40
|
+
1h TTL (idle buckets garbage-collect, capacity rebuilds on next acquire).
|
|
41
|
+
Read-modify-write isn't strictly atomic per call — under heavy contention
|
|
42
|
+
a small over-spend window is possible, acceptable for MP rate limiting
|
|
43
|
+
where the actual budget exceeds what we provision.
|
|
44
|
+
|
|
45
|
+
### Discoverability
|
|
46
|
+
|
|
47
|
+
- Expanded `keywords` from 7 → 29 in `package.json` (mp, payments, webhook,
|
|
48
|
+
fraud-detection, idempotency, circuit-breaker, opentelemetry, etc).
|
|
49
|
+
- Added `funding` field pointing to GitHub Sponsors.
|
|
50
|
+
|
|
3
51
|
## 0.12.0
|
|
4
52
|
|
|
5
53
|
### Minor Changes — Idempotency-by-default for state-mutating writes
|
package/dist/vercel-kv.cjs
CHANGED
|
@@ -65,6 +65,114 @@ var VercelKVOAuthTokenStore = class {
|
|
|
65
65
|
return ids.map(String);
|
|
66
66
|
}
|
|
67
67
|
};
|
|
68
|
+
var DEFAULT_RATELIMIT_PREFIX = "mp:rl:";
|
|
69
|
+
var VercelKVRateLimiter = class {
|
|
70
|
+
kv;
|
|
71
|
+
prefix;
|
|
72
|
+
key;
|
|
73
|
+
capacity;
|
|
74
|
+
refillPerSecond;
|
|
75
|
+
acquireTimeoutMs;
|
|
76
|
+
adaptive;
|
|
77
|
+
constructor(options) {
|
|
78
|
+
if (!options.key) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
"VercelKVRateLimiter requires a `key` (use distinct keys per rate-limit scope, e.g., per-environment or per-seller)."
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
this.kv = options.kv ?? kv.kv;
|
|
84
|
+
this.prefix = options.prefix ?? DEFAULT_RATELIMIT_PREFIX;
|
|
85
|
+
this.key = options.key;
|
|
86
|
+
this.capacity = options.capacity ?? 50;
|
|
87
|
+
this.refillPerSecond = options.refillPerSecond ?? 25;
|
|
88
|
+
this.acquireTimeoutMs = options.acquireTimeoutMs ?? 3e4;
|
|
89
|
+
this.adaptive = options.adaptive ?? true;
|
|
90
|
+
}
|
|
91
|
+
fullKey() {
|
|
92
|
+
return `${this.prefix}${this.key}`;
|
|
93
|
+
}
|
|
94
|
+
async readState() {
|
|
95
|
+
const stored = await this.kv.get(this.fullKey());
|
|
96
|
+
if (stored && typeof stored.tokens === "number" && typeof stored.lastRefillMs === "number") {
|
|
97
|
+
return stored;
|
|
98
|
+
}
|
|
99
|
+
return { tokens: this.capacity, lastRefillMs: Date.now() };
|
|
100
|
+
}
|
|
101
|
+
refill(state, nowMs) {
|
|
102
|
+
const elapsedMs = Math.max(0, nowMs - state.lastRefillMs);
|
|
103
|
+
const refilled = Math.min(
|
|
104
|
+
this.capacity,
|
|
105
|
+
state.tokens + elapsedMs / 1e3 * this.refillPerSecond
|
|
106
|
+
);
|
|
107
|
+
return { tokens: refilled, lastRefillMs: nowMs };
|
|
108
|
+
}
|
|
109
|
+
async writeState(state) {
|
|
110
|
+
await this.kv.set(this.fullKey(), state, { ex: 3600 });
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Acquire a token. Resolves immediately if the distributed bucket has
|
|
114
|
+
* one available; otherwise waits until refilled. Throws if the wait
|
|
115
|
+
* exceeds `acquireTimeoutMs`.
|
|
116
|
+
*/
|
|
117
|
+
async acquire() {
|
|
118
|
+
const start = Date.now();
|
|
119
|
+
while (true) {
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
const state = this.refill(await this.readState(), now);
|
|
122
|
+
if (state.tokens >= 1) {
|
|
123
|
+
state.tokens -= 1;
|
|
124
|
+
await this.writeState(state);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const tokensNeeded = 1 - state.tokens;
|
|
128
|
+
const waitMs = Math.ceil(tokensNeeded / this.refillPerSecond * 1e3);
|
|
129
|
+
const elapsed = now - start;
|
|
130
|
+
if (elapsed + waitMs > this.acquireTimeoutMs) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`VercelKVRateLimiter acquire timed out after ${elapsed + waitMs}ms (key=${this.key}).`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Best-effort acquire — returns true if a token was available, false otherwise. */
|
|
139
|
+
async tryAcquire() {
|
|
140
|
+
const state = this.refill(await this.readState(), Date.now());
|
|
141
|
+
if (state.tokens >= 1) {
|
|
142
|
+
state.tokens -= 1;
|
|
143
|
+
await this.writeState(state);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Adaptive learning — call after each MP API response. If MP's stated
|
|
150
|
+
* `x-rate-limit-remaining` is lower than our local count, trust MP and
|
|
151
|
+
* drop the bucket to match (prevents over-spending).
|
|
152
|
+
*/
|
|
153
|
+
async learnFromHeaders(headers) {
|
|
154
|
+
if (!this.adaptive) return;
|
|
155
|
+
if (headers.remaining === null) return;
|
|
156
|
+
const state = this.refill(await this.readState(), Date.now());
|
|
157
|
+
if (headers.remaining < state.tokens) {
|
|
158
|
+
state.tokens = Math.max(0, headers.remaining);
|
|
159
|
+
await this.writeState(state);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Inspect bucket state. */
|
|
163
|
+
async getStats() {
|
|
164
|
+
const state = this.refill(await this.readState(), Date.now());
|
|
165
|
+
return {
|
|
166
|
+
tokens: state.tokens,
|
|
167
|
+
capacity: this.capacity,
|
|
168
|
+
refillPerSecond: this.refillPerSecond
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/** Reset the bucket to full. Use sparingly (e.g., after a known-clean window). */
|
|
172
|
+
async reset() {
|
|
173
|
+
await this.writeState({ tokens: this.capacity, lastRefillMs: Date.now() });
|
|
174
|
+
}
|
|
175
|
+
};
|
|
68
176
|
var VercelKVIdempotencyCache = class {
|
|
69
177
|
kv;
|
|
70
178
|
prefix;
|
|
@@ -168,6 +276,7 @@ var VercelKVAuditLog = class {
|
|
|
168
276
|
exports.VercelKVAuditLog = VercelKVAuditLog;
|
|
169
277
|
exports.VercelKVIdempotencyCache = VercelKVIdempotencyCache;
|
|
170
278
|
exports.VercelKVOAuthTokenStore = VercelKVOAuthTokenStore;
|
|
279
|
+
exports.VercelKVRateLimiter = VercelKVRateLimiter;
|
|
171
280
|
exports.VercelKVSubscriptionStateAdapter = VercelKVSubscriptionStateAdapter;
|
|
172
281
|
//# sourceMappingURL=vercel-kv.cjs.map
|
|
173
282
|
//# sourceMappingURL=vercel-kv.cjs.map
|
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// 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: 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\n *\n * ```ts\n * import { MercadoPagoClient } from \"@ar-agents/mercadopago\";\n * import { VercelKVRateLimiter } from \"@ar-agents/mercadopago/vercel-kv\";\n *\n * // ONE rate limit 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({\n * accessToken: process.env.MP_ACCESS_TOKEN!,\n * rateLimiter: limiter, // (See client.ts — wired the same as in-memory)\n * });\n * ```\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`.\n */\n async acquire(): Promise<void> {\n const start = Date.now();\n while (true) {\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 waitMs = Math.ceil((tokensNeeded / this.refillPerSecond) * 1000);\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 }\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"]}
|
package/dist/vercel-kv.d.cts
CHANGED
|
@@ -94,6 +94,130 @@ declare class VercelKVOAuthTokenStore implements OAuthTokenStore {
|
|
|
94
94
|
delete(userId: string): Promise<void>;
|
|
95
95
|
list(): Promise<string[]>;
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Distributed token bucket rate limiter backed by Vercel KV.
|
|
99
|
+
*
|
|
100
|
+
* # Why distributed
|
|
101
|
+
*
|
|
102
|
+
* The default in-memory `TokenBucketRateLimiter` is per-process. In
|
|
103
|
+
* serverless (Vercel Functions, Lambda, Cloudflare Workers), each cold
|
|
104
|
+
* start gets its own bucket — meaning N concurrent instances effectively
|
|
105
|
+
* have N×capacity. For multi-region deployments or marketplace setups
|
|
106
|
+
* with shared MP rate budget, that's a footgun.
|
|
107
|
+
*
|
|
108
|
+
* This adapter uses a single Vercel KV (Upstash Redis) bucket per `key`,
|
|
109
|
+
* shared across all instances. Two instances acquiring at the same time
|
|
110
|
+
* decrement the same counter atomically — the rate limit holds globally.
|
|
111
|
+
*
|
|
112
|
+
* # Algorithm
|
|
113
|
+
*
|
|
114
|
+
* Standard token bucket with lazy refill: every `acquire()` call:
|
|
115
|
+
* 1. Reads `{ tokens, lastRefill }` from KV
|
|
116
|
+
* 2. Computes refill since `lastRefill`
|
|
117
|
+
* 3. If tokens >= 1: decrements and writes back
|
|
118
|
+
* 4. Otherwise: computes wait time, sleeps, retries
|
|
119
|
+
*
|
|
120
|
+
* The read-modify-write isn't atomic per-call, so under heavy contention
|
|
121
|
+
* a small over-spend window is possible (worst case: ~N concurrent
|
|
122
|
+
* acquires can succeed when only 1 token was available). Acceptable for
|
|
123
|
+
* MP rate limiting — the "actual" budget is much higher than what we
|
|
124
|
+
* provision.
|
|
125
|
+
*
|
|
126
|
+
* # Usage
|
|
127
|
+
*
|
|
128
|
+
* ```ts
|
|
129
|
+
* import { MercadoPagoClient } from "@ar-agents/mercadopago";
|
|
130
|
+
* import { VercelKVRateLimiter } from "@ar-agents/mercadopago/vercel-kv";
|
|
131
|
+
*
|
|
132
|
+
* // ONE rate limit shared across all serverless instances of this app:
|
|
133
|
+
* const limiter = new VercelKVRateLimiter({
|
|
134
|
+
* key: "mp-account-prod",
|
|
135
|
+
* capacity: 50,
|
|
136
|
+
* refillPerSecond: 25,
|
|
137
|
+
* });
|
|
138
|
+
*
|
|
139
|
+
* const client = new MercadoPagoClient({
|
|
140
|
+
* accessToken: process.env.MP_ACCESS_TOKEN!,
|
|
141
|
+
* rateLimiter: limiter, // (See client.ts — wired the same as in-memory)
|
|
142
|
+
* });
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* # Marketplace setups (per-seller rate limit)
|
|
146
|
+
*
|
|
147
|
+
* Use the seller's MP user_id as part of the `key`:
|
|
148
|
+
*
|
|
149
|
+
* ```ts
|
|
150
|
+
* function makeLimiter(sellerUserId: string) {
|
|
151
|
+
* return new VercelKVRateLimiter({
|
|
152
|
+
* key: `mp-seller-${sellerUserId}`,
|
|
153
|
+
* capacity: 10,
|
|
154
|
+
* refillPerSecond: 5,
|
|
155
|
+
* });
|
|
156
|
+
* }
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* Each seller now has their own globally-distributed bucket.
|
|
160
|
+
*/
|
|
161
|
+
interface VercelKVRateLimiterOptions extends VercelKVAdapterOptions {
|
|
162
|
+
/**
|
|
163
|
+
* Unique key for this bucket. Use distinct keys per logical "rate-limit
|
|
164
|
+
* scope" (per-environment, per-seller, per-region, etc.). Required.
|
|
165
|
+
*/
|
|
166
|
+
key: string;
|
|
167
|
+
/** Bucket capacity (max burst). Default 50. */
|
|
168
|
+
capacity?: number;
|
|
169
|
+
/** Refill rate in tokens per second. Default 25. */
|
|
170
|
+
refillPerSecond?: number;
|
|
171
|
+
/**
|
|
172
|
+
* Hard cap on how long `acquire()` will wait. If the bucket can't
|
|
173
|
+
* refill in this time, `acquire()` throws. Default 30s.
|
|
174
|
+
*/
|
|
175
|
+
acquireTimeoutMs?: number;
|
|
176
|
+
/**
|
|
177
|
+
* If true, `learnFromHeaders` syncs the bucket with MP's stated
|
|
178
|
+
* `x-rate-limit-remaining`. Default true.
|
|
179
|
+
*/
|
|
180
|
+
adaptive?: boolean;
|
|
181
|
+
}
|
|
182
|
+
declare class VercelKVRateLimiter {
|
|
183
|
+
private readonly kv;
|
|
184
|
+
private readonly prefix;
|
|
185
|
+
private readonly key;
|
|
186
|
+
private readonly capacity;
|
|
187
|
+
private readonly refillPerSecond;
|
|
188
|
+
private readonly acquireTimeoutMs;
|
|
189
|
+
private readonly adaptive;
|
|
190
|
+
constructor(options: VercelKVRateLimiterOptions);
|
|
191
|
+
private fullKey;
|
|
192
|
+
private readState;
|
|
193
|
+
private refill;
|
|
194
|
+
private writeState;
|
|
195
|
+
/**
|
|
196
|
+
* Acquire a token. Resolves immediately if the distributed bucket has
|
|
197
|
+
* one available; otherwise waits until refilled. Throws if the wait
|
|
198
|
+
* exceeds `acquireTimeoutMs`.
|
|
199
|
+
*/
|
|
200
|
+
acquire(): Promise<void>;
|
|
201
|
+
/** Best-effort acquire — returns true if a token was available, false otherwise. */
|
|
202
|
+
tryAcquire(): Promise<boolean>;
|
|
203
|
+
/**
|
|
204
|
+
* Adaptive learning — call after each MP API response. If MP's stated
|
|
205
|
+
* `x-rate-limit-remaining` is lower than our local count, trust MP and
|
|
206
|
+
* drop the bucket to match (prevents over-spending).
|
|
207
|
+
*/
|
|
208
|
+
learnFromHeaders(headers: {
|
|
209
|
+
remaining: number | null;
|
|
210
|
+
resetSeconds: number | null;
|
|
211
|
+
}): Promise<void>;
|
|
212
|
+
/** Inspect bucket state. */
|
|
213
|
+
getStats(): Promise<{
|
|
214
|
+
tokens: number;
|
|
215
|
+
capacity: number;
|
|
216
|
+
refillPerSecond: number;
|
|
217
|
+
}>;
|
|
218
|
+
/** Reset the bucket to full. Use sparingly (e.g., after a known-clean window). */
|
|
219
|
+
reset(): Promise<void>;
|
|
220
|
+
}
|
|
97
221
|
declare class VercelKVIdempotencyCache implements IdempotencyCache {
|
|
98
222
|
private readonly kv;
|
|
99
223
|
private readonly prefix;
|
|
@@ -140,4 +264,4 @@ declare class VercelKVAuditLog implements AuditLogAdapter {
|
|
|
140
264
|
private zrangeByScore;
|
|
141
265
|
}
|
|
142
266
|
|
|
143
|
-
export { VercelKVAuditLog, VercelKVIdempotencyCache, VercelKVOAuthTokenStore, VercelKVSubscriptionStateAdapter };
|
|
267
|
+
export { VercelKVAuditLog, VercelKVIdempotencyCache, VercelKVOAuthTokenStore, VercelKVRateLimiter, type VercelKVRateLimiterOptions, VercelKVSubscriptionStateAdapter };
|
package/dist/vercel-kv.d.ts
CHANGED
|
@@ -94,6 +94,130 @@ declare class VercelKVOAuthTokenStore implements OAuthTokenStore {
|
|
|
94
94
|
delete(userId: string): Promise<void>;
|
|
95
95
|
list(): Promise<string[]>;
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Distributed token bucket rate limiter backed by Vercel KV.
|
|
99
|
+
*
|
|
100
|
+
* # Why distributed
|
|
101
|
+
*
|
|
102
|
+
* The default in-memory `TokenBucketRateLimiter` is per-process. In
|
|
103
|
+
* serverless (Vercel Functions, Lambda, Cloudflare Workers), each cold
|
|
104
|
+
* start gets its own bucket — meaning N concurrent instances effectively
|
|
105
|
+
* have N×capacity. For multi-region deployments or marketplace setups
|
|
106
|
+
* with shared MP rate budget, that's a footgun.
|
|
107
|
+
*
|
|
108
|
+
* This adapter uses a single Vercel KV (Upstash Redis) bucket per `key`,
|
|
109
|
+
* shared across all instances. Two instances acquiring at the same time
|
|
110
|
+
* decrement the same counter atomically — the rate limit holds globally.
|
|
111
|
+
*
|
|
112
|
+
* # Algorithm
|
|
113
|
+
*
|
|
114
|
+
* Standard token bucket with lazy refill: every `acquire()` call:
|
|
115
|
+
* 1. Reads `{ tokens, lastRefill }` from KV
|
|
116
|
+
* 2. Computes refill since `lastRefill`
|
|
117
|
+
* 3. If tokens >= 1: decrements and writes back
|
|
118
|
+
* 4. Otherwise: computes wait time, sleeps, retries
|
|
119
|
+
*
|
|
120
|
+
* The read-modify-write isn't atomic per-call, so under heavy contention
|
|
121
|
+
* a small over-spend window is possible (worst case: ~N concurrent
|
|
122
|
+
* acquires can succeed when only 1 token was available). Acceptable for
|
|
123
|
+
* MP rate limiting — the "actual" budget is much higher than what we
|
|
124
|
+
* provision.
|
|
125
|
+
*
|
|
126
|
+
* # Usage
|
|
127
|
+
*
|
|
128
|
+
* ```ts
|
|
129
|
+
* import { MercadoPagoClient } from "@ar-agents/mercadopago";
|
|
130
|
+
* import { VercelKVRateLimiter } from "@ar-agents/mercadopago/vercel-kv";
|
|
131
|
+
*
|
|
132
|
+
* // ONE rate limit shared across all serverless instances of this app:
|
|
133
|
+
* const limiter = new VercelKVRateLimiter({
|
|
134
|
+
* key: "mp-account-prod",
|
|
135
|
+
* capacity: 50,
|
|
136
|
+
* refillPerSecond: 25,
|
|
137
|
+
* });
|
|
138
|
+
*
|
|
139
|
+
* const client = new MercadoPagoClient({
|
|
140
|
+
* accessToken: process.env.MP_ACCESS_TOKEN!,
|
|
141
|
+
* rateLimiter: limiter, // (See client.ts — wired the same as in-memory)
|
|
142
|
+
* });
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* # Marketplace setups (per-seller rate limit)
|
|
146
|
+
*
|
|
147
|
+
* Use the seller's MP user_id as part of the `key`:
|
|
148
|
+
*
|
|
149
|
+
* ```ts
|
|
150
|
+
* function makeLimiter(sellerUserId: string) {
|
|
151
|
+
* return new VercelKVRateLimiter({
|
|
152
|
+
* key: `mp-seller-${sellerUserId}`,
|
|
153
|
+
* capacity: 10,
|
|
154
|
+
* refillPerSecond: 5,
|
|
155
|
+
* });
|
|
156
|
+
* }
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* Each seller now has their own globally-distributed bucket.
|
|
160
|
+
*/
|
|
161
|
+
interface VercelKVRateLimiterOptions extends VercelKVAdapterOptions {
|
|
162
|
+
/**
|
|
163
|
+
* Unique key for this bucket. Use distinct keys per logical "rate-limit
|
|
164
|
+
* scope" (per-environment, per-seller, per-region, etc.). Required.
|
|
165
|
+
*/
|
|
166
|
+
key: string;
|
|
167
|
+
/** Bucket capacity (max burst). Default 50. */
|
|
168
|
+
capacity?: number;
|
|
169
|
+
/** Refill rate in tokens per second. Default 25. */
|
|
170
|
+
refillPerSecond?: number;
|
|
171
|
+
/**
|
|
172
|
+
* Hard cap on how long `acquire()` will wait. If the bucket can't
|
|
173
|
+
* refill in this time, `acquire()` throws. Default 30s.
|
|
174
|
+
*/
|
|
175
|
+
acquireTimeoutMs?: number;
|
|
176
|
+
/**
|
|
177
|
+
* If true, `learnFromHeaders` syncs the bucket with MP's stated
|
|
178
|
+
* `x-rate-limit-remaining`. Default true.
|
|
179
|
+
*/
|
|
180
|
+
adaptive?: boolean;
|
|
181
|
+
}
|
|
182
|
+
declare class VercelKVRateLimiter {
|
|
183
|
+
private readonly kv;
|
|
184
|
+
private readonly prefix;
|
|
185
|
+
private readonly key;
|
|
186
|
+
private readonly capacity;
|
|
187
|
+
private readonly refillPerSecond;
|
|
188
|
+
private readonly acquireTimeoutMs;
|
|
189
|
+
private readonly adaptive;
|
|
190
|
+
constructor(options: VercelKVRateLimiterOptions);
|
|
191
|
+
private fullKey;
|
|
192
|
+
private readState;
|
|
193
|
+
private refill;
|
|
194
|
+
private writeState;
|
|
195
|
+
/**
|
|
196
|
+
* Acquire a token. Resolves immediately if the distributed bucket has
|
|
197
|
+
* one available; otherwise waits until refilled. Throws if the wait
|
|
198
|
+
* exceeds `acquireTimeoutMs`.
|
|
199
|
+
*/
|
|
200
|
+
acquire(): Promise<void>;
|
|
201
|
+
/** Best-effort acquire — returns true if a token was available, false otherwise. */
|
|
202
|
+
tryAcquire(): Promise<boolean>;
|
|
203
|
+
/**
|
|
204
|
+
* Adaptive learning — call after each MP API response. If MP's stated
|
|
205
|
+
* `x-rate-limit-remaining` is lower than our local count, trust MP and
|
|
206
|
+
* drop the bucket to match (prevents over-spending).
|
|
207
|
+
*/
|
|
208
|
+
learnFromHeaders(headers: {
|
|
209
|
+
remaining: number | null;
|
|
210
|
+
resetSeconds: number | null;
|
|
211
|
+
}): Promise<void>;
|
|
212
|
+
/** Inspect bucket state. */
|
|
213
|
+
getStats(): Promise<{
|
|
214
|
+
tokens: number;
|
|
215
|
+
capacity: number;
|
|
216
|
+
refillPerSecond: number;
|
|
217
|
+
}>;
|
|
218
|
+
/** Reset the bucket to full. Use sparingly (e.g., after a known-clean window). */
|
|
219
|
+
reset(): Promise<void>;
|
|
220
|
+
}
|
|
97
221
|
declare class VercelKVIdempotencyCache implements IdempotencyCache {
|
|
98
222
|
private readonly kv;
|
|
99
223
|
private readonly prefix;
|
|
@@ -140,4 +264,4 @@ declare class VercelKVAuditLog implements AuditLogAdapter {
|
|
|
140
264
|
private zrangeByScore;
|
|
141
265
|
}
|
|
142
266
|
|
|
143
|
-
export { VercelKVAuditLog, VercelKVIdempotencyCache, VercelKVOAuthTokenStore, VercelKVSubscriptionStateAdapter };
|
|
267
|
+
export { VercelKVAuditLog, VercelKVIdempotencyCache, VercelKVOAuthTokenStore, VercelKVRateLimiter, type VercelKVRateLimiterOptions, VercelKVSubscriptionStateAdapter };
|
package/dist/vercel-kv.js
CHANGED
|
@@ -63,6 +63,114 @@ var VercelKVOAuthTokenStore = class {
|
|
|
63
63
|
return ids.map(String);
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
|
+
var DEFAULT_RATELIMIT_PREFIX = "mp:rl:";
|
|
67
|
+
var VercelKVRateLimiter = class {
|
|
68
|
+
kv;
|
|
69
|
+
prefix;
|
|
70
|
+
key;
|
|
71
|
+
capacity;
|
|
72
|
+
refillPerSecond;
|
|
73
|
+
acquireTimeoutMs;
|
|
74
|
+
adaptive;
|
|
75
|
+
constructor(options) {
|
|
76
|
+
if (!options.key) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"VercelKVRateLimiter requires a `key` (use distinct keys per rate-limit scope, e.g., per-environment or per-seller)."
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
this.kv = options.kv ?? kv;
|
|
82
|
+
this.prefix = options.prefix ?? DEFAULT_RATELIMIT_PREFIX;
|
|
83
|
+
this.key = options.key;
|
|
84
|
+
this.capacity = options.capacity ?? 50;
|
|
85
|
+
this.refillPerSecond = options.refillPerSecond ?? 25;
|
|
86
|
+
this.acquireTimeoutMs = options.acquireTimeoutMs ?? 3e4;
|
|
87
|
+
this.adaptive = options.adaptive ?? true;
|
|
88
|
+
}
|
|
89
|
+
fullKey() {
|
|
90
|
+
return `${this.prefix}${this.key}`;
|
|
91
|
+
}
|
|
92
|
+
async readState() {
|
|
93
|
+
const stored = await this.kv.get(this.fullKey());
|
|
94
|
+
if (stored && typeof stored.tokens === "number" && typeof stored.lastRefillMs === "number") {
|
|
95
|
+
return stored;
|
|
96
|
+
}
|
|
97
|
+
return { tokens: this.capacity, lastRefillMs: Date.now() };
|
|
98
|
+
}
|
|
99
|
+
refill(state, nowMs) {
|
|
100
|
+
const elapsedMs = Math.max(0, nowMs - state.lastRefillMs);
|
|
101
|
+
const refilled = Math.min(
|
|
102
|
+
this.capacity,
|
|
103
|
+
state.tokens + elapsedMs / 1e3 * this.refillPerSecond
|
|
104
|
+
);
|
|
105
|
+
return { tokens: refilled, lastRefillMs: nowMs };
|
|
106
|
+
}
|
|
107
|
+
async writeState(state) {
|
|
108
|
+
await this.kv.set(this.fullKey(), state, { ex: 3600 });
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Acquire a token. Resolves immediately if the distributed bucket has
|
|
112
|
+
* one available; otherwise waits until refilled. Throws if the wait
|
|
113
|
+
* exceeds `acquireTimeoutMs`.
|
|
114
|
+
*/
|
|
115
|
+
async acquire() {
|
|
116
|
+
const start = Date.now();
|
|
117
|
+
while (true) {
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
const state = this.refill(await this.readState(), now);
|
|
120
|
+
if (state.tokens >= 1) {
|
|
121
|
+
state.tokens -= 1;
|
|
122
|
+
await this.writeState(state);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const tokensNeeded = 1 - state.tokens;
|
|
126
|
+
const waitMs = Math.ceil(tokensNeeded / this.refillPerSecond * 1e3);
|
|
127
|
+
const elapsed = now - start;
|
|
128
|
+
if (elapsed + waitMs > this.acquireTimeoutMs) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`VercelKVRateLimiter acquire timed out after ${elapsed + waitMs}ms (key=${this.key}).`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Best-effort acquire — returns true if a token was available, false otherwise. */
|
|
137
|
+
async tryAcquire() {
|
|
138
|
+
const state = this.refill(await this.readState(), Date.now());
|
|
139
|
+
if (state.tokens >= 1) {
|
|
140
|
+
state.tokens -= 1;
|
|
141
|
+
await this.writeState(state);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Adaptive learning — call after each MP API response. If MP's stated
|
|
148
|
+
* `x-rate-limit-remaining` is lower than our local count, trust MP and
|
|
149
|
+
* drop the bucket to match (prevents over-spending).
|
|
150
|
+
*/
|
|
151
|
+
async learnFromHeaders(headers) {
|
|
152
|
+
if (!this.adaptive) return;
|
|
153
|
+
if (headers.remaining === null) return;
|
|
154
|
+
const state = this.refill(await this.readState(), Date.now());
|
|
155
|
+
if (headers.remaining < state.tokens) {
|
|
156
|
+
state.tokens = Math.max(0, headers.remaining);
|
|
157
|
+
await this.writeState(state);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Inspect bucket state. */
|
|
161
|
+
async getStats() {
|
|
162
|
+
const state = this.refill(await this.readState(), Date.now());
|
|
163
|
+
return {
|
|
164
|
+
tokens: state.tokens,
|
|
165
|
+
capacity: this.capacity,
|
|
166
|
+
refillPerSecond: this.refillPerSecond
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** Reset the bucket to full. Use sparingly (e.g., after a known-clean window). */
|
|
170
|
+
async reset() {
|
|
171
|
+
await this.writeState({ tokens: this.capacity, lastRefillMs: Date.now() });
|
|
172
|
+
}
|
|
173
|
+
};
|
|
66
174
|
var VercelKVIdempotencyCache = class {
|
|
67
175
|
kv;
|
|
68
176
|
prefix;
|
|
@@ -163,6 +271,6 @@ var VercelKVAuditLog = class {
|
|
|
163
271
|
}
|
|
164
272
|
};
|
|
165
273
|
|
|
166
|
-
export { VercelKVAuditLog, VercelKVIdempotencyCache, VercelKVOAuthTokenStore, VercelKVSubscriptionStateAdapter };
|
|
274
|
+
export { VercelKVAuditLog, VercelKVIdempotencyCache, VercelKVOAuthTokenStore, VercelKVRateLimiter, VercelKVSubscriptionStateAdapter };
|
|
167
275
|
//# sourceMappingURL=vercel-kv.js.map
|
|
168
276
|
//# sourceMappingURL=vercel-kv.js.map
|
package/dist/vercel-kv.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;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// IdempotencyCache (KV-backed dedup of agent retries)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVIdempotencyCache implements IdempotencyCache {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_IDEMPOTENCY_PREFIX;\n }\n\n private key(k: string): string {\n return `${this.prefix}${k}`;\n }\n\n async get<T>(key: string): Promise<T | null> {\n return (await this.kv.get<T>(this.key(key))) ?? null;\n }\n\n async set<T>(key: string, value: T, ttlSeconds = 86_400): Promise<void> {\n // Vercel KV's `set` supports a TTL in seconds via the `ex` option.\n await this.kv.set(this.key(key), value, { ex: ttlSeconds });\n }\n\n async delete(key: string): Promise<void> {\n await this.kv.del(this.key(key));\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// AuditLogAdapter — production audit trail with daily-bucket indexing\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Vercel KV–backed audit log adapter. Stores each entry under\n * `mp:audit:entry:{id}` AND adds the id to a daily index sorted set\n * `mp:audit:day:{YYYY-MM-DD}` (score = timestamp ms). This gives O(log N)\n * time-range queries (\"all entries from May 1 to May 5\") without scanning\n * the entire log.\n *\n * # Storage layout\n *\n * - `mp:audit:entry:{id}` → the full entry JSON\n * - `mp:audit:day:{YYYY-MM-DD}` → ZSET of entry ids by timestamp (ms)\n * - `mp:audit:actor:{actor}` → ZSET of entry ids by timestamp (for \"all\n * entries by actor X\")\n * - `mp:audit:tenant:{tenantId}` → same, by tenant\n *\n * # Cost considerations\n *\n * Each `append()` does 1-3 KV writes (entry + 1-2 indexes). For high-traffic\n * deployments (>10/s sustained), batch via your own queue (e.g., Vercel\n * Queues with daily flush) and provide a custom adapter that batches.\n */\nexport class VercelKVAuditLog implements AuditLogAdapter {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_AUDIT_PREFIX;\n }\n\n async append(entry: AuditEntry): Promise<void> {\n const ts = new Date(entry.timestamp).getTime();\n const day = entry.timestamp.slice(0, 10); // YYYY-MM-DD\n await Promise.all([\n this.kv.set(`${this.prefix}entry:${entry.id}`, entry),\n this.kv.zadd(`${this.prefix}day:${day}`, { score: ts, member: entry.id }),\n this.kv.zadd(`${this.prefix}actor:${entry.actor}`, { score: ts, member: entry.id }),\n ...(entry.tenantId\n ? [\n this.kv.zadd(`${this.prefix}tenant:${entry.tenantId}`, {\n score: ts,\n member: entry.id,\n }),\n ]\n : []),\n ]);\n }\n\n async query(filter: {\n actor?: string;\n operation?: AuditOperation;\n tenantId?: string;\n from?: string;\n to?: string;\n limit?: number;\n }): Promise<AuditEntry[]> {\n const limit = filter.limit ?? 100;\n let ids: string[];\n\n // Pick the most selective index available\n if (filter.actor) {\n ids = await this.zrangeByScore(\n `${this.prefix}actor:${filter.actor}`,\n filter.from,\n filter.to,\n limit,\n );\n } else if (filter.tenantId) {\n ids = await this.zrangeByScore(\n `${this.prefix}tenant:${filter.tenantId}`,\n filter.from,\n filter.to,\n limit,\n );\n } else if (filter.from || filter.to) {\n // Walk daily buckets for the date range\n const fromDate = filter.from?.slice(0, 10) ?? \"0000-00-00\";\n const toDate = filter.to?.slice(0, 10) ?? \"9999-99-99\";\n ids = [];\n // Cap walk to ~1 year max to avoid runaway\n const fromTs = new Date(fromDate).getTime();\n const toTs = new Date(toDate).getTime();\n for (let d = fromTs; d <= toTs && ids.length < limit; d += 86_400_000) {\n const day = new Date(d).toISOString().slice(0, 10);\n const dayIds = await this.zrangeByScore(\n `${this.prefix}day:${day}`,\n filter.from,\n filter.to,\n limit - ids.length,\n );\n ids.push(...dayIds);\n }\n } else {\n // No filter — bail (full scan would be unbounded)\n return [];\n }\n\n // Load entries\n const entries: AuditEntry[] = [];\n for (const id of ids) {\n const entry = await this.kv.get<AuditEntry>(`${this.prefix}entry:${id}`);\n if (!entry) continue;\n if (filter.operation && entry.operation !== filter.operation) continue;\n entries.push(entry);\n }\n return entries;\n }\n\n private async zrangeByScore(\n key: string,\n from?: string,\n to?: string,\n limit?: number,\n ): Promise<string[]> {\n const min = from ? new Date(from).getTime() : 0;\n const max = to ? new Date(to).getTime() : Number.MAX_SAFE_INTEGER;\n const opts = {\n byScore: true as const,\n offset: 0,\n ...(limit !== undefined ? { count: limit } : { count: 100 }),\n };\n const ids = await this.kv.zrange(key, min, max, opts);\n return ids.map(String);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/vercel-kv.ts"],"names":["defaultKv"],"mappings":"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","file":"vercel-kv.js","sourcesContent":["/**\n * Vercel KV adapters — drop-in `SubscriptionStateAdapter`,\n * `OAuthTokenStore`, and `IdempotencyCache` implementations backed by\n * [Vercel KV](https://vercel.com/docs/storage/vercel-kv) (Upstash Redis).\n *\n * # Why a separate subpath?\n *\n * `@vercel/kv` is a peer dependency — only consumers who actually use Vercel\n * KV install it. Importing from `@ar-agents/mercadopago/vercel-kv` is\n * lazy: the main `@ar-agents/mercadopago` bundle stays tiny for callers who\n * use the in-memory adapters or a different store.\n *\n * # Setup\n *\n * 1. Create a KV store at https://vercel.com/dashboard/stores\n * 2. Connect it to your project — Vercel auto-injects `KV_*` env vars\n * 3. `pnpm add @vercel/kv`\n * 4. Wire the adapters:\n *\n * ```ts\n * import { mercadoPagoTools, MercadoPagoClient } from \"@ar-agents/mercadopago\";\n * import {\n * VercelKVSubscriptionStateAdapter,\n * VercelKVOAuthTokenStore,\n * } from \"@ar-agents/mercadopago/vercel-kv\";\n *\n * const tools = mercadoPagoTools(client, {\n * state: new VercelKVSubscriptionStateAdapter(),\n * backUrl: \"https://mysite.com/done\",\n * // ... oauth, webhookSecret, etc.\n * });\n *\n * // For marketplace flows, also wire the OAuth token store:\n * const oauthStore = new VercelKVOAuthTokenStore();\n * await oauthStore.set(token.user_id, {\n * user_id: token.user_id,\n * access_token: token.access_token,\n * refresh_token: token.refresh_token!,\n * expires_at: Date.now() + (token.expires_in ?? 21600) * 1000,\n * });\n * ```\n *\n * # Edge Runtime\n *\n * `@vercel/kv` works in Vercel Edge Runtime, Node.js, and any environment\n * with `fetch` (it's a thin REST client over Upstash). All adapters here\n * are async and Edge-safe.\n *\n * # Key namespacing\n *\n * Each adapter uses its own prefix so multiple adapters can share the same\n * KV store without collisions:\n * - Subscriptions: `mp:sub:{id}`\n * - OAuth tokens: `mp:oauth:{userId}`\n * - Idempotency: `mp:idem:{key}`\n *\n * Pass a custom prefix via the constructor if you need to share the store\n * with other apps.\n */\n\nimport { kv as defaultKv } from \"@vercel/kv\";\nimport type { VercelKV } from \"@vercel/kv\";\nimport type { AuditEntry, AuditLogAdapter, AuditOperation } from \"./audit\";\nimport type {\n IdempotencyCache,\n OAuthTokenRecord,\n OAuthTokenStore,\n SubscriptionStateAdapter,\n SubscriptionStateRecord,\n} from \"./state\";\n\nconst DEFAULT_SUBSCRIPTION_PREFIX = \"mp:sub:\";\nconst DEFAULT_OAUTH_PREFIX = \"mp:oauth:\";\nconst DEFAULT_IDEMPOTENCY_PREFIX = \"mp:idem:\";\nconst DEFAULT_AUDIT_PREFIX = \"mp:audit:\";\n\ninterface VercelKVAdapterOptions {\n /**\n * Custom KV client. If omitted, uses the default `kv` export from\n * `@vercel/kv` (which reads `KV_REST_API_URL` + `KV_REST_API_TOKEN` from\n * env — auto-injected when you connect a KV store to your Vercel project).\n */\n kv?: VercelKV;\n /** Override the key prefix. */\n prefix?: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// SubscriptionStateAdapter\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVSubscriptionStateAdapter\n implements SubscriptionStateAdapter\n{\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly indexKey: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_SUBSCRIPTION_PREFIX;\n this.indexKey = `${this.prefix}__index`;\n }\n\n private key(id: string): string {\n return `${this.prefix}${id}`;\n }\n\n async set(\n id: string,\n state: Partial<SubscriptionStateRecord>,\n ): Promise<void> {\n const existing = (await this.kv.get<SubscriptionStateRecord>(this.key(id))) ?? {};\n await this.kv.set(this.key(id), { ...existing, ...state });\n await this.kv.sadd(this.indexKey, id);\n }\n\n async get(id: string): Promise<SubscriptionStateRecord | null> {\n return (await this.kv.get<SubscriptionStateRecord>(this.key(id))) ?? null;\n }\n\n async list(): Promise<string[]> {\n const ids = await this.kv.smembers(this.indexKey);\n return ids.map(String);\n }\n\n /** Forget a subscription record. NOT part of the adapter interface. */\n async delete(id: string): Promise<void> {\n await this.kv.del(this.key(id));\n await this.kv.srem(this.indexKey, id);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// OAuthTokenStore (per-seller marketplace token persistence)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVOAuthTokenStore implements OAuthTokenStore {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly indexKey: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_OAUTH_PREFIX;\n this.indexKey = `${this.prefix}__index`;\n }\n\n private key(userId: string): string {\n return `${this.prefix}${userId}`;\n }\n\n async set(userId: string, token: OAuthTokenRecord): Promise<void> {\n await this.kv.set(this.key(userId), token);\n await this.kv.sadd(this.indexKey, userId);\n }\n\n async get(userId: string): Promise<OAuthTokenRecord | null> {\n return (await this.kv.get<OAuthTokenRecord>(this.key(userId))) ?? null;\n }\n\n async delete(userId: string): Promise<void> {\n await this.kv.del(this.key(userId));\n await this.kv.srem(this.indexKey, userId);\n }\n\n async list(): Promise<string[]> {\n const ids = await this.kv.smembers(this.indexKey);\n return ids.map(String);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Distributed Token Bucket Rate Limiter (KV-backed)\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst DEFAULT_RATELIMIT_PREFIX = \"mp:rl:\";\n\n/**\n * Distributed token bucket rate limiter backed by Vercel KV.\n *\n * # Why distributed\n *\n * The default in-memory `TokenBucketRateLimiter` is per-process. In\n * serverless (Vercel Functions, Lambda, Cloudflare Workers), each cold\n * start gets its own bucket — meaning N concurrent instances effectively\n * have N×capacity. For multi-region deployments or marketplace setups\n * with shared MP rate budget, that's a footgun.\n *\n * This adapter uses a single Vercel KV (Upstash Redis) bucket per `key`,\n * shared across all instances. Two instances acquiring at the same time\n * decrement the same counter atomically — the rate limit holds globally.\n *\n * # Algorithm\n *\n * Standard token bucket with lazy refill: 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\n *\n * ```ts\n * import { MercadoPagoClient } from \"@ar-agents/mercadopago\";\n * import { VercelKVRateLimiter } from \"@ar-agents/mercadopago/vercel-kv\";\n *\n * // ONE rate limit 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({\n * accessToken: process.env.MP_ACCESS_TOKEN!,\n * rateLimiter: limiter, // (See client.ts — wired the same as in-memory)\n * });\n * ```\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`.\n */\n async acquire(): Promise<void> {\n const start = Date.now();\n while (true) {\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 waitMs = Math.ceil((tokensNeeded / this.refillPerSecond) * 1000);\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 }\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"]}
|
package/package.json
CHANGED
|
@@ -1,18 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ar-agents/mercadopago",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "The most complete Mercado Pago agent toolkit for Vercel AI SDK 6+. 82 tools, Edge Runtime, Vercel KV adapters, circuit breaker + deadline propagation + W3C trace context, HMAC webhook verify with replay protection, OAuth Marketplace, Order Management, Point Devices, marketplace splits, status_detail explainer in Spanish, AR-specific knowledge baked in. Property-tested + integration-tested vs MP sandbox + benchmarked.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mercadopago",
|
|
7
|
+
"mp",
|
|
7
8
|
"ai-sdk",
|
|
8
9
|
"vercel-ai",
|
|
10
|
+
"vercel-ai-sdk",
|
|
9
11
|
"agent",
|
|
12
|
+
"agents",
|
|
13
|
+
"agentic",
|
|
14
|
+
"agentic-payments",
|
|
10
15
|
"argentina",
|
|
16
|
+
"argentine",
|
|
17
|
+
"ar",
|
|
18
|
+
"latam",
|
|
11
19
|
"subscriptions",
|
|
12
|
-
"
|
|
20
|
+
"preapproval",
|
|
21
|
+
"checkout-pro",
|
|
22
|
+
"payments",
|
|
23
|
+
"webhook",
|
|
24
|
+
"mcp",
|
|
25
|
+
"edge-runtime",
|
|
26
|
+
"cuotas",
|
|
27
|
+
"qr-pago",
|
|
28
|
+
"fraud-detection",
|
|
29
|
+
"3ds",
|
|
30
|
+
"marketplace",
|
|
31
|
+
"oauth",
|
|
32
|
+
"idempotency",
|
|
33
|
+
"circuit-breaker",
|
|
34
|
+
"opentelemetry"
|
|
13
35
|
],
|
|
14
36
|
"license": "MIT",
|
|
15
37
|
"author": "Nazareno Clemente <naza@helloastro.co>",
|
|
38
|
+
"funding": {
|
|
39
|
+
"type": "github",
|
|
40
|
+
"url": "https://github.com/sponsors/naza00000"
|
|
41
|
+
},
|
|
16
42
|
"homepage": "https://github.com/ar-agents/ar-agents/tree/main/packages/mercadopago",
|
|
17
43
|
"repository": {
|
|
18
44
|
"type": "git",
|