@ar-agents/mercadopago 0.7.0 → 0.8.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 +47 -0
- package/README.md +63 -1
- package/cookbook/01-checkout-pro-basic.ts +99 -0
- package/cookbook/02-saas-subscription.ts +137 -0
- package/cookbook/03-webhook-handler.ts +162 -0
- package/cookbook/04-marketplace-split.ts +194 -0
- package/cookbook/05-qr-in-store.ts +142 -0
- package/cookbook/06-3ds-challenge.ts +139 -0
- package/cookbook/07-auth-only-order.ts +127 -0
- package/cookbook/08-recovery-patterns.ts +191 -0
- package/cookbook/README.md +36 -0
- package/dist/index.cjs +109 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -49
- package/dist/index.d.ts +28 -49
- package/dist/index.js +108 -13
- package/dist/index.js.map +1 -1
- package/dist/state-C6Wzb_XX.d.cts +106 -0
- package/dist/state-C6Wzb_XX.d.ts +106 -0
- package/dist/vercel-kv.cjs +92 -0
- package/dist/vercel-kv.cjs.map +1 -0
- package/dist/vercel-kv.d.cts +107 -0
- package/dist/vercel-kv.d.ts +107 -0
- package/dist/vercel-kv.js +88 -0
- package/dist/vercel-kv.js.map +1 -0
- package/package.json +25 -2
- package/tools.manifest.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes — Edge Runtime + Vercel KV + Cookbook
|
|
6
|
+
|
|
7
|
+
**Edge Runtime support (was: Node-only)**
|
|
8
|
+
|
|
9
|
+
- Replaced `node:crypto` with the universal Web Crypto API across all crypto helpers.
|
|
10
|
+
- The toolkit now runs in **Vercel Edge Runtime, Cloudflare Workers, Deno, browsers, and Node 18+** with zero changes.
|
|
11
|
+
- New module `./crypto` exposes `hmacSha256Hex`, `sha256Hex`, `timingSafeEqualHex`.
|
|
12
|
+
|
|
13
|
+
**Webhook signature verify is now async + replay-attack protected**
|
|
14
|
+
|
|
15
|
+
- `verifyWebhookSignature(...)` returns `Promise<boolean>` (was `boolean`). All call sites in `handle_webhook` tool already awaited.
|
|
16
|
+
- New default 5-minute replay window: signatures with `ts` more than `replayToleranceSeconds` (default 300) old are rejected as replay attempts.
|
|
17
|
+
- Override the window per-call with the new `replayToleranceSeconds` option.
|
|
18
|
+
- **Breaking**: callers using the exported `verifyWebhookSignature` directly need to add `await`.
|
|
19
|
+
|
|
20
|
+
**Vercel KV adapters via subpath `@ar-agents/mercadopago/vercel-kv`**
|
|
21
|
+
|
|
22
|
+
- `VercelKVSubscriptionStateAdapter` — drop-in `SubscriptionStateAdapter` backed by Vercel KV (Upstash Redis).
|
|
23
|
+
- `VercelKVOAuthTokenStore` — persists per-seller OAuth tokens for marketplace flows. Key namespace `mp:oauth:{userId}`.
|
|
24
|
+
- `VercelKVIdempotencyCache` — TTL-aware cache for short-circuiting agent retries.
|
|
25
|
+
- `@vercel/kv` is an **optional** peer dependency — only consumers who use the subpath install it. Main bundle untouched.
|
|
26
|
+
- All three adapters work in Edge Runtime.
|
|
27
|
+
|
|
28
|
+
**New state adapter interfaces in main package**
|
|
29
|
+
|
|
30
|
+
- `OAuthTokenStore` + `InMemoryOAuthTokenStore` — token bundle persistence for marketplace OAuth.
|
|
31
|
+
- `IdempotencyCache` + `InMemoryIdempotencyCache` — agent-retry deduplication layer on top of MP's server-side dedup.
|
|
32
|
+
|
|
33
|
+
**Cookbook (8 recipes)**
|
|
34
|
+
|
|
35
|
+
- `cookbook/01-checkout-pro-basic.ts` — first-time hosted checkout
|
|
36
|
+
- `cookbook/02-saas-subscription.ts` — reusable plan + first payment + card swap on rejection
|
|
37
|
+
- `cookbook/03-webhook-handler.ts` — production-grade Edge handler with HMAC verify
|
|
38
|
+
- `cookbook/04-marketplace-split.ts` — OAuth seller link → preference with fee → reconciliation
|
|
39
|
+
- `cookbook/05-qr-in-store.ts` — QR generation → buyer scan → WhatsApp notify
|
|
40
|
+
- `cookbook/06-3ds-challenge.ts` — detect → redirect → recover via webhook
|
|
41
|
+
- `cookbook/07-auth-only-order.ts` — Order with manual capture (ride-share / hotel pattern)
|
|
42
|
+
- `cookbook/08-recovery-patterns.ts` — recover stuck-pending, card-swap on rejected sub, idempotent upsert via search, cron-driven monitoring
|
|
43
|
+
|
|
44
|
+
**Quality**
|
|
45
|
+
|
|
46
|
+
- 185 tests pass (was 169; +16 for KV adapters + 2 for replay protection).
|
|
47
|
+
- publint clean, attw all 🟢 across both subpaths.
|
|
48
|
+
- Bundle: main 31.9 KB brotli'd; vercel-kv subpath 0.6 KB brotli'd.
|
|
49
|
+
|
|
3
50
|
## 0.7.0
|
|
4
51
|
|
|
5
52
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -25,6 +25,9 @@ Compatible with any caller that uses `tool()`.
|
|
|
25
25
|
| Side effects | `create_subscription` creates a preapproval. `cancel`/`pause`/`resume` mutate state. `get_status` is read-only. |
|
|
26
26
|
| Agent safety | `cancel_subscription` description triggers confirm-before-call in Claude Sonnet 4.6+ |
|
|
27
27
|
| Sites supported | MLA (Argentina) verified end-to-end. Other LATAM sites should work but aren't exercised by tests. |
|
|
28
|
+
| Runtime | **Edge Runtime + Node 18+** — Web Crypto under the hood, no `node:crypto`. Drops into Vercel Edge Functions, Cloudflare Workers, Deno deploy, or any modern Node. |
|
|
29
|
+
| Vercel-native | First-class adapters for **Vercel KV** (subscription state, OAuth tokens, idempotency cache) via `@ar-agents/mercadopago/vercel-kv` subpath. |
|
|
30
|
+
| Cookbook | 8 production-grade recipes shipped in `cookbook/` — checkout, subscriptions, webhook handler, marketplace OAuth, QR in-store, 3DS challenge, auth-only Order, recovery patterns. |
|
|
28
31
|
|
|
29
32
|
## Why this exists
|
|
30
33
|
|
|
@@ -361,11 +364,70 @@ and `mpResponse` for inspection. Specific subclasses:
|
|
|
361
364
|
- `MercadoPagoAuthorizeForbiddenError` — see gotcha #6
|
|
362
365
|
- `MercadoPagoRateLimitError` — 429 from MP
|
|
363
366
|
|
|
367
|
+
## Vercel-native (v0.8+)
|
|
368
|
+
|
|
369
|
+
The toolkit ships first-class adapters for Vercel infrastructure via the
|
|
370
|
+
`@ar-agents/mercadopago/vercel-kv` subpath. `@vercel/kv` is an **optional**
|
|
371
|
+
peer dep — only install it if you use the subpath.
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
import { mercadoPagoTools, MercadoPagoClient } from "@ar-agents/mercadopago";
|
|
375
|
+
import {
|
|
376
|
+
VercelKVSubscriptionStateAdapter,
|
|
377
|
+
VercelKVOAuthTokenStore,
|
|
378
|
+
VercelKVIdempotencyCache,
|
|
379
|
+
} from "@ar-agents/mercadopago/vercel-kv";
|
|
380
|
+
|
|
381
|
+
const tools = mercadoPagoTools(
|
|
382
|
+
new MercadoPagoClient({ accessToken: process.env.MP_ACCESS_TOKEN! }),
|
|
383
|
+
{
|
|
384
|
+
state: new VercelKVSubscriptionStateAdapter(),
|
|
385
|
+
backUrl: "https://yourapp.com/done",
|
|
386
|
+
webhookSecret: process.env.MP_WEBHOOK_SECRET,
|
|
387
|
+
oauth: {
|
|
388
|
+
clientId: process.env.MP_CLIENT_ID!,
|
|
389
|
+
clientSecret: process.env.MP_CLIENT_SECRET!,
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
);
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Edge Runtime
|
|
396
|
+
|
|
397
|
+
The toolkit (including HMAC webhook verification) is fully Edge-Runtime
|
|
398
|
+
compatible. Add `export const runtime = "edge"` to any Vercel route handler
|
|
399
|
+
that uses MP tools — sub-100ms global cold starts.
|
|
400
|
+
|
|
401
|
+
### Vercel Cron + Blob + Functions
|
|
402
|
+
|
|
403
|
+
See `cookbook/08-recovery-patterns.ts` for a Vercel Cron Job example that
|
|
404
|
+
monitors stuck-pending payments. For label/invoice PDF storage, the
|
|
405
|
+
`crear_envio` tool (in `@ar-agents/shipping`) returns label URLs you can
|
|
406
|
+
mirror to [Vercel Blob](https://vercel.com/docs/storage/vercel-blob).
|
|
407
|
+
|
|
408
|
+
## Cookbook
|
|
409
|
+
|
|
410
|
+
Production-grade recipes shipped in [`cookbook/`](./cookbook):
|
|
411
|
+
|
|
412
|
+
| Recipe | What it shows |
|
|
413
|
+
|---|---|
|
|
414
|
+
| `01-checkout-pro-basic.ts` | First-time hosted checkout sale via the agent |
|
|
415
|
+
| `02-saas-subscription.ts` | Reusable plan + first payment + card swap on rejection |
|
|
416
|
+
| `03-webhook-handler.ts` | Edge Runtime webhook handler with HMAC verify + dispatch |
|
|
417
|
+
| `04-marketplace-split.ts` | OAuth seller link + preference with `marketplace_fee` + reconciliation |
|
|
418
|
+
| `05-qr-in-store.ts` | QR generation → buyer scan → cashier WhatsApp notify |
|
|
419
|
+
| `06-3ds-challenge.ts` | Detect → redirect to challenge → recover via webhook |
|
|
420
|
+
| `07-auth-only-order.ts` | Order with `capture_mode: "manual"` (ride-share / hotel pattern) |
|
|
421
|
+
| `08-recovery-patterns.ts` | Card swap on subscription, stuck-pending recovery, idempotent upsert via search, Vercel Cron monitoring |
|
|
422
|
+
|
|
423
|
+
Each recipe is copy-pasteable into a Next.js route handler.
|
|
424
|
+
|
|
364
425
|
## Compatibility
|
|
365
426
|
|
|
366
|
-
- Node.js
|
|
427
|
+
- **Node.js 18+** (Web Crypto required) or **Vercel Edge Runtime** / **Cloudflare Workers** / **Deno**
|
|
367
428
|
- Vercel AI SDK 6+
|
|
368
429
|
- Zod 3+
|
|
430
|
+
- Optional: `@vercel/kv >=2` for the `vercel-kv` subpath
|
|
369
431
|
- Pairs cleanly with [Vercel AI Gateway](https://vercel.com/ai-gateway) for model routing.
|
|
370
432
|
|
|
371
433
|
## License
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipe 01 — First-time Checkout Pro sale via an agent.
|
|
3
|
+
*
|
|
4
|
+
* # Pattern
|
|
5
|
+
*
|
|
6
|
+
* 1. Agent receives the user's intent ("comprar 3 unidades a $X")
|
|
7
|
+
* 2. Agent calls `create_payment_preference` to get a hosted checkout URL
|
|
8
|
+
* 3. Agent surfaces the `init_point_url` to the user (or sends via WhatsApp)
|
|
9
|
+
* 4. The buyer completes payment on MP's hosted form (no PCI scope for you)
|
|
10
|
+
* 5. MP fires a `payment` webhook to your endpoint (see recipe 03)
|
|
11
|
+
*
|
|
12
|
+
* # When to use
|
|
13
|
+
*
|
|
14
|
+
* - Single one-off purchase (not recurring → use recipe 02 for that)
|
|
15
|
+
* - You only have a payer email; no card token from MP frontend SDK
|
|
16
|
+
* - You want PCI-out-of-scope (buyer enters card data on MP's form)
|
|
17
|
+
*
|
|
18
|
+
* # Edge Runtime
|
|
19
|
+
*
|
|
20
|
+
* Fully Edge-compatible. Uncomment `export const runtime = "edge"` to deploy
|
|
21
|
+
* as a Vercel Edge Function for sub-100ms global cold starts.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { Experimental_Agent as Agent, stepCountIs } from "ai";
|
|
25
|
+
import {
|
|
26
|
+
InMemoryStateAdapter,
|
|
27
|
+
MercadoPagoClient,
|
|
28
|
+
mercadoPagoTools,
|
|
29
|
+
} from "@ar-agents/mercadopago";
|
|
30
|
+
|
|
31
|
+
// export const runtime = "edge"; // Uncomment for Edge deployment
|
|
32
|
+
|
|
33
|
+
const mp = new MercadoPagoClient({
|
|
34
|
+
accessToken: process.env.MP_ACCESS_TOKEN!,
|
|
35
|
+
// Production robustez defaults: 30s timeout, 1 retry on 5xx, exponential backoff
|
|
36
|
+
requestTimeoutMs: 30_000,
|
|
37
|
+
maxRetries: 1,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const agent = new Agent({
|
|
41
|
+
model: "anthropic/claude-sonnet-4-6",
|
|
42
|
+
instructions: `Sos el asistente de checkout de un e-commerce argentino.
|
|
43
|
+
Cuando el cliente quiere comprar:
|
|
44
|
+
1. Confirmá el monto y la descripción del producto.
|
|
45
|
+
2. Llamá a create_payment_preference con back_urls de éxito/error.
|
|
46
|
+
3. Devolvele el init_point_url (Checkout Pro) al cliente.
|
|
47
|
+
4. NO pidas datos de tarjeta — los cargan en MP.`,
|
|
48
|
+
tools: mercadoPagoTools(mp, {
|
|
49
|
+
state: new InMemoryStateAdapter(),
|
|
50
|
+
backUrl: "https://yourapp.com/payment-result",
|
|
51
|
+
notificationUrl: "https://yourapp.com/api/mp/webhook",
|
|
52
|
+
}),
|
|
53
|
+
stopWhen: stepCountIs(5),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// In a Next.js route handler:
|
|
57
|
+
export async function POST(req: Request) {
|
|
58
|
+
const { prompt } = (await req.json()) as { prompt: string };
|
|
59
|
+
const result = await agent.generate({ prompt });
|
|
60
|
+
return Response.json({ text: result.text });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
// Manual / non-agent path (for direct API use)
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export async function createCheckoutPreference(input: {
|
|
68
|
+
customerEmail: string;
|
|
69
|
+
productTitle: string;
|
|
70
|
+
unitPriceArs: number;
|
|
71
|
+
quantity: number;
|
|
72
|
+
externalReference: string;
|
|
73
|
+
}) {
|
|
74
|
+
const preference = await mp.createPreference({
|
|
75
|
+
items: [
|
|
76
|
+
{
|
|
77
|
+
title: input.productTitle,
|
|
78
|
+
quantity: input.quantity,
|
|
79
|
+
unit_price: input.unitPriceArs,
|
|
80
|
+
currency_id: "ARS",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
payer: { email: input.customerEmail },
|
|
84
|
+
backUrls: {
|
|
85
|
+
success: "https://yourapp.com/payment-success",
|
|
86
|
+
failure: "https://yourapp.com/payment-failure",
|
|
87
|
+
pending: "https://yourapp.com/payment-pending",
|
|
88
|
+
},
|
|
89
|
+
autoReturn: "approved",
|
|
90
|
+
externalReference: input.externalReference,
|
|
91
|
+
notificationUrl: "https://yourapp.com/api/mp/webhook",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
preferenceId: preference.id,
|
|
96
|
+
initPoint: preference.init_point,
|
|
97
|
+
sandboxInitPoint: preference.sandbox_init_point, // Use this in TEST mode
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipe 02 — SaaS subscription with reusable plan + first payment + card swap.
|
|
3
|
+
*
|
|
4
|
+
* # Pattern
|
|
5
|
+
*
|
|
6
|
+
* **One-time setup**: create a `Plan` (price + frequency) — re-use across customers.
|
|
7
|
+
*
|
|
8
|
+
* **Per-customer**:
|
|
9
|
+
* 1. `subscribe_to_plan` → returns init_point for first-payment authorization
|
|
10
|
+
* 2. Buyer pays first installment with card+CVV (MP requirement, can't bypass)
|
|
11
|
+
* 3. `subscription_preapproval` webhook fires → status flips to `authorized`
|
|
12
|
+
* 4. MP auto-charges at the configured frequency thereafter
|
|
13
|
+
*
|
|
14
|
+
* **Card swap on failure** (when buyer's card expires):
|
|
15
|
+
* - You receive a `subscription_authorized_payment` webhook with rejection
|
|
16
|
+
* - Generate a fresh card token via MP frontend SDK on the buyer's side
|
|
17
|
+
* - Call `update_subscription({ card_token_id })` to swap without recreating
|
|
18
|
+
*
|
|
19
|
+
* # When to use
|
|
20
|
+
*
|
|
21
|
+
* - Monthly/quarterly SaaS billing (Básico/Pro/Enterprise tiers)
|
|
22
|
+
* - You want one Plan definition shared across all subscribers
|
|
23
|
+
* - You need the option to update price for new subscribers without
|
|
24
|
+
* touching existing ones
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
InMemoryStateAdapter,
|
|
29
|
+
MercadoPagoClient,
|
|
30
|
+
} from "@ar-agents/mercadopago";
|
|
31
|
+
|
|
32
|
+
const mp = new MercadoPagoClient({
|
|
33
|
+
accessToken: process.env.MP_ACCESS_TOKEN!,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const state = new InMemoryStateAdapter();
|
|
37
|
+
// In production, swap for VercelKVSubscriptionStateAdapter:
|
|
38
|
+
// import { VercelKVSubscriptionStateAdapter } from "@ar-agents/mercadopago/vercel-kv";
|
|
39
|
+
// const state = new VercelKVSubscriptionStateAdapter();
|
|
40
|
+
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
// Step 1 — One-time setup: create the plan
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export async function createPlanProMonthly() {
|
|
46
|
+
const plan = await mp.createSubscriptionPlan({
|
|
47
|
+
reason: "Plan Pro mensual",
|
|
48
|
+
backUrl: "https://yourapp.com/subscription-result",
|
|
49
|
+
frequency: 1,
|
|
50
|
+
frequencyType: "months",
|
|
51
|
+
amount: 25_000,
|
|
52
|
+
currency: "ARS",
|
|
53
|
+
});
|
|
54
|
+
return plan; // persist plan.id in your DB
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
// Step 2 — Per-customer: subscribe to the plan
|
|
59
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
export async function subscribeUserToProPlan(input: {
|
|
62
|
+
planId: string;
|
|
63
|
+
customerEmail: string;
|
|
64
|
+
externalReference: string; // your-system user id
|
|
65
|
+
}) {
|
|
66
|
+
const sub = await mp.subscribeToPlan({
|
|
67
|
+
planId: input.planId,
|
|
68
|
+
payerEmail: input.customerEmail,
|
|
69
|
+
externalReference: input.externalReference,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Persist locally for fast lookups + webhook routing
|
|
73
|
+
await state.set(sub.id, {
|
|
74
|
+
payerEmail: input.customerEmail,
|
|
75
|
+
initPoint: sub.init_point,
|
|
76
|
+
externalReference: input.externalReference,
|
|
77
|
+
createdAt: new Date().toISOString(),
|
|
78
|
+
status: sub.status,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
subscriptionId: sub.id,
|
|
83
|
+
initPoint: sub.init_point,
|
|
84
|
+
nextStep:
|
|
85
|
+
"Send init_point to the customer. They must complete the first payment with card+CVV. Listen for subscription_preapproval webhook to confirm activation.",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
90
|
+
// Step 3 — Webhook: subscription_preapproval activation
|
|
91
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
export async function handlePreapprovalWebhook(subscriptionId: string) {
|
|
94
|
+
const sub = await mp.getPreapproval(subscriptionId);
|
|
95
|
+
await state.set(sub.id, {
|
|
96
|
+
status: sub.status,
|
|
97
|
+
lastWebhookStatus: sub.status,
|
|
98
|
+
lastWebhookAt: new Date().toISOString(),
|
|
99
|
+
});
|
|
100
|
+
if (sub.status === "authorized") {
|
|
101
|
+
// First payment cleared — provision the user's plan in your DB
|
|
102
|
+
// await db.users.update({ where: { externalReference: sub.external_reference }, data: { plan: "pro", status: "active" } });
|
|
103
|
+
}
|
|
104
|
+
return sub;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
108
|
+
// Step 4 — Card swap (when buyer's card is rejected on a recurring charge)
|
|
109
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export async function swapCardOnSubscription(input: {
|
|
112
|
+
subscriptionId: string;
|
|
113
|
+
newCardToken: string; // from MP frontend SDK / Cardform on buyer's side
|
|
114
|
+
}) {
|
|
115
|
+
const sub = await mp.updatePreapproval(input.subscriptionId, {
|
|
116
|
+
card_token_id: input.newCardToken,
|
|
117
|
+
});
|
|
118
|
+
await state.set(sub.id, {
|
|
119
|
+
status: sub.status,
|
|
120
|
+
lastWebhookStatus: "card_swapped",
|
|
121
|
+
lastWebhookAt: new Date().toISOString(),
|
|
122
|
+
});
|
|
123
|
+
return sub;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
127
|
+
// Step 5 — Cancel
|
|
128
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
export async function cancelSubscription(subscriptionId: string) {
|
|
131
|
+
const sub = await mp.cancelPreapproval(subscriptionId);
|
|
132
|
+
await state.set(sub.id, {
|
|
133
|
+
status: sub.status,
|
|
134
|
+
cancelledAt: new Date().toISOString(),
|
|
135
|
+
});
|
|
136
|
+
return sub;
|
|
137
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipe 03 — Production-grade webhook handler.
|
|
3
|
+
*
|
|
4
|
+
* # The 3-line summary
|
|
5
|
+
*
|
|
6
|
+
* - Verify HMAC-SHA256 signature → reject with 401 if invalid (replay protection included)
|
|
7
|
+
* - Parse the event (topic + dataId) from query/body (MP sends in both)
|
|
8
|
+
* - Auto-fetch the underlying resource (Payment / Preapproval / Order)
|
|
9
|
+
* - Dispatch by topic to your business logic
|
|
10
|
+
*
|
|
11
|
+
* Without HMAC verify, ANYONE can POST to your webhook URL and forge
|
|
12
|
+
* payments/cancellations. The lib's `verifyWebhookSignature` rejects
|
|
13
|
+
* stale signatures (>5min old) too — replay attack protection.
|
|
14
|
+
*
|
|
15
|
+
* # Why use the agent's `handle_webhook` tool vs calling primitives manually
|
|
16
|
+
*
|
|
17
|
+
* The tool consolidates verify + parse + auto-fetch + dispatch into one
|
|
18
|
+
* call. Saves ~30 lines per webhook handler vs the manual chain.
|
|
19
|
+
*
|
|
20
|
+
* # Edge Runtime
|
|
21
|
+
*
|
|
22
|
+
* This recipe is fully Edge-compatible. Webhook handlers benefit from Edge
|
|
23
|
+
* (lower cold-start = faster MP-acked, fewer 500s during traffic spikes).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
MercadoPagoClient,
|
|
28
|
+
parseWebhookEvent,
|
|
29
|
+
verifyWebhookSignature,
|
|
30
|
+
} from "@ar-agents/mercadopago";
|
|
31
|
+
|
|
32
|
+
export const runtime = "edge";
|
|
33
|
+
|
|
34
|
+
const mp = new MercadoPagoClient({
|
|
35
|
+
accessToken: process.env.MP_ACCESS_TOKEN!,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const WEBHOOK_SECRET = process.env.MP_WEBHOOK_SECRET!;
|
|
39
|
+
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
// Approach A — Manual primitives (more control, more code)
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export async function POST(req: Request) {
|
|
45
|
+
// 1. Read the RAW body — DO NOT use req.json() before HMAC verify, as
|
|
46
|
+
// JSON.stringify changes whitespace and breaks the signature.
|
|
47
|
+
const rawBody = await req.text();
|
|
48
|
+
|
|
49
|
+
const signatureHeader = req.headers.get("x-signature");
|
|
50
|
+
const requestId = req.headers.get("x-request-id");
|
|
51
|
+
const url = new URL(req.url);
|
|
52
|
+
|
|
53
|
+
// 2. Parse the event from body or query (MP sends in both).
|
|
54
|
+
let parsedBody: unknown;
|
|
55
|
+
try {
|
|
56
|
+
parsedBody = JSON.parse(rawBody);
|
|
57
|
+
} catch {
|
|
58
|
+
return new Response("invalid JSON", { status: 400 });
|
|
59
|
+
}
|
|
60
|
+
const event = parseWebhookEvent(parsedBody, url.searchParams);
|
|
61
|
+
if (!event) {
|
|
62
|
+
return new Response("unrecognized webhook shape", { status: 400 });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 3. Verify HMAC + replay-tolerance window.
|
|
66
|
+
const verified = await verifyWebhookSignature({
|
|
67
|
+
requestId,
|
|
68
|
+
dataId: event.dataId,
|
|
69
|
+
signatureHeader,
|
|
70
|
+
secret: WEBHOOK_SECRET,
|
|
71
|
+
});
|
|
72
|
+
if (!verified) {
|
|
73
|
+
return new Response("unauthorized", { status: 401 });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 4. Dispatch by topic.
|
|
77
|
+
try {
|
|
78
|
+
switch (event.topic) {
|
|
79
|
+
case "payment":
|
|
80
|
+
case "payment.created":
|
|
81
|
+
case "payment.updated": {
|
|
82
|
+
const payment = await mp.getPayment(event.dataId);
|
|
83
|
+
await handlePayment(payment);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case "subscription_preapproval":
|
|
87
|
+
case "preapproval": {
|
|
88
|
+
const sub = await mp.getPreapproval(event.dataId);
|
|
89
|
+
await handleSubscription(sub);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "subscription_authorized_payment": {
|
|
93
|
+
// The dataId IS the authorized_payment id — list under parent
|
|
94
|
+
// preapproval to get full context.
|
|
95
|
+
await handleRecurringCharge(event.dataId);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case "merchant_order": {
|
|
99
|
+
const mo = await mp.getMerchantOrder(event.dataId);
|
|
100
|
+
await handleMerchantOrder(mo);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case "point_integration_wh": {
|
|
104
|
+
const intent = await mp.getPointPaymentIntent(event.dataId);
|
|
105
|
+
await handlePointPaymentIntent(intent);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
default:
|
|
109
|
+
// Unknown topic — log and acknowledge so MP doesn't retry forever.
|
|
110
|
+
console.warn(`Unhandled webhook topic: ${event.topic}`);
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
// Return 5xx so MP retries (it has built-in exponential backoff).
|
|
114
|
+
console.error("webhook handler failed:", err);
|
|
115
|
+
return new Response("internal error", { status: 500 });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return new Response("ok", { status: 200 });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
122
|
+
// Approach B — Agent tool (let the agent dispatch)
|
|
123
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Alternative: pass everything to an agent + the `handle_webhook` tool.
|
|
127
|
+
* Useful when your business logic varies by webhook content and an LLM
|
|
128
|
+
* makes the decision (e.g., "if this payment is for an old SKU, refund it").
|
|
129
|
+
*
|
|
130
|
+
* Note: this is HIGHER LATENCY than approach A and uses LLM tokens. Only
|
|
131
|
+
* use when LLM reasoning is genuinely required.
|
|
132
|
+
*/
|
|
133
|
+
// export async function POST_via_agent(req: Request) {
|
|
134
|
+
// const rawBody = await req.text();
|
|
135
|
+
// const result = await agent.generate({
|
|
136
|
+
// prompt: `Procesá este webhook de MP. Topic + body:\n${rawBody}`,
|
|
137
|
+
// toolChoice: "required",
|
|
138
|
+
// tools: mercadoPagoTools(mp, {
|
|
139
|
+
// state, backUrl, webhookSecret: WEBHOOK_SECRET, oauth: {...}
|
|
140
|
+
// }),
|
|
141
|
+
// });
|
|
142
|
+
// }
|
|
143
|
+
|
|
144
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
145
|
+
// Business logic stubs
|
|
146
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
async function handlePayment(payment: unknown) {
|
|
149
|
+
// Update your DB, fire shipping flow, send notification, etc.
|
|
150
|
+
}
|
|
151
|
+
async function handleSubscription(sub: unknown) {
|
|
152
|
+
/* ... */
|
|
153
|
+
}
|
|
154
|
+
async function handleRecurringCharge(authorizedPaymentId: string) {
|
|
155
|
+
/* ... */
|
|
156
|
+
}
|
|
157
|
+
async function handleMerchantOrder(mo: unknown) {
|
|
158
|
+
/* ... */
|
|
159
|
+
}
|
|
160
|
+
async function handlePointPaymentIntent(intent: unknown) {
|
|
161
|
+
/* ... */
|
|
162
|
+
}
|