@ar-agents/mercadopago 0.8.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
@@ -164,6 +164,8 @@ var MercadoPagoClient = class {
164
164
  requestTimeoutMs;
165
165
  maxRetries;
166
166
  onCall;
167
+ circuitBreaker;
168
+ traceContext;
167
169
  constructor(options) {
168
170
  if (!options.accessToken) {
169
171
  throw new Error(
@@ -176,8 +178,24 @@ var MercadoPagoClient = class {
176
178
  this.requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
177
179
  this.maxRetries = Math.max(0, options.maxRetries ?? 1);
178
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;
179
190
  }
180
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) {
181
199
  const headers = {
182
200
  Authorization: `Bearer ${this.accessToken}`,
183
201
  "Content-Type": "application/json"
@@ -185,6 +203,11 @@ var MercadoPagoClient = class {
185
203
  if (options?.idempotencyKey) {
186
204
  headers["X-Idempotency-Key"] = options.idempotencyKey;
187
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
+ }
188
211
  let url = `${this.baseUrl}${path}`;
189
212
  if (options?.query) {
190
213
  const search = new URLSearchParams();
@@ -201,24 +224,54 @@ var MercadoPagoClient = class {
201
224
  let attempt = 0;
202
225
  let lastError;
203
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
+ };
204
241
  while (attempt <= this.maxRetries) {
205
242
  const controller = new AbortController();
206
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
+ }
207
253
  const init = { method, headers, signal: controller.signal };
208
254
  if (body !== void 0) init.body = JSON.stringify(body);
209
255
  try {
210
256
  const res = await fetchFn(url, init);
211
257
  clearTimeout(timer);
258
+ if (parentSignal) parentSignal.removeEventListener("abort", onParentAbort);
212
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
+ };
213
267
  if (res.ok) {
214
268
  const text2 = await res.text();
215
- this.onCall?.({
216
- method,
217
- path,
218
- durationMs: Date.now() - t0,
269
+ fireOnCall({
270
+ success: true,
219
271
  httpStatus: res.status,
220
272
  retried: attempt,
221
- success: true
273
+ requestId,
274
+ rateLimit
222
275
  });
223
276
  if (!text2) return void 0;
224
277
  return JSON.parse(text2);
@@ -233,13 +286,12 @@ var MercadoPagoClient = class {
233
286
  }
234
287
  const contentType = res.headers.get("content-type") ?? "";
235
288
  if (res.status >= 500 && !contentType.includes("application/json")) {
236
- this.onCall?.({
237
- method,
238
- path,
239
- durationMs: Date.now() - t0,
289
+ fireOnCall({
290
+ success: false,
240
291
  httpStatus: res.status,
241
292
  retried: attempt,
242
- success: false
293
+ requestId,
294
+ rateLimit
243
295
  });
244
296
  throw new MercadoPagoOverloadedError(path, res.status);
245
297
  }
@@ -251,33 +303,33 @@ var MercadoPagoClient = class {
251
303
  parsed = text;
252
304
  }
253
305
  const err = classifyError(res.status, path, parsed, options?.classifyContext);
254
- this.onCall?.({
255
- method,
256
- path,
257
- durationMs: Date.now() - t0,
306
+ fireOnCall({
307
+ success: false,
258
308
  httpStatus: res.status,
259
309
  retried: attempt,
260
- success: false
310
+ requestId,
311
+ rateLimit
261
312
  });
262
313
  throw err;
263
314
  } catch (err) {
264
315
  clearTimeout(timer);
316
+ if (parentSignal) parentSignal.removeEventListener("abort", onParentAbort);
265
317
  if (err instanceof MercadoPagoError) throw err;
266
318
  const isAbort = err instanceof Error && err.name === "AbortError";
319
+ const isParentAbort = parentSignal?.aborted ?? false;
267
320
  const isNetwork = !lastStatus && !isAbort;
268
- if ((isNetwork || isAbort) && attempt < this.maxRetries) {
321
+ if ((isNetwork || isAbort && !isParentAbort) && attempt < this.maxRetries) {
269
322
  lastError = err;
270
323
  attempt++;
271
324
  await sleep(250 * Math.pow(2, attempt - 1));
272
325
  continue;
273
326
  }
274
- this.onCall?.({
275
- method,
276
- path,
277
- durationMs: Date.now() - t0,
327
+ fireOnCall({
328
+ success: false,
278
329
  httpStatus: lastStatus,
279
330
  retried: attempt,
280
- success: false
331
+ requestId: null,
332
+ rateLimit: { remaining: null, resetSeconds: null }
281
333
  });
282
334
  if (isAbort) {
283
335
  throw new MercadoPagoTimeoutError(path, this.requestTimeoutMs);
@@ -1231,6 +1283,53 @@ var MercadoPagoClient = class {
1231
1283
  );
1232
1284
  return { id: intentId, canceled: true };
1233
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
+ }
1234
1333
  };
1235
1334
 
1236
1335
  // src/crypto.ts
@@ -2298,7 +2397,9 @@ var DEFAULT_DESCRIPTIONS = {
2298
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.",
2299
2398
  // ── Pure helpers (v0.7) ──────────────────────────────────────────────────
2300
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.",
2301
- 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."
2302
2403
  };
2303
2404
  function mercadoPagoTools(client, options) {
2304
2405
  const desc = (name) => options.descriptions?.[name] ?? DEFAULT_DESCRIPTIONS[name];
@@ -4056,6 +4157,21 @@ function mercadoPagoTools(client, options) {
4056
4157
  };
4057
4158
  }
4058
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
+ }),
4059
4175
  explain_payment_status: tool({
4060
4176
  description: desc("explain_payment_status"),
4061
4177
  inputSchema: z.object({
@@ -4146,6 +4262,164 @@ var InMemoryIdempotencyCache = class {
4146
4262
  }
4147
4263
  };
4148
4264
 
4149
- export { 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 };
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
+ };
4422
+
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 };
4150
4424
  //# sourceMappingURL=index.js.map
4151
4425
  //# sourceMappingURL=index.js.map