better-auth-mercadopago 0.1.3 → 0.1.5

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.mjs ADDED
@@ -0,0 +1,1653 @@
1
+ // index.ts
2
+ import { generateId } from "better-auth";
3
+ import { APIError as APIError2, createAuthEndpoint } from "better-auth/api";
4
+ import {
5
+ Customer,
6
+ MercadoPagoConfig,
7
+ Payment,
8
+ PreApproval,
9
+ PreApprovalPlan,
10
+ Preference
11
+ } from "mercadopago";
12
+ import { z } from "zod";
13
+
14
+ // security.ts
15
+ import crypto from "crypto";
16
+ import { APIError } from "better-auth/api";
17
+ function verifyWebhookSignature(params) {
18
+ const { xSignature, xRequestId, dataId, secret } = params;
19
+ if (!xSignature || !xRequestId) {
20
+ return false;
21
+ }
22
+ const parts = xSignature.split(",");
23
+ const ts = parts.find((p) => p.startsWith("ts="))?.split("=")[1];
24
+ const hash = parts.find((p) => p.startsWith("v1="))?.split("=")[1];
25
+ if (!ts || !hash) {
26
+ return false;
27
+ }
28
+ const manifest = `id:${dataId};request-id:${xRequestId};ts:${ts};`;
29
+ const hmac = crypto.createHmac("sha256", secret);
30
+ hmac.update(manifest);
31
+ const expectedHash = hmac.digest("hex");
32
+ return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(expectedHash));
33
+ }
34
+ var RateLimiter = class {
35
+ constructor() {
36
+ this.attempts = /* @__PURE__ */ new Map();
37
+ }
38
+ check(key, maxAttempts, windowMs) {
39
+ const now = Date.now();
40
+ const record = this.attempts.get(key);
41
+ if (!record || now > record.resetAt) {
42
+ this.attempts.set(key, {
43
+ count: 1,
44
+ resetAt: now + windowMs
45
+ });
46
+ return true;
47
+ }
48
+ if (record.count >= maxAttempts) {
49
+ return false;
50
+ }
51
+ record.count++;
52
+ return true;
53
+ }
54
+ cleanup() {
55
+ const now = Date.now();
56
+ for (const [key, record] of this.attempts.entries()) {
57
+ if (now > record.resetAt) {
58
+ this.attempts.delete(key);
59
+ }
60
+ }
61
+ }
62
+ };
63
+ var rateLimiter = new RateLimiter();
64
+ setInterval(() => rateLimiter.cleanup(), 5 * 60 * 1e3);
65
+ function validatePaymentAmount(requestedAmount, mpPaymentAmount, tolerance = 0.01) {
66
+ const diff = Math.abs(requestedAmount - mpPaymentAmount);
67
+ return diff <= tolerance;
68
+ }
69
+ function sanitizeMetadata(metadata) {
70
+ const sanitized = {};
71
+ for (const [key, value] of Object.entries(metadata)) {
72
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
73
+ continue;
74
+ }
75
+ if (typeof value === "string" && value.length > 5e3) {
76
+ sanitized[key] = value.substring(0, 5e3);
77
+ } else if (typeof value === "object" && value !== null) {
78
+ sanitized[key] = sanitizeMetadata(value);
79
+ } else {
80
+ sanitized[key] = value;
81
+ }
82
+ }
83
+ return sanitized;
84
+ }
85
+ function validateCallbackUrl(url, allowedDomains) {
86
+ try {
87
+ const parsed = new URL(url);
88
+ if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
89
+ return false;
90
+ }
91
+ const hostname = parsed.hostname;
92
+ return allowedDomains.some((domain) => {
93
+ if (domain.startsWith("*.")) {
94
+ const baseDomain = domain.substring(2);
95
+ return hostname.endsWith(baseDomain);
96
+ }
97
+ return hostname === domain;
98
+ });
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+ function validateIdempotencyKey(key) {
104
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
105
+ const customRegex = /^[a-zA-Z0-9_-]{8,64}$/;
106
+ return uuidRegex.test(key) || customRegex.test(key);
107
+ }
108
+ var MercadoPagoError = class extends Error {
109
+ constructor(code, message, statusCode = 400, details) {
110
+ super(message);
111
+ this.code = code;
112
+ this.message = message;
113
+ this.statusCode = statusCode;
114
+ this.details = details;
115
+ this.name = "MercadoPagoError";
116
+ }
117
+ toAPIError() {
118
+ const errorMap = {
119
+ 400: "BAD_REQUEST",
120
+ 401: "UNAUTHORIZED",
121
+ 403: "FORBIDDEN",
122
+ 404: "NOT_FOUND",
123
+ 429: "TOO_MANY_REQUESTS",
124
+ 500: "INTERNAL_SERVER_ERROR"
125
+ };
126
+ const type = errorMap[this.statusCode] || "BAD_REQUEST";
127
+ return new APIError(type, {
128
+ message: this.message,
129
+ details: this.details
130
+ });
131
+ }
132
+ };
133
+ function handleMercadoPagoError(error) {
134
+ if (error.status) {
135
+ const mpError = new MercadoPagoError(
136
+ error.code || "unknown_error",
137
+ error.message || "An error occurred with Mercado Pago",
138
+ error.status,
139
+ error.cause
140
+ );
141
+ throw mpError.toAPIError();
142
+ }
143
+ throw new APIError("INTERNAL_SERVER_ERROR", {
144
+ message: "Failed to process Mercado Pago request"
145
+ });
146
+ }
147
+ var VALID_WEBHOOK_TOPICS = [
148
+ "payment",
149
+ "merchant_order",
150
+ "subscription_preapproval",
151
+ "subscription_preapproval_plan",
152
+ "subscription_authorized_payment",
153
+ "point_integration_wh",
154
+ "topic_claims_integration_wh",
155
+ "topic_merchant_order_wh",
156
+ "delivery_cancellation"
157
+ ];
158
+ function isValidWebhookTopic(topic) {
159
+ return VALID_WEBHOOK_TOPICS.includes(topic);
160
+ }
161
+ var IdempotencyStore = class {
162
+ constructor() {
163
+ // biome-ignore lint/suspicious/noExplicitAny: <necessary>
164
+ this.store = /* @__PURE__ */ new Map();
165
+ }
166
+ // biome-ignore lint/suspicious/noExplicitAny: <necessary>
167
+ get(key) {
168
+ const record = this.store.get(key);
169
+ if (!record || Date.now() > record.expiresAt) {
170
+ this.store.delete(key);
171
+ return null;
172
+ }
173
+ return record.result;
174
+ }
175
+ // biome-ignore lint/suspicious/noExplicitAny: <necessary>
176
+ set(key, result, ttlMs = 24 * 60 * 60 * 1e3) {
177
+ this.store.set(key, {
178
+ result,
179
+ expiresAt: Date.now() + ttlMs
180
+ });
181
+ }
182
+ cleanup() {
183
+ const now = Date.now();
184
+ for (const [key, record] of this.store.entries()) {
185
+ if (now > record.expiresAt) {
186
+ this.store.delete(key);
187
+ }
188
+ }
189
+ }
190
+ };
191
+ var idempotencyStore = new IdempotencyStore();
192
+ setInterval(() => idempotencyStore.cleanup(), 60 * 60 * 1e3);
193
+ var ValidationRules = {
194
+ email: (email) => {
195
+ const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
196
+ return regex.test(email) && email.length <= 255;
197
+ },
198
+ amount: (amount) => {
199
+ return amount > 0 && amount <= 999999999 && !Number.isNaN(amount);
200
+ },
201
+ currency: (currency) => {
202
+ const validCurrencies = [
203
+ "ARS",
204
+ "BRL",
205
+ "CLP",
206
+ "MXN",
207
+ "COP",
208
+ "PEN",
209
+ "UYU",
210
+ "USD"
211
+ ];
212
+ return validCurrencies.includes(currency);
213
+ },
214
+ frequency: (frequency) => {
215
+ return frequency > 0 && frequency <= 365 && Number.isInteger(frequency);
216
+ },
217
+ userId: (userId) => {
218
+ return /^[a-zA-Z0-9_-]{1,100}$/.test(userId);
219
+ }
220
+ };
221
+
222
+ // client.ts
223
+ var mercadoPagoClient = () => {
224
+ return {
225
+ id: "mercado-pago",
226
+ $InferServerPlugin: {},
227
+ getActions: ($fetch) => ({
228
+ /**
229
+ * Get or create a Mercado Pago customer for the authenticated user
230
+ */
231
+ getOrCreateCustomer: async (data, fetchOptions) => {
232
+ return await $fetch(
233
+ "/mercado-pago/customer",
234
+ {
235
+ method: "POST",
236
+ body: data || {},
237
+ ...fetchOptions
238
+ }
239
+ );
240
+ },
241
+ /**
242
+ * Create a payment and get checkout URL
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * const { data } = await authClient.mercadoPago.createPayment({
247
+ * items: [{
248
+ * title: "Premium Plan",
249
+ * quantity: 1,
250
+ * unitPrice: 99.90,
251
+ * currencyId: "ARS"
252
+ * }]
253
+ * });
254
+ *
255
+ * // Redirect user to checkout
256
+ * window.location.href = data.checkoutUrl;
257
+ * ```
258
+ */
259
+ createPayment: async (data, fetchOptions) => {
260
+ return await $fetch(
261
+ "/mercado-pago/payment/create",
262
+ {
263
+ method: "POST",
264
+ body: data,
265
+ ...fetchOptions
266
+ }
267
+ );
268
+ },
269
+ /**
270
+ * Create a marketplace payment with automatic split
271
+ *
272
+ * You need to have the seller's MP User ID (collector_id) which they get
273
+ * after authorizing your app via OAuth.
274
+ *
275
+ * @example
276
+ * ```ts
277
+ * const { data } = await authClient.mercadoPago.createPayment({
278
+ * items: [{
279
+ * title: "Product from Seller",
280
+ * quantity: 1,
281
+ * unitPrice: 100
282
+ * }],
283
+ * marketplace: {
284
+ * collectorId: "123456789", // Seller's MP User ID
285
+ * applicationFeePercentage: 10 // Platform keeps 10%
286
+ * }
287
+ * });
288
+ * ```
289
+ */
290
+ createMarketplacePayment: async (data, fetchOptions) => {
291
+ return await $fetch(
292
+ "/mercado-pago/payment/create",
293
+ {
294
+ method: "POST",
295
+ body: data,
296
+ ...fetchOptions
297
+ }
298
+ );
299
+ },
300
+ /**
301
+ * Create a subscription with recurring payments
302
+ *
303
+ * Supports two modes:
304
+ * 1. With preapproval plan (reusable): Pass preapprovalPlanId
305
+ * 2. Direct subscription (one-off): Pass reason + autoRecurring
306
+ *
307
+ * @example With plan
308
+ * ```ts
309
+ * const { data } = await authClient.mercadoPago.createSubscription({
310
+ * preapprovalPlanId: "plan_abc123"
311
+ * });
312
+ * ```
313
+ *
314
+ * @example Direct (without plan)
315
+ * ```ts
316
+ * const { data } = await authClient.mercadoPago.createSubscription({
317
+ * reason: "Premium Monthly Plan",
318
+ * autoRecurring: {
319
+ * frequency: 1,
320
+ * frequencyType: "months",
321
+ * transactionAmount: 99.90,
322
+ * currencyId: "ARS"
323
+ * }
324
+ * });
325
+ * ```
326
+ */
327
+ createSubscription: async (data, fetchOptions) => {
328
+ return await $fetch(
329
+ "/mercado-pago/subscription/create",
330
+ {
331
+ method: "POST",
332
+ body: data,
333
+ ...fetchOptions
334
+ }
335
+ );
336
+ },
337
+ /**
338
+ * Cancel a subscription
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * await authClient.mercadoPago.cancelSubscription({
343
+ * subscriptionId: "sub_123"
344
+ * });
345
+ * ```
346
+ */
347
+ cancelSubscription: async (data, fetchOptions) => {
348
+ return await $fetch(
349
+ "/mercado-pago/subscription/cancel",
350
+ {
351
+ method: "POST",
352
+ body: data,
353
+ ...fetchOptions
354
+ }
355
+ );
356
+ },
357
+ /**
358
+ * Create a reusable preapproval plan (subscription template)
359
+ *
360
+ * Plans can be reused for multiple subscriptions. Create once,
361
+ * use many times with createSubscription({ preapprovalPlanId })
362
+ *
363
+ * @example
364
+ * ```ts
365
+ * const { data } = await authClient.mercadoPago.createPreapprovalPlan({
366
+ * reason: "Premium Monthly",
367
+ * autoRecurring: {
368
+ * frequency: 1,
369
+ * frequencyType: "months",
370
+ * transactionAmount: 99.90,
371
+ * freeTrial: {
372
+ * frequency: 7,
373
+ * frequencyType: "days"
374
+ * }
375
+ * },
376
+ * repetitions: 12 // 12 months, omit for infinite
377
+ * });
378
+ *
379
+ * // Use the plan
380
+ * const planId = data.plan.mercadoPagoPlanId;
381
+ * ```
382
+ */
383
+ createPreapprovalPlan: async (data, fetchOptions) => {
384
+ return await $fetch(
385
+ "/mercado-pago/plan/create",
386
+ {
387
+ method: "POST",
388
+ body: data,
389
+ ...fetchOptions
390
+ }
391
+ );
392
+ },
393
+ /**
394
+ * List all preapproval plans
395
+ *
396
+ * @example
397
+ * ```ts
398
+ * const { data } = await authClient.mercadoPago.listPreapprovalPlans();
399
+ *
400
+ * data.plans.forEach(plan => {
401
+ * console.log(plan.reason); // "Premium Monthly"
402
+ * console.log(plan.transactionAmount); // 99.90
403
+ * });
404
+ * ```
405
+ */
406
+ listPreapprovalPlans: async (fetchOptions) => {
407
+ return await $fetch(
408
+ "/mercado-pago/plans",
409
+ {
410
+ method: "GET",
411
+ ...fetchOptions
412
+ }
413
+ );
414
+ },
415
+ /**
416
+ * Get payment by ID
417
+ */
418
+ getPayment: async (paymentId, fetchOptions) => {
419
+ return await $fetch(
420
+ `/mercado-pago/payment/${paymentId}`,
421
+ {
422
+ method: "GET",
423
+ ...fetchOptions
424
+ }
425
+ );
426
+ },
427
+ /**
428
+ * List all payments for the authenticated user
429
+ *
430
+ * @example
431
+ * ```ts
432
+ * const { data } = await authClient.mercadoPago.listPayments({
433
+ * limit: 20,
434
+ * offset: 0
435
+ * });
436
+ * ```
437
+ */
438
+ listPayments: async (params, fetchOptions) => {
439
+ const query = new URLSearchParams();
440
+ if (params?.limit) query.set("limit", params.limit.toString());
441
+ if (params?.offset) query.set("offset", params.offset.toString());
442
+ return await $fetch(
443
+ `/mercado-pago/payments?${query.toString()}`,
444
+ {
445
+ method: "GET",
446
+ ...fetchOptions
447
+ }
448
+ );
449
+ },
450
+ /**
451
+ * List all subscriptions for the authenticated user
452
+ *
453
+ * @example
454
+ * ```ts
455
+ * const { data } = await authClient.mercadoPago.listSubscriptions();
456
+ * ```
457
+ */
458
+ listSubscriptions: async (fetchOptions) => {
459
+ return await $fetch(
460
+ `/mercado-pago/subscriptions`,
461
+ {
462
+ method: "GET",
463
+ ...fetchOptions
464
+ }
465
+ );
466
+ },
467
+ /**
468
+ * Get OAuth authorization URL for marketplace sellers
469
+ *
470
+ * This is Step 1 of OAuth flow. Redirect the seller to this URL so they
471
+ * can authorize your app to process payments on their behalf.
472
+ *
473
+ * @example
474
+ * ```ts
475
+ * const { data } = await authClient.mercadoPago.getOAuthUrl({
476
+ * redirectUri: "https://myapp.com/oauth/callback"
477
+ * });
478
+ *
479
+ * // Redirect seller to authorize
480
+ * window.location.href = data.authUrl;
481
+ * ```
482
+ */
483
+ getOAuthUrl: async (params, fetchOptions) => {
484
+ const query = new URLSearchParams();
485
+ query.set("redirectUri", params.redirectUri);
486
+ return await $fetch(
487
+ `/mercado-pago/oauth/authorize?${query.toString()}`,
488
+ {
489
+ method: "GET",
490
+ ...fetchOptions
491
+ }
492
+ );
493
+ },
494
+ /**
495
+ * Exchange OAuth code for access token
496
+ *
497
+ * This is Step 2 of OAuth flow. After the seller authorizes and MP redirects
498
+ * them back with a code, exchange that code for an access token.
499
+ *
500
+ * @example
501
+ * ```ts
502
+ * // In your /oauth/callback page:
503
+ * const code = new URLSearchParams(window.location.search).get("code");
504
+ *
505
+ * const { data } = await authClient.mercadoPago.exchangeOAuthCode({
506
+ * code,
507
+ * redirectUri: "https://myapp.com/oauth/callback"
508
+ * });
509
+ *
510
+ * // Now you have the seller's MP User ID
511
+ * console.log(data.oauthToken.mercadoPagoUserId);
512
+ * ```
513
+ */
514
+ exchangeOAuthCode: async (data, fetchOptions) => {
515
+ return await $fetch(
516
+ "/mercado-pago/oauth/callback",
517
+ {
518
+ method: "POST",
519
+ body: data,
520
+ ...fetchOptions
521
+ }
522
+ );
523
+ }
524
+ })
525
+ };
526
+ };
527
+
528
+ // index.ts
529
+ var mercadoPagoPlugin = (options) => {
530
+ const client = new MercadoPagoConfig({
531
+ accessToken: options.accessToken
532
+ });
533
+ const preferenceClient = new Preference(client);
534
+ const paymentClient = new Payment(client);
535
+ const customerClient = new Customer(client);
536
+ const preApprovalClient = new PreApproval(client);
537
+ const preApprovalPlanClient = new PreApprovalPlan(client);
538
+ return {
539
+ id: "mercado-pago",
540
+ schema: {
541
+ // Customer table - stores MP customer info
542
+ mercadoPagoCustomer: {
543
+ fields: {
544
+ id: { type: "string", required: true },
545
+ userId: {
546
+ type: "string",
547
+ required: true,
548
+ references: { model: "user", field: "id", onDelete: "cascade" }
549
+ },
550
+ mercadoPagoId: { type: "string", required: true, unique: true },
551
+ email: { type: "string", required: true },
552
+ createdAt: { type: "date", required: true },
553
+ updatedAt: { type: "date", required: true }
554
+ }
555
+ },
556
+ // Payment table - one-time payments
557
+ mercadoPagoPayment: {
558
+ fields: {
559
+ id: { type: "string", required: true },
560
+ userId: {
561
+ type: "string",
562
+ required: true,
563
+ references: { model: "user", field: "id", onDelete: "cascade" }
564
+ },
565
+ mercadoPagoPaymentId: {
566
+ type: "string",
567
+ required: true,
568
+ unique: true
569
+ },
570
+ preferenceId: { type: "string", required: true },
571
+ status: { type: "string", required: true },
572
+ // pending, approved, authorized, rejected, cancelled, refunded, charged_back
573
+ statusDetail: { type: "string" },
574
+ // accredited, pending_contingency, pending_review_manual, cc_rejected_*, etc
575
+ amount: { type: "number", required: true },
576
+ currency: { type: "string", required: true },
577
+ paymentMethodId: { type: "string" },
578
+ // visa, master, pix, etc
579
+ paymentTypeId: { type: "string" },
580
+ // credit_card, debit_card, ticket, etc
581
+ metadata: { type: "string" },
582
+ // JSON stringified
583
+ createdAt: { type: "date", required: true },
584
+ updatedAt: { type: "date", required: true }
585
+ }
586
+ },
587
+ // Subscription table
588
+ mercadoPagoSubscription: {
589
+ fields: {
590
+ id: { type: "string", required: true },
591
+ userId: {
592
+ type: "string",
593
+ required: true,
594
+ references: { model: "user", field: "id", onDelete: "cascade" }
595
+ },
596
+ mercadoPagoSubscriptionId: {
597
+ type: "string",
598
+ required: true,
599
+ unique: true
600
+ },
601
+ planId: { type: "string", required: true },
602
+ status: { type: "string", required: true },
603
+ // authorized, paused, cancelled, pending
604
+ reason: { type: "string" },
605
+ // Reason for status (e.g., payment_failed, user_cancelled)
606
+ nextPaymentDate: { type: "date" },
607
+ lastPaymentDate: { type: "date" },
608
+ summarized: { type: "string" },
609
+ // JSON with charges, charged_amount, pending_charge_amount
610
+ metadata: { type: "string" },
611
+ // JSON stringified
612
+ createdAt: { type: "date", required: true },
613
+ updatedAt: { type: "date", required: true }
614
+ }
615
+ },
616
+ // Preapproval Plan table (reusable subscription plans)
617
+ mercadoPagoPreapprovalPlan: {
618
+ fields: {
619
+ id: { type: "string", required: true },
620
+ mercadoPagoPlanId: { type: "string", required: true, unique: true },
621
+ reason: { type: "string", required: true },
622
+ // Plan description
623
+ frequency: { type: "number", required: true },
624
+ frequencyType: { type: "string", required: true },
625
+ // days, months
626
+ transactionAmount: { type: "number", required: true },
627
+ currencyId: { type: "string", required: true },
628
+ repetitions: { type: "number" },
629
+ // null = infinite
630
+ freeTrial: { type: "string" },
631
+ // JSON with frequency and frequency_type
632
+ metadata: { type: "string" },
633
+ // JSON stringified
634
+ createdAt: { type: "date", required: true },
635
+ updatedAt: { type: "date", required: true }
636
+ }
637
+ },
638
+ // Split payments table (for marketplace)
639
+ mercadoPagoMarketplaceSplit: {
640
+ fields: {
641
+ id: { type: "string", required: true },
642
+ paymentId: {
643
+ type: "string",
644
+ required: true,
645
+ references: {
646
+ model: "mercadoPagoPayment",
647
+ field: "id",
648
+ onDelete: "cascade"
649
+ }
650
+ },
651
+ // Changed naming to be more clear
652
+ collectorId: { type: "string", required: true },
653
+ // MP User ID who receives the money (seller)
654
+ collectorEmail: { type: "string", required: true },
655
+ // Email of who receives money
656
+ applicationFeeAmount: { type: "number" },
657
+ // Platform commission in absolute value
658
+ applicationFeePercentage: { type: "number" },
659
+ // Platform commission percentage
660
+ netAmount: { type: "number", required: true },
661
+ // Amount that goes to collector (seller)
662
+ metadata: { type: "string" },
663
+ createdAt: { type: "date", required: true }
664
+ }
665
+ },
666
+ // OAuth tokens for marketplace (to make payments on behalf of sellers)
667
+ mercadoPagoOAuthToken: {
668
+ fields: {
669
+ id: { type: "string", required: true },
670
+ userId: {
671
+ type: "string",
672
+ required: true,
673
+ references: { model: "user", field: "id", onDelete: "cascade" }
674
+ },
675
+ accessToken: { type: "string", required: true },
676
+ refreshToken: { type: "string", required: true },
677
+ publicKey: { type: "string", required: true },
678
+ mercadoPagoUserId: { type: "string", required: true, unique: true },
679
+ expiresAt: { type: "date", required: true },
680
+ createdAt: { type: "date", required: true },
681
+ updatedAt: { type: "date", required: true }
682
+ }
683
+ }
684
+ },
685
+ endpoints: {
686
+ // Get or create customer automatically
687
+ getOrCreateCustomer: createAuthEndpoint(
688
+ "/mercado-pago/customer",
689
+ {
690
+ method: "POST",
691
+ requireAuth: true,
692
+ body: z.object({
693
+ email: z.string().email().optional(),
694
+ firstName: z.string().optional(),
695
+ lastName: z.string().optional()
696
+ })
697
+ },
698
+ async (ctx) => {
699
+ const session = ctx.context.session;
700
+ if (!session) {
701
+ throw new APIError2("UNAUTHORIZED", {
702
+ message: "You must be logged in"
703
+ });
704
+ }
705
+ const { email, firstName, lastName } = ctx.body;
706
+ const userEmail = email || session.user.email;
707
+ const existingCustomer = await ctx.context.adapter.findOne({
708
+ model: "mercadoPagoCustomer",
709
+ where: [{ field: "userId", value: session.user.id }]
710
+ });
711
+ if (existingCustomer) {
712
+ return ctx.json({ customer: existingCustomer });
713
+ }
714
+ const mpCustomer = await customerClient.create({
715
+ body: {
716
+ email: userEmail,
717
+ first_name: firstName,
718
+ last_name: lastName
719
+ }
720
+ });
721
+ const customer = await ctx.context.adapter.create({
722
+ model: "mercadoPagoCustomer",
723
+ data: {
724
+ id: generateId(),
725
+ userId: session.user.id,
726
+ mercadoPagoId: mpCustomer.id,
727
+ email: userEmail,
728
+ createdAt: /* @__PURE__ */ new Date(),
729
+ updatedAt: /* @__PURE__ */ new Date()
730
+ }
731
+ });
732
+ return ctx.json({ customer });
733
+ }
734
+ ),
735
+ // OAuth: Get authorization URL for marketplace sellers
736
+ getOAuthUrl: createAuthEndpoint(
737
+ "/mercado-pago/oauth/authorize",
738
+ {
739
+ method: "GET",
740
+ requireAuth: true,
741
+ query: z.object({
742
+ redirectUri: z.string().url()
743
+ })
744
+ },
745
+ async (ctx) => {
746
+ const session = ctx.context.session;
747
+ if (!session) {
748
+ throw new APIError2("UNAUTHORIZED");
749
+ }
750
+ if (!options.appId) {
751
+ throw new APIError2("BAD_REQUEST", {
752
+ message: "OAuth not configured. Please provide appId in plugin options"
753
+ });
754
+ }
755
+ const { redirectUri } = ctx.query;
756
+ if (!ctx.context.isTrustedOrigin(redirectUri)) {
757
+ throw new APIError2("FORBIDDEN", {
758
+ message: "Redirect URI not in trusted origins"
759
+ });
760
+ }
761
+ const authUrl = `https://auth.mercadopago.com/authorization?client_id=${options.appId}&response_type=code&platform_id=mp&state=${session.user.id}&redirect_uri=${encodeURIComponent(redirectUri)}`;
762
+ return ctx.json({ authUrl });
763
+ }
764
+ ),
765
+ // OAuth: Exchange code for access token
766
+ exchangeOAuthCode: createAuthEndpoint(
767
+ "/mercado-pago/oauth/callback",
768
+ {
769
+ method: "POST",
770
+ requireAuth: true,
771
+ body: z.object({
772
+ code: z.string(),
773
+ redirectUri: z.string().url()
774
+ })
775
+ },
776
+ async (ctx) => {
777
+ const session = ctx.context.session;
778
+ if (!session) {
779
+ throw new APIError2("UNAUTHORIZED");
780
+ }
781
+ if (!options.appId || !options.appSecret) {
782
+ throw new APIError2("BAD_REQUEST", {
783
+ message: "OAuth not configured"
784
+ });
785
+ }
786
+ const { code, redirectUri } = ctx.body;
787
+ const tokenResponse = await fetch(
788
+ "https://api.mercadopago.com/oauth/token",
789
+ {
790
+ method: "POST",
791
+ headers: { "Content-Type": "application/json" },
792
+ body: JSON.stringify({
793
+ client_id: options.appId,
794
+ client_secret: options.appSecret,
795
+ grant_type: "authorization_code",
796
+ code,
797
+ redirect_uri: redirectUri
798
+ })
799
+ }
800
+ );
801
+ if (!tokenResponse.ok) {
802
+ throw new APIError2("BAD_REQUEST", {
803
+ message: "Failed to exchange OAuth code"
804
+ });
805
+ }
806
+ const tokenData = await tokenResponse.json();
807
+ const oauthToken = await ctx.context.adapter.create({
808
+ model: "mercadoPagoOAuthToken",
809
+ data: {
810
+ id: generateId(),
811
+ userId: session.user.id,
812
+ accessToken: tokenData.access_token,
813
+ refreshToken: tokenData.refresh_token,
814
+ publicKey: tokenData.public_key,
815
+ mercadoPagoUserId: tokenData.user_id.toString(),
816
+ expiresAt: new Date(Date.now() + tokenData.expires_in * 1e3),
817
+ createdAt: /* @__PURE__ */ new Date(),
818
+ updatedAt: /* @__PURE__ */ new Date()
819
+ }
820
+ });
821
+ return ctx.json({
822
+ success: true,
823
+ oauthToken: {
824
+ id: oauthToken.id,
825
+ mercadoPagoUserId: oauthToken.mercadoPagoUserId,
826
+ expiresAt: oauthToken.expiresAt
827
+ }
828
+ });
829
+ }
830
+ ),
831
+ // Create a reusable preapproval plan (subscription plan)
832
+ createPreapprovalPlan: createAuthEndpoint(
833
+ "/mercado-pago/plan/create",
834
+ {
835
+ method: "POST",
836
+ body: z.object({
837
+ reason: z.string(),
838
+ // Plan description (e.g., "Premium Monthly")
839
+ autoRecurring: z.object({
840
+ frequency: z.number(),
841
+ // 1, 7, 30, etc
842
+ frequencyType: z.enum(["days", "months"]),
843
+ transactionAmount: z.number(),
844
+ currencyId: z.string().default("ARS"),
845
+ freeTrial: z.object({
846
+ frequency: z.number(),
847
+ frequencyType: z.enum(["days", "months"])
848
+ }).optional()
849
+ }),
850
+ repetitions: z.number().optional(),
851
+ // null = infinite
852
+ backUrl: z.string().optional(),
853
+ metadata: z.record(z.any()).optional()
854
+ })
855
+ },
856
+ async (ctx) => {
857
+ const { reason, autoRecurring, repetitions, backUrl, metadata } = ctx.body;
858
+ const baseUrl = options.baseUrl || ctx.context.baseURL;
859
+ const planBody = {
860
+ reason,
861
+ auto_recurring: {
862
+ frequency: autoRecurring.frequency,
863
+ frequency_type: autoRecurring.frequencyType,
864
+ transaction_amount: autoRecurring.transactionAmount,
865
+ currency_id: autoRecurring.currencyId
866
+ },
867
+ back_url: backUrl || `${baseUrl}/plan/created`
868
+ };
869
+ if (repetitions && planBody.auto_recurring) {
870
+ planBody.auto_recurring.repetitions = repetitions;
871
+ }
872
+ if (autoRecurring.freeTrial && planBody.auto_recurring) {
873
+ planBody.auto_recurring.free_trial = {
874
+ frequency: autoRecurring.freeTrial.frequency,
875
+ frequency_type: autoRecurring.freeTrial.frequencyType
876
+ };
877
+ }
878
+ const mpPlan = await preApprovalPlanClient.create({ body: planBody });
879
+ const plan = await ctx.context.adapter.create({
880
+ model: "mercadoPagoPreapprovalPlan",
881
+ data: {
882
+ id: generateId(),
883
+ mercadoPagoPlanId: mpPlan.id,
884
+ reason,
885
+ frequency: autoRecurring.frequency,
886
+ frequencyType: autoRecurring.frequencyType,
887
+ transactionAmount: autoRecurring.transactionAmount,
888
+ currencyId: autoRecurring.currencyId,
889
+ repetitions: repetitions || null,
890
+ freeTrial: autoRecurring.freeTrial ? JSON.stringify(autoRecurring.freeTrial) : null,
891
+ metadata: JSON.stringify(metadata || {}),
892
+ createdAt: /* @__PURE__ */ new Date(),
893
+ updatedAt: /* @__PURE__ */ new Date()
894
+ }
895
+ });
896
+ return ctx.json({ plan });
897
+ }
898
+ ),
899
+ // List all preapproval plans
900
+ listPreapprovalPlans: createAuthEndpoint(
901
+ "/mercado-pago/plans",
902
+ {
903
+ method: "GET"
904
+ },
905
+ async (ctx) => {
906
+ const plans = await ctx.context.adapter.findMany({
907
+ model: "mercadoPagoPreapprovalPlan"
908
+ });
909
+ return ctx.json({ plans });
910
+ }
911
+ ),
912
+ // Create payment preference
913
+ createPayment: createAuthEndpoint(
914
+ "/mercado-pago/payment/create",
915
+ {
916
+ method: "POST",
917
+ requireAuth: true,
918
+ body: z.object({
919
+ items: z.array(
920
+ z.object({
921
+ id: z.string(),
922
+ title: z.string().min(1).max(256),
923
+ quantity: z.number().int().min(1).max(1e4),
924
+ unitPrice: z.number().positive().max(999999999),
925
+ currencyId: z.string().default("ARS")
926
+ })
927
+ ).min(1).max(100),
928
+ metadata: z.record(z.any()).optional(),
929
+ marketplace: z.object({
930
+ collectorId: z.string(),
931
+ applicationFee: z.number().positive().optional(),
932
+ applicationFeePercentage: z.number().min(0).max(100).optional()
933
+ }).optional(),
934
+ successUrl: z.string().url().optional(),
935
+ failureUrl: z.string().url().optional(),
936
+ pendingUrl: z.string().url().optional(),
937
+ idempotencyKey: z.string().optional()
938
+ })
939
+ },
940
+ async (ctx) => {
941
+ const session = ctx.context.session;
942
+ if (!session) {
943
+ throw new APIError2("UNAUTHORIZED");
944
+ }
945
+ const rateLimitKey = `payment:create:${session.user.id}`;
946
+ if (!rateLimiter.check(rateLimitKey, 10, 60 * 1e3)) {
947
+ throw new APIError2("TOO_MANY_REQUESTS", {
948
+ message: "Too many payment creation attempts. Please try again later."
949
+ });
950
+ }
951
+ const {
952
+ items,
953
+ metadata,
954
+ marketplace,
955
+ successUrl,
956
+ failureUrl,
957
+ pendingUrl,
958
+ idempotencyKey
959
+ } = ctx.body;
960
+ if (idempotencyKey) {
961
+ if (!validateIdempotencyKey(idempotencyKey)) {
962
+ throw new APIError2("BAD_REQUEST", {
963
+ message: "Invalid idempotency key format"
964
+ });
965
+ }
966
+ const cachedResult = idempotencyStore.get(idempotencyKey);
967
+ if (cachedResult) {
968
+ return ctx.json(cachedResult);
969
+ }
970
+ }
971
+ if (options.trustedOrigins) {
972
+ const urls = [successUrl, failureUrl, pendingUrl].filter(
973
+ Boolean
974
+ );
975
+ for (const url of urls) {
976
+ if (!validateCallbackUrl(url, options.trustedOrigins)) {
977
+ throw new APIError2("FORBIDDEN", {
978
+ message: `URL ${url} is not in trusted origins`
979
+ });
980
+ }
981
+ }
982
+ }
983
+ if (items.some((item) => !ValidationRules.currency(item.currencyId))) {
984
+ throw new APIError2("BAD_REQUEST", {
985
+ message: "Invalid currency code"
986
+ });
987
+ }
988
+ const sanitizedMetadata = metadata ? sanitizeMetadata(metadata) : {};
989
+ let customer = await ctx.context.adapter.findOne({
990
+ model: "mercadoPagoCustomer",
991
+ where: [{ field: "userId", value: session.user.id }]
992
+ });
993
+ if (!customer) {
994
+ try {
995
+ const mpCustomer = await customerClient.create({
996
+ body: { email: session.user.email }
997
+ });
998
+ customer = await ctx.context.adapter.create({
999
+ model: "mercadoPagoCustomer",
1000
+ data: {
1001
+ id: generateId(),
1002
+ userId: session.user.id,
1003
+ mercadoPagoId: mpCustomer.id,
1004
+ email: session.user.email,
1005
+ createdAt: /* @__PURE__ */ new Date(),
1006
+ updatedAt: /* @__PURE__ */ new Date()
1007
+ }
1008
+ });
1009
+ } catch (error) {
1010
+ handleMercadoPagoError(error);
1011
+ }
1012
+ }
1013
+ const baseUrl = options.baseUrl || ctx.context.baseURL;
1014
+ const totalAmount = items.reduce(
1015
+ (sum, item) => sum + item.unitPrice * item.quantity,
1016
+ 0
1017
+ );
1018
+ if (!ValidationRules.amount(totalAmount)) {
1019
+ throw new APIError2("BAD_REQUEST", {
1020
+ message: "Invalid payment amount"
1021
+ });
1022
+ }
1023
+ let applicationFeeAmount = 0;
1024
+ if (marketplace) {
1025
+ if (marketplace.applicationFee) {
1026
+ applicationFeeAmount = marketplace.applicationFee;
1027
+ } else if (marketplace.applicationFeePercentage) {
1028
+ applicationFeeAmount = totalAmount * marketplace.applicationFeePercentage / 100;
1029
+ }
1030
+ if (applicationFeeAmount >= totalAmount) {
1031
+ throw new APIError2("BAD_REQUEST", {
1032
+ message: "Application fee cannot exceed total amount"
1033
+ });
1034
+ }
1035
+ }
1036
+ const preferenceBody = {
1037
+ items: items.map((item) => ({
1038
+ id: item.id,
1039
+ title: item.title,
1040
+ quantity: item.quantity,
1041
+ unit_price: item.unitPrice,
1042
+ currency_id: item.currencyId
1043
+ })),
1044
+ payer: {
1045
+ email: session.user.email
1046
+ },
1047
+ back_urls: {
1048
+ success: successUrl || `${baseUrl}/payment/success`,
1049
+ failure: failureUrl || `${baseUrl}/payment/failure`,
1050
+ pending: pendingUrl || `${baseUrl}/payment/pending`
1051
+ },
1052
+ notification_url: `${baseUrl}/api/auth/mercado-pago/webhook`,
1053
+ metadata: {
1054
+ ...sanitizedMetadata,
1055
+ userId: session.user.id,
1056
+ customerId: customer?.id
1057
+ },
1058
+ expires: true,
1059
+ expiration_date_from: (/* @__PURE__ */ new Date()).toISOString(),
1060
+ expiration_date_to: new Date(
1061
+ Date.now() + 30 * 24 * 60 * 60 * 1e3
1062
+ ).toISOString()
1063
+ // 30 days
1064
+ };
1065
+ if (marketplace) {
1066
+ preferenceBody.marketplace = marketplace.collectorId;
1067
+ preferenceBody.marketplace_fee = applicationFeeAmount;
1068
+ }
1069
+ let preference;
1070
+ try {
1071
+ preference = await preferenceClient.create({
1072
+ body: preferenceBody
1073
+ });
1074
+ } catch (error) {
1075
+ handleMercadoPagoError(error);
1076
+ }
1077
+ const payment = await ctx.context.adapter.create({
1078
+ model: "mercadoPagoPayment",
1079
+ data: {
1080
+ id: generateId(),
1081
+ userId: session.user.id,
1082
+ mercadoPagoPaymentId: preference.id,
1083
+ preferenceId: preference.id,
1084
+ status: "pending",
1085
+ amount: totalAmount,
1086
+ currency: items[0].currencyId,
1087
+ metadata: JSON.stringify(sanitizedMetadata),
1088
+ createdAt: /* @__PURE__ */ new Date(),
1089
+ updatedAt: /* @__PURE__ */ new Date()
1090
+ }
1091
+ });
1092
+ if (marketplace) {
1093
+ await ctx.context.adapter.create({
1094
+ model: "mercadoPagoMarketplaceSplit",
1095
+ data: {
1096
+ id: generateId(),
1097
+ paymentId: payment.id,
1098
+ collectorId: marketplace.collectorId,
1099
+ collectorEmail: "",
1100
+ // Will be updated via webhook
1101
+ applicationFeeAmount,
1102
+ applicationFeePercentage: marketplace.applicationFeePercentage,
1103
+ netAmount: totalAmount - applicationFeeAmount,
1104
+ metadata: JSON.stringify({}),
1105
+ createdAt: /* @__PURE__ */ new Date()
1106
+ }
1107
+ });
1108
+ }
1109
+ const result = {
1110
+ checkoutUrl: preference.init_point,
1111
+ preferenceId: preference.id,
1112
+ payment
1113
+ };
1114
+ if (idempotencyKey) {
1115
+ idempotencyStore.set(idempotencyKey, result);
1116
+ }
1117
+ return ctx.json(result);
1118
+ }
1119
+ ),
1120
+ // Create subscription (supports both with and without preapproval plan)
1121
+ createSubscription: createAuthEndpoint(
1122
+ "/mercado-pago/subscription/create",
1123
+ {
1124
+ method: "POST",
1125
+ requireAuth: true,
1126
+ body: z.object({
1127
+ // Option 1: Use existing preapproval plan
1128
+ preapprovalPlanId: z.string().optional(),
1129
+ // Option 2: Create subscription directly without plan
1130
+ reason: z.string().optional(),
1131
+ // Description of subscription
1132
+ autoRecurring: z.object({
1133
+ frequency: z.number(),
1134
+ // 1 for monthly
1135
+ frequencyType: z.enum(["days", "months"]),
1136
+ transactionAmount: z.number(),
1137
+ currencyId: z.string().default("ARS"),
1138
+ startDate: z.string().optional(),
1139
+ // ISO date
1140
+ endDate: z.string().optional(),
1141
+ // ISO date
1142
+ freeTrial: z.object({
1143
+ frequency: z.number(),
1144
+ frequencyType: z.enum(["days", "months"])
1145
+ }).optional()
1146
+ }).optional(),
1147
+ backUrl: z.string().optional(),
1148
+ metadata: z.record(z.any()).optional()
1149
+ })
1150
+ },
1151
+ async (ctx) => {
1152
+ const session = ctx.context.session;
1153
+ if (!session) {
1154
+ throw new APIError2("UNAUTHORIZED");
1155
+ }
1156
+ const {
1157
+ preapprovalPlanId,
1158
+ reason,
1159
+ autoRecurring,
1160
+ backUrl,
1161
+ metadata
1162
+ } = ctx.body;
1163
+ if (!preapprovalPlanId) {
1164
+ if (!reason || !autoRecurring) {
1165
+ throw new APIError2("BAD_REQUEST", {
1166
+ message: "Must provide either preapprovalPlanId or (reason + autoRecurring)"
1167
+ });
1168
+ }
1169
+ }
1170
+ let customer = await ctx.context.adapter.findOne({
1171
+ model: "mercadoPagoCustomer",
1172
+ where: [{ field: "userId", value: session.user.id }]
1173
+ });
1174
+ if (!customer) {
1175
+ const mpCustomer = await customerClient.create({
1176
+ body: { email: session.user.email }
1177
+ });
1178
+ customer = await ctx.context.adapter.create({
1179
+ model: "mercadoPagoCustomer",
1180
+ data: {
1181
+ id: generateId(),
1182
+ userId: session.user.id,
1183
+ mercadoPagoId: mpCustomer.id,
1184
+ email: session.user.email,
1185
+ createdAt: /* @__PURE__ */ new Date(),
1186
+ updatedAt: /* @__PURE__ */ new Date()
1187
+ }
1188
+ });
1189
+ }
1190
+ const baseUrl = options.baseUrl || ctx.context.baseURL;
1191
+ const subscriptionId = generateId();
1192
+ let preapproval;
1193
+ if (preapprovalPlanId) {
1194
+ preapproval = await preApprovalClient.create({
1195
+ body: {
1196
+ preapproval_plan_id: preapprovalPlanId,
1197
+ payer_email: session.user.email,
1198
+ card_token_id: void 0,
1199
+ // Will be provided in checkout
1200
+ back_url: backUrl || `${baseUrl}/subscription/success`,
1201
+ status: "pending",
1202
+ external_reference: subscriptionId
1203
+ }
1204
+ });
1205
+ } else if (autoRecurring) {
1206
+ const ar = autoRecurring;
1207
+ const autoRecurringBody = {
1208
+ frequency: ar.frequency,
1209
+ frequency_type: ar.frequencyType,
1210
+ transaction_amount: ar.transactionAmount,
1211
+ currency_id: ar.currencyId
1212
+ };
1213
+ if (ar.startDate) {
1214
+ autoRecurringBody.start_date = ar.startDate;
1215
+ }
1216
+ if (ar.endDate) {
1217
+ autoRecurringBody.end_date = ar.endDate;
1218
+ }
1219
+ if (ar.freeTrial) {
1220
+ autoRecurringBody.free_trial = {
1221
+ frequency: ar.freeTrial.frequency,
1222
+ frequency_type: ar.freeTrial.frequencyType
1223
+ };
1224
+ }
1225
+ preapproval = await preApprovalClient.create({
1226
+ body: {
1227
+ reason,
1228
+ auto_recurring: autoRecurringBody,
1229
+ payer_email: session.user.email,
1230
+ back_url: backUrl || `${baseUrl}/subscription/success`,
1231
+ status: "pending",
1232
+ external_reference: subscriptionId
1233
+ }
1234
+ });
1235
+ }
1236
+ if (!preapproval) {
1237
+ throw new APIError2("BAD_REQUEST", {
1238
+ message: "Failed to create subscription"
1239
+ });
1240
+ }
1241
+ const subscription = await ctx.context.adapter.create({
1242
+ model: "mercadoPagoSubscription",
1243
+ data: {
1244
+ id: subscriptionId,
1245
+ userId: session.user.id,
1246
+ mercadoPagoSubscriptionId: preapproval.id,
1247
+ planId: preapprovalPlanId || reason || "direct",
1248
+ status: "pending",
1249
+ metadata: JSON.stringify(metadata || {}),
1250
+ createdAt: /* @__PURE__ */ new Date(),
1251
+ updatedAt: /* @__PURE__ */ new Date()
1252
+ }
1253
+ });
1254
+ return ctx.json({
1255
+ checkoutUrl: preapproval.init_point,
1256
+ subscription
1257
+ });
1258
+ }
1259
+ ),
1260
+ // Cancel subscription
1261
+ cancelSubscription: createAuthEndpoint(
1262
+ "/mercado-pago/subscription/cancel",
1263
+ {
1264
+ method: "POST",
1265
+ requireAuth: true,
1266
+ body: z.object({
1267
+ subscriptionId: z.string()
1268
+ })
1269
+ },
1270
+ async (ctx) => {
1271
+ const session = ctx.context.session;
1272
+ if (!session) {
1273
+ throw new APIError2("UNAUTHORIZED");
1274
+ }
1275
+ const { subscriptionId } = ctx.body;
1276
+ const subscription = await ctx.context.adapter.findOne({
1277
+ model: "mercadoPagoSubscription",
1278
+ where: [
1279
+ { field: "id", value: subscriptionId },
1280
+ { field: "userId", value: session.user.id }
1281
+ ]
1282
+ });
1283
+ if (!subscription) {
1284
+ throw new APIError2("NOT_FOUND", {
1285
+ message: "Subscription not found"
1286
+ });
1287
+ }
1288
+ await preApprovalClient.update({
1289
+ id: subscription.mercadoPagoSubscriptionId,
1290
+ body: { status: "cancelled" }
1291
+ });
1292
+ await ctx.context.adapter.update({
1293
+ model: "mercadoPagoSubscription",
1294
+ where: [{ field: "id", value: subscriptionId }],
1295
+ update: {
1296
+ status: "cancelled",
1297
+ updatedAt: /* @__PURE__ */ new Date()
1298
+ }
1299
+ });
1300
+ return ctx.json({ success: true });
1301
+ }
1302
+ ),
1303
+ // Get payment status
1304
+ getPayment: createAuthEndpoint(
1305
+ "/mercado-pago/payment/:id",
1306
+ {
1307
+ method: "GET",
1308
+ requireAuth: true
1309
+ },
1310
+ async (ctx) => {
1311
+ const paymentId = ctx.params.id;
1312
+ const session = ctx.context.session;
1313
+ if (!session) {
1314
+ throw new APIError2("UNAUTHORIZED");
1315
+ }
1316
+ const payment = await ctx.context.adapter.findOne({
1317
+ model: "mercadoPagoPayment",
1318
+ where: [
1319
+ { field: "id", value: paymentId },
1320
+ { field: "userId", value: session.user.id }
1321
+ ]
1322
+ });
1323
+ if (!payment) {
1324
+ throw new APIError2("NOT_FOUND", {
1325
+ message: "Payment not found"
1326
+ });
1327
+ }
1328
+ return ctx.json({ payment });
1329
+ }
1330
+ ),
1331
+ // List user payments
1332
+ listPayments: createAuthEndpoint(
1333
+ "/mercado-pago/payments",
1334
+ {
1335
+ method: "GET",
1336
+ requireAuth: true,
1337
+ query: z.object({
1338
+ limit: z.coerce.number().optional().default(10),
1339
+ offset: z.coerce.number().optional().default(0)
1340
+ })
1341
+ },
1342
+ async (ctx) => {
1343
+ const session = ctx.context.session;
1344
+ const { limit, offset } = ctx.query;
1345
+ if (!session) {
1346
+ throw new APIError2("UNAUTHORIZED");
1347
+ }
1348
+ const payments = await ctx.context.adapter.findMany({
1349
+ model: "mercadoPagoPayment",
1350
+ where: [{ field: "userId", value: session.user.id }],
1351
+ limit,
1352
+ offset
1353
+ });
1354
+ return ctx.json({ payments });
1355
+ }
1356
+ ),
1357
+ // List user subscriptions
1358
+ listSubscriptions: createAuthEndpoint(
1359
+ "/mercado-pago/subscriptions",
1360
+ {
1361
+ method: "GET",
1362
+ requireAuth: true
1363
+ },
1364
+ async (ctx) => {
1365
+ const session = ctx.context.session;
1366
+ if (!session) {
1367
+ throw new APIError2("UNAUTHORIZED");
1368
+ }
1369
+ const subscriptions = await ctx.context.adapter.findMany({
1370
+ model: "mercadoPagoSubscription",
1371
+ where: [{ field: "userId", value: session.user.id }]
1372
+ });
1373
+ return ctx.json({ subscriptions });
1374
+ }
1375
+ ),
1376
+ // Webhook handler
1377
+ webhook: createAuthEndpoint(
1378
+ "/mercado-pago/webhook",
1379
+ {
1380
+ method: "POST"
1381
+ },
1382
+ async (ctx) => {
1383
+ const webhookRateLimitKey = "webhook:global";
1384
+ if (!rateLimiter.check(webhookRateLimitKey, 1e3, 60 * 1e3)) {
1385
+ throw new APIError2("TOO_MANY_REQUESTS", {
1386
+ message: "Webhook rate limit exceeded"
1387
+ });
1388
+ }
1389
+ let notification;
1390
+ try {
1391
+ notification = ctx.body;
1392
+ } catch {
1393
+ throw new APIError2("BAD_REQUEST", {
1394
+ message: "Invalid JSON payload"
1395
+ });
1396
+ }
1397
+ if (!notification.type || !isValidWebhookTopic(notification.type) || !notification.data?.id) {
1398
+ ctx.context.logger.warn("Invalid webhook topic received", {
1399
+ type: notification.type
1400
+ });
1401
+ return ctx.json({ received: true });
1402
+ }
1403
+ if (!ctx.request) {
1404
+ throw new APIError2("BAD_REQUEST", {
1405
+ message: "Missing request"
1406
+ });
1407
+ }
1408
+ if (options.webhookSecret) {
1409
+ const xSignature = ctx.request.headers.get("x-signature");
1410
+ const xRequestId = ctx.request.headers.get("x-request-id");
1411
+ const dataId = notification.data?.id?.toString();
1412
+ if (!dataId) {
1413
+ throw new APIError2("BAD_REQUEST", {
1414
+ message: "Missing data.id in webhook payload"
1415
+ });
1416
+ }
1417
+ const isValid = verifyWebhookSignature({
1418
+ xSignature,
1419
+ xRequestId,
1420
+ dataId,
1421
+ secret: options.webhookSecret
1422
+ });
1423
+ if (!isValid) {
1424
+ ctx.context.logger.error("Invalid webhook signature", {
1425
+ xSignature,
1426
+ xRequestId,
1427
+ dataId
1428
+ });
1429
+ throw new APIError2("UNAUTHORIZED", {
1430
+ message: "Invalid webhook signature"
1431
+ });
1432
+ }
1433
+ }
1434
+ const webhookId = `webhook:${notification.data?.id}:${notification.type}`;
1435
+ const alreadyProcessed = idempotencyStore.get(webhookId);
1436
+ if (alreadyProcessed) {
1437
+ ctx.context.logger.info("Webhook already processed", { webhookId });
1438
+ return ctx.json({ received: true });
1439
+ }
1440
+ idempotencyStore.set(webhookId, true, 24 * 60 * 60 * 1e3);
1441
+ try {
1442
+ if (notification.type === "payment") {
1443
+ const paymentId = notification.data.id;
1444
+ if (!paymentId) {
1445
+ throw new APIError2("BAD_REQUEST", {
1446
+ message: "Missing payment ID"
1447
+ });
1448
+ }
1449
+ let mpPayment;
1450
+ try {
1451
+ mpPayment = await paymentClient.get({
1452
+ id: paymentId
1453
+ });
1454
+ } catch (error) {
1455
+ ctx.context.logger.error("Failed to fetch payment from MP", {
1456
+ paymentId,
1457
+ error
1458
+ });
1459
+ throw new APIError2("BAD_REQUEST", {
1460
+ message: "Failed to fetch payment details"
1461
+ });
1462
+ }
1463
+ const existingPayment = await ctx.context.adapter.findOne({
1464
+ model: "mercadoPagoPayment",
1465
+ where: [
1466
+ {
1467
+ field: "mercadoPagoPaymentId",
1468
+ value: paymentId.toString()
1469
+ }
1470
+ ]
1471
+ });
1472
+ if (existingPayment) {
1473
+ if (!validatePaymentAmount(
1474
+ existingPayment.amount,
1475
+ mpPayment.transaction_amount || 0
1476
+ )) {
1477
+ ctx.context.logger.error("Payment amount mismatch", {
1478
+ expected: existingPayment.amount,
1479
+ received: mpPayment.transaction_amount
1480
+ });
1481
+ throw new APIError2("BAD_REQUEST", {
1482
+ message: "Payment amount mismatch"
1483
+ });
1484
+ }
1485
+ await ctx.context.adapter.update({
1486
+ model: "mercadoPagoPayment",
1487
+ where: [{ field: "id", value: existingPayment.id }],
1488
+ update: {
1489
+ status: mpPayment.status,
1490
+ statusDetail: mpPayment.status_detail || void 0,
1491
+ paymentMethodId: mpPayment.payment_method_id || void 0,
1492
+ paymentTypeId: mpPayment.payment_type_id || void 0,
1493
+ updatedAt: /* @__PURE__ */ new Date()
1494
+ }
1495
+ });
1496
+ if (options.onPaymentUpdate) {
1497
+ try {
1498
+ await options.onPaymentUpdate({
1499
+ payment: existingPayment,
1500
+ status: mpPayment.status,
1501
+ statusDetail: mpPayment.status_detail || "",
1502
+ mpPayment
1503
+ });
1504
+ } catch (error) {
1505
+ ctx.context.logger.error(
1506
+ "Error in onPaymentUpdate callback",
1507
+ { error }
1508
+ );
1509
+ }
1510
+ }
1511
+ }
1512
+ }
1513
+ if (notification.type === "subscription_preapproval" || notification.type === "subscription_preapproval_plan") {
1514
+ const preapprovalId = notification.data.id;
1515
+ if (!preapprovalId) {
1516
+ throw new APIError2("BAD_REQUEST", {
1517
+ message: "Missing preapproval ID"
1518
+ });
1519
+ }
1520
+ let mpPreapproval;
1521
+ try {
1522
+ mpPreapproval = await preApprovalClient.get({
1523
+ id: preapprovalId
1524
+ });
1525
+ } catch (error) {
1526
+ ctx.context.logger.error(
1527
+ "Failed to fetch preapproval from MP",
1528
+ { preapprovalId, error }
1529
+ );
1530
+ throw new APIError2("BAD_REQUEST", {
1531
+ message: "Failed to fetch subscription details"
1532
+ });
1533
+ }
1534
+ const existingSubscription = await ctx.context.adapter.findOne({
1535
+ model: "mercadoPagoSubscription",
1536
+ where: [
1537
+ {
1538
+ field: "mercadoPagoSubscriptionId",
1539
+ value: preapprovalId
1540
+ }
1541
+ ]
1542
+ });
1543
+ if (existingSubscription) {
1544
+ await ctx.context.adapter.update({
1545
+ model: "mercadoPagoSubscription",
1546
+ where: [{ field: "id", value: existingSubscription.id }],
1547
+ update: {
1548
+ status: mpPreapproval.status,
1549
+ reason: mpPreapproval.reason || void 0,
1550
+ nextPaymentDate: mpPreapproval.next_payment_date ? new Date(mpPreapproval.next_payment_date) : void 0,
1551
+ lastPaymentDate: mpPreapproval.last_modified ? new Date(mpPreapproval.last_modified) : void 0,
1552
+ summarized: mpPreapproval.summarized ? JSON.stringify(mpPreapproval.summarized) : void 0,
1553
+ updatedAt: /* @__PURE__ */ new Date()
1554
+ }
1555
+ });
1556
+ if (options.onSubscriptionUpdate) {
1557
+ try {
1558
+ await options.onSubscriptionUpdate({
1559
+ subscription: existingSubscription,
1560
+ status: mpPreapproval.status,
1561
+ reason: mpPreapproval.reason || "",
1562
+ mpPreapproval
1563
+ });
1564
+ } catch (error) {
1565
+ ctx.context.logger.error(
1566
+ "Error in onSubscriptionUpdate callback",
1567
+ { error }
1568
+ );
1569
+ }
1570
+ }
1571
+ }
1572
+ }
1573
+ if (notification.type === "subscription_authorized_payment" || notification.type === "authorized_payment") {
1574
+ const paymentId = notification.data.id;
1575
+ if (!paymentId) {
1576
+ throw new APIError2("BAD_REQUEST", {
1577
+ message: "Missing payment ID"
1578
+ });
1579
+ }
1580
+ let mpPayment;
1581
+ try {
1582
+ mpPayment = await paymentClient.get({
1583
+ id: paymentId
1584
+ });
1585
+ } catch (error) {
1586
+ ctx.context.logger.error(
1587
+ "Failed to fetch authorized payment from MP",
1588
+ { paymentId, error }
1589
+ );
1590
+ throw new APIError2("BAD_REQUEST", {
1591
+ message: "Failed to fetch payment details"
1592
+ });
1593
+ }
1594
+ if (mpPayment.external_reference) {
1595
+ const subscription = await ctx.context.adapter.findOne({
1596
+ model: "mercadoPagoSubscription",
1597
+ where: [
1598
+ {
1599
+ field: "id",
1600
+ // External reference holds the local subscription ID
1601
+ value: mpPayment.external_reference
1602
+ }
1603
+ ]
1604
+ });
1605
+ if (subscription) {
1606
+ if (options.onSubscriptionPayment) {
1607
+ try {
1608
+ await options.onSubscriptionPayment({
1609
+ subscription,
1610
+ // In a real app, we should map this properly or align types
1611
+ payment: mpPayment,
1612
+ status: mpPayment.status
1613
+ });
1614
+ } catch (error) {
1615
+ ctx.context.logger.error(
1616
+ "Error in onSubscriptionPayment callback",
1617
+ { error }
1618
+ );
1619
+ }
1620
+ }
1621
+ } else {
1622
+ ctx.context.logger.warn(
1623
+ "Subscription not found for authorized payment",
1624
+ {
1625
+ paymentId,
1626
+ externalReference: mpPayment.external_reference
1627
+ }
1628
+ );
1629
+ }
1630
+ }
1631
+ }
1632
+ } catch (error) {
1633
+ ctx.context.logger.error("Error processing webhook", {
1634
+ error,
1635
+ notification
1636
+ });
1637
+ if (error instanceof APIError2) {
1638
+ throw error;
1639
+ }
1640
+ }
1641
+ return ctx.json({ received: true });
1642
+ }
1643
+ )
1644
+ },
1645
+ // Add trusted origins from options
1646
+ ...options.trustedOrigins && { trustedOrigins: options.trustedOrigins }
1647
+ };
1648
+ };
1649
+ export {
1650
+ mercadoPagoClient,
1651
+ mercadoPagoPlugin
1652
+ };
1653
+ //# sourceMappingURL=index.mjs.map