@ar-agents/mercadopago 0.7.0 → 0.9.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 +125 -0
- package/README.md +162 -2
- package/cookbook/01-checkout-pro-basic.ts +99 -0
- package/cookbook/02-saas-subscription.ts +137 -0
- package/cookbook/03-webhook-handler.ts +162 -0
- package/cookbook/04-marketplace-split.ts +194 -0
- package/cookbook/05-qr-in-store.ts +142 -0
- package/cookbook/06-3ds-challenge.ts +139 -0
- package/cookbook/07-auth-only-order.ts +127 -0
- package/cookbook/08-recovery-patterns.ts +191 -0
- package/cookbook/README.md +36 -0
- package/dist/index.cjs +407 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +278 -50
- package/dist/index.d.ts +278 -50
- package/dist/index.js +404 -35
- package/dist/index.js.map +1 -1
- package/dist/state-C6Wzb_XX.d.cts +106 -0
- package/dist/state-C6Wzb_XX.d.ts +106 -0
- package/dist/vercel-kv.cjs +92 -0
- package/dist/vercel-kv.cjs.map +1 -0
- package/dist/vercel-kv.d.cts +107 -0
- package/dist/vercel-kv.d.ts +107 -0
- package/dist/vercel-kv.js +88 -0
- package/dist/vercel-kv.js.map +1 -0
- package/package.json +32 -3
- package/tools.manifest.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHmac, timingSafeEqual, createHash } from 'crypto';
|
|
2
1
|
import { tool } from 'ai';
|
|
3
2
|
import { z } from 'zod';
|
|
4
3
|
|
|
@@ -165,6 +164,8 @@ var MercadoPagoClient = class {
|
|
|
165
164
|
requestTimeoutMs;
|
|
166
165
|
maxRetries;
|
|
167
166
|
onCall;
|
|
167
|
+
circuitBreaker;
|
|
168
|
+
traceContext;
|
|
168
169
|
constructor(options) {
|
|
169
170
|
if (!options.accessToken) {
|
|
170
171
|
throw new Error(
|
|
@@ -177,8 +178,24 @@ var MercadoPagoClient = class {
|
|
|
177
178
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
|
|
178
179
|
this.maxRetries = Math.max(0, options.maxRetries ?? 1);
|
|
179
180
|
this.onCall = options.onCall;
|
|
181
|
+
this.circuitBreaker = options.circuitBreaker;
|
|
182
|
+
this.traceContext = options.traceContext;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* v0.9 — Inspect the circuit breaker state (when configured). Returns
|
|
186
|
+
* `null` when no circuit breaker is wired. Useful for health checks.
|
|
187
|
+
*/
|
|
188
|
+
getCircuitState() {
|
|
189
|
+
return this.circuitBreaker?.getStats() ?? null;
|
|
180
190
|
}
|
|
181
191
|
async request(method, path, body, options) {
|
|
192
|
+
const exec = () => this.requestUnprotected(method, path, body, options);
|
|
193
|
+
if (this.circuitBreaker) {
|
|
194
|
+
return this.circuitBreaker.execute(exec);
|
|
195
|
+
}
|
|
196
|
+
return exec();
|
|
197
|
+
}
|
|
198
|
+
async requestUnprotected(method, path, body, options) {
|
|
182
199
|
const headers = {
|
|
183
200
|
Authorization: `Bearer ${this.accessToken}`,
|
|
184
201
|
"Content-Type": "application/json"
|
|
@@ -186,6 +203,11 @@ var MercadoPagoClient = class {
|
|
|
186
203
|
if (options?.idempotencyKey) {
|
|
187
204
|
headers["X-Idempotency-Key"] = options.idempotencyKey;
|
|
188
205
|
}
|
|
206
|
+
const trace = this.traceContext?.();
|
|
207
|
+
if (trace?.traceId && trace?.spanId) {
|
|
208
|
+
const flags = (trace.traceFlags ?? 1).toString(16).padStart(2, "0");
|
|
209
|
+
headers["traceparent"] = `00-${trace.traceId}-${trace.spanId}-${flags}`;
|
|
210
|
+
}
|
|
189
211
|
let url = `${this.baseUrl}${path}`;
|
|
190
212
|
if (options?.query) {
|
|
191
213
|
const search = new URLSearchParams();
|
|
@@ -202,24 +224,54 @@ var MercadoPagoClient = class {
|
|
|
202
224
|
let attempt = 0;
|
|
203
225
|
let lastError;
|
|
204
226
|
let lastStatus = null;
|
|
227
|
+
const fireOnCall = (event) => {
|
|
228
|
+
const traceCtx = trace?.traceId ? {
|
|
229
|
+
traceId: trace.traceId,
|
|
230
|
+
...trace.spanId !== void 0 ? { spanId: trace.spanId } : {}
|
|
231
|
+
} : void 0;
|
|
232
|
+
this.onCall?.({
|
|
233
|
+
method,
|
|
234
|
+
path,
|
|
235
|
+
durationMs: Date.now() - t0,
|
|
236
|
+
...event,
|
|
237
|
+
...this.circuitBreaker ? { circuitState: this.circuitBreaker.getState() } : {},
|
|
238
|
+
...traceCtx ? { traceContext: traceCtx } : {}
|
|
239
|
+
});
|
|
240
|
+
};
|
|
205
241
|
while (attempt <= this.maxRetries) {
|
|
206
242
|
const controller = new AbortController();
|
|
207
243
|
const timer = setTimeout(() => controller.abort(), this.requestTimeoutMs);
|
|
244
|
+
const parentSignal = options?.signal;
|
|
245
|
+
const onParentAbort = () => controller.abort();
|
|
246
|
+
if (parentSignal) {
|
|
247
|
+
if (parentSignal.aborted) {
|
|
248
|
+
clearTimeout(timer);
|
|
249
|
+
throw new MercadoPagoTimeoutError(path, 0);
|
|
250
|
+
}
|
|
251
|
+
parentSignal.addEventListener("abort", onParentAbort, { once: true });
|
|
252
|
+
}
|
|
208
253
|
const init = { method, headers, signal: controller.signal };
|
|
209
254
|
if (body !== void 0) init.body = JSON.stringify(body);
|
|
210
255
|
try {
|
|
211
256
|
const res = await fetchFn(url, init);
|
|
212
257
|
clearTimeout(timer);
|
|
258
|
+
if (parentSignal) parentSignal.removeEventListener("abort", onParentAbort);
|
|
213
259
|
lastStatus = res.status;
|
|
260
|
+
const requestId = res.headers.get("x-request-id");
|
|
261
|
+
const rlRemaining = res.headers.get("x-rate-limit-remaining");
|
|
262
|
+
const rlReset = res.headers.get("x-rate-limit-reset");
|
|
263
|
+
const rateLimit = {
|
|
264
|
+
remaining: rlRemaining !== null ? Number(rlRemaining) : null,
|
|
265
|
+
resetSeconds: rlReset !== null ? Number(rlReset) : null
|
|
266
|
+
};
|
|
214
267
|
if (res.ok) {
|
|
215
268
|
const text2 = await res.text();
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
path,
|
|
219
|
-
durationMs: Date.now() - t0,
|
|
269
|
+
fireOnCall({
|
|
270
|
+
success: true,
|
|
220
271
|
httpStatus: res.status,
|
|
221
272
|
retried: attempt,
|
|
222
|
-
|
|
273
|
+
requestId,
|
|
274
|
+
rateLimit
|
|
223
275
|
});
|
|
224
276
|
if (!text2) return void 0;
|
|
225
277
|
return JSON.parse(text2);
|
|
@@ -234,13 +286,12 @@ var MercadoPagoClient = class {
|
|
|
234
286
|
}
|
|
235
287
|
const contentType = res.headers.get("content-type") ?? "";
|
|
236
288
|
if (res.status >= 500 && !contentType.includes("application/json")) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
path,
|
|
240
|
-
durationMs: Date.now() - t0,
|
|
289
|
+
fireOnCall({
|
|
290
|
+
success: false,
|
|
241
291
|
httpStatus: res.status,
|
|
242
292
|
retried: attempt,
|
|
243
|
-
|
|
293
|
+
requestId,
|
|
294
|
+
rateLimit
|
|
244
295
|
});
|
|
245
296
|
throw new MercadoPagoOverloadedError(path, res.status);
|
|
246
297
|
}
|
|
@@ -252,33 +303,33 @@ var MercadoPagoClient = class {
|
|
|
252
303
|
parsed = text;
|
|
253
304
|
}
|
|
254
305
|
const err = classifyError(res.status, path, parsed, options?.classifyContext);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
path,
|
|
258
|
-
durationMs: Date.now() - t0,
|
|
306
|
+
fireOnCall({
|
|
307
|
+
success: false,
|
|
259
308
|
httpStatus: res.status,
|
|
260
309
|
retried: attempt,
|
|
261
|
-
|
|
310
|
+
requestId,
|
|
311
|
+
rateLimit
|
|
262
312
|
});
|
|
263
313
|
throw err;
|
|
264
314
|
} catch (err) {
|
|
265
315
|
clearTimeout(timer);
|
|
316
|
+
if (parentSignal) parentSignal.removeEventListener("abort", onParentAbort);
|
|
266
317
|
if (err instanceof MercadoPagoError) throw err;
|
|
267
318
|
const isAbort = err instanceof Error && err.name === "AbortError";
|
|
319
|
+
const isParentAbort = parentSignal?.aborted ?? false;
|
|
268
320
|
const isNetwork = !lastStatus && !isAbort;
|
|
269
|
-
if ((isNetwork || isAbort) && attempt < this.maxRetries) {
|
|
321
|
+
if ((isNetwork || isAbort && !isParentAbort) && attempt < this.maxRetries) {
|
|
270
322
|
lastError = err;
|
|
271
323
|
attempt++;
|
|
272
324
|
await sleep(250 * Math.pow(2, attempt - 1));
|
|
273
325
|
continue;
|
|
274
326
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
path,
|
|
278
|
-
durationMs: Date.now() - t0,
|
|
327
|
+
fireOnCall({
|
|
328
|
+
success: false,
|
|
279
329
|
httpStatus: lastStatus,
|
|
280
330
|
retried: attempt,
|
|
281
|
-
|
|
331
|
+
requestId: null,
|
|
332
|
+
rateLimit: { remaining: null, resetSeconds: null }
|
|
282
333
|
});
|
|
283
334
|
if (isAbort) {
|
|
284
335
|
throw new MercadoPagoTimeoutError(path, this.requestTimeoutMs);
|
|
@@ -1232,7 +1283,102 @@ var MercadoPagoClient = class {
|
|
|
1232
1283
|
);
|
|
1233
1284
|
return { id: intentId, canceled: true };
|
|
1234
1285
|
}
|
|
1286
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1287
|
+
// v0.9 — Health check
|
|
1288
|
+
//
|
|
1289
|
+
// No dedicated ping endpoint exists in MP's public API. We use `getMe()`
|
|
1290
|
+
// (`/users/me`) as a lightweight liveness probe — it requires only a valid
|
|
1291
|
+
// accessToken, returns ~200 bytes of JSON, and is the same call MP's own
|
|
1292
|
+
// dashboard makes on startup. A successful response proves: (a) network
|
|
1293
|
+
// path to MP is up, (b) accessToken is valid, (c) MP is responding.
|
|
1294
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1295
|
+
/**
|
|
1296
|
+
* Liveness probe against MP. Returns latency + circuit-breaker state.
|
|
1297
|
+
* Use as a /health endpoint for k8s, Vercel cron, or status-page checks.
|
|
1298
|
+
*
|
|
1299
|
+
* Returns `{ ok: false, ... }` instead of throwing — designed for
|
|
1300
|
+
* monitoring loops that want to keep running.
|
|
1301
|
+
*
|
|
1302
|
+
* @param signal Optional AbortSignal to cap wait time (e.g., 2s for
|
|
1303
|
+
* status-page polling).
|
|
1304
|
+
*/
|
|
1305
|
+
async healthCheck(signal) {
|
|
1306
|
+
const t0 = Date.now();
|
|
1307
|
+
const circuitBefore = this.circuitBreaker?.getStats() ?? null;
|
|
1308
|
+
try {
|
|
1309
|
+
const me = await this.request(
|
|
1310
|
+
"GET",
|
|
1311
|
+
"/users/me",
|
|
1312
|
+
void 0,
|
|
1313
|
+
signal ? { signal } : {}
|
|
1314
|
+
);
|
|
1315
|
+
return {
|
|
1316
|
+
ok: true,
|
|
1317
|
+
latencyMs: Date.now() - t0,
|
|
1318
|
+
userId: String(me.id),
|
|
1319
|
+
error: null,
|
|
1320
|
+
circuit: this.circuitBreaker?.getStats() ?? null
|
|
1321
|
+
};
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1324
|
+
return {
|
|
1325
|
+
ok: false,
|
|
1326
|
+
latencyMs: Date.now() - t0,
|
|
1327
|
+
userId: null,
|
|
1328
|
+
error: message,
|
|
1329
|
+
circuit: this.circuitBreaker?.getStats() ?? circuitBefore
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1235
1333
|
};
|
|
1334
|
+
|
|
1335
|
+
// src/crypto.ts
|
|
1336
|
+
var subtle = (() => {
|
|
1337
|
+
const c = globalThis.crypto;
|
|
1338
|
+
if (!c?.subtle) {
|
|
1339
|
+
throw new Error(
|
|
1340
|
+
"@ar-agents/mercadopago: Web Crypto API is not available in this runtime. Use Node 18+, Vercel Edge Runtime, Cloudflare Workers, or any modern browser."
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
return c.subtle;
|
|
1344
|
+
})();
|
|
1345
|
+
var encoder = new TextEncoder();
|
|
1346
|
+
async function hmacSha256Hex(secret, message) {
|
|
1347
|
+
const keyMaterial = await subtle.importKey(
|
|
1348
|
+
"raw",
|
|
1349
|
+
encoder.encode(secret),
|
|
1350
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
1351
|
+
false,
|
|
1352
|
+
["sign"]
|
|
1353
|
+
);
|
|
1354
|
+
const sigBuf = await subtle.sign(
|
|
1355
|
+
"HMAC",
|
|
1356
|
+
keyMaterial,
|
|
1357
|
+
encoder.encode(message)
|
|
1358
|
+
);
|
|
1359
|
+
return bufferToHex(sigBuf);
|
|
1360
|
+
}
|
|
1361
|
+
async function sha256Hex(input) {
|
|
1362
|
+
const digest = await subtle.digest("SHA-256", encoder.encode(input));
|
|
1363
|
+
return bufferToHex(digest);
|
|
1364
|
+
}
|
|
1365
|
+
function timingSafeEqualHex(a, b) {
|
|
1366
|
+
if (a.length !== b.length) return false;
|
|
1367
|
+
let diff = 0;
|
|
1368
|
+
for (let i = 0; i < a.length; i++) {
|
|
1369
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
1370
|
+
}
|
|
1371
|
+
return diff === 0;
|
|
1372
|
+
}
|
|
1373
|
+
function bufferToHex(buf) {
|
|
1374
|
+
const bytes = new Uint8Array(buf);
|
|
1375
|
+
let hex = "";
|
|
1376
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1377
|
+
const b = bytes[i];
|
|
1378
|
+
hex += (b < 16 ? "0" : "") + b.toString(16);
|
|
1379
|
+
}
|
|
1380
|
+
return hex;
|
|
1381
|
+
}
|
|
1236
1382
|
z.enum(["MLA", "MLB", "MLM", "MCO", "MLC", "MLU"]);
|
|
1237
1383
|
var CurrencyIdSchema = z.enum(["ARS", "USD", "BRL", "MXN"]);
|
|
1238
1384
|
var FrequencyTypeSchema = z.enum(["months", "days"]);
|
|
@@ -2100,6 +2246,8 @@ function analyze3DS(payment) {
|
|
|
2100
2246
|
description: "No se pudo determinar el estado 3DS \u2014 revisar payment.three_d_secure_mode + payment.status_detail manualmente."
|
|
2101
2247
|
};
|
|
2102
2248
|
}
|
|
2249
|
+
|
|
2250
|
+
// src/webhook.ts
|
|
2103
2251
|
function parseWebhookEvent(body, searchParams) {
|
|
2104
2252
|
const parseResult = WebhookBodySchema.safeParse(body ?? {});
|
|
2105
2253
|
const parsedBody = parseResult.success ? parseResult.data : {};
|
|
@@ -2115,7 +2263,8 @@ function parseWebhookEvent(body, searchParams) {
|
|
|
2115
2263
|
raw: parsedBody
|
|
2116
2264
|
};
|
|
2117
2265
|
}
|
|
2118
|
-
|
|
2266
|
+
var DEFAULT_REPLAY_TOLERANCE_SECONDS = 300;
|
|
2267
|
+
async function verifyWebhookSignature(params) {
|
|
2119
2268
|
if (!params.signatureHeader || !params.requestId) return false;
|
|
2120
2269
|
const parts = Object.fromEntries(
|
|
2121
2270
|
params.signatureHeader.split(",").map((segment) => segment.trim().split("="))
|
|
@@ -2123,16 +2272,20 @@ function verifyWebhookSignature(params) {
|
|
|
2123
2272
|
const ts = parts.ts;
|
|
2124
2273
|
const v1 = parts.v1;
|
|
2125
2274
|
if (!ts || !v1) return false;
|
|
2275
|
+
const tolerance = params.replayToleranceSeconds ?? DEFAULT_REPLAY_TOLERANCE_SECONDS;
|
|
2276
|
+
const tsNumber = Number(ts);
|
|
2277
|
+
if (!Number.isFinite(tsNumber)) return false;
|
|
2278
|
+
const ageSeconds = Math.abs(Math.floor(Date.now() / 1e3) - tsNumber);
|
|
2279
|
+
if (ageSeconds > tolerance) return false;
|
|
2126
2280
|
const manifest = `id:${params.dataId};request-id:${params.requestId};ts:${ts};`;
|
|
2127
|
-
const expected =
|
|
2128
|
-
|
|
2129
|
-
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
|
|
2281
|
+
const expected = await hmacSha256Hex(params.secret, manifest);
|
|
2282
|
+
return timingSafeEqualHex(expected, v1);
|
|
2130
2283
|
}
|
|
2131
2284
|
|
|
2132
2285
|
// src/tools.ts
|
|
2133
|
-
function deterministicIdempotencyKey(...parts) {
|
|
2286
|
+
async function deterministicIdempotencyKey(...parts) {
|
|
2134
2287
|
const payload = parts.filter((p) => p !== void 0 && p !== null).map(String).join("|");
|
|
2135
|
-
return
|
|
2288
|
+
return (await sha256Hex(payload)).slice(0, 32);
|
|
2136
2289
|
}
|
|
2137
2290
|
var DEFAULT_DESCRIPTIONS = {
|
|
2138
2291
|
// ── Subscriptions ────────────────────────────────────────────────────────
|
|
@@ -2244,7 +2397,9 @@ var DEFAULT_DESCRIPTIONS = {
|
|
|
2244
2397
|
cancel_point_payment_intent: "Cancel an OPEN point payment intent before the buyer interacts with the device. ONLY WORKS while state='OPEN' \u2014 once the buyer taps, you can't cancel; refund_payment after the fact instead.",
|
|
2245
2398
|
// ── Pure helpers (v0.7) ──────────────────────────────────────────────────
|
|
2246
2399
|
compute_marketplace_fee: "PURE HELPER (no network) \u2014 given a transaction amount + fee rule (% or flat ARS, with optional min/max floors), returns the exact `marketplace_fee` value in ARS to pass to create_order or create_payment_preference. USE WHEN your platform takes a commission and you need to compute the exact fee per transaction. Examples: { percent: 5, minArs: 50, maxArs: 5000 } for percentage with floor + cap; { flatArs: 200, percent: 2 } for fixed + percentage.",
|
|
2247
|
-
explain_payment_status: "PURE HELPER (no network) \u2014 given a Payment object (from get_payment / create_payment / handle_webhook), returns { summary, recommendedAction, final, paid, retryable } in Spanish. Translates MP's cryptic status_detail codes to plain Spanish + actionable guidance ('reintentar con otra tarjeta' vs 'esperar webhook' vs 'estado final'). USE THIS instead of having to memorize 30+ status_detail codes \u2014 surface summary + recommendedAction directly to the user."
|
|
2400
|
+
explain_payment_status: "PURE HELPER (no network) \u2014 given a Payment object (from get_payment / create_payment / handle_webhook), returns { summary, recommendedAction, final, paid, retryable } in Spanish. Translates MP's cryptic status_detail codes to plain Spanish + actionable guidance ('reintentar con otra tarjeta' vs 'esperar webhook' vs 'estado final'). USE THIS instead of having to memorize 30+ status_detail codes \u2014 surface summary + recommendedAction directly to the user.",
|
|
2401
|
+
// ── v0.9 — Health check + observability ──────────────────────────────────
|
|
2402
|
+
mp_health_check: "Liveness probe against MP. Returns { ok, latencyMs, userId, circuit }. USE THIS as the first call in long-running agent workflows to verify (a) network path to MP is up, (b) accessToken is valid, (c) MP is responding. Circuit-breaker state included when configured \u2014 surface to ops dashboards. Returns ok=false instead of throwing \u2014 safe to call in monitoring loops without try/catch."
|
|
2248
2403
|
};
|
|
2249
2404
|
function mercadoPagoTools(client, options) {
|
|
2250
2405
|
const desc = (name) => options.descriptions?.[name] ?? DEFAULT_DESCRIPTIONS[name];
|
|
@@ -2388,7 +2543,7 @@ function mercadoPagoTools(client, options) {
|
|
|
2388
2543
|
...options.notificationUrl !== void 0 ? { notificationUrl: options.notificationUrl } : {},
|
|
2389
2544
|
// Deterministic idempotency key — safe to retry, same inputs always
|
|
2390
2545
|
// produce the same key (MP dedupes on its side).
|
|
2391
|
-
idempotencyKey: deterministicIdempotencyKey(
|
|
2546
|
+
idempotencyKey: await deterministicIdempotencyKey(
|
|
2392
2547
|
"create_payment",
|
|
2393
2548
|
input.external_reference ?? input.payer_email,
|
|
2394
2549
|
input.amount_ars,
|
|
@@ -2511,7 +2666,7 @@ function mercadoPagoTools(client, options) {
|
|
|
2511
2666
|
const refund = await client.createRefund({
|
|
2512
2667
|
paymentId: payment_id,
|
|
2513
2668
|
...amount_ars !== void 0 ? { amount: amount_ars } : {},
|
|
2514
|
-
idempotencyKey: deterministicIdempotencyKey("refund", payment_id, amount_ars ?? "full")
|
|
2669
|
+
idempotencyKey: await deterministicIdempotencyKey("refund", payment_id, amount_ars ?? "full")
|
|
2515
2670
|
});
|
|
2516
2671
|
return {
|
|
2517
2672
|
refund_id: refund.id,
|
|
@@ -2777,7 +2932,7 @@ function mercadoPagoTools(client, options) {
|
|
|
2777
2932
|
...input.installments !== void 0 ? { installments: input.installments } : {},
|
|
2778
2933
|
...input.external_reference !== void 0 ? { externalReference: input.external_reference } : {},
|
|
2779
2934
|
...input.statement_descriptor !== void 0 ? { statementDescriptor: input.statement_descriptor } : {},
|
|
2780
|
-
idempotencyKey: deterministicIdempotencyKey(
|
|
2935
|
+
idempotencyKey: await deterministicIdempotencyKey(
|
|
2781
2936
|
"charge_saved_card",
|
|
2782
2937
|
input.card_id,
|
|
2783
2938
|
input.amount_ars,
|
|
@@ -3288,7 +3443,7 @@ function mercadoPagoTools(client, options) {
|
|
|
3288
3443
|
resource: null
|
|
3289
3444
|
};
|
|
3290
3445
|
}
|
|
3291
|
-
const verified = verifyWebhookSignature({
|
|
3446
|
+
const verified = await verifyWebhookSignature({
|
|
3292
3447
|
requestId: request_id_header,
|
|
3293
3448
|
dataId: event.dataId,
|
|
3294
3449
|
signatureHeader: signature_header,
|
|
@@ -3486,7 +3641,7 @@ function mercadoPagoTools(client, options) {
|
|
|
3486
3641
|
if (input.marketplace_fee !== void 0) params.marketplace_fee = input.marketplace_fee;
|
|
3487
3642
|
if (input.collector_id !== void 0) params.collector_id = input.collector_id;
|
|
3488
3643
|
const order = await client.createOrder(params, {
|
|
3489
|
-
idempotencyKey: deterministicIdempotencyKey(
|
|
3644
|
+
idempotencyKey: await deterministicIdempotencyKey(
|
|
3490
3645
|
"create_order",
|
|
3491
3646
|
input.external_reference,
|
|
3492
3647
|
input.total_amount,
|
|
@@ -4002,6 +4157,21 @@ function mercadoPagoTools(client, options) {
|
|
|
4002
4157
|
};
|
|
4003
4158
|
}
|
|
4004
4159
|
}),
|
|
4160
|
+
mp_health_check: tool({
|
|
4161
|
+
description: desc("mp_health_check"),
|
|
4162
|
+
inputSchema: z.object({
|
|
4163
|
+
timeout_ms: z.number().int().positive().max(3e4).optional().describe("Cap the wait time (default 5s). Use lower for status-page polling.")
|
|
4164
|
+
}),
|
|
4165
|
+
execute: async ({ timeout_ms }) => {
|
|
4166
|
+
const controller = new AbortController();
|
|
4167
|
+
const t = setTimeout(() => controller.abort(), timeout_ms ?? 5e3);
|
|
4168
|
+
try {
|
|
4169
|
+
return await client.healthCheck(controller.signal);
|
|
4170
|
+
} finally {
|
|
4171
|
+
clearTimeout(t);
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
}),
|
|
4005
4175
|
explain_payment_status: tool({
|
|
4006
4176
|
description: desc("explain_payment_status"),
|
|
4007
4177
|
inputSchema: z.object({
|
|
@@ -4050,7 +4220,206 @@ var InMemoryStateAdapter = class {
|
|
|
4050
4220
|
this.store.clear();
|
|
4051
4221
|
}
|
|
4052
4222
|
};
|
|
4223
|
+
var InMemoryOAuthTokenStore = class {
|
|
4224
|
+
store = /* @__PURE__ */ new Map();
|
|
4225
|
+
async set(userId, token) {
|
|
4226
|
+
this.store.set(userId, token);
|
|
4227
|
+
}
|
|
4228
|
+
async get(userId) {
|
|
4229
|
+
return this.store.get(userId) ?? null;
|
|
4230
|
+
}
|
|
4231
|
+
async delete(userId) {
|
|
4232
|
+
this.store.delete(userId);
|
|
4233
|
+
}
|
|
4234
|
+
async list() {
|
|
4235
|
+
return Array.from(this.store.keys());
|
|
4236
|
+
}
|
|
4237
|
+
/** Test helper. */
|
|
4238
|
+
reset() {
|
|
4239
|
+
this.store.clear();
|
|
4240
|
+
}
|
|
4241
|
+
};
|
|
4242
|
+
var InMemoryIdempotencyCache = class {
|
|
4243
|
+
store = /* @__PURE__ */ new Map();
|
|
4244
|
+
async get(key) {
|
|
4245
|
+
const entry = this.store.get(key);
|
|
4246
|
+
if (!entry) return null;
|
|
4247
|
+
if (Date.now() > entry.expiresAt) {
|
|
4248
|
+
this.store.delete(key);
|
|
4249
|
+
return null;
|
|
4250
|
+
}
|
|
4251
|
+
return entry.value;
|
|
4252
|
+
}
|
|
4253
|
+
async set(key, value, ttlSeconds = 86400) {
|
|
4254
|
+
this.store.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1e3 });
|
|
4255
|
+
}
|
|
4256
|
+
async delete(key) {
|
|
4257
|
+
this.store.delete(key);
|
|
4258
|
+
}
|
|
4259
|
+
/** Test helper. */
|
|
4260
|
+
reset() {
|
|
4261
|
+
this.store.clear();
|
|
4262
|
+
}
|
|
4263
|
+
};
|
|
4264
|
+
|
|
4265
|
+
// src/circuit-breaker.ts
|
|
4266
|
+
var CircuitOpenError = class extends Error {
|
|
4267
|
+
constructor(retryAfterMs, consecutiveFailures) {
|
|
4268
|
+
super(
|
|
4269
|
+
`Circuit breaker is OPEN \u2014 failing fast. Retry in ${Math.ceil(
|
|
4270
|
+
retryAfterMs / 1e3
|
|
4271
|
+
)}s. Consecutive upstream failures: ${consecutiveFailures}.`
|
|
4272
|
+
);
|
|
4273
|
+
this.retryAfterMs = retryAfterMs;
|
|
4274
|
+
this.consecutiveFailures = consecutiveFailures;
|
|
4275
|
+
this.name = "CircuitOpenError";
|
|
4276
|
+
}
|
|
4277
|
+
retryAfterMs;
|
|
4278
|
+
consecutiveFailures;
|
|
4279
|
+
};
|
|
4280
|
+
var CircuitBreaker = class {
|
|
4281
|
+
state = "CLOSED";
|
|
4282
|
+
consecutiveFailures = 0;
|
|
4283
|
+
halfOpenSuccesses = 0;
|
|
4284
|
+
openedAt = 0;
|
|
4285
|
+
/** Timestamps of failures within the monitoring window. */
|
|
4286
|
+
failureWindow = [];
|
|
4287
|
+
failureThreshold;
|
|
4288
|
+
successThreshold;
|
|
4289
|
+
resetTimeoutMs;
|
|
4290
|
+
monitoringWindowMs;
|
|
4291
|
+
onStateChange;
|
|
4292
|
+
isFailureFn;
|
|
4293
|
+
now;
|
|
4294
|
+
constructor(opts = {}) {
|
|
4295
|
+
this.failureThreshold = opts.failureThreshold ?? 5;
|
|
4296
|
+
this.successThreshold = opts.successThreshold ?? 2;
|
|
4297
|
+
this.resetTimeoutMs = opts.resetTimeoutMs ?? 3e4;
|
|
4298
|
+
this.monitoringWindowMs = opts.monitoringWindowMs ?? 6e4;
|
|
4299
|
+
this.onStateChange = opts.onStateChange ?? null;
|
|
4300
|
+
this.isFailureFn = opts.isFailure ?? (() => true);
|
|
4301
|
+
this.now = opts.now ?? Date.now;
|
|
4302
|
+
}
|
|
4303
|
+
/** Read the current state. Useful for health checks + metrics. */
|
|
4304
|
+
getState() {
|
|
4305
|
+
if (this.state === "OPEN" && this.now() - this.openedAt >= this.resetTimeoutMs) {
|
|
4306
|
+
this.transitionTo("HALF_OPEN");
|
|
4307
|
+
}
|
|
4308
|
+
return this.state;
|
|
4309
|
+
}
|
|
4310
|
+
/** Read diagnostic state for health checks + dashboards. */
|
|
4311
|
+
getStats() {
|
|
4312
|
+
const state = this.getState();
|
|
4313
|
+
const msSinceOpened = this.openedAt > 0 ? this.now() - this.openedAt : null;
|
|
4314
|
+
const msUntilHalfOpen = state === "OPEN" && msSinceOpened !== null ? Math.max(0, this.resetTimeoutMs - msSinceOpened) : null;
|
|
4315
|
+
return {
|
|
4316
|
+
state,
|
|
4317
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
4318
|
+
failuresInWindow: this.failuresInCurrentWindow(),
|
|
4319
|
+
msSinceOpened,
|
|
4320
|
+
msUntilHalfOpen
|
|
4321
|
+
};
|
|
4322
|
+
}
|
|
4323
|
+
/**
|
|
4324
|
+
* Execute `fn` under the breaker's protection.
|
|
4325
|
+
* - If the breaker is OPEN, throws `CircuitOpenError` immediately.
|
|
4326
|
+
* - If `fn` succeeds, may transition HALF_OPEN → CLOSED.
|
|
4327
|
+
* - If `fn` fails (and the error counts as a failure), records the
|
|
4328
|
+
* failure; may transition CLOSED → OPEN or HALF_OPEN → OPEN.
|
|
4329
|
+
*/
|
|
4330
|
+
async execute(fn) {
|
|
4331
|
+
const state = this.getState();
|
|
4332
|
+
if (state === "OPEN") {
|
|
4333
|
+
const elapsed = this.now() - this.openedAt;
|
|
4334
|
+
throw new CircuitOpenError(
|
|
4335
|
+
Math.max(0, this.resetTimeoutMs - elapsed),
|
|
4336
|
+
this.consecutiveFailures
|
|
4337
|
+
);
|
|
4338
|
+
}
|
|
4339
|
+
try {
|
|
4340
|
+
const result = await fn();
|
|
4341
|
+
this.recordSuccess();
|
|
4342
|
+
return result;
|
|
4343
|
+
} catch (err) {
|
|
4344
|
+
if (this.isFailureFn(err)) {
|
|
4345
|
+
this.recordFailure(err);
|
|
4346
|
+
}
|
|
4347
|
+
throw err;
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
/** Manually force the breaker open. Useful for runbook / manual ops. */
|
|
4351
|
+
trip(reason) {
|
|
4352
|
+
if (this.state !== "OPEN") {
|
|
4353
|
+
this.transitionTo("OPEN", reason);
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
/** Manually reset the breaker to CLOSED. */
|
|
4357
|
+
reset() {
|
|
4358
|
+
this.consecutiveFailures = 0;
|
|
4359
|
+
this.halfOpenSuccesses = 0;
|
|
4360
|
+
this.failureWindow = [];
|
|
4361
|
+
if (this.state !== "CLOSED") {
|
|
4362
|
+
this.transitionTo("CLOSED");
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
4366
|
+
// Internal
|
|
4367
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
4368
|
+
recordSuccess() {
|
|
4369
|
+
this.consecutiveFailures = 0;
|
|
4370
|
+
if (this.state === "HALF_OPEN") {
|
|
4371
|
+
this.halfOpenSuccesses++;
|
|
4372
|
+
if (this.halfOpenSuccesses >= this.successThreshold) {
|
|
4373
|
+
this.transitionTo("CLOSED");
|
|
4374
|
+
}
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
recordFailure(cause) {
|
|
4378
|
+
this.consecutiveFailures++;
|
|
4379
|
+
this.failureWindow.push(this.now());
|
|
4380
|
+
this.pruneWindow();
|
|
4381
|
+
if (this.state === "HALF_OPEN") {
|
|
4382
|
+
this.transitionTo("OPEN", cause);
|
|
4383
|
+
return;
|
|
4384
|
+
}
|
|
4385
|
+
if (this.state === "CLOSED" && this.failuresInCurrentWindow() >= this.failureThreshold) {
|
|
4386
|
+
this.transitionTo("OPEN", cause);
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
transitionTo(to, cause) {
|
|
4390
|
+
const from = this.state;
|
|
4391
|
+
if (from === to) return;
|
|
4392
|
+
this.state = to;
|
|
4393
|
+
if (to === "OPEN") {
|
|
4394
|
+
this.openedAt = this.now();
|
|
4395
|
+
this.halfOpenSuccesses = 0;
|
|
4396
|
+
} else if (to === "CLOSED") {
|
|
4397
|
+
this.consecutiveFailures = 0;
|
|
4398
|
+
this.halfOpenSuccesses = 0;
|
|
4399
|
+
this.failureWindow = [];
|
|
4400
|
+
this.openedAt = 0;
|
|
4401
|
+
} else if (to === "HALF_OPEN") {
|
|
4402
|
+
this.halfOpenSuccesses = 0;
|
|
4403
|
+
}
|
|
4404
|
+
this.onStateChange?.({
|
|
4405
|
+
from,
|
|
4406
|
+
to,
|
|
4407
|
+
cause,
|
|
4408
|
+
consecutiveFailures: this.consecutiveFailures
|
|
4409
|
+
});
|
|
4410
|
+
}
|
|
4411
|
+
pruneWindow() {
|
|
4412
|
+
const cutoff = this.now() - this.monitoringWindowMs;
|
|
4413
|
+
while (this.failureWindow.length > 0 && this.failureWindow[0] < cutoff) {
|
|
4414
|
+
this.failureWindow.shift();
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
failuresInCurrentWindow() {
|
|
4418
|
+
this.pruneWindow();
|
|
4419
|
+
return this.failureWindow.length;
|
|
4420
|
+
}
|
|
4421
|
+
};
|
|
4053
4422
|
|
|
4054
|
-
export { InMemoryStateAdapter, MercadoPagoAccountTypeMismatchError, MercadoPagoAuthError, MercadoPagoAuthorizeForbiddenError, MercadoPagoBackUrlInvalidError, MercadoPagoClient, MercadoPagoError, MercadoPagoOverloadedError, MercadoPagoPaymentRejectedError, MercadoPagoRateLimitError, MercadoPagoSelfPaymentError, MercadoPagoTimeoutError, TEST_CARDS_AR, TEST_PAYERS_AR, analyze3DS, buildAuthorizeUrl, buildTestCardScenario, classifyError, computeMarketplaceFee, exchangeCodeForToken, expirationTimeMs, explainPaymentStatus, isExpiringSoon, mercadoPagoTools, parseWebhookEvent, refreshAccessToken, verifyWebhookSignature };
|
|
4423
|
+
export { CircuitBreaker, CircuitOpenError, InMemoryIdempotencyCache, InMemoryOAuthTokenStore, InMemoryStateAdapter, MercadoPagoAccountTypeMismatchError, MercadoPagoAuthError, MercadoPagoAuthorizeForbiddenError, MercadoPagoBackUrlInvalidError, MercadoPagoClient, MercadoPagoError, MercadoPagoOverloadedError, MercadoPagoPaymentRejectedError, MercadoPagoRateLimitError, MercadoPagoSelfPaymentError, MercadoPagoTimeoutError, TEST_CARDS_AR, TEST_PAYERS_AR, analyze3DS, buildAuthorizeUrl, buildTestCardScenario, classifyError, computeMarketplaceFee, exchangeCodeForToken, expirationTimeMs, explainPaymentStatus, isExpiringSoon, mercadoPagoTools, parseWebhookEvent, refreshAccessToken, verifyWebhookSignature };
|
|
4055
4424
|
//# sourceMappingURL=index.js.map
|
|
4056
4425
|
//# sourceMappingURL=index.js.map
|