@ar-agents/mercadopago 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,88 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes — Distributed rate limiter via Vercel KV
6
+
7
+ `VercelKVRateLimiter` — drop-in distributed token bucket backed by Vercel
8
+ KV (Upstash Redis). The default `TokenBucketRateLimiter` is per-process,
9
+ which is fine for single-instance deploys but breaks down in serverless:
10
+ each cold start gets its own bucket, so N concurrent instances effectively
11
+ have N×capacity. For multi-region deployments or marketplace setups with
12
+ shared MP rate budget, that's a footgun.
13
+
14
+ `VercelKVRateLimiter` shares one logical bucket across all instances via
15
+ KV. Same `acquire` / `tryAcquire` / `learnFromHeaders` interface as the
16
+ in-memory version — drop-in replacement.
17
+
18
+ ```ts
19
+ import { MercadoPagoClient } from "@ar-agents/mercadopago";
20
+ import { VercelKVRateLimiter } from "@ar-agents/mercadopago/vercel-kv";
21
+
22
+ // One global bucket shared across all serverless instances
23
+ const limiter = new VercelKVRateLimiter({
24
+ key: "mp-account-prod",
25
+ capacity: 50,
26
+ refillPerSecond: 25,
27
+ });
28
+
29
+ // Marketplace: one bucket per seller
30
+ function makeSellerLimiter(sellerUserId: string) {
31
+ return new VercelKVRateLimiter({
32
+ key: `mp-seller-${sellerUserId}`,
33
+ capacity: 10,
34
+ refillPerSecond: 5,
35
+ });
36
+ }
37
+ ```
38
+
39
+ Storage: `mp:rl:{key}` → `{ tokens: number, lastRefillMs: number }` with
40
+ 1h TTL (idle buckets garbage-collect, capacity rebuilds on next acquire).
41
+ Read-modify-write isn't strictly atomic per call — under heavy contention
42
+ a small over-spend window is possible, acceptable for MP rate limiting
43
+ where the actual budget exceeds what we provision.
44
+
45
+ ### Discoverability
46
+
47
+ - Expanded `keywords` from 7 → 29 in `package.json` (mp, payments, webhook,
48
+ fraud-detection, idempotency, circuit-breaker, opentelemetry, etc).
49
+ - Added `funding` field pointing to GitHub Sponsors.
50
+
51
+ ## 0.12.0
52
+
53
+ ### Minor Changes — Idempotency-by-default for state-mutating writes
54
+
55
+ `MercadoPagoClient` now auto-generates a UUID v4 X-Idempotency-Key header on
56
+ every state-mutating POST request when the caller doesn't provide one
57
+ explicitly. Naive callers (and the LLM tools layer) often forget to pass an
58
+ idempotency key, leaving them exposed to double-charge bugs on network
59
+ partitions. This makes the safe default: safe.
60
+
61
+ - **Auto-generated keys are unique per call** (Web Crypto's `randomUUID()` —
62
+ Edge Runtime + Node 19+ + Cloudflare Workers + browsers).
63
+ - **Caller-supplied keys still win** — pass `idempotencyKey: "..."` for
64
+ deterministic retries from a job queue (e.g., same key across retry
65
+ attempts).
66
+ - **Only POST requests are auto-keyed.** GET / DELETE are HTTP-idempotent
67
+ by spec. PUT skips auto-gen because MP's PUT endpoints encode the dedup
68
+ key in the resource path (`/v1/payments/:id` → cancel; `/preapproval/:id` →
69
+ pause/resume — already deduped by id).
70
+
71
+ 6 new tests in `idempotency-default.test.ts` verify:
72
+ - UUID v4 format on auto-gen
73
+ - Different keys per call
74
+ - Caller-supplied keys honored over auto-gen
75
+ - GET requests NOT keyed
76
+ - Works for `createPayment`, `createPreference`, `createPreapproval`
77
+
78
+ ### New cookbook recipe
79
+
80
+ - `cookbook/09-otel-wired.ts` — full OpenTelemetry wiring example. Shows
81
+ how to wire `traceContext` for distributed-trace correlation, instrument
82
+ the client + tools, and what the resulting trace + metric shape looks
83
+ like in your APM. Closes the half-finished OTel story (lib + subpath
84
+ existed since v0.10 but no recipe wired it end-to-end).
85
+
3
86
  ## 0.11.0
4
87
 
5
88
  ### Minor Changes — Composability + cross-LATAM + fraud scoring
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Recipe 09 — OpenTelemetry wired end-to-end.
3
+ *
4
+ * # Pattern
5
+ *
6
+ * 1. Wire an OTel SDK at app boot (NodeSDK or Edge equivalent)
7
+ * 2. Build the MP client with `traceContext` so each MP request injects a
8
+ * W3C `traceparent` header (correlates with your distributed traces)
9
+ * 3. Wrap the client / tools with the OTel instrumentation from
10
+ * `@ar-agents/mercadopago/otel` to get spans + metrics for every call:
11
+ * - `mp.request` span per MP API call (with attrs for endpoint, method,
12
+ * status, duration, retry count)
13
+ * - `mp.tool` span per agent-invoked tool (with input + output attrs)
14
+ * - Metrics: latency p50/p95/p99, error rate, rate-limit-remaining
15
+ * 4. Ship traces + metrics to your OTel collector (Honeycomb, Datadog,
16
+ * New Relic, Grafana Tempo, ...)
17
+ *
18
+ * # Why this matters
19
+ *
20
+ * MP's API is the slow path of any agent that uses it (200-600ms per call).
21
+ * Without observability you can't tell:
22
+ * - Which tool calls are slow (`create_payment` vs `create_subscription` vs `get_payment`)
23
+ * - When MP is degraded (rate-limit-remaining trending down before failures)
24
+ * - Where retries kick in (so you can size your timeout budget correctly)
25
+ *
26
+ * # Setup (one-time at app boot)
27
+ *
28
+ * ```ts
29
+ * // instrumentation.ts (Vercel auto-loads this if present)
30
+ * import { NodeSDK } from "@opentelemetry/sdk-node";
31
+ * import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
32
+ * import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
33
+ * import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
34
+ *
35
+ * const sdk = new NodeSDK({
36
+ * serviceName: "my-ar-agent",
37
+ * traceExporter: new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT! }),
38
+ * metricReader: new PeriodicExportingMetricReader({
39
+ * exporter: new OTLPMetricExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT! }),
40
+ * exportIntervalMillis: 30_000,
41
+ * }),
42
+ * });
43
+ * sdk.start();
44
+ * ```
45
+ *
46
+ * # Edge Runtime
47
+ *
48
+ * Edge-compatible. Use `@vercel/otel` instead of `@opentelemetry/sdk-node`.
49
+ * The instrumentation in `@ar-agents/mercadopago/otel` is runtime-agnostic
50
+ * (uses the `@opentelemetry/api` interface, no Node-only deps).
51
+ */
52
+
53
+ import { trace, context as otelContext } from "@opentelemetry/api";
54
+ import { Experimental_Agent as Agent, stepCountIs } from "ai";
55
+ import {
56
+ MercadoPagoClient,
57
+ mercadoPagoTools,
58
+ InMemoryStateAdapter,
59
+ CircuitBreaker,
60
+ } from "@ar-agents/mercadopago";
61
+ import {
62
+ instrumentMercadoPagoClient,
63
+ instrumentMercadoPagoTools,
64
+ } from "@ar-agents/mercadopago/otel";
65
+
66
+ const tracer = trace.getTracer("my-ar-agent");
67
+ const meter = trace.getTracer("my-ar-agent"); // simplified — use `metrics.getMeter` in real code
68
+
69
+ // 1. Build the client with traceContext so each MP request injects traceparent.
70
+ // The function returns whatever active OTel context exists at call time.
71
+ const mp = new MercadoPagoClient({
72
+ accessToken: process.env.MP_ACCESS_TOKEN!,
73
+ // Wire OTel context propagation. MP logs will be correlated with your trace
74
+ // graph, and downstream tooling (Datadog APM, Honeycomb) can join the dots.
75
+ traceContext: () => {
76
+ const span = trace.getActiveSpan();
77
+ if (!span) return undefined;
78
+ const ctx = span.spanContext();
79
+ return {
80
+ traceId: ctx.traceId,
81
+ spanId: ctx.spanId,
82
+ traceFlags: ctx.traceFlags,
83
+ };
84
+ },
85
+ // Production hardening — circuit breaker observed by OTel as well.
86
+ circuitBreaker: new CircuitBreaker({
87
+ failureThreshold: 5,
88
+ rollingWindowMs: 60_000,
89
+ cooldownMs: 30_000,
90
+ }),
91
+ maxRetries: 2,
92
+ });
93
+
94
+ // 2. Instrument the client. Wraps every public method with a span + metric.
95
+ const instrumentedMp = instrumentMercadoPagoClient(mp, { tracer, meter });
96
+
97
+ // 3. Build tools as usual, then wrap with OTel tool instrumentation.
98
+ const baseTools = mercadoPagoTools(instrumentedMp, {
99
+ state: new InMemoryStateAdapter(),
100
+ backUrl: "https://example.com/done",
101
+ });
102
+ const tools = instrumentMercadoPagoTools(baseTools, { tracer });
103
+
104
+ // 4. Use the agent. Every tool call becomes a span; every MP request is a
105
+ // nested span with full request context. Open Honeycomb / Tempo and you
106
+ // see the full picture.
107
+ export const agent = new Agent({
108
+ model: "anthropic/claude-sonnet-4-6",
109
+ instructions: "You are a billing assistant for a SaaS in Argentina.",
110
+ tools,
111
+ stopWhen: stepCountIs(8),
112
+ });
113
+
114
+ /**
115
+ * Trace shape (in your APM):
116
+ *
117
+ * tool: agent.generate (root span)
118
+ * ├── tool: create_payment_preference (mp.tool)
119
+ * │ └── http: POST /checkout/preferences (mp.request)
120
+ * │ attrs:
121
+ * │ mp.method = POST
122
+ * │ mp.path = /checkout/preferences
123
+ * │ mp.status = 201
124
+ * │ mp.duration_ms = 287
125
+ * │ mp.retried = false
126
+ * │ mp.idempotency_key = <uuid>
127
+ * │
128
+ * └── tool: get_payment (mp.tool)
129
+ * └── http: GET /v1/payments/9999 (mp.request)
130
+ * attrs:
131
+ * mp.method = GET
132
+ * mp.duration_ms = 142
133
+ *
134
+ * Metrics emitted (with attributes [endpoint, method, status_class]):
135
+ *
136
+ * mp_request_duration_ms (histogram)
137
+ * mp_request_total (counter)
138
+ * mp_request_error_total (counter, by status_class=4xx|5xx|network)
139
+ * mp_circuit_breaker_state (gauge: closed=0, open=1, half_open=0.5)
140
+ * mp_rate_limit_remaining (gauge, from x-ratelimit-remaining response header)
141
+ */
142
+
143
+ // Example: wrap a request handler with a parent span so MP calls join the trace.
144
+ export async function handleAgentRequest(userMessage: string) {
145
+ return tracer.startActiveSpan("agent.request", async (span) => {
146
+ try {
147
+ const result = await agent.generate({ prompt: userMessage });
148
+ span.setAttribute("agent.steps", result.steps.length);
149
+ span.setAttribute("agent.finish_reason", result.finishReason);
150
+ return result;
151
+ } finally {
152
+ span.end();
153
+ }
154
+ });
155
+ }
@@ -16,6 +16,7 @@ deploy on Vercel as-is.
16
16
  | 06 | `06-3ds-challenge.ts` | Detect challenge → redirect buyer → recover via webhook |
17
17
  | 07 | `07-auth-only-order.ts` | `Order` with manual capture → capture later when service completes |
18
18
  | 08 | `08-recovery-patterns.ts` | Retry expired subscriptions, recover stuck-pending payments, etc. |
19
+ | 09 | `09-otel-wired.ts` | Full OpenTelemetry wiring — spans + metrics for every MP call + tool |
19
20
 
20
21
  ## Conventions
21
22
 
package/dist/index.cjs CHANGED
@@ -228,6 +228,23 @@ var DEFAULT_BASE_URL = "https://api.mercadopago.com";
228
228
  function sleep(ms) {
229
229
  return new Promise((resolve) => setTimeout(resolve, ms));
230
230
  }
231
+ function randomUuid() {
232
+ const c = globalThis.crypto;
233
+ if (c?.randomUUID) {
234
+ return c.randomUUID();
235
+ }
236
+ if (c?.getRandomValues) {
237
+ const bytes = new Uint8Array(16);
238
+ c.getRandomValues(bytes);
239
+ bytes[6] = bytes[6] & 15 | 64;
240
+ bytes[8] = bytes[8] & 63 | 128;
241
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
242
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
243
+ }
244
+ throw new Error(
245
+ "MercadoPagoClient: no Web Crypto available for idempotency key generation. This shouldn't happen on Node 19+, Edge Runtime, or modern browsers."
246
+ );
247
+ }
231
248
  var MercadoPagoClient = class {
232
249
  accessToken;
233
250
  baseUrl;
@@ -271,8 +288,10 @@ var MercadoPagoClient = class {
271
288
  Authorization: `Bearer ${this.accessToken}`,
272
289
  "Content-Type": "application/json"
273
290
  };
274
- if (options?.idempotencyKey) {
275
- headers["X-Idempotency-Key"] = options.idempotencyKey;
291
+ const isMutatingPost = method === "POST";
292
+ const idempotencyKey = options?.idempotencyKey ?? (isMutatingPost ? randomUuid() : void 0);
293
+ if (idempotencyKey) {
294
+ headers["X-Idempotency-Key"] = idempotencyKey;
276
295
  }
277
296
  const trace = this.traceContext?.();
278
297
  if (trace?.traceId && trace?.spanId) {