@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/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
- this.onCall?.({
219
- method,
220
- path,
221
- durationMs: Date.now() - t0,
271
+ fireOnCall({
272
+ success: true,
222
273
  httpStatus: res.status,
223
274
  retried: attempt,
224
- success: true
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
- this.onCall?.({
240
- method,
241
- path,
242
- durationMs: Date.now() - t0,
291
+ fireOnCall({
292
+ success: false,
243
293
  httpStatus: res.status,
244
294
  retried: attempt,
245
- success: false
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
- this.onCall?.({
258
- method,
259
- path,
260
- durationMs: Date.now() - t0,
308
+ fireOnCall({
309
+ success: false,
261
310
  httpStatus: res.status,
262
311
  retried: attempt,
263
- success: false
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
- this.onCall?.({
278
- method,
279
- path,
280
- durationMs: Date.now() - t0,
329
+ fireOnCall({
330
+ success: false,
281
331
  httpStatus: lastStatus,
282
332
  retried: attempt,
283
- success: false
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
- function verifyWebhookSignature(params) {
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 = crypto.createHmac("sha256", params.secret).update(manifest).digest("hex");
2130
- if (expected.length !== v1.length) return false;
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 crypto.createHash("sha256").update(payload).digest("hex").slice(0, 32);
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;