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