better-auth-mercadopago 0.1.1

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/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # Mercado Pago Plugin for Better Auth
2
+
3
+ ![CI Status](https://github.com/ivantsxx/better-auth-mercadopago/actions/workflows/ci.yml/badge.svg)
4
+ ![NPM Version](https://img.shields.io/npm/v/better-auth-mercadopago)
5
+ ![License](https://img.shields.io/npm/l/better-auth-mercadopago)
6
+
7
+
8
+ A robust and type-safe Mercado Pago plugin for [Better Auth](https://better-auth.com). seamless integration for one-time payments, subscriptions, and webhook handling.
9
+
10
+ ## Features
11
+
12
+ - 💳 **One-time Payments**: Easy API to create payments.
13
+ - 🔄 **Subscriptions**: Full support for recurring payments (PreApproval).
14
+ - 🔗 **Automatic Linking**: Robustly links recurring payments to subscriptions using `external_reference`.
15
+ - 🪝 **Webhook Handling**: Built-in, secure webhook processing for payment updates.
16
+ - 🛡️ **Type Safe**: Fully typed requests and responses for a great developer experience.
17
+ - 👥 **Customer Management**: Automatically manages Mercado Pago customers for your users.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pnpm add @better-auth/mercadopago
23
+ # or
24
+ npm install @better-auth/mercadopago
25
+ # or
26
+ yarn add @better-auth/mercadopago
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ### 1. Configure the Plugin
32
+
33
+ Add the plugin to your Better Auth configuration. You need your Mercado Pago Access Token.
34
+
35
+ ```typescript
36
+ import { betterAuth } from "better-auth";
37
+ import { mercadoPagoPlugin } from "@better-auth/mercadopago";
38
+
39
+ export const auth = betterAuth({
40
+ // ... other config
41
+ plugins: [
42
+ mercadoPagoPlugin({
43
+ accessToken: process.env.MP_ACCESS_TOKEN!,
44
+ onSubscriptionUpdate: async ({ subscription, status, reason, mpPreapproval }) => {
45
+ // Handle subscription status changes (e.g., update DB)
46
+ console.log(`Subscription ${subscription.id} is now ${status}`);
47
+ },
48
+ onPaymentUpdate: async ({ payment, status, mpPayment }) => {
49
+ // Handle one-time payment updates
50
+ }
51
+ })
52
+ ]
53
+ });
54
+ ```
55
+
56
+ ### 2. Client-Side Usage
57
+
58
+ The plugin exposes client-side methods to create payments and subscriptions.
59
+
60
+ ```typescript
61
+ import { createAuthClient } from "better-auth/client";
62
+ import { mercadoPagoClient } from "@better-auth/mercadopago/client";
63
+
64
+ const authClient = createAuthClient({
65
+ plugins: [mercadoPagoClient()]
66
+ });
67
+
68
+ // Create a Subscription
69
+ async function subscribe() {
70
+ const { data, error } = await authClient.mercadoPago.createSubscription({
71
+ reason: "Pro Plan",
72
+ autoRecurring: {
73
+ frequency: 1,
74
+ frequencyType: "months",
75
+ transactionAmount: 10,
76
+ currencyId: "ARS"
77
+ },
78
+ backUrl: "https://your-app.com/success"
79
+ });
80
+
81
+ if (data) {
82
+ window.location.href = data.init_point; // Redirect to Mercado Pago
83
+ }
84
+ }
85
+ ```
86
+
87
+ ## Usage Guide
88
+
89
+ ### Subscriptions
90
+
91
+ To create a subscription, you use `createSubscription`. The plugin handles the complexity of:
92
+ 1. Creating a PreApproval Plan.
93
+ 2. Creating a PreApproval (Subscription) linked to that plan.
94
+ 3. Returning the `init_point` for user redirection.
95
+
96
+ ### Webhooks
97
+
98
+ The plugin automatically exposes a webhook endpoint at `/api/auth/mercado-pago/webhook`.
99
+ You must configure this URL in your Mercado Pago Dashboard (or use ngrok for local dev).
100
+
101
+ **Events Handled:**
102
+ - `subscription_authorized_payment`: recurring payments.
103
+ - `payment`: one-time payments.
104
+ - `preapproval`: subscription status updates.
105
+
106
+ ## API Reference
107
+
108
+ ### `mercadoPagoPlugin(options)`
109
+
110
+ **Options:**
111
+ - `accessToken` (required): Your Mercado Pago Production or Sandbox Access Token.
112
+ - `onSubscriptionUpdate`: Callback when a subscription changes status.
113
+ - `onPaymentUpdate`: Callback when a payment changes status.
114
+ - `onSubscriptionPayment`: Callback when a recurring payment is received.
115
+
116
+ ## Contributing
117
+
118
+ Contributions are welcome!
119
+
120
+ 1. Clone the repo
121
+ 2. Install dependencies: `pnpm install`
122
+ 3. Run tests: `pnpm test`
123
+ 4. Create a changeset for your changes: `pnpm changeset`
124
+
125
+ ## License
126
+
127
+ MIT
package/SECURITY.md ADDED
@@ -0,0 +1,469 @@
1
+ # Security Guide
2
+
3
+ This document outlines the security measures implemented in the Mercado Pago Better Auth plugin.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Webhook Security](#webhook-security)
8
+ 2. [Rate Limiting](#rate-limiting)
9
+ 3. [Input Validation](#input-validation)
10
+ 4. [Idempotency](#idempotency)
11
+ 5. [Error Handling](#error-handling)
12
+ 6. [Attack Prevention](#attack-prevention)
13
+ 7. [Best Practices](#best-practices)
14
+
15
+ ## Webhook Security
16
+
17
+ ### Signature Verification
18
+
19
+ All webhooks are verified using HMAC SHA256 signatures to ensure they come from Mercado Pago.
20
+
21
+ **Configuration:**
22
+
23
+ ```typescript
24
+ mercadoPago({
25
+ webhookSecret: process.env.MERCADO_PAGO_WEBHOOK_SECRET!,
26
+ // ...
27
+ })
28
+ ```
29
+
30
+ **How it works:**
31
+
32
+ 1. Mercado Pago sends a signature in the `x-signature` header
33
+ 2. Format: `ts=1234567890,v1=hash`
34
+ 3. The plugin verifies using: `HMAC-SHA256(secret, "id:DATA_ID;request-id:REQUEST_ID;ts:TIMESTAMP;")`
35
+ 4. Invalid signatures are rejected with 401
36
+
37
+ **Get your webhook secret:**
38
+ - Go to https://www.mercadopago.com/developers/panel/notifications/webhooks
39
+ - Click on your webhook
40
+ - Copy the "Secret" value
41
+
42
+ ### Webhook Topics Validation
43
+
44
+ Only valid webhook topics are processed:
45
+
46
+ - `payment`
47
+ - `merchant_order`
48
+ - `subscription_preapproval`
49
+ - `subscription_preapproval_plan`
50
+ - `subscription_authorized_payment`
51
+
52
+ Invalid topics are logged and ignored.
53
+
54
+ ### Idempotency Protection
55
+
56
+ Prevents duplicate webhook processing:
57
+
58
+ ```typescript
59
+ // Each webhook is processed only once
60
+ const webhookId = `webhook:${notification.id}:${notification.type}`;
61
+ ```
62
+
63
+ Webhooks are cached for 24 hours to prevent reprocessing.
64
+
65
+ ## Rate Limiting
66
+
67
+ ### Per-User Limits
68
+
69
+ ```typescript
70
+ // Payment creation: 10 per minute per user
71
+ const rateLimitKey = `payment:create:${userId}`;
72
+ rateLimiter.check(rateLimitKey, 10, 60 * 1000);
73
+ ```
74
+
75
+ ### Global Limits
76
+
77
+ ```typescript
78
+ // Webhooks: 1000 per minute globally
79
+ rateLimiter.check("webhook:global", 1000, 60 * 1000);
80
+ ```
81
+
82
+ ### Production Recommendations
83
+
84
+ For production, replace the in-memory rate limiter with Redis:
85
+
86
+ ```typescript
87
+ import Redis from "ioredis";
88
+
89
+ const redis = new Redis(process.env.REDIS_URL);
90
+
91
+ class RedisRateLimiter {
92
+ async check(key: string, max: number, windowMs: number): Promise<boolean> {
93
+ const count = await redis.incr(key);
94
+
95
+ if (count === 1) {
96
+ await redis.pexpire(key, windowMs);
97
+ }
98
+
99
+ return count <= max;
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## Input Validation
105
+
106
+ ### Amount Validation
107
+
108
+ ```typescript
109
+ // Prevents negative or excessive amounts
110
+ ValidationRules.amount(amount); // 0 < amount <= 999,999,999
111
+ ```
112
+
113
+ ### Currency Validation
114
+
115
+ ```typescript
116
+ // Only accepts valid currencies
117
+ ValidationRules.currency("ARS"); // true
118
+ ValidationRules.currency("INVALID"); // false
119
+
120
+ // Supported: ARS, BRL, CLP, MXN, COP, PEN, UYU, USD
121
+ ```
122
+
123
+ ### URL Validation
124
+
125
+ Prevents open redirect vulnerabilities:
126
+
127
+ ```typescript
128
+ validateCallbackUrl(
129
+ "https://myapp.com/callback",
130
+ ["myapp.com", "*.myapp.com"]
131
+ ); // true
132
+
133
+ validateCallbackUrl(
134
+ "https://evil.com/phishing",
135
+ ["myapp.com"]
136
+ ); // false
137
+ ```
138
+
139
+ **Configuration:**
140
+
141
+ ```typescript
142
+ mercadoPago({
143
+ trustedOrigins: [
144
+ "https://myapp.com",
145
+ "https://*.myapp.com", // Wildcard subdomains
146
+ ],
147
+ })
148
+ ```
149
+
150
+ ### Metadata Sanitization
151
+
152
+ Prevents prototype pollution and XSS:
153
+
154
+ ```typescript
155
+ const sanitized = sanitizeMetadata({
156
+ orderId: "123",
157
+ __proto__: { isAdmin: true }, // ❌ Removed
158
+ userInput: "<script>alert('xss')</script>", // Kept but limited to 5000 chars
159
+ });
160
+ ```
161
+
162
+ ## Idempotency
163
+
164
+ ### Payment Creation
165
+
166
+ ```typescript
167
+ const { data } = await authClient.mercadoPago.createPayment({
168
+ items: [/* ... */],
169
+ idempotencyKey: "unique-key-123", // Same key = same result
170
+ });
171
+ ```
172
+
173
+ **Rules:**
174
+
175
+ - Key format: UUID v4 or alphanumeric (8-64 chars)
176
+ - Cached for 24 hours
177
+ - Same key = returns cached response (no duplicate payments)
178
+
179
+ **Example:**
180
+
181
+ ```typescript
182
+ // First request
183
+ const result1 = await createPayment({
184
+ idempotencyKey: "abc-123",
185
+ items: [{ title: "Product", quantity: 1, unitPrice: 100 }]
186
+ });
187
+ // Creates new payment
188
+
189
+ // Second request (network retry, user double-click, etc.)
190
+ const result2 = await createPayment({
191
+ idempotencyKey: "abc-123",
192
+ items: [{ title: "Product", quantity: 1, unitPrice: 100 }]
193
+ });
194
+ // Returns cached result, no duplicate payment ✅
195
+ ```
196
+
197
+ ## Error Handling
198
+
199
+ ### Graceful Degradation
200
+
201
+ ```typescript
202
+ try {
203
+ await mpAPI.createPayment();
204
+ } catch (error) {
205
+ handleMercadoPagoError(error);
206
+ // Converts MP errors to Better Auth APIError format
207
+ }
208
+ ```
209
+
210
+ ### Error Types
211
+
212
+ | Status | Better Auth Type | Use Case |
213
+ |--------|-----------------|----------|
214
+ | 400 | BAD_REQUEST | Invalid input |
215
+ | 401 | UNAUTHORIZED | Invalid credentials |
216
+ | 403 | FORBIDDEN | Not allowed |
217
+ | 404 | NOT_FOUND | Resource missing |
218
+ | 429 | TOO_MANY_REQUESTS | Rate limited |
219
+ | 500 | INTERNAL_SERVER_ERROR | Server error |
220
+
221
+ ### Webhook Error Handling
222
+
223
+ Webhooks return 200 even on processing errors to prevent infinite retries:
224
+
225
+ ```typescript
226
+ try {
227
+ await processWebhook(notification);
228
+ } catch (error) {
229
+ logger.error("Webhook processing failed", { error });
230
+ // Still return 200 to acknowledge receipt
231
+ }
232
+
233
+ return ctx.json({ received: true });
234
+ ```
235
+
236
+ ## Attack Prevention
237
+
238
+ ### SQL Injection
239
+
240
+ ✅ **Protected** - Uses parameterized queries via Better Auth adapter:
241
+
242
+ ```typescript
243
+ // ✅ Safe
244
+ await ctx.context.adapter.findOne({
245
+ model: "mercadoPagoPayment",
246
+ where: [{ field: "id", value: userInput }] // Parameterized
247
+ });
248
+
249
+ // ❌ Never do this
250
+ await db.raw(`SELECT * FROM payments WHERE id = '${userInput}'`);
251
+ ```
252
+
253
+ ### XSS (Cross-Site Scripting)
254
+
255
+ ✅ **Protected** - Metadata is sanitized and limited to 5000 chars:
256
+
257
+ ```typescript
258
+ const sanitized = sanitizeMetadata(userInput);
259
+ // Scripts, iframes, etc. are stored but limited
260
+ // Rendering layer must still escape HTML
261
+ ```
262
+
263
+ ### CSRF (Cross-Site Request Forgery)
264
+
265
+ ✅ **Protected** - Better Auth handles CSRF tokens automatically:
266
+
267
+ ```typescript
268
+ // Better Auth adds CSRF protection to all POST endpoints
269
+ // No additional configuration needed
270
+ ```
271
+
272
+ ### Prototype Pollution
273
+
274
+ ✅ **Protected** - Dangerous keys are filtered:
275
+
276
+ ```typescript
277
+ sanitizeMetadata({
278
+ __proto__: { isAdmin: true }, // ❌ Removed
279
+ constructor: { ... }, // ❌ Removed
280
+ prototype: { ... }, // ❌ Removed
281
+ normalKey: "value", // ✅ Kept
282
+ });
283
+ ```
284
+
285
+ ### Timing Attacks
286
+
287
+ ✅ **Protected** - Uses constant-time comparison for signatures:
288
+
289
+ ```typescript
290
+ // ❌ Vulnerable
291
+ if (signature === expectedSignature) { }
292
+
293
+ // ✅ Safe
294
+ if (crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { }
295
+ ```
296
+
297
+ ### Payment Amount Manipulation
298
+
299
+ ✅ **Protected** - Validates amounts haven't been tampered:
300
+
301
+ ```typescript
302
+ // In webhook
303
+ if (!validatePaymentAmount(storedAmount, mpPayment.amount)) {
304
+ throw new Error("Amount mismatch - possible tampering");
305
+ }
306
+ ```
307
+
308
+ ### Replay Attacks
309
+
310
+ ✅ **Protected** - Webhooks are deduplicated:
311
+
312
+ ```typescript
313
+ const webhookId = `webhook:${id}:${type}`;
314
+ if (alreadyProcessed(webhookId)) {
315
+ return; // Ignore duplicate
316
+ }
317
+ ```
318
+
319
+ ## Best Practices
320
+
321
+ ### 1. Always Use HTTPS in Production
322
+
323
+ ```typescript
324
+ // In production, validate URLs are HTTPS
325
+ if (process.env.NODE_ENV === "production" && !url.startsWith("https://")) {
326
+ throw new Error("URLs must use HTTPS in production");
327
+ }
328
+ ```
329
+
330
+ ### 2. Set Strict Trusted Origins
331
+
332
+ ```typescript
333
+ mercadoPago({
334
+ trustedOrigins: [
335
+ "https://myapp.com", // ✅ Specific domain
336
+ "https://*.myapp.com", // ✅ Wildcard subdomains
337
+ // ❌ Don't use wildcards like "*" or "*.com"
338
+ ],
339
+ })
340
+ ```
341
+
342
+ ### 3. Use Environment Variables
343
+
344
+ ```env
345
+ # ✅ Good
346
+ MERCADO_PAGO_ACCESS_TOKEN=APP_USR-xxx
347
+ MERCADO_PAGO_WEBHOOK_SECRET=xxx
348
+ APP_URL=https://myapp.com
349
+
350
+ # ❌ Bad - never hardcode secrets
351
+ const accessToken = "APP_USR-123456...";
352
+ ```
353
+
354
+ ### 4. Monitor Webhook Failures
355
+
356
+ ```typescript
357
+ mercadoPago({
358
+ onPaymentUpdate: async ({ payment, status }) => {
359
+ if (status === "rejected") {
360
+ // Alert your team
361
+ await alerting.notify("Payment rejected", { payment });
362
+ }
363
+ },
364
+ })
365
+ ```
366
+
367
+ ### 5. Implement Proper Logging
368
+
369
+ ```typescript
370
+ // Log security events
371
+ logger.warn("Invalid webhook signature", {
372
+ xSignature,
373
+ xRequestId,
374
+ ip: request.ip,
375
+ });
376
+
377
+ // Don't log sensitive data
378
+ // ❌ logger.info("Payment", { cardNumber: "..." });
379
+ // ✅ logger.info("Payment", { paymentId: "..." });
380
+ ```
381
+
382
+ ### 6. Use Redis for Production
383
+
384
+ Replace in-memory stores with Redis:
385
+
386
+ - Rate limiting
387
+ - Idempotency cache
388
+ - Webhook deduplication
389
+
390
+ ### 7. Regular Security Audits
391
+
392
+ - Review logs for suspicious patterns
393
+ - Update dependencies regularly
394
+ - Test webhook signature validation
395
+ - Validate rate limits are working
396
+
397
+ ### 8. Implement Monitoring
398
+
399
+ ```typescript
400
+ // Track failed webhooks
401
+ if (webhookProcessingFailed) {
402
+ metrics.increment("webhook.failed", {
403
+ type: notification.type,
404
+ error: error.message,
405
+ });
406
+ }
407
+ ```
408
+
409
+ ### 9. Handle PCI Compliance
410
+
411
+ ⚠️ **Never store card data:**
412
+
413
+ ```typescript
414
+ // ❌ Never do this
415
+ const cardData = {
416
+ number: "4111111111111111",
417
+ cvv: "123",
418
+ expiry: "12/25"
419
+ };
420
+
421
+ // ✅ Let Mercado Pago handle it
422
+ // Users enter card details directly in MP's hosted checkout
423
+ ```
424
+
425
+ ### 10. Database Security
426
+
427
+ ```typescript
428
+ // Use row-level security
429
+ CREATE POLICY user_payments ON mercadoPagoPayment
430
+ FOR SELECT
431
+ USING (userId = current_user_id());
432
+
433
+ // Encrypt sensitive fields
434
+ encryptedMetadata = encrypt(metadata, encryptionKey);
435
+ ```
436
+
437
+ ## Security Checklist
438
+
439
+ Before deploying to production:
440
+
441
+ - [ ] Webhook secret configured
442
+ - [ ] HTTPS enforced
443
+ - [ ] Trusted origins set
444
+ - [ ] Rate limiting configured (Redis recommended)
445
+ - [ ] Error logging implemented
446
+ - [ ] Monitoring and alerts set up
447
+ - [ ] Never log sensitive data (cards, tokens)
448
+ - [ ] Database has proper indexes
449
+ - [ ] Row-level security enabled (if using Postgres)
450
+ - [ ] Regular dependency updates scheduled
451
+ - [ ] Incident response plan documented
452
+
453
+ ## Reporting Security Issues
454
+
455
+ If you discover a security vulnerability:
456
+
457
+ 1. **DO NOT** open a public GitHub issue
458
+ 2. Email security@yourcompany.com
459
+ 3. Include:
460
+ - Description of the vulnerability
461
+ - Steps to reproduce
462
+ - Potential impact
463
+ - Suggested fix (if any)
464
+
465
+ We'll respond within 48 hours.
466
+
467
+ ## License
468
+
469
+ MIT
@@ -0,0 +1,4 @@
1
+ import 'better-auth/client';
2
+ export { mercadoPagoClient } from './index.mjs';
3
+ import 'better-auth';
4
+ import 'zod';
@@ -0,0 +1,4 @@
1
+ import 'better-auth/client';
2
+ export { mercadoPagoClient } from './index.js';
3
+ import 'better-auth';
4
+ import 'zod';