@ar-agents/mercadopago 0.8.0 → 0.10.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/dist/index.cjs CHANGED
@@ -3,6 +3,75 @@
3
3
  var ai = require('ai');
4
4
  var zod = require('zod');
5
5
 
6
+ var __defProp = Object.defineProperty;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+
16
+ // src/crypto.ts
17
+ var crypto_exports = {};
18
+ __export(crypto_exports, {
19
+ hmacSha256Hex: () => hmacSha256Hex,
20
+ sha256Hex: () => sha256Hex,
21
+ timingSafeEqualHex: () => timingSafeEqualHex
22
+ });
23
+ async function hmacSha256Hex(secret, message) {
24
+ const keyMaterial = await subtle.importKey(
25
+ "raw",
26
+ encoder.encode(secret),
27
+ { name: "HMAC", hash: "SHA-256" },
28
+ false,
29
+ ["sign"]
30
+ );
31
+ const sigBuf = await subtle.sign(
32
+ "HMAC",
33
+ keyMaterial,
34
+ encoder.encode(message)
35
+ );
36
+ return bufferToHex(sigBuf);
37
+ }
38
+ async function sha256Hex(input) {
39
+ const digest = await subtle.digest("SHA-256", encoder.encode(input));
40
+ return bufferToHex(digest);
41
+ }
42
+ function timingSafeEqualHex(a, b) {
43
+ if (a.length !== b.length) return false;
44
+ let diff = 0;
45
+ for (let i = 0; i < a.length; i++) {
46
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
47
+ }
48
+ return diff === 0;
49
+ }
50
+ function bufferToHex(buf) {
51
+ const bytes = new Uint8Array(buf);
52
+ let hex = "";
53
+ for (let i = 0; i < bytes.length; i++) {
54
+ const b = bytes[i];
55
+ hex += (b < 16 ? "0" : "") + b.toString(16);
56
+ }
57
+ return hex;
58
+ }
59
+ var subtle, encoder;
60
+ var init_crypto = __esm({
61
+ "src/crypto.ts"() {
62
+ subtle = (() => {
63
+ const c = globalThis.crypto;
64
+ if (!c?.subtle) {
65
+ throw new Error(
66
+ "@ar-agents/mercadopago: Web Crypto API is not available in this runtime. Use Node 18+, Vercel Edge Runtime, Cloudflare Workers, or any modern browser."
67
+ );
68
+ }
69
+ return c.subtle;
70
+ })();
71
+ encoder = new TextEncoder();
72
+ }
73
+ });
74
+
6
75
  // src/errors.ts
7
76
  var MercadoPagoError = class extends Error {
8
77
  constructor(message, status, endpoint, mpResponse) {
@@ -166,6 +235,8 @@ var MercadoPagoClient = class {
166
235
  requestTimeoutMs;
167
236
  maxRetries;
168
237
  onCall;
238
+ circuitBreaker;
239
+ traceContext;
169
240
  constructor(options) {
170
241
  if (!options.accessToken) {
171
242
  throw new Error(
@@ -178,8 +249,24 @@ var MercadoPagoClient = class {
178
249
  this.requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
179
250
  this.maxRetries = Math.max(0, options.maxRetries ?? 1);
180
251
  this.onCall = options.onCall;
252
+ this.circuitBreaker = options.circuitBreaker;
253
+ this.traceContext = options.traceContext;
254
+ }
255
+ /**
256
+ * v0.9 — Inspect the circuit breaker state (when configured). Returns
257
+ * `null` when no circuit breaker is wired. Useful for health checks.
258
+ */
259
+ getCircuitState() {
260
+ return this.circuitBreaker?.getStats() ?? null;
181
261
  }
182
262
  async request(method, path, body, options) {
263
+ const exec = () => this.requestUnprotected(method, path, body, options);
264
+ if (this.circuitBreaker) {
265
+ return this.circuitBreaker.execute(exec);
266
+ }
267
+ return exec();
268
+ }
269
+ async requestUnprotected(method, path, body, options) {
183
270
  const headers = {
184
271
  Authorization: `Bearer ${this.accessToken}`,
185
272
  "Content-Type": "application/json"
@@ -187,6 +274,11 @@ var MercadoPagoClient = class {
187
274
  if (options?.idempotencyKey) {
188
275
  headers["X-Idempotency-Key"] = options.idempotencyKey;
189
276
  }
277
+ const trace = this.traceContext?.();
278
+ if (trace?.traceId && trace?.spanId) {
279
+ const flags = (trace.traceFlags ?? 1).toString(16).padStart(2, "0");
280
+ headers["traceparent"] = `00-${trace.traceId}-${trace.spanId}-${flags}`;
281
+ }
190
282
  let url = `${this.baseUrl}${path}`;
191
283
  if (options?.query) {
192
284
  const search = new URLSearchParams();
@@ -203,24 +295,54 @@ var MercadoPagoClient = class {
203
295
  let attempt = 0;
204
296
  let lastError;
205
297
  let lastStatus = null;
298
+ const fireOnCall = (event) => {
299
+ const traceCtx = trace?.traceId ? {
300
+ traceId: trace.traceId,
301
+ ...trace.spanId !== void 0 ? { spanId: trace.spanId } : {}
302
+ } : void 0;
303
+ this.onCall?.({
304
+ method,
305
+ path,
306
+ durationMs: Date.now() - t0,
307
+ ...event,
308
+ ...this.circuitBreaker ? { circuitState: this.circuitBreaker.getState() } : {},
309
+ ...traceCtx ? { traceContext: traceCtx } : {}
310
+ });
311
+ };
206
312
  while (attempt <= this.maxRetries) {
207
313
  const controller = new AbortController();
208
314
  const timer = setTimeout(() => controller.abort(), this.requestTimeoutMs);
315
+ const parentSignal = options?.signal;
316
+ const onParentAbort = () => controller.abort();
317
+ if (parentSignal) {
318
+ if (parentSignal.aborted) {
319
+ clearTimeout(timer);
320
+ throw new MercadoPagoTimeoutError(path, 0);
321
+ }
322
+ parentSignal.addEventListener("abort", onParentAbort, { once: true });
323
+ }
209
324
  const init = { method, headers, signal: controller.signal };
210
325
  if (body !== void 0) init.body = JSON.stringify(body);
211
326
  try {
212
327
  const res = await fetchFn(url, init);
213
328
  clearTimeout(timer);
329
+ if (parentSignal) parentSignal.removeEventListener("abort", onParentAbort);
214
330
  lastStatus = res.status;
331
+ const requestId = res.headers.get("x-request-id");
332
+ const rlRemaining = res.headers.get("x-rate-limit-remaining");
333
+ const rlReset = res.headers.get("x-rate-limit-reset");
334
+ const rateLimit = {
335
+ remaining: rlRemaining !== null ? Number(rlRemaining) : null,
336
+ resetSeconds: rlReset !== null ? Number(rlReset) : null
337
+ };
215
338
  if (res.ok) {
216
339
  const text2 = await res.text();
217
- this.onCall?.({
218
- method,
219
- path,
220
- durationMs: Date.now() - t0,
340
+ fireOnCall({
341
+ success: true,
221
342
  httpStatus: res.status,
222
343
  retried: attempt,
223
- success: true
344
+ requestId,
345
+ rateLimit
224
346
  });
225
347
  if (!text2) return void 0;
226
348
  return JSON.parse(text2);
@@ -235,13 +357,12 @@ var MercadoPagoClient = class {
235
357
  }
236
358
  const contentType = res.headers.get("content-type") ?? "";
237
359
  if (res.status >= 500 && !contentType.includes("application/json")) {
238
- this.onCall?.({
239
- method,
240
- path,
241
- durationMs: Date.now() - t0,
360
+ fireOnCall({
361
+ success: false,
242
362
  httpStatus: res.status,
243
363
  retried: attempt,
244
- success: false
364
+ requestId,
365
+ rateLimit
245
366
  });
246
367
  throw new MercadoPagoOverloadedError(path, res.status);
247
368
  }
@@ -253,33 +374,33 @@ var MercadoPagoClient = class {
253
374
  parsed = text;
254
375
  }
255
376
  const err = classifyError(res.status, path, parsed, options?.classifyContext);
256
- this.onCall?.({
257
- method,
258
- path,
259
- durationMs: Date.now() - t0,
377
+ fireOnCall({
378
+ success: false,
260
379
  httpStatus: res.status,
261
380
  retried: attempt,
262
- success: false
381
+ requestId,
382
+ rateLimit
263
383
  });
264
384
  throw err;
265
385
  } catch (err) {
266
386
  clearTimeout(timer);
387
+ if (parentSignal) parentSignal.removeEventListener("abort", onParentAbort);
267
388
  if (err instanceof MercadoPagoError) throw err;
268
389
  const isAbort = err instanceof Error && err.name === "AbortError";
390
+ const isParentAbort = parentSignal?.aborted ?? false;
269
391
  const isNetwork = !lastStatus && !isAbort;
270
- if ((isNetwork || isAbort) && attempt < this.maxRetries) {
392
+ if ((isNetwork || isAbort && !isParentAbort) && attempt < this.maxRetries) {
271
393
  lastError = err;
272
394
  attempt++;
273
395
  await sleep(250 * Math.pow(2, attempt - 1));
274
396
  continue;
275
397
  }
276
- this.onCall?.({
277
- method,
278
- path,
279
- durationMs: Date.now() - t0,
398
+ fireOnCall({
399
+ success: false,
280
400
  httpStatus: lastStatus,
281
401
  retried: attempt,
282
- success: false
402
+ requestId: null,
403
+ rateLimit: { remaining: null, resetSeconds: null }
283
404
  });
284
405
  if (isAbort) {
285
406
  throw new MercadoPagoTimeoutError(path, this.requestTimeoutMs);
@@ -1233,55 +1354,57 @@ var MercadoPagoClient = class {
1233
1354
  );
1234
1355
  return { id: intentId, canceled: true };
1235
1356
  }
1357
+ // ──────────────────────────────────────────────────────────────────────────
1358
+ // v0.9 — Health check
1359
+ //
1360
+ // No dedicated ping endpoint exists in MP's public API. We use `getMe()`
1361
+ // (`/users/me`) as a lightweight liveness probe — it requires only a valid
1362
+ // accessToken, returns ~200 bytes of JSON, and is the same call MP's own
1363
+ // dashboard makes on startup. A successful response proves: (a) network
1364
+ // path to MP is up, (b) accessToken is valid, (c) MP is responding.
1365
+ // ──────────────────────────────────────────────────────────────────────────
1366
+ /**
1367
+ * Liveness probe against MP. Returns latency + circuit-breaker state.
1368
+ * Use as a /health endpoint for k8s, Vercel cron, or status-page checks.
1369
+ *
1370
+ * Returns `{ ok: false, ... }` instead of throwing — designed for
1371
+ * monitoring loops that want to keep running.
1372
+ *
1373
+ * @param signal Optional AbortSignal to cap wait time (e.g., 2s for
1374
+ * status-page polling).
1375
+ */
1376
+ async healthCheck(signal) {
1377
+ const t0 = Date.now();
1378
+ const circuitBefore = this.circuitBreaker?.getStats() ?? null;
1379
+ try {
1380
+ const me = await this.request(
1381
+ "GET",
1382
+ "/users/me",
1383
+ void 0,
1384
+ signal ? { signal } : {}
1385
+ );
1386
+ return {
1387
+ ok: true,
1388
+ latencyMs: Date.now() - t0,
1389
+ userId: String(me.id),
1390
+ error: null,
1391
+ circuit: this.circuitBreaker?.getStats() ?? null
1392
+ };
1393
+ } catch (err) {
1394
+ const message = err instanceof Error ? err.message : String(err);
1395
+ return {
1396
+ ok: false,
1397
+ latencyMs: Date.now() - t0,
1398
+ userId: null,
1399
+ error: message,
1400
+ circuit: this.circuitBreaker?.getStats() ?? circuitBefore
1401
+ };
1402
+ }
1403
+ }
1236
1404
  };
1237
1405
 
1238
- // src/crypto.ts
1239
- var subtle = (() => {
1240
- const c = globalThis.crypto;
1241
- if (!c?.subtle) {
1242
- throw new Error(
1243
- "@ar-agents/mercadopago: Web Crypto API is not available in this runtime. Use Node 18+, Vercel Edge Runtime, Cloudflare Workers, or any modern browser."
1244
- );
1245
- }
1246
- return c.subtle;
1247
- })();
1248
- var encoder = new TextEncoder();
1249
- async function hmacSha256Hex(secret, message) {
1250
- const keyMaterial = await subtle.importKey(
1251
- "raw",
1252
- encoder.encode(secret),
1253
- { name: "HMAC", hash: "SHA-256" },
1254
- false,
1255
- ["sign"]
1256
- );
1257
- const sigBuf = await subtle.sign(
1258
- "HMAC",
1259
- keyMaterial,
1260
- encoder.encode(message)
1261
- );
1262
- return bufferToHex(sigBuf);
1263
- }
1264
- async function sha256Hex(input) {
1265
- const digest = await subtle.digest("SHA-256", encoder.encode(input));
1266
- return bufferToHex(digest);
1267
- }
1268
- function timingSafeEqualHex(a, b) {
1269
- if (a.length !== b.length) return false;
1270
- let diff = 0;
1271
- for (let i = 0; i < a.length; i++) {
1272
- diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
1273
- }
1274
- return diff === 0;
1275
- }
1276
- function bufferToHex(buf) {
1277
- const bytes = new Uint8Array(buf);
1278
- let hex = "";
1279
- for (let i = 0; i < bytes.length; i++) {
1280
- const b = bytes[i];
1281
- hex += (b < 16 ? "0" : "") + b.toString(16);
1282
- }
1283
- return hex;
1284
- }
1406
+ // src/tools.ts
1407
+ init_crypto();
1285
1408
  zod.z.enum(["MLA", "MLB", "MLM", "MCO", "MLC", "MLU"]);
1286
1409
  var CurrencyIdSchema = zod.z.enum(["ARS", "USD", "BRL", "MXN"]);
1287
1410
  var FrequencyTypeSchema = zod.z.enum(["months", "days"]);
@@ -1785,6 +1908,180 @@ function isExpiringSoon(expirationMs, skewSeconds = 300) {
1785
1908
  return Date.now() + skewSeconds * 1e3 >= expirationMs;
1786
1909
  }
1787
1910
 
1911
+ // src/ar-issuer-promos.ts
1912
+ var AHORA_PROGRAM_PROMOS = [
1913
+ {
1914
+ issuer: "*",
1915
+ // any AR-resident card
1916
+ paymentMethodId: "*",
1917
+ installments: 3,
1918
+ description: "Ahora 3 \u2014 3 cuotas sin inter\xE9s (programa nacional)",
1919
+ categories: ["electronics", "appliances", "clothing", "general"]
1920
+ },
1921
+ {
1922
+ issuer: "*",
1923
+ paymentMethodId: "*",
1924
+ installments: 6,
1925
+ description: "Ahora 6 \u2014 6 cuotas sin inter\xE9s (programa nacional, electrodom\xE9sticos l\xEDnea blanca)",
1926
+ categories: ["appliances"]
1927
+ },
1928
+ {
1929
+ issuer: "*",
1930
+ paymentMethodId: "*",
1931
+ installments: 12,
1932
+ description: "Ahora 12 \u2014 12 cuotas sin inter\xE9s (programa nacional)",
1933
+ categories: ["electronics", "appliances", "clothing"]
1934
+ },
1935
+ {
1936
+ issuer: "*",
1937
+ paymentMethodId: "*",
1938
+ installments: 18,
1939
+ description: "Ahora 18 \u2014 18 cuotas sin inter\xE9s (turismo nacional + electrodom\xE9sticos)",
1940
+ categories: ["appliances", "travel"]
1941
+ },
1942
+ {
1943
+ issuer: "*",
1944
+ paymentMethodId: "*",
1945
+ installments: 24,
1946
+ description: "Ahora 24 \u2014 24 cuotas sin inter\xE9s (electrodom\xE9sticos l\xEDnea blanca premium)",
1947
+ categories: ["appliances"]
1948
+ }
1949
+ ];
1950
+ var AR_ISSUER_PROMOS = [
1951
+ // Naranja X
1952
+ {
1953
+ issuer: "Naranja X",
1954
+ paymentMethodId: "naranja",
1955
+ installments: 3,
1956
+ description: "Naranja Z (Plan Z) \u2014 3 cuotas con CFT promocional, todos los rubros"
1957
+ },
1958
+ {
1959
+ issuer: "Naranja X",
1960
+ paymentMethodId: "naranja",
1961
+ installments: 6,
1962
+ description: "Naranja \u2014 6 cuotas sin inter\xE9s con comercios adheridos (electro/indumentaria)",
1963
+ daysOfWeek: ["thu"],
1964
+ categories: ["electronics", "appliances", "clothing"]
1965
+ },
1966
+ // Galicia
1967
+ {
1968
+ issuer: "Banco Galicia",
1969
+ paymentMethodId: "visa",
1970
+ installments: 12,
1971
+ description: "Galicia Eminent / Quiero! \u2014 12 cuotas sin inter\xE9s en supermercados (jueves)",
1972
+ daysOfWeek: ["thu"],
1973
+ categories: ["supermarket"]
1974
+ },
1975
+ {
1976
+ issuer: "Banco Galicia",
1977
+ paymentMethodId: "master",
1978
+ installments: 6,
1979
+ description: "Galicia \u2014 6 cuotas sin inter\xE9s en gastronom\xEDa (viernes y s\xE1bados)",
1980
+ daysOfWeek: ["fri", "sat"]
1981
+ },
1982
+ // Santander
1983
+ {
1984
+ issuer: "Banco Santander",
1985
+ paymentMethodId: "visa",
1986
+ installments: 6,
1987
+ description: "Santander Black / Platinum \u2014 6 cuotas sin inter\xE9s en cines + viajes",
1988
+ categories: ["travel"]
1989
+ },
1990
+ {
1991
+ issuer: "Banco Santander",
1992
+ paymentMethodId: "amex",
1993
+ installments: 9,
1994
+ description: "Santander American Express \u2014 9 cuotas sin inter\xE9s en supermercados (martes y mi\xE9rcoles)",
1995
+ daysOfWeek: ["tue", "wed"],
1996
+ categories: ["supermarket"]
1997
+ },
1998
+ // Macro
1999
+ {
2000
+ issuer: "Banco Macro",
2001
+ paymentMethodId: "visa",
2002
+ installments: 6,
2003
+ description: "Macro Selecta / Premia \u2014 6 cuotas sin inter\xE9s en farmacias y librer\xEDas",
2004
+ categories: ["health", "education"]
2005
+ },
2006
+ // BBVA
2007
+ {
2008
+ issuer: "BBVA Banco Franc\xE9s",
2009
+ paymentMethodId: "visa",
2010
+ installments: 3,
2011
+ description: "BBVA Lat / Black \u2014 3 cuotas sin inter\xE9s en restaurantes (lunes a mi\xE9rcoles)",
2012
+ daysOfWeek: ["mon", "tue", "wed"]
2013
+ },
2014
+ // ICBC
2015
+ {
2016
+ issuer: "ICBC",
2017
+ paymentMethodId: "visa",
2018
+ installments: 6,
2019
+ description: "ICBC Cuenta Corriente \u2014 6 cuotas sin inter\xE9s en electro y indumentaria",
2020
+ categories: ["electronics", "appliances", "clothing"]
2021
+ },
2022
+ // Patagonia
2023
+ {
2024
+ issuer: "Banco Patagonia",
2025
+ paymentMethodId: "visa",
2026
+ installments: 3,
2027
+ description: "Patagonia 365 / Eminent \u2014 3 cuotas sin inter\xE9s en supermercados (s\xE1bados)",
2028
+ daysOfWeek: ["sat"],
2029
+ categories: ["supermarket"]
2030
+ },
2031
+ // Banco Nación
2032
+ {
2033
+ issuer: "Banco de la Naci\xF3n Argentina",
2034
+ paymentMethodId: "visa",
2035
+ installments: 12,
2036
+ description: "BNA \u2014 12 cuotas sin inter\xE9s con plan 'Ahora 12' del programa nacional",
2037
+ categories: ["electronics", "appliances", "clothing"]
2038
+ },
2039
+ // Banco Provincia
2040
+ {
2041
+ issuer: "Banco de la Provincia de Buenos Aires",
2042
+ paymentMethodId: "visa",
2043
+ installments: 6,
2044
+ description: "Cuenta DNI \u2014 6 cuotas sin inter\xE9s (mensual cap aplica)",
2045
+ maxAmountArs: 2e5
2046
+ },
2047
+ // Banco Ciudad
2048
+ {
2049
+ issuer: "Banco de la Ciudad de Buenos Aires",
2050
+ paymentMethodId: "visa",
2051
+ installments: 12,
2052
+ description: "Banco Ciudad \u2014 12 cuotas sin inter\xE9s en electrodom\xE9sticos (Plan Sue\xF1os)",
2053
+ categories: ["appliances"]
2054
+ }
2055
+ ];
2056
+ function findApplicablePromos(args) {
2057
+ const date = args.date ?? /* @__PURE__ */ new Date();
2058
+ const dayOfWeek = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"][date.getDay()];
2059
+ const candidates = [
2060
+ ...args.includeAhoraProgram !== false ? AHORA_PROGRAM_PROMOS : [],
2061
+ ...AR_ISSUER_PROMOS
2062
+ ];
2063
+ return candidates.filter((promo) => {
2064
+ if (args.issuer !== void 0 && promo.issuer !== "*" && promo.issuer !== args.issuer) {
2065
+ return false;
2066
+ }
2067
+ if (args.paymentMethodId !== void 0 && promo.paymentMethodId !== "*" && promo.paymentMethodId !== args.paymentMethodId) {
2068
+ return false;
2069
+ }
2070
+ if (promo.daysOfWeek && promo.daysOfWeek.length > 0 && !promo.daysOfWeek.includes(dayOfWeek)) {
2071
+ return false;
2072
+ }
2073
+ if (args.category !== void 0 && promo.categories && promo.categories.length > 0 && !promo.categories.includes(args.category)) {
2074
+ return false;
2075
+ }
2076
+ if (args.amountArs !== void 0 && promo.minAmountArs !== void 0 && args.amountArs < promo.minAmountArs) {
2077
+ return false;
2078
+ }
2079
+ if (promo.startDate && date < new Date(promo.startDate)) return false;
2080
+ if (promo.endDate && date > new Date(promo.endDate)) return false;
2081
+ return true;
2082
+ });
2083
+ }
2084
+
1788
2085
  // src/helpers.ts
1789
2086
  function computeMarketplaceFee(amountArs, rule) {
1790
2087
  if (amountArs <= 0) return 0;
@@ -2022,6 +2319,116 @@ function explainPaymentStatus(payment) {
2022
2319
  }
2023
2320
  }
2024
2321
 
2322
+ // src/pagination.ts
2323
+ async function* paginate(fetchPage, opts) {
2324
+ const pageSize = opts.pageSize ?? 100;
2325
+ const concurrency = Math.max(1, opts.concurrency ?? 1);
2326
+ let yielded = 0;
2327
+ let offset = 0;
2328
+ let knownTotal = void 0;
2329
+ while (true) {
2330
+ if (opts.maxItems !== void 0 && yielded >= opts.maxItems) return;
2331
+ const inFlight = [];
2332
+ for (let i = 0; i < concurrency; i++) {
2333
+ const pageOffset = offset + i * pageSize;
2334
+ if (knownTotal !== void 0 && pageOffset >= knownTotal) break;
2335
+ inFlight.push(fetchPage(pageOffset, pageSize));
2336
+ }
2337
+ if (inFlight.length === 0) return;
2338
+ const pages = await Promise.all(inFlight);
2339
+ let allEmpty = true;
2340
+ for (const page of pages) {
2341
+ const items = opts.extractItems(page);
2342
+ const total = opts.extractTotal?.(page);
2343
+ if (total !== void 0) knownTotal = total;
2344
+ if (items.length > 0) allEmpty = false;
2345
+ for (const item of items) {
2346
+ if (opts.maxItems !== void 0 && yielded >= opts.maxItems) return;
2347
+ yield item;
2348
+ yielded++;
2349
+ }
2350
+ }
2351
+ if (allEmpty) return;
2352
+ offset += pages.length * pageSize;
2353
+ if (knownTotal !== void 0 && offset >= knownTotal) return;
2354
+ }
2355
+ }
2356
+ async function collect(iter) {
2357
+ const out = [];
2358
+ for await (const item of iter) out.push(item);
2359
+ return out;
2360
+ }
2361
+ function paginatePayments(client, filter = {}, opts = {}) {
2362
+ return paginate(
2363
+ (offset, limit) => client.searchPayments({ ...filter, offset, limit }),
2364
+ {
2365
+ extractItems: (p) => p.results ?? [],
2366
+ extractTotal: (p) => p.paging?.total,
2367
+ ...opts
2368
+ }
2369
+ );
2370
+ }
2371
+ function paginateSubscriptions(client, filter = {}, opts = {}) {
2372
+ return paginate(
2373
+ (offset, limit) => client.searchPreapprovals({ ...filter, offset, limit }),
2374
+ {
2375
+ extractItems: (p) => p.results,
2376
+ extractTotal: (p) => p.paging.total,
2377
+ ...opts
2378
+ }
2379
+ );
2380
+ }
2381
+ function paginateAccountMovements(client, filter = {}, opts = {}) {
2382
+ return paginate(
2383
+ (offset, limit) => client.listAccountMovements({ ...filter, offset, limit }),
2384
+ {
2385
+ extractItems: (p) => p.movements,
2386
+ extractTotal: (p) => p.paging.total,
2387
+ ...opts
2388
+ }
2389
+ );
2390
+ }
2391
+ function paginateSettlements(client, filter = {}, opts = {}) {
2392
+ return paginate(
2393
+ (offset, limit) => client.listSettlements({ ...filter, offset, limit }),
2394
+ {
2395
+ extractItems: (p) => p.settlements,
2396
+ extractTotal: (p) => p.paging.total,
2397
+ ...opts
2398
+ }
2399
+ );
2400
+ }
2401
+ function paginateMerchantOrders(client, filter = {}, opts = {}) {
2402
+ return paginate(
2403
+ (offset, limit) => client.searchMerchantOrders({ ...filter, offset, limit }),
2404
+ {
2405
+ extractItems: (p) => p.elements,
2406
+ extractTotal: (p) => p.paging.total,
2407
+ ...opts
2408
+ }
2409
+ );
2410
+ }
2411
+ function paginateSubscriptionPlans(client, filter = {}, opts = {}) {
2412
+ return paginate(
2413
+ (offset, limit) => client.listSubscriptionPlans({ ...filter, offset, limit }),
2414
+ {
2415
+ extractItems: (p) => p.results,
2416
+ extractTotal: (p) => p.paging.total,
2417
+ ...opts
2418
+ }
2419
+ );
2420
+ }
2421
+ function paginateSubscriptionPayments(client, preapprovalId, opts = {}) {
2422
+ return paginate(
2423
+ (offset, limit) => client.listSubscriptionPayments(preapprovalId, { offset, limit }),
2424
+ {
2425
+ extractItems: (p) => p.results,
2426
+ extractTotal: (p) => p.paging.total,
2427
+ ...opts
2428
+ }
2429
+ );
2430
+ }
2431
+
2025
2432
  // src/test-cards.ts
2026
2433
  var TEST_CARDS_AR = {
2027
2434
  VISA_CREDIT: {
@@ -2149,8 +2556,40 @@ function analyze3DS(payment) {
2149
2556
  description: "No se pudo determinar el estado 3DS \u2014 revisar payment.three_d_secure_mode + payment.status_detail manualmente."
2150
2557
  };
2151
2558
  }
2559
+ async function confirmChallengeAndPoll(client, paymentId, options = {}) {
2560
+ const maxAttempts = options.maxAttempts ?? 5;
2561
+ const interval = options.pollIntervalMs ?? 1e3;
2562
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2563
+ if (options.signal?.aborted) {
2564
+ const payment3 = await client.getPayment(paymentId);
2565
+ return { payment: payment3, threeDs: analyze3DS(payment3), resolved: false, attempts: attempt };
2566
+ }
2567
+ const payment2 = await client.getPayment(paymentId);
2568
+ const threeDs = analyze3DS(payment2);
2569
+ const stillWaiting = threeDs.status === "challenge_required" || payment2.status === "pending" || payment2.status === "in_process";
2570
+ if (!stillWaiting) {
2571
+ return { payment: payment2, threeDs, resolved: true, attempts: attempt };
2572
+ }
2573
+ if (attempt < maxAttempts) {
2574
+ await new Promise((resolve) => {
2575
+ const timer = setTimeout(resolve, interval);
2576
+ options.signal?.addEventListener(
2577
+ "abort",
2578
+ () => {
2579
+ clearTimeout(timer);
2580
+ resolve(void 0);
2581
+ },
2582
+ { once: true }
2583
+ );
2584
+ });
2585
+ }
2586
+ }
2587
+ const payment = await client.getPayment(paymentId);
2588
+ return { payment, threeDs: analyze3DS(payment), resolved: false, attempts: maxAttempts };
2589
+ }
2152
2590
 
2153
2591
  // src/webhook.ts
2592
+ init_crypto();
2154
2593
  function parseWebhookEvent(body, searchParams) {
2155
2594
  const parseResult = WebhookBodySchema.safeParse(body ?? {});
2156
2595
  const parsedBody = parseResult.success ? parseResult.data : {};
@@ -2300,7 +2739,16 @@ var DEFAULT_DESCRIPTIONS = {
2300
2739
  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.",
2301
2740
  // ── Pure helpers (v0.7) ──────────────────────────────────────────────────
2302
2741
  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.",
2303
- 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."
2742
+ 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.",
2743
+ // ── v0.9 — Health check + observability ──────────────────────────────────
2744
+ 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.",
2745
+ // ── v0.10 — AR issuer cuotas promos (pure) ───────────────────────────────
2746
+ find_applicable_promos: "PURE HELPER (no network, sub-ms) \u2014 returns the 'cuotas sin inter\xE9s' promotions applicable to a given (issuer, paymentMethodId, amount, category, date) tuple. Includes the federal Ahora 3/6/12/18/24/30 program AND issuer-specific deals (Naranja con Galicia los jueves, Santander Amex en supermercados los martes, etc.). USE THIS BEFORE checkout to surface 'pag\xE1 en 12 cuotas sin inter\xE9s con tu Galicia' hints to the buyer \u2014 drives conversion. Returns an array of CuotasPromo objects; the `description` field is in Spanish and ALWAYS surface verbatim. Catalog updated quarterly.",
2747
+ // ── v0.10 — 3DS challenge resolution ────────────────────────────────────
2748
+ confirm_3ds_challenge: "After the buyer completes a 3DS challenge (redirected back from challengeUrl), call this to poll MP and confirm whether the payment is now resolved. Polls get_payment up to N times with exponential backoff. Returns { payment, threeDs, resolved, attempts }. USE THIS as the FINAL step in the 3DS flow (after analyze_payment_3ds detected a challenge_required). Without confirming, the payment stays in 'pending' indefinitely from the buyer's perspective.",
2749
+ // ── v0.10 — Auto-paginate variants ──────────────────────────────────────
2750
+ search_payments_all: "Collect ALL payments matching a filter \u2014 auto-paginates under the hood. Returns an array (NOT paginated) so the agent doesn't have to manage offset/limit loops manually. SAFETY: pass `max_items` to cap; without it, MP traversal is bounded by the toolkit's internal max (10,000 items) to prevent runaway iterations. USE WHEN the agent needs to enumerate everything (e.g., monthly reconciliation 'all approved payments in March'). For agent flows that only need 'first N matches', pass `max_items` directly.",
2751
+ list_settlements_all: "Collect ALL settlements matching a filter \u2014 auto-paginates. Pass `max_items` to cap. Use for monthly bank-conciliation reports."
2304
2752
  };
2305
2753
  function mercadoPagoTools(client, options) {
2306
2754
  const desc = (name) => options.descriptions?.[name] ?? DEFAULT_DESCRIPTIONS[name];
@@ -4058,6 +4506,21 @@ function mercadoPagoTools(client, options) {
4058
4506
  };
4059
4507
  }
4060
4508
  }),
4509
+ mp_health_check: ai.tool({
4510
+ description: desc("mp_health_check"),
4511
+ inputSchema: zod.z.object({
4512
+ timeout_ms: zod.z.number().int().positive().max(3e4).optional().describe("Cap the wait time (default 5s). Use lower for status-page polling.")
4513
+ }),
4514
+ execute: async ({ timeout_ms }) => {
4515
+ const controller = new AbortController();
4516
+ const t = setTimeout(() => controller.abort(), timeout_ms ?? 5e3);
4517
+ try {
4518
+ return await client.healthCheck(controller.signal);
4519
+ } finally {
4520
+ clearTimeout(t);
4521
+ }
4522
+ }
4523
+ }),
4061
4524
  explain_payment_status: ai.tool({
4062
4525
  description: desc("explain_payment_status"),
4063
4526
  inputSchema: zod.z.object({
@@ -4084,6 +4547,101 @@ function mercadoPagoTools(client, options) {
4084
4547
  ...explanation
4085
4548
  };
4086
4549
  }
4550
+ }),
4551
+ // ─────────────────────────────────────────────────────────────────────
4552
+ // v0.10 — AR issuer promos (pure)
4553
+ // ─────────────────────────────────────────────────────────────────────
4554
+ find_applicable_promos: ai.tool({
4555
+ description: desc("find_applicable_promos"),
4556
+ inputSchema: zod.z.object({
4557
+ issuer: zod.z.string().optional().describe("Issuer name (e.g. 'Banco Galicia')"),
4558
+ payment_method_id: zod.z.string().optional().describe("e.g. 'visa', 'master', 'naranja'"),
4559
+ amount_ars: zod.z.number().positive().optional(),
4560
+ category: zod.z.enum([
4561
+ "electronics",
4562
+ "appliances",
4563
+ "clothing",
4564
+ "supermarket",
4565
+ "travel",
4566
+ "education",
4567
+ "health",
4568
+ "general"
4569
+ ]).optional(),
4570
+ date: zod.z.string().datetime().optional(),
4571
+ include_ahora_program: zod.z.boolean().optional()
4572
+ }),
4573
+ execute: async (input) => {
4574
+ const args = {};
4575
+ if (input.issuer !== void 0) args.issuer = input.issuer;
4576
+ if (input.payment_method_id !== void 0) args.paymentMethodId = input.payment_method_id;
4577
+ if (input.amount_ars !== void 0) args.amountArs = input.amount_ars;
4578
+ if (input.category !== void 0) args.category = input.category;
4579
+ if (input.date !== void 0) args.date = new Date(input.date);
4580
+ if (input.include_ahora_program !== void 0) args.includeAhoraProgram = input.include_ahora_program;
4581
+ const promos = findApplicablePromos(args);
4582
+ return { ok: true, count: promos.length, promos };
4583
+ }
4584
+ }),
4585
+ // ─────────────────────────────────────────────────────────────────────
4586
+ // v0.10 — 3DS challenge resolution (poll-and-confirm)
4587
+ // ─────────────────────────────────────────────────────────────────────
4588
+ confirm_3ds_challenge: ai.tool({
4589
+ description: desc("confirm_3ds_challenge"),
4590
+ inputSchema: zod.z.object({
4591
+ payment_id: zod.z.string(),
4592
+ max_attempts: zod.z.number().int().positive().max(20).optional(),
4593
+ poll_interval_ms: zod.z.number().int().positive().max(1e4).optional()
4594
+ }),
4595
+ execute: async ({ payment_id, max_attempts, poll_interval_ms }) => {
4596
+ const args = {};
4597
+ if (max_attempts !== void 0) args.maxAttempts = max_attempts;
4598
+ if (poll_interval_ms !== void 0) args.pollIntervalMs = poll_interval_ms;
4599
+ return confirmChallengeAndPoll(client, payment_id, args);
4600
+ }
4601
+ }),
4602
+ // ─────────────────────────────────────────────────────────────────────
4603
+ // v0.10 — Auto-paginate variants (collect-all)
4604
+ // ─────────────────────────────────────────────────────────────────────
4605
+ search_payments_all: ai.tool({
4606
+ description: desc("search_payments_all"),
4607
+ inputSchema: zod.z.object({
4608
+ status: zod.z.string().optional(),
4609
+ external_reference: zod.z.string().optional(),
4610
+ from: zod.z.string().optional(),
4611
+ to: zod.z.string().optional(),
4612
+ max_items: zod.z.number().int().positive().max(1e4).optional().describe("Cap on total items returned (default 10,000 hard limit).")
4613
+ }),
4614
+ execute: async ({ max_items, ...filter }) => {
4615
+ const filterClean = {};
4616
+ for (const [k, v] of Object.entries(filter)) {
4617
+ if (v !== void 0) filterClean[k] = v;
4618
+ }
4619
+ const opts = {};
4620
+ if (max_items !== void 0) opts.maxItems = max_items;
4621
+ else opts.maxItems = 1e4;
4622
+ const all = await collect(paginatePayments(client, filterClean, opts));
4623
+ return { ok: true, count: all.length, payments: all };
4624
+ }
4625
+ }),
4626
+ list_settlements_all: ai.tool({
4627
+ description: desc("list_settlements_all"),
4628
+ inputSchema: zod.z.object({
4629
+ from: zod.z.string().optional(),
4630
+ to: zod.z.string().optional(),
4631
+ status: zod.z.string().optional(),
4632
+ max_items: zod.z.number().int().positive().max(1e4).optional()
4633
+ }),
4634
+ execute: async ({ max_items, ...filter }) => {
4635
+ const filterClean = {};
4636
+ if (filter.from !== void 0) filterClean.from = filter.from;
4637
+ if (filter.to !== void 0) filterClean.to = filter.to;
4638
+ if (filter.status !== void 0) filterClean.status = filter.status;
4639
+ const opts = {};
4640
+ if (max_items !== void 0) opts.maxItems = max_items;
4641
+ else opts.maxItems = 1e4;
4642
+ const all = await collect(paginateSettlements(client, filterClean, opts));
4643
+ return { ok: true, count: all.length, settlements: all };
4644
+ }
4087
4645
  })
4088
4646
  };
4089
4647
  }
@@ -4148,6 +4706,458 @@ var InMemoryIdempotencyCache = class {
4148
4706
  }
4149
4707
  };
4150
4708
 
4709
+ // src/circuit-breaker.ts
4710
+ var CircuitOpenError = class extends Error {
4711
+ constructor(retryAfterMs, consecutiveFailures) {
4712
+ super(
4713
+ `Circuit breaker is OPEN \u2014 failing fast. Retry in ${Math.ceil(
4714
+ retryAfterMs / 1e3
4715
+ )}s. Consecutive upstream failures: ${consecutiveFailures}.`
4716
+ );
4717
+ this.retryAfterMs = retryAfterMs;
4718
+ this.consecutiveFailures = consecutiveFailures;
4719
+ this.name = "CircuitOpenError";
4720
+ }
4721
+ retryAfterMs;
4722
+ consecutiveFailures;
4723
+ };
4724
+ var CircuitBreaker = class {
4725
+ state = "CLOSED";
4726
+ consecutiveFailures = 0;
4727
+ halfOpenSuccesses = 0;
4728
+ openedAt = 0;
4729
+ /** Timestamps of failures within the monitoring window. */
4730
+ failureWindow = [];
4731
+ failureThreshold;
4732
+ successThreshold;
4733
+ resetTimeoutMs;
4734
+ monitoringWindowMs;
4735
+ onStateChange;
4736
+ isFailureFn;
4737
+ now;
4738
+ constructor(opts = {}) {
4739
+ this.failureThreshold = opts.failureThreshold ?? 5;
4740
+ this.successThreshold = opts.successThreshold ?? 2;
4741
+ this.resetTimeoutMs = opts.resetTimeoutMs ?? 3e4;
4742
+ this.monitoringWindowMs = opts.monitoringWindowMs ?? 6e4;
4743
+ this.onStateChange = opts.onStateChange ?? null;
4744
+ this.isFailureFn = opts.isFailure ?? (() => true);
4745
+ this.now = opts.now ?? Date.now;
4746
+ }
4747
+ /** Read the current state. Useful for health checks + metrics. */
4748
+ getState() {
4749
+ if (this.state === "OPEN" && this.now() - this.openedAt >= this.resetTimeoutMs) {
4750
+ this.transitionTo("HALF_OPEN");
4751
+ }
4752
+ return this.state;
4753
+ }
4754
+ /** Read diagnostic state for health checks + dashboards. */
4755
+ getStats() {
4756
+ const state = this.getState();
4757
+ const msSinceOpened = this.openedAt > 0 ? this.now() - this.openedAt : null;
4758
+ const msUntilHalfOpen = state === "OPEN" && msSinceOpened !== null ? Math.max(0, this.resetTimeoutMs - msSinceOpened) : null;
4759
+ return {
4760
+ state,
4761
+ consecutiveFailures: this.consecutiveFailures,
4762
+ failuresInWindow: this.failuresInCurrentWindow(),
4763
+ msSinceOpened,
4764
+ msUntilHalfOpen
4765
+ };
4766
+ }
4767
+ /**
4768
+ * Execute `fn` under the breaker's protection.
4769
+ * - If the breaker is OPEN, throws `CircuitOpenError` immediately.
4770
+ * - If `fn` succeeds, may transition HALF_OPEN → CLOSED.
4771
+ * - If `fn` fails (and the error counts as a failure), records the
4772
+ * failure; may transition CLOSED → OPEN or HALF_OPEN → OPEN.
4773
+ */
4774
+ async execute(fn) {
4775
+ const state = this.getState();
4776
+ if (state === "OPEN") {
4777
+ const elapsed = this.now() - this.openedAt;
4778
+ throw new CircuitOpenError(
4779
+ Math.max(0, this.resetTimeoutMs - elapsed),
4780
+ this.consecutiveFailures
4781
+ );
4782
+ }
4783
+ try {
4784
+ const result = await fn();
4785
+ this.recordSuccess();
4786
+ return result;
4787
+ } catch (err) {
4788
+ if (this.isFailureFn(err)) {
4789
+ this.recordFailure(err);
4790
+ }
4791
+ throw err;
4792
+ }
4793
+ }
4794
+ /** Manually force the breaker open. Useful for runbook / manual ops. */
4795
+ trip(reason) {
4796
+ if (this.state !== "OPEN") {
4797
+ this.transitionTo("OPEN", reason);
4798
+ }
4799
+ }
4800
+ /** Manually reset the breaker to CLOSED. */
4801
+ reset() {
4802
+ this.consecutiveFailures = 0;
4803
+ this.halfOpenSuccesses = 0;
4804
+ this.failureWindow = [];
4805
+ if (this.state !== "CLOSED") {
4806
+ this.transitionTo("CLOSED");
4807
+ }
4808
+ }
4809
+ // ─────────────────────────────────────────────────────────────────────────
4810
+ // Internal
4811
+ // ─────────────────────────────────────────────────────────────────────────
4812
+ recordSuccess() {
4813
+ this.consecutiveFailures = 0;
4814
+ if (this.state === "HALF_OPEN") {
4815
+ this.halfOpenSuccesses++;
4816
+ if (this.halfOpenSuccesses >= this.successThreshold) {
4817
+ this.transitionTo("CLOSED");
4818
+ }
4819
+ }
4820
+ }
4821
+ recordFailure(cause) {
4822
+ this.consecutiveFailures++;
4823
+ this.failureWindow.push(this.now());
4824
+ this.pruneWindow();
4825
+ if (this.state === "HALF_OPEN") {
4826
+ this.transitionTo("OPEN", cause);
4827
+ return;
4828
+ }
4829
+ if (this.state === "CLOSED" && this.failuresInCurrentWindow() >= this.failureThreshold) {
4830
+ this.transitionTo("OPEN", cause);
4831
+ }
4832
+ }
4833
+ transitionTo(to, cause) {
4834
+ const from = this.state;
4835
+ if (from === to) return;
4836
+ this.state = to;
4837
+ if (to === "OPEN") {
4838
+ this.openedAt = this.now();
4839
+ this.halfOpenSuccesses = 0;
4840
+ } else if (to === "CLOSED") {
4841
+ this.consecutiveFailures = 0;
4842
+ this.halfOpenSuccesses = 0;
4843
+ this.failureWindow = [];
4844
+ this.openedAt = 0;
4845
+ } else if (to === "HALF_OPEN") {
4846
+ this.halfOpenSuccesses = 0;
4847
+ }
4848
+ this.onStateChange?.({
4849
+ from,
4850
+ to,
4851
+ cause,
4852
+ consecutiveFailures: this.consecutiveFailures
4853
+ });
4854
+ }
4855
+ pruneWindow() {
4856
+ const cutoff = this.now() - this.monitoringWindowMs;
4857
+ while (this.failureWindow.length > 0 && this.failureWindow[0] < cutoff) {
4858
+ this.failureWindow.shift();
4859
+ }
4860
+ }
4861
+ failuresInCurrentWindow() {
4862
+ this.pruneWindow();
4863
+ return this.failureWindow.length;
4864
+ }
4865
+ };
4866
+
4867
+ // src/audit.ts
4868
+ var InMemoryAuditLog = class {
4869
+ entries = [];
4870
+ async append(entry) {
4871
+ this.entries.push(entry);
4872
+ }
4873
+ async query(filter) {
4874
+ const filtered = this.entries.filter((e) => {
4875
+ if (filter.actor && e.actor !== filter.actor) return false;
4876
+ if (filter.operation && e.operation !== filter.operation) return false;
4877
+ if (filter.tenantId && e.tenantId !== filter.tenantId) return false;
4878
+ if (filter.from && e.timestamp < filter.from) return false;
4879
+ if (filter.to && e.timestamp > filter.to) return false;
4880
+ return true;
4881
+ });
4882
+ return filter.limit ? filtered.slice(0, filter.limit) : filtered;
4883
+ }
4884
+ /** All entries (test helper, not part of the adapter interface). */
4885
+ all() {
4886
+ return [...this.entries];
4887
+ }
4888
+ reset() {
4889
+ this.entries.length = 0;
4890
+ }
4891
+ };
4892
+ var AuditLogger = class {
4893
+ adapter;
4894
+ defaultActor;
4895
+ redact;
4896
+ hash;
4897
+ constructor(options) {
4898
+ this.adapter = options.adapter;
4899
+ this.defaultActor = options.defaultActor ?? "unknown";
4900
+ this.redact = options.redact ?? true;
4901
+ this.hash = options.hashFn ?? defaultHasher;
4902
+ }
4903
+ /**
4904
+ * Wrap a tool execute() function with auto-audit. The returned function:
4905
+ * 1. Computes inputHash before the call.
4906
+ * 2. Invokes the original execute().
4907
+ * 3. On success, appends an entry with outcome="ok" + resourceId.
4908
+ * 4. On failure, appends an entry with outcome="error" + errorCode/Message.
4909
+ * 5. Re-throws the error transparently.
4910
+ */
4911
+ async record(args) {
4912
+ const t0 = Date.now();
4913
+ const inputHash = await this.hash(stableStringify(args.input));
4914
+ try {
4915
+ const result = await args.fn();
4916
+ const resourceId = (args.extractResourceId ?? defaultExtractResourceId)(result);
4917
+ const entry = {
4918
+ id: `mpaud-${(/* @__PURE__ */ new Date()).toISOString()}-${Math.random().toString(36).slice(2, 10)}`,
4919
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4920
+ operation: args.operation,
4921
+ actor: args.actor ?? this.defaultActor,
4922
+ inputHash,
4923
+ outcome: "ok",
4924
+ durationMs: Date.now() - t0
4925
+ };
4926
+ if (args.tenantId !== void 0) entry.tenantId = args.tenantId;
4927
+ if (resourceId !== void 0) entry.resourceId = resourceId;
4928
+ if (args.idempotencyKey !== void 0) entry.idempotencyKey = args.idempotencyKey;
4929
+ if (!this.redact) entry.inputRaw = args.input;
4930
+ await this.adapter.append(entry);
4931
+ return result;
4932
+ } catch (err) {
4933
+ const entry = {
4934
+ id: `mpaud-${(/* @__PURE__ */ new Date()).toISOString()}-${Math.random().toString(36).slice(2, 10)}`,
4935
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4936
+ operation: args.operation,
4937
+ actor: args.actor ?? this.defaultActor,
4938
+ inputHash,
4939
+ outcome: "error",
4940
+ errorCode: extractErrorCode(err),
4941
+ errorMessage: err instanceof Error ? err.message : String(err),
4942
+ durationMs: Date.now() - t0
4943
+ };
4944
+ if (args.tenantId !== void 0) entry.tenantId = args.tenantId;
4945
+ if (args.idempotencyKey !== void 0) entry.idempotencyKey = args.idempotencyKey;
4946
+ if (!this.redact) entry.inputRaw = args.input;
4947
+ await this.adapter.append(entry);
4948
+ throw err;
4949
+ }
4950
+ }
4951
+ };
4952
+ async function defaultHasher(input) {
4953
+ const { sha256Hex: sha256Hex2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
4954
+ return sha256Hex2(input);
4955
+ }
4956
+ function stableStringify(obj) {
4957
+ return JSON.stringify(obj, (_, v) => {
4958
+ if (v && typeof v === "object" && !Array.isArray(v)) {
4959
+ const sorted = {};
4960
+ for (const k of Object.keys(v).sort()) {
4961
+ sorted[k] = v[k];
4962
+ }
4963
+ return sorted;
4964
+ }
4965
+ return v;
4966
+ });
4967
+ }
4968
+ function defaultExtractResourceId(result) {
4969
+ if (!result || typeof result !== "object") return void 0;
4970
+ const r = result;
4971
+ for (const key of [
4972
+ "id",
4973
+ "payment_id",
4974
+ "subscription_id",
4975
+ "order_id",
4976
+ "preference_id",
4977
+ "customer_id",
4978
+ "refund_id",
4979
+ "store_id",
4980
+ "pos_id",
4981
+ "merchant_order_id",
4982
+ "intent_id",
4983
+ "device_id"
4984
+ ]) {
4985
+ const v = r[key];
4986
+ if (typeof v === "string" || typeof v === "number") return String(v);
4987
+ }
4988
+ return void 0;
4989
+ }
4990
+ function extractErrorCode(err) {
4991
+ if (err && typeof err === "object") {
4992
+ const e = err;
4993
+ return e.code ?? e.name ?? "unknown_error";
4994
+ }
4995
+ return "unknown_error";
4996
+ }
4997
+
4998
+ // src/webhook-dedup.ts
4999
+ var WebhookDedup = class {
5000
+ cache;
5001
+ ttlSeconds;
5002
+ onDuplicate;
5003
+ constructor(opts) {
5004
+ this.cache = opts.cache;
5005
+ this.ttlSeconds = opts.ttlSeconds ?? 7 * 24 * 3600;
5006
+ this.onDuplicate = opts.onDuplicate;
5007
+ }
5008
+ /**
5009
+ * Check whether a webhook delivery has been seen before. If new, mark it
5010
+ * as seen (so subsequent retries return shouldProcess=false). If seen,
5011
+ * return shouldProcess=false WITHOUT marking again.
5012
+ *
5013
+ * **Important**: this method is not atomic across concurrent calls — two
5014
+ * simultaneous deliveries with the same key may both pass shouldProcess=true.
5015
+ * For strict at-most-once processing, follow with a transaction or use a
5016
+ * cache that supports `setNX`-style semantics (Redis, Cloudflare KV with
5017
+ * conditional writes).
5018
+ *
5019
+ * For most webhook handlers this race is acceptable: even if two get
5020
+ * through, the downstream business logic (e.g., "charge if not already
5021
+ * charged") will be idempotent on its own.
5022
+ */
5023
+ async check(args) {
5024
+ const deliveryKey = this.deriveKey(args);
5025
+ const seen = await this.cache.get(deliveryKey);
5026
+ if (seen) {
5027
+ this.onDuplicate?.(deliveryKey);
5028
+ return { shouldProcess: false, deliveryKey };
5029
+ }
5030
+ await this.cache.set(deliveryKey, true, this.ttlSeconds);
5031
+ return { shouldProcess: true, deliveryKey };
5032
+ }
5033
+ /**
5034
+ * Manually mark a delivery as processed. Call this AFTER your business
5035
+ * logic succeeds — useful when you want to control when the dedup
5036
+ * marker is written (e.g., only on success).
5037
+ *
5038
+ * Combined with calling `check()` BEFORE the work, this gives "at-least-once"
5039
+ * semantics: failed processing → no marker → retry will be processed again.
5040
+ */
5041
+ async markProcessed(args) {
5042
+ const deliveryKey = this.deriveKey(args);
5043
+ await this.cache.set(deliveryKey, true, this.ttlSeconds);
5044
+ }
5045
+ /**
5046
+ * Variant of `check` that doesn't mark on first sight — caller must
5047
+ * explicitly `markProcessed` when their business logic succeeds.
5048
+ * Use this for at-least-once semantics (each delivery processed at
5049
+ * least once, possibly more if processing fails before mark).
5050
+ */
5051
+ async peekIsDuplicate(args) {
5052
+ const deliveryKey = this.deriveKey(args);
5053
+ const seen = await this.cache.get(deliveryKey);
5054
+ if (seen) this.onDuplicate?.(deliveryKey);
5055
+ return { shouldProcess: !seen, deliveryKey };
5056
+ }
5057
+ deriveKey(args) {
5058
+ return `mp:webhook:${args.topic}:${args.dataId}:${args.requestId ?? "noreqid"}`;
5059
+ }
5060
+ };
5061
+
5062
+ // src/rate-limiter.ts
5063
+ var RateLimitTimeoutError = class extends Error {
5064
+ constructor(waitedMs) {
5065
+ super(`Rate limit acquire timed out after ${waitedMs}ms.`);
5066
+ this.waitedMs = waitedMs;
5067
+ this.name = "RateLimitTimeoutError";
5068
+ }
5069
+ waitedMs;
5070
+ };
5071
+ var TokenBucketRateLimiter = class {
5072
+ tokens;
5073
+ lastRefill;
5074
+ capacity;
5075
+ refillPerSecond;
5076
+ adaptive;
5077
+ acquireTimeoutMs;
5078
+ now;
5079
+ constructor(opts = {}) {
5080
+ this.capacity = opts.capacity ?? 50;
5081
+ this.refillPerSecond = opts.refillPerSecond ?? 25;
5082
+ this.adaptive = opts.adaptive ?? true;
5083
+ this.acquireTimeoutMs = opts.acquireTimeoutMs ?? 3e4;
5084
+ this.now = opts.now ?? Date.now;
5085
+ this.tokens = this.capacity;
5086
+ this.lastRefill = this.now();
5087
+ }
5088
+ /**
5089
+ * Acquire a token. Resolves immediately if tokens are available;
5090
+ * otherwise waits until one is. Rejects with `RateLimitTimeoutError`
5091
+ * if the wait exceeds `acquireTimeoutMs`.
5092
+ */
5093
+ async acquire() {
5094
+ this.refill();
5095
+ if (this.tokens >= 1) {
5096
+ this.tokens -= 1;
5097
+ return;
5098
+ }
5099
+ const tokensNeeded = 1 - this.tokens;
5100
+ const waitMs = Math.ceil(tokensNeeded / this.refillPerSecond * 1e3);
5101
+ if (waitMs > this.acquireTimeoutMs) {
5102
+ throw new RateLimitTimeoutError(waitMs);
5103
+ }
5104
+ await sleep2(waitMs);
5105
+ this.refill();
5106
+ this.tokens -= 1;
5107
+ }
5108
+ /**
5109
+ * Best-effort acquire: returns true if a token was available, false
5110
+ * otherwise. Doesn't wait. Useful for "non-blocking" code paths that
5111
+ * want to fall back to a cached response or queue the request elsewhere.
5112
+ */
5113
+ tryAcquire() {
5114
+ this.refill();
5115
+ if (this.tokens >= 1) {
5116
+ this.tokens -= 1;
5117
+ return true;
5118
+ }
5119
+ return false;
5120
+ }
5121
+ /**
5122
+ * Adaptive learning hook — call after every API response with MP's
5123
+ * rate-limit headers to keep the bucket in sync with reality.
5124
+ */
5125
+ learnFromHeaders(headers) {
5126
+ if (!this.adaptive) return;
5127
+ if (headers.remaining === null) return;
5128
+ this.refill();
5129
+ if (headers.remaining < this.tokens) {
5130
+ this.tokens = Math.max(0, headers.remaining);
5131
+ }
5132
+ }
5133
+ /** Inspect the current bucket state. */
5134
+ getStats() {
5135
+ this.refill();
5136
+ return {
5137
+ tokens: this.tokens,
5138
+ capacity: this.capacity,
5139
+ refillPerSecond: this.refillPerSecond
5140
+ };
5141
+ }
5142
+ refill() {
5143
+ const now = this.now();
5144
+ const elapsedMs = now - this.lastRefill;
5145
+ if (elapsedMs <= 0) return;
5146
+ const refilled = elapsedMs / 1e3 * this.refillPerSecond;
5147
+ this.tokens = Math.min(this.capacity, this.tokens + refilled);
5148
+ this.lastRefill = now;
5149
+ }
5150
+ };
5151
+ function sleep2(ms) {
5152
+ return new Promise((resolve) => setTimeout(resolve, ms));
5153
+ }
5154
+
5155
+ exports.AHORA_PROGRAM_PROMOS = AHORA_PROGRAM_PROMOS;
5156
+ exports.AR_ISSUER_PROMOS = AR_ISSUER_PROMOS;
5157
+ exports.AuditLogger = AuditLogger;
5158
+ exports.CircuitBreaker = CircuitBreaker;
5159
+ exports.CircuitOpenError = CircuitOpenError;
5160
+ exports.InMemoryAuditLog = InMemoryAuditLog;
4151
5161
  exports.InMemoryIdempotencyCache = InMemoryIdempotencyCache;
4152
5162
  exports.InMemoryOAuthTokenStore = InMemoryOAuthTokenStore;
4153
5163
  exports.InMemoryStateAdapter = InMemoryStateAdapter;
@@ -4162,18 +5172,32 @@ exports.MercadoPagoPaymentRejectedError = MercadoPagoPaymentRejectedError;
4162
5172
  exports.MercadoPagoRateLimitError = MercadoPagoRateLimitError;
4163
5173
  exports.MercadoPagoSelfPaymentError = MercadoPagoSelfPaymentError;
4164
5174
  exports.MercadoPagoTimeoutError = MercadoPagoTimeoutError;
5175
+ exports.RateLimitTimeoutError = RateLimitTimeoutError;
4165
5176
  exports.TEST_CARDS_AR = TEST_CARDS_AR;
4166
5177
  exports.TEST_PAYERS_AR = TEST_PAYERS_AR;
5178
+ exports.TokenBucketRateLimiter = TokenBucketRateLimiter;
5179
+ exports.WebhookDedup = WebhookDedup;
4167
5180
  exports.analyze3DS = analyze3DS;
4168
5181
  exports.buildAuthorizeUrl = buildAuthorizeUrl;
4169
5182
  exports.buildTestCardScenario = buildTestCardScenario;
4170
5183
  exports.classifyError = classifyError;
5184
+ exports.collect = collect;
4171
5185
  exports.computeMarketplaceFee = computeMarketplaceFee;
5186
+ exports.confirmChallengeAndPoll = confirmChallengeAndPoll;
4172
5187
  exports.exchangeCodeForToken = exchangeCodeForToken;
4173
5188
  exports.expirationTimeMs = expirationTimeMs;
4174
5189
  exports.explainPaymentStatus = explainPaymentStatus;
5190
+ exports.findApplicablePromos = findApplicablePromos;
4175
5191
  exports.isExpiringSoon = isExpiringSoon;
4176
5192
  exports.mercadoPagoTools = mercadoPagoTools;
5193
+ exports.paginate = paginate;
5194
+ exports.paginateAccountMovements = paginateAccountMovements;
5195
+ exports.paginateMerchantOrders = paginateMerchantOrders;
5196
+ exports.paginatePayments = paginatePayments;
5197
+ exports.paginateSettlements = paginateSettlements;
5198
+ exports.paginateSubscriptionPayments = paginateSubscriptionPayments;
5199
+ exports.paginateSubscriptionPlans = paginateSubscriptionPlans;
5200
+ exports.paginateSubscriptions = paginateSubscriptions;
4177
5201
  exports.parseWebhookEvent = parseWebhookEvent;
4178
5202
  exports.refreshAccessToken = refreshAccessToken;
4179
5203
  exports.verifyWebhookSignature = verifyWebhookSignature;