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