better-auth-mercadopago 0.1.7 → 0.1.9

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