better-auth-mercadopago 0.1.1 → 0.1.4

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