@mixrpay/merchant-sdk 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,217 +1,3 @@
1
- // src/verify.ts
2
- import { recoverTypedDataAddress } from "viem";
3
-
4
- // src/utils.ts
5
- var USDC_CONTRACTS = {
6
- 8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
7
- // Base Mainnet
8
- 84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
9
- // Base Sepolia
10
- };
11
- var DEFAULT_FACILITATOR = "https://x402.org/facilitator";
12
- function getUSDCDomain(chainId) {
13
- const verifyingContract = USDC_CONTRACTS[chainId];
14
- if (!verifyingContract) {
15
- throw new Error(`Unsupported chain ID: ${chainId}. Supported: ${Object.keys(USDC_CONTRACTS).join(", ")}`);
16
- }
17
- return {
18
- name: "USD Coin",
19
- version: "2",
20
- chainId,
21
- verifyingContract
22
- };
23
- }
24
- var TRANSFER_WITH_AUTHORIZATION_TYPES = {
25
- TransferWithAuthorization: [
26
- { name: "from", type: "address" },
27
- { name: "to", type: "address" },
28
- { name: "value", type: "uint256" },
29
- { name: "validAfter", type: "uint256" },
30
- { name: "validBefore", type: "uint256" },
31
- { name: "nonce", type: "bytes32" }
32
- ]
33
- };
34
- function usdToMinor(usd) {
35
- return BigInt(Math.round(usd * 1e6));
36
- }
37
- function minorToUsd(minor) {
38
- return Number(minor) / 1e6;
39
- }
40
- function generateNonce() {
41
- const bytes = new Uint8Array(32);
42
- crypto.getRandomValues(bytes);
43
- return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
44
- }
45
- function isValidAddress(address) {
46
- return /^0x[a-fA-F0-9]{40}$/.test(address);
47
- }
48
- function normalizeAddress(address) {
49
- if (!isValidAddress(address)) {
50
- throw new Error(`Invalid address: ${address}`);
51
- }
52
- return address.toLowerCase();
53
- }
54
- function base64Decode(str) {
55
- if (typeof Buffer !== "undefined") {
56
- return Buffer.from(str, "base64").toString("utf-8");
57
- }
58
- return atob(str);
59
- }
60
-
61
- // src/verify.ts
62
- async function verifyX402Payment(paymentHeader, options) {
63
- try {
64
- let decoded;
65
- try {
66
- const jsonStr = base64Decode(paymentHeader);
67
- decoded = JSON.parse(jsonStr);
68
- } catch (e) {
69
- return { valid: false, error: "Invalid payment header encoding" };
70
- }
71
- if (!decoded.payload?.authorization || !decoded.payload?.signature) {
72
- return { valid: false, error: "Missing authorization or signature in payment" };
73
- }
74
- const { authorization, signature } = decoded.payload;
75
- const chainId = options.chainId || 8453;
76
- const message = {
77
- from: authorization.from,
78
- to: authorization.to,
79
- value: BigInt(authorization.value),
80
- validAfter: BigInt(authorization.validAfter),
81
- validBefore: BigInt(authorization.validBefore),
82
- nonce: authorization.nonce
83
- };
84
- const domain = getUSDCDomain(chainId);
85
- let signerAddress;
86
- try {
87
- signerAddress = await recoverTypedDataAddress({
88
- domain,
89
- types: TRANSFER_WITH_AUTHORIZATION_TYPES,
90
- primaryType: "TransferWithAuthorization",
91
- message,
92
- signature
93
- });
94
- } catch (e) {
95
- return { valid: false, error: "Failed to recover signer from signature" };
96
- }
97
- if (signerAddress.toLowerCase() !== authorization.from.toLowerCase()) {
98
- return {
99
- valid: false,
100
- error: `Signature mismatch: expected ${authorization.from}, got ${signerAddress}`
101
- };
102
- }
103
- const paymentAmount = BigInt(authorization.value);
104
- if (paymentAmount < options.expectedAmount) {
105
- return {
106
- valid: false,
107
- error: `Insufficient payment: expected ${options.expectedAmount}, got ${paymentAmount}`
108
- };
109
- }
110
- if (authorization.to.toLowerCase() !== options.expectedRecipient.toLowerCase()) {
111
- return {
112
- valid: false,
113
- error: `Wrong recipient: expected ${options.expectedRecipient}, got ${authorization.to}`
114
- };
115
- }
116
- const now = Math.floor(Date.now() / 1e3);
117
- if (Number(authorization.validBefore) < now) {
118
- return { valid: false, error: "Payment authorization has expired" };
119
- }
120
- if (Number(authorization.validAfter) > now) {
121
- return { valid: false, error: "Payment authorization is not yet valid" };
122
- }
123
- let txHash;
124
- let settledAt;
125
- if (!options.skipSettlement) {
126
- const facilitatorUrl = options.facilitator || DEFAULT_FACILITATOR;
127
- try {
128
- const settlementResponse = await fetch(`${facilitatorUrl}/settle`, {
129
- method: "POST",
130
- headers: { "Content-Type": "application/json" },
131
- body: JSON.stringify({
132
- authorization,
133
- signature,
134
- chainId
135
- })
136
- });
137
- if (!settlementResponse.ok) {
138
- const errorBody = await settlementResponse.text();
139
- let errorMessage = "Settlement failed";
140
- try {
141
- const errorJson = JSON.parse(errorBody);
142
- errorMessage = errorJson.message || errorJson.error || errorMessage;
143
- } catch {
144
- errorMessage = errorBody || errorMessage;
145
- }
146
- return { valid: false, error: `Settlement failed: ${errorMessage}` };
147
- }
148
- const settlement = await settlementResponse.json();
149
- txHash = settlement.txHash || settlement.tx_hash;
150
- settledAt = /* @__PURE__ */ new Date();
151
- } catch (e) {
152
- return {
153
- valid: false,
154
- error: `Settlement request failed: ${e.message}`
155
- };
156
- }
157
- }
158
- return {
159
- valid: true,
160
- payer: authorization.from,
161
- amount: minorToUsd(paymentAmount),
162
- amountMinor: paymentAmount,
163
- txHash,
164
- settledAt: settledAt || /* @__PURE__ */ new Date(),
165
- nonce: authorization.nonce
166
- };
167
- } catch (error) {
168
- return {
169
- valid: false,
170
- error: `Verification error: ${error.message}`
171
- };
172
- }
173
- }
174
- async function parseX402Payment(paymentHeader, chainId = 8453) {
175
- try {
176
- const jsonStr = base64Decode(paymentHeader);
177
- const decoded = JSON.parse(jsonStr);
178
- if (!decoded.payload?.authorization) {
179
- return { valid: false, error: "Missing authorization in payment" };
180
- }
181
- const { authorization, signature } = decoded.payload;
182
- const domain = getUSDCDomain(chainId);
183
- const message = {
184
- from: authorization.from,
185
- to: authorization.to,
186
- value: BigInt(authorization.value),
187
- validAfter: BigInt(authorization.validAfter),
188
- validBefore: BigInt(authorization.validBefore),
189
- nonce: authorization.nonce
190
- };
191
- const signerAddress = await recoverTypedDataAddress({
192
- domain,
193
- types: TRANSFER_WITH_AUTHORIZATION_TYPES,
194
- primaryType: "TransferWithAuthorization",
195
- message,
196
- signature
197
- });
198
- if (signerAddress.toLowerCase() !== authorization.from.toLowerCase()) {
199
- return { valid: false, error: "Signature mismatch" };
200
- }
201
- const amountMinor = BigInt(authorization.value);
202
- return {
203
- valid: true,
204
- payer: authorization.from,
205
- recipient: authorization.to,
206
- amount: minorToUsd(amountMinor),
207
- amountMinor,
208
- expiresAt: new Date(Number(authorization.validBefore) * 1e3)
209
- };
210
- } catch (error) {
211
- return { valid: false, error: `Parse error: ${error.message}` };
212
- }
213
- }
214
-
215
1
  // src/receipt.ts
216
2
  import * as jose from "jose";
217
3
  var DEFAULT_JWKS_URL = process.env.MIXRPAY_JWKS_URL || "https://mixrpay.com/.well-known/jwks";
@@ -280,1057 +66,18 @@ async function verifyPaymentReceipt(receipt, options) {
280
66
  throw error;
281
67
  }
282
68
  }
283
- function parsePaymentReceipt(receipt) {
284
- const decoded = jose.decodeJwt(receipt);
285
- return {
286
- paymentId: decoded.paymentId,
287
- amount: decoded.amount,
288
- amountUsd: decoded.amountUsd,
289
- payer: decoded.payer,
290
- recipient: decoded.recipient,
291
- chainId: decoded.chainId,
292
- txHash: decoded.txHash,
293
- settledAt: decoded.settledAt,
294
- issuedAt: decoded.iat ? new Date(decoded.iat * 1e3) : void 0,
295
- expiresAt: decoded.exp ? new Date(decoded.exp * 1e3) : void 0
296
- };
297
- }
298
- function isReceiptExpired(receipt) {
299
- const parsed = typeof receipt === "string" ? parsePaymentReceipt(receipt) : receipt;
300
- if (!parsed.expiresAt) {
301
- return false;
302
- }
303
- return parsed.expiresAt.getTime() < Date.now();
304
- }
305
- function clearJWKSCache() {
306
- jwksCache.clear();
307
- }
308
- function getDefaultJWKSUrl() {
309
- return DEFAULT_JWKS_URL;
310
- }
311
69
 
312
70
  // src/charges.ts
313
- import crypto2 from "crypto";
314
- var ChargesClient = class {
315
- constructor(options) {
316
- this.apiKey = options.apiKey;
317
- this.baseUrl = options.baseUrl || process.env.MIXRPAY_BASE_URL || "https://mixrpay.com";
318
- }
319
- /**
320
- * Create a new charge.
321
- *
322
- * @example
323
- * ```typescript
324
- * const result = await charges.create({
325
- * sessionId: 'sk_xxx',
326
- * amountUsd: 0.05,
327
- * reference: 'image_gen_123',
328
- * metadata: { feature: 'image_generation' }
329
- * });
330
- *
331
- * if (result.success) {
332
- * console.log(`Charged ${result.charge.amountUsdc}`);
333
- * console.log(`TX: ${result.charge.explorerUrl}`);
334
- * }
335
- * ```
336
- */
337
- async create(params) {
338
- const response = await fetch(`${this.baseUrl}/api/v1/charges`, {
339
- method: "POST",
340
- headers: {
341
- "Content-Type": "application/json",
342
- "Authorization": `Bearer ${this.apiKey}`
343
- },
344
- body: JSON.stringify({
345
- session_id: params.sessionId,
346
- amount_usd: params.amountUsd,
347
- reference: params.reference,
348
- metadata: params.metadata
349
- })
350
- });
351
- const data = await response.json();
352
- if (!response.ok) {
353
- return {
354
- success: false,
355
- error: data.error,
356
- details: data.details
357
- };
358
- }
359
- return {
360
- success: data.success,
361
- charge: data.charge_id ? {
362
- id: data.charge_id,
363
- status: data.status,
364
- amountUsd: data.amount_usd ?? 0,
365
- amountUsdc: data.amount_usdc ?? "0",
366
- txHash: data.tx_hash,
367
- explorerUrl: data.explorer_url,
368
- fromAddress: "",
369
- toAddress: "",
370
- reference: params.reference,
371
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
372
- } : void 0,
373
- idempotentReplay: data.idempotent_replay,
374
- error: data.error
375
- };
376
- }
377
- /**
378
- * Get a charge by ID.
379
- *
380
- * @example
381
- * ```typescript
382
- * const charge = await charges.get('chg_xxx');
383
- * console.log(charge.status); // 'confirmed'
384
- * ```
385
- */
386
- async get(chargeId) {
387
- const response = await fetch(`${this.baseUrl}/api/v1/charges?id=${chargeId}`, {
388
- headers: {
389
- "Authorization": `Bearer ${this.apiKey}`
390
- }
391
- });
392
- if (!response.ok) {
393
- if (response.status === 404) {
394
- return null;
395
- }
396
- throw new Error(`Failed to get charge: ${response.statusText}`);
397
- }
398
- const data = await response.json();
399
- return {
400
- id: data.id,
401
- status: data.status,
402
- amountUsd: data.amount_usd,
403
- amountUsdc: data.amount_usdc,
404
- txHash: data.tx_hash,
405
- explorerUrl: data.explorer_url,
406
- fromAddress: data.from_address,
407
- toAddress: data.to_address,
408
- reference: data.reference,
409
- createdAt: data.created_at,
410
- confirmedAt: data.confirmed_at,
411
- sessionName: data.session_name,
412
- userWallet: data.user_wallet
413
- };
414
- }
415
- /**
416
- * Get a charge by transaction hash.
417
- */
418
- async getByTxHash(txHash) {
419
- const response = await fetch(`${this.baseUrl}/api/v1/charges?tx_hash=${txHash}`, {
420
- headers: {
421
- "Authorization": `Bearer ${this.apiKey}`
422
- }
423
- });
424
- if (!response.ok) {
425
- if (response.status === 404) {
426
- return null;
427
- }
428
- throw new Error(`Failed to get charge: ${response.statusText}`);
429
- }
430
- const data = await response.json();
431
- return {
432
- id: data.id,
433
- status: data.status,
434
- amountUsd: data.amount_usd,
435
- amountUsdc: data.amount_usdc,
436
- txHash: data.tx_hash,
437
- explorerUrl: data.explorer_url,
438
- fromAddress: data.from_address,
439
- toAddress: data.to_address,
440
- reference: data.reference,
441
- createdAt: data.created_at,
442
- confirmedAt: data.confirmed_at,
443
- sessionName: data.session_name,
444
- userWallet: data.user_wallet
445
- };
446
- }
447
- };
71
+ import crypto from "crypto";
448
72
  function verifySessionWebhook(payload, signature, secret) {
449
- const expectedSignature = crypto2.createHmac("sha256", secret).update(payload).digest("hex");
450
- return crypto2.timingSafeEqual(
73
+ const expectedSignature = crypto.createHmac("sha256", secret).update(payload).digest("hex");
74
+ return crypto.timingSafeEqual(
451
75
  Buffer.from(signature),
452
76
  Buffer.from(expectedSignature)
453
77
  );
454
78
  }
455
- function parseSessionGrant(payload) {
456
- if (payload.event !== "session.granted") {
457
- throw new Error(`Unexpected event type: ${payload.event}`);
458
- }
459
- return {
460
- sessionId: payload.session_id,
461
- userWallet: payload.user_wallet,
462
- merchantWallet: payload.merchant_wallet,
463
- limits: {
464
- maxTotalUsd: payload.limits.max_total_usd,
465
- maxPerTxUsd: payload.limits.max_per_tx_usd,
466
- expiresAt: payload.limits.expires_at
467
- },
468
- grantedAt: payload.granted_at
469
- };
470
- }
471
-
472
- // src/receipt-fetcher.ts
473
- var DEFAULT_MIXRPAY_API_URL = process.env.MIXRPAY_BASE_URL || "https://mixrpay.com";
474
- async function fetchPaymentReceipt(params) {
475
- const apiUrl = params.apiUrl || DEFAULT_MIXRPAY_API_URL;
476
- const endpoint = `${apiUrl}/api/v1/receipts`;
477
- try {
478
- const response = await fetch(endpoint, {
479
- method: "POST",
480
- headers: {
481
- "Content-Type": "application/json"
482
- },
483
- body: JSON.stringify({
484
- tx_hash: params.txHash,
485
- payer: params.payer,
486
- recipient: params.recipient,
487
- amount: params.amount.toString(),
488
- chain_id: params.chainId
489
- })
490
- });
491
- if (!response.ok) {
492
- console.warn(`[x402] Receipt fetch failed: ${response.status} ${response.statusText}`);
493
- return null;
494
- }
495
- const data = await response.json();
496
- return data.receipt || null;
497
- } catch (error) {
498
- console.warn("[x402] Receipt fetch error:", error.message);
499
- return null;
500
- }
501
- }
502
-
503
- // src/middleware/express.ts
504
- function x402(options) {
505
- return async (req, res, next) => {
506
- try {
507
- const context = {
508
- path: req.path,
509
- method: req.method,
510
- headers: req.headers,
511
- body: req.body
512
- };
513
- if (options.skip) {
514
- const shouldSkip = await options.skip(context);
515
- if (shouldSkip) {
516
- return next();
517
- }
518
- }
519
- const recipient = options.recipient || process.env.MIXRPAY_MERCHANT_ADDRESS;
520
- if (!recipient) {
521
- console.error("[x402] No recipient address configured. Set MIXRPAY_MERCHANT_ADDRESS env var or pass recipient option.");
522
- return res.status(500).json({
523
- error: "Payment configuration error",
524
- message: "Merchant wallet address not configured"
525
- });
526
- }
527
- const price = options.getPrice ? await options.getPrice(context) : options.price;
528
- const priceMinor = usdToMinor(price);
529
- const chainId = options.chainId || 8453;
530
- const facilitator = options.facilitator || DEFAULT_FACILITATOR;
531
- const paymentHeader = req.headers["x-payment"];
532
- if (!paymentHeader) {
533
- const nonce = generateNonce();
534
- const expiresAt = Math.floor(Date.now() / 1e3) + 300;
535
- const paymentRequired = {
536
- recipient,
537
- amount: priceMinor.toString(),
538
- currency: "USDC",
539
- chainId,
540
- facilitator,
541
- nonce,
542
- expiresAt,
543
- description: options.description
544
- };
545
- res.setHeader("X-Payment-Required", JSON.stringify(paymentRequired));
546
- res.setHeader("WWW-Authenticate", `X-402 ${Buffer.from(JSON.stringify(paymentRequired)).toString("base64")}`);
547
- return res.status(402).json({
548
- error: "Payment required",
549
- payment: paymentRequired
550
- });
551
- }
552
- const result = await verifyX402Payment(paymentHeader, {
553
- expectedAmount: priceMinor,
554
- expectedRecipient: recipient,
555
- chainId,
556
- facilitator,
557
- skipSettlement: options.testMode
558
- });
559
- if (!result.valid) {
560
- return res.status(402).json({
561
- error: "Invalid payment",
562
- reason: result.error
563
- });
564
- }
565
- req.x402Payment = result;
566
- if (result.txHash) {
567
- res.setHeader("X-Payment-TxHash", result.txHash);
568
- }
569
- const receiptMode = options.receiptMode || "webhook";
570
- if ((receiptMode === "jwt" || receiptMode === "both") && result.txHash && result.payer) {
571
- try {
572
- const receipt = await fetchPaymentReceipt({
573
- txHash: result.txHash,
574
- payer: result.payer,
575
- recipient,
576
- amount: priceMinor,
577
- chainId,
578
- apiUrl: options.mixrpayApiUrl
579
- });
580
- if (receipt) {
581
- result.receipt = receipt;
582
- res.setHeader("X-Payment-Receipt", receipt);
583
- }
584
- } catch (receiptError) {
585
- console.warn("[x402] Failed to fetch JWT receipt:", receiptError);
586
- }
587
- }
588
- if (options.onPayment) {
589
- await options.onPayment(result);
590
- }
591
- next();
592
- } catch (error) {
593
- console.error("[x402] Middleware error:", error);
594
- return res.status(500).json({
595
- error: "Payment processing error",
596
- message: error.message
597
- });
598
- }
599
- };
600
- }
601
- function mixrpay(options) {
602
- return async (req, res, next) => {
603
- try {
604
- const context = {
605
- path: req.path,
606
- method: req.method,
607
- headers: req.headers,
608
- body: req.body
609
- };
610
- if (options.skip) {
611
- const shouldSkip = await options.skip(context);
612
- if (shouldSkip) {
613
- return next();
614
- }
615
- }
616
- const priceUsd = options.getPrice ? await options.getPrice(context) : options.priceUsd;
617
- const sessionHeader = req.headers["x-mixr-session"];
618
- const widgetHeader = req.headers["x-mixr-payment"];
619
- const x402Header = req.headers["x-payment"];
620
- let result;
621
- if (sessionHeader) {
622
- result = await verifySessionPayment(sessionHeader, priceUsd, options, req);
623
- } else if (widgetHeader) {
624
- result = await verifyWidgetPayment(widgetHeader, priceUsd, options, req);
625
- } else if (x402Header) {
626
- result = await verifyX402PaymentUnified(x402Header, priceUsd, options, res);
627
- } else {
628
- return returnPaymentRequired(priceUsd, options, res);
629
- }
630
- if (!result.valid) {
631
- return res.status(402).json({
632
- error: "Invalid payment",
633
- reason: result.error,
634
- method: result.method
635
- });
636
- }
637
- req.mixrPayment = result;
638
- if (result.method === "x402" && result.x402Result) {
639
- req.x402Payment = result.x402Result;
640
- }
641
- if (result.txHash) {
642
- res.setHeader("X-Payment-TxHash", result.txHash);
643
- }
644
- if (result.chargeId) {
645
- res.setHeader("X-Mixr-Charge-Id", result.chargeId);
646
- }
647
- if (result.amountUsd !== void 0) {
648
- res.setHeader("X-Mixr-Charged", result.amountUsd.toString());
649
- }
650
- if (options.onPayment) {
651
- await options.onPayment(result);
652
- }
653
- next();
654
- } catch (error) {
655
- console.error("[mixrpay] Middleware error:", error);
656
- return res.status(500).json({
657
- error: "Payment processing error",
658
- message: error.message
659
- });
660
- }
661
- };
662
- }
663
- async function verifySessionPayment(sessionId, priceUsd, options, req) {
664
- const baseUrl = options.mixrpayApiUrl || process.env.MIXRPAY_API_URL || "https://mixrpay.com";
665
- const secretKey = process.env.MIXRPAY_SECRET_KEY;
666
- if (!secretKey) {
667
- return {
668
- valid: false,
669
- method: "session",
670
- error: "MIXRPAY_SECRET_KEY not configured"
671
- };
672
- }
673
- try {
674
- const response = await fetch(`${baseUrl}/api/v2/charge`, {
675
- method: "POST",
676
- headers: {
677
- "Content-Type": "application/json",
678
- "Authorization": `Bearer ${secretKey}`
679
- },
680
- body: JSON.stringify({
681
- sessionId,
682
- amountUsd: priceUsd,
683
- feature: options.feature || req.headers["x-mixr-feature"],
684
- idempotencyKey: req.headers["x-idempotency-key"]
685
- })
686
- });
687
- if (!response.ok) {
688
- const errorData = await response.json().catch(() => ({}));
689
- return {
690
- valid: false,
691
- method: "session",
692
- error: errorData.message || errorData.error || `Charge failed: ${response.status}`,
693
- sessionId
694
- };
695
- }
696
- const data = await response.json();
697
- return {
698
- valid: true,
699
- method: "session",
700
- payer: data.payer || data.walletAddress,
701
- amountUsd: data.amountUsd || priceUsd,
702
- txHash: data.txHash,
703
- chargeId: data.chargeId,
704
- sessionId,
705
- feature: options.feature,
706
- settledAt: data.settledAt ? new Date(data.settledAt) : /* @__PURE__ */ new Date()
707
- };
708
- } catch (error) {
709
- return {
710
- valid: false,
711
- method: "session",
712
- error: `Session verification failed: ${error.message}`,
713
- sessionId
714
- };
715
- }
716
- }
717
- async function verifyWidgetPayment(paymentJwt, priceUsd, options, req) {
718
- const baseUrl = options.mixrpayApiUrl || process.env.MIXRPAY_API_URL || "https://mixrpay.com";
719
- const secretKey = process.env.MIXRPAY_SECRET_KEY;
720
- if (!secretKey) {
721
- return {
722
- valid: false,
723
- method: "widget",
724
- error: "MIXRPAY_SECRET_KEY not configured"
725
- };
726
- }
727
- try {
728
- const response = await fetch(`${baseUrl}/api/widget/verify`, {
729
- method: "POST",
730
- headers: {
731
- "Content-Type": "application/json",
732
- "Authorization": `Bearer ${secretKey}`
733
- },
734
- body: JSON.stringify({
735
- paymentJwt,
736
- expectedAmountUsd: priceUsd,
737
- feature: options.feature || req.headers["x-mixr-feature"]
738
- })
739
- });
740
- if (!response.ok) {
741
- const errorData = await response.json().catch(() => ({}));
742
- return {
743
- valid: false,
744
- method: "widget",
745
- error: errorData.message || errorData.error || `Widget verification failed: ${response.status}`
746
- };
747
- }
748
- const data = await response.json();
749
- return {
750
- valid: true,
751
- method: "widget",
752
- payer: data.payer || data.walletAddress,
753
- amountUsd: data.amountUsd || priceUsd,
754
- txHash: data.txHash,
755
- chargeId: data.chargeId,
756
- feature: options.feature,
757
- settledAt: data.settledAt ? new Date(data.settledAt) : /* @__PURE__ */ new Date()
758
- };
759
- } catch (error) {
760
- return {
761
- valid: false,
762
- method: "widget",
763
- error: `Widget verification failed: ${error.message}`
764
- };
765
- }
766
- }
767
- async function verifyX402PaymentUnified(paymentHeader, priceUsd, options, res) {
768
- const recipient = options.recipient || process.env.MIXRPAY_MERCHANT_ADDRESS;
769
- if (!recipient) {
770
- return {
771
- valid: false,
772
- method: "x402",
773
- error: "MIXRPAY_MERCHANT_ADDRESS not configured for x402 payments"
774
- };
775
- }
776
- const priceMinor = usdToMinor(priceUsd);
777
- const chainId = options.chainId || 8453;
778
- const facilitator = options.facilitator || DEFAULT_FACILITATOR;
779
- const x402Result = await verifyX402Payment(paymentHeader, {
780
- expectedAmount: priceMinor,
781
- expectedRecipient: recipient,
782
- chainId,
783
- facilitator,
784
- skipSettlement: options.testMode
785
- });
786
- if (!x402Result.valid) {
787
- return {
788
- valid: false,
789
- method: "x402",
790
- error: x402Result.error,
791
- x402Result
792
- };
793
- }
794
- const receiptMode = options.receiptMode || "webhook";
795
- if ((receiptMode === "jwt" || receiptMode === "both") && x402Result.txHash && x402Result.payer) {
796
- try {
797
- const receipt = await fetchPaymentReceipt({
798
- txHash: x402Result.txHash,
799
- payer: x402Result.payer,
800
- recipient,
801
- amount: priceMinor,
802
- chainId,
803
- apiUrl: options.mixrpayApiUrl
804
- });
805
- if (receipt) {
806
- x402Result.receipt = receipt;
807
- res.setHeader("X-Payment-Receipt", receipt);
808
- }
809
- } catch (receiptError) {
810
- console.warn("[mixrpay] Failed to fetch JWT receipt:", receiptError);
811
- }
812
- }
813
- return {
814
- valid: true,
815
- method: "x402",
816
- payer: x402Result.payer,
817
- amountUsd: x402Result.amount,
818
- txHash: x402Result.txHash,
819
- receipt: x402Result.receipt,
820
- settledAt: x402Result.settledAt,
821
- x402Result
822
- };
823
- }
824
- function returnPaymentRequired(priceUsd, options, res) {
825
- const priceMinor = usdToMinor(priceUsd);
826
- const recipient = options.recipient || process.env.MIXRPAY_MERCHANT_ADDRESS;
827
- const chainId = options.chainId || 8453;
828
- const facilitator = options.facilitator || DEFAULT_FACILITATOR;
829
- const nonce = generateNonce();
830
- const expiresAt = Math.floor(Date.now() / 1e3) + 300;
831
- const x402PaymentRequired = recipient ? {
832
- recipient,
833
- amount: priceMinor.toString(),
834
- currency: "USDC",
835
- chainId,
836
- facilitator,
837
- nonce,
838
- expiresAt,
839
- description: options.description
840
- } : null;
841
- if (x402PaymentRequired) {
842
- res.setHeader("X-Payment-Required", JSON.stringify(x402PaymentRequired));
843
- res.setHeader("WWW-Authenticate", `X-402 ${Buffer.from(JSON.stringify(x402PaymentRequired)).toString("base64")}`);
844
- }
845
- return res.status(402).json({
846
- error: "Payment required",
847
- priceUsd,
848
- acceptedMethods: [
849
- { method: "session", header: "X-Mixr-Session", description: "Session authorization ID" },
850
- { method: "widget", header: "X-Mixr-Payment", description: "Widget payment JWT" },
851
- ...recipient ? [{ method: "x402", header: "X-PAYMENT", description: "x402 protocol payment" }] : []
852
- ],
853
- x402: x402PaymentRequired,
854
- description: options.description
855
- });
856
- }
857
-
858
- // src/middleware/nextjs.ts
859
- import { NextResponse } from "next/server";
860
- function withX402(config, handler) {
861
- return async (req) => {
862
- try {
863
- const recipient = config.recipient || process.env.MIXRPAY_MERCHANT_ADDRESS;
864
- if (!recipient) {
865
- console.error("[x402] No recipient address configured");
866
- return NextResponse.json(
867
- { error: "Payment configuration error" },
868
- { status: 500 }
869
- );
870
- }
871
- const price = config.getPrice ? await config.getPrice(req) : config.price;
872
- const priceMinor = usdToMinor(price);
873
- const chainId = config.chainId || 8453;
874
- const facilitator = config.facilitator || DEFAULT_FACILITATOR;
875
- const paymentHeader = req.headers.get("x-payment");
876
- if (!paymentHeader) {
877
- const nonce = generateNonce();
878
- const expiresAt = Math.floor(Date.now() / 1e3) + 300;
879
- const paymentRequired = {
880
- recipient,
881
- amount: priceMinor.toString(),
882
- currency: "USDC",
883
- chainId,
884
- facilitator,
885
- nonce,
886
- expiresAt,
887
- description: config.description
888
- };
889
- return NextResponse.json(
890
- { error: "Payment required", payment: paymentRequired },
891
- {
892
- status: 402,
893
- headers: {
894
- "X-Payment-Required": JSON.stringify(paymentRequired),
895
- "WWW-Authenticate": `X-402 ${Buffer.from(JSON.stringify(paymentRequired)).toString("base64")}`
896
- }
897
- }
898
- );
899
- }
900
- const result = await verifyX402Payment(paymentHeader, {
901
- expectedAmount: priceMinor,
902
- expectedRecipient: recipient,
903
- chainId,
904
- facilitator,
905
- skipSettlement: config.testMode
906
- });
907
- if (!result.valid) {
908
- return NextResponse.json(
909
- { error: "Invalid payment", reason: result.error },
910
- { status: 402 }
911
- );
912
- }
913
- const receiptMode = config.receiptMode || "webhook";
914
- if ((receiptMode === "jwt" || receiptMode === "both") && result.txHash && result.payer) {
915
- try {
916
- const receipt = await fetchPaymentReceipt({
917
- txHash: result.txHash,
918
- payer: result.payer,
919
- recipient,
920
- amount: priceMinor,
921
- chainId,
922
- apiUrl: config.mixrpayApiUrl
923
- });
924
- if (receipt) {
925
- result.receipt = receipt;
926
- }
927
- } catch (receiptError) {
928
- console.warn("[x402] Failed to fetch JWT receipt:", receiptError);
929
- }
930
- }
931
- if (config.onPayment) {
932
- await config.onPayment(result, req);
933
- }
934
- const response = await handler(req, result);
935
- if (result.txHash) {
936
- response.headers.set("X-Payment-TxHash", result.txHash);
937
- }
938
- if (result.receipt) {
939
- response.headers.set("X-Payment-Receipt", result.receipt);
940
- }
941
- return response;
942
- } catch (error) {
943
- console.error("[x402] Handler error:", error);
944
- return NextResponse.json(
945
- { error: "Payment processing error" },
946
- { status: 500 }
947
- );
948
- }
949
- };
950
- }
951
- function createPaymentRequired(options) {
952
- const priceMinor = usdToMinor(options.amount);
953
- const nonce = generateNonce();
954
- const expiresAt = Math.floor(Date.now() / 1e3) + 300;
955
- const paymentRequired = {
956
- recipient: options.recipient,
957
- amount: priceMinor.toString(),
958
- currency: "USDC",
959
- chainId: options.chainId || 8453,
960
- facilitator: options.facilitator || DEFAULT_FACILITATOR,
961
- nonce,
962
- expiresAt,
963
- description: options.description
964
- };
965
- return NextResponse.json(
966
- { error: "Payment required", payment: paymentRequired },
967
- {
968
- status: 402,
969
- headers: {
970
- "X-Payment-Required": JSON.stringify(paymentRequired)
971
- }
972
- }
973
- );
974
- }
975
- function withMixrPay(config, handler) {
976
- return async (req) => {
977
- try {
978
- const priceUsd = config.getPrice ? await config.getPrice(req) : config.priceUsd;
979
- const sessionHeader = req.headers.get("x-mixr-session");
980
- const widgetHeader = req.headers.get("x-mixr-payment");
981
- const x402Header = req.headers.get("x-payment");
982
- let result;
983
- if (sessionHeader) {
984
- result = await verifySessionPaymentNext(sessionHeader, priceUsd, config, req);
985
- } else if (widgetHeader) {
986
- result = await verifyWidgetPaymentNext(widgetHeader, priceUsd, config, req);
987
- } else if (x402Header) {
988
- result = await verifyX402PaymentNext(x402Header, priceUsd, config);
989
- } else {
990
- return returnPaymentRequiredNext(priceUsd, config);
991
- }
992
- if (!result.valid) {
993
- return NextResponse.json(
994
- { error: "Invalid payment", reason: result.error, method: result.method },
995
- { status: 402 }
996
- );
997
- }
998
- if (config.onPayment) {
999
- await config.onPayment(result, req);
1000
- }
1001
- const response = await handler(req, result);
1002
- if (result.txHash) {
1003
- response.headers.set("X-Payment-TxHash", result.txHash);
1004
- }
1005
- if (result.chargeId) {
1006
- response.headers.set("X-Mixr-Charge-Id", result.chargeId);
1007
- }
1008
- if (result.amountUsd !== void 0) {
1009
- response.headers.set("X-Mixr-Charged", result.amountUsd.toString());
1010
- }
1011
- return response;
1012
- } catch (error) {
1013
- console.error("[withMixrPay] Handler error:", error);
1014
- return NextResponse.json(
1015
- { error: "Payment processing error" },
1016
- { status: 500 }
1017
- );
1018
- }
1019
- };
1020
- }
1021
- async function verifySessionPaymentNext(sessionId, priceUsd, config, req) {
1022
- const baseUrl = config.mixrpayApiUrl || process.env.MIXRPAY_API_URL || "https://mixrpay.com";
1023
- const secretKey = process.env.MIXRPAY_SECRET_KEY;
1024
- if (!secretKey) {
1025
- return { valid: false, method: "session", error: "MIXRPAY_SECRET_KEY not configured" };
1026
- }
1027
- try {
1028
- const response = await fetch(`${baseUrl}/api/v2/charge`, {
1029
- method: "POST",
1030
- headers: {
1031
- "Content-Type": "application/json",
1032
- "Authorization": `Bearer ${secretKey}`
1033
- },
1034
- body: JSON.stringify({
1035
- sessionId,
1036
- amountUsd: priceUsd,
1037
- feature: config.feature || req.headers.get("x-mixr-feature"),
1038
- idempotencyKey: req.headers.get("x-idempotency-key")
1039
- })
1040
- });
1041
- if (!response.ok) {
1042
- const errorData = await response.json().catch(() => ({}));
1043
- return {
1044
- valid: false,
1045
- method: "session",
1046
- error: errorData.message || errorData.error || `Charge failed: ${response.status}`,
1047
- sessionId
1048
- };
1049
- }
1050
- const data = await response.json();
1051
- return {
1052
- valid: true,
1053
- method: "session",
1054
- payer: data.payer || data.walletAddress,
1055
- amountUsd: data.amountUsd || priceUsd,
1056
- txHash: data.txHash,
1057
- chargeId: data.chargeId,
1058
- sessionId,
1059
- feature: config.feature,
1060
- settledAt: data.settledAt ? new Date(data.settledAt) : /* @__PURE__ */ new Date()
1061
- };
1062
- } catch (error) {
1063
- return {
1064
- valid: false,
1065
- method: "session",
1066
- error: `Session verification failed: ${error.message}`,
1067
- sessionId
1068
- };
1069
- }
1070
- }
1071
- async function verifyWidgetPaymentNext(paymentJwt, priceUsd, config, req) {
1072
- const baseUrl = config.mixrpayApiUrl || process.env.MIXRPAY_API_URL || "https://mixrpay.com";
1073
- const secretKey = process.env.MIXRPAY_SECRET_KEY;
1074
- if (!secretKey) {
1075
- return { valid: false, method: "widget", error: "MIXRPAY_SECRET_KEY not configured" };
1076
- }
1077
- try {
1078
- const response = await fetch(`${baseUrl}/api/widget/verify`, {
1079
- method: "POST",
1080
- headers: {
1081
- "Content-Type": "application/json",
1082
- "Authorization": `Bearer ${secretKey}`
1083
- },
1084
- body: JSON.stringify({
1085
- paymentJwt,
1086
- expectedAmountUsd: priceUsd,
1087
- feature: config.feature || req.headers.get("x-mixr-feature")
1088
- })
1089
- });
1090
- if (!response.ok) {
1091
- const errorData = await response.json().catch(() => ({}));
1092
- return {
1093
- valid: false,
1094
- method: "widget",
1095
- error: errorData.message || errorData.error || `Widget verification failed: ${response.status}`
1096
- };
1097
- }
1098
- const data = await response.json();
1099
- return {
1100
- valid: true,
1101
- method: "widget",
1102
- payer: data.payer || data.walletAddress,
1103
- amountUsd: data.amountUsd || priceUsd,
1104
- txHash: data.txHash,
1105
- chargeId: data.chargeId,
1106
- feature: config.feature,
1107
- settledAt: data.settledAt ? new Date(data.settledAt) : /* @__PURE__ */ new Date()
1108
- };
1109
- } catch (error) {
1110
- return {
1111
- valid: false,
1112
- method: "widget",
1113
- error: `Widget verification failed: ${error.message}`
1114
- };
1115
- }
1116
- }
1117
- async function verifyX402PaymentNext(paymentHeader, priceUsd, config) {
1118
- const recipient = config.recipient || process.env.MIXRPAY_MERCHANT_ADDRESS;
1119
- if (!recipient) {
1120
- return {
1121
- valid: false,
1122
- method: "x402",
1123
- error: "MIXRPAY_MERCHANT_ADDRESS not configured for x402 payments"
1124
- };
1125
- }
1126
- const priceMinor = usdToMinor(priceUsd);
1127
- const chainId = config.chainId || 8453;
1128
- const facilitator = config.facilitator || DEFAULT_FACILITATOR;
1129
- const x402Result = await verifyX402Payment(paymentHeader, {
1130
- expectedAmount: priceMinor,
1131
- expectedRecipient: recipient,
1132
- chainId,
1133
- facilitator,
1134
- skipSettlement: config.testMode
1135
- });
1136
- if (!x402Result.valid) {
1137
- return {
1138
- valid: false,
1139
- method: "x402",
1140
- error: x402Result.error,
1141
- x402Result
1142
- };
1143
- }
1144
- const receiptMode = config.receiptMode || "webhook";
1145
- if ((receiptMode === "jwt" || receiptMode === "both") && x402Result.txHash && x402Result.payer) {
1146
- try {
1147
- const receipt = await fetchPaymentReceipt({
1148
- txHash: x402Result.txHash,
1149
- payer: x402Result.payer,
1150
- recipient,
1151
- amount: priceMinor,
1152
- chainId,
1153
- apiUrl: config.mixrpayApiUrl
1154
- });
1155
- if (receipt) {
1156
- x402Result.receipt = receipt;
1157
- }
1158
- } catch (receiptError) {
1159
- console.warn("[withMixrPay] Failed to fetch JWT receipt:", receiptError);
1160
- }
1161
- }
1162
- return {
1163
- valid: true,
1164
- method: "x402",
1165
- payer: x402Result.payer,
1166
- amountUsd: x402Result.amount,
1167
- txHash: x402Result.txHash,
1168
- receipt: x402Result.receipt,
1169
- settledAt: x402Result.settledAt,
1170
- x402Result
1171
- };
1172
- }
1173
- function returnPaymentRequiredNext(priceUsd, config) {
1174
- const priceMinor = usdToMinor(priceUsd);
1175
- const recipient = config.recipient || process.env.MIXRPAY_MERCHANT_ADDRESS;
1176
- const chainId = config.chainId || 8453;
1177
- const facilitator = config.facilitator || DEFAULT_FACILITATOR;
1178
- const nonce = generateNonce();
1179
- const expiresAt = Math.floor(Date.now() / 1e3) + 300;
1180
- const x402PaymentRequired = recipient ? {
1181
- recipient,
1182
- amount: priceMinor.toString(),
1183
- currency: "USDC",
1184
- chainId,
1185
- facilitator,
1186
- nonce,
1187
- expiresAt,
1188
- description: config.description
1189
- } : null;
1190
- const headers = {};
1191
- if (x402PaymentRequired) {
1192
- headers["X-Payment-Required"] = JSON.stringify(x402PaymentRequired);
1193
- headers["WWW-Authenticate"] = `X-402 ${Buffer.from(JSON.stringify(x402PaymentRequired)).toString("base64")}`;
1194
- }
1195
- return NextResponse.json(
1196
- {
1197
- error: "Payment required",
1198
- priceUsd,
1199
- acceptedMethods: [
1200
- { method: "session", header: "X-Mixr-Session", description: "Session authorization ID" },
1201
- { method: "widget", header: "X-Mixr-Payment", description: "Widget payment JWT" },
1202
- ...recipient ? [{ method: "x402", header: "X-PAYMENT", description: "x402 protocol payment" }] : []
1203
- ],
1204
- x402: x402PaymentRequired,
1205
- description: config.description
1206
- },
1207
- { status: 402, headers }
1208
- );
1209
- }
1210
-
1211
- // src/middleware/fastify.ts
1212
- var x402Plugin = (fastify, options, done) => {
1213
- fastify.decorateRequest("x402Payment", null);
1214
- fastify.decorate("x402Defaults", {
1215
- recipient: options.recipient || process.env.MIXRPAY_MERCHANT_ADDRESS,
1216
- chainId: options.chainId || 8453,
1217
- facilitator: options.facilitator || DEFAULT_FACILITATOR
1218
- });
1219
- done();
1220
- };
1221
- function x4022(options) {
1222
- return async (request, reply) => {
1223
- const context = {
1224
- path: request.url,
1225
- method: request.method,
1226
- headers: request.headers,
1227
- body: request.body
1228
- };
1229
- if (options.skip) {
1230
- const shouldSkip = await options.skip(context);
1231
- if (shouldSkip) {
1232
- return;
1233
- }
1234
- }
1235
- const defaults = request.server.x402Defaults || {};
1236
- const recipient = options.recipient || defaults.recipient || process.env.MIXRPAY_MERCHANT_ADDRESS;
1237
- if (!recipient) {
1238
- request.log.error("[x402] No recipient address configured");
1239
- return reply.status(500).send({
1240
- error: "Payment configuration error",
1241
- message: "Merchant wallet address not configured"
1242
- });
1243
- }
1244
- const price = options.getPrice ? await options.getPrice(context) : options.price;
1245
- const priceMinor = usdToMinor(price);
1246
- const chainId = options.chainId || defaults.chainId || 8453;
1247
- const facilitator = options.facilitator || defaults.facilitator || DEFAULT_FACILITATOR;
1248
- const paymentHeader = request.headers["x-payment"];
1249
- if (!paymentHeader) {
1250
- const nonce = generateNonce();
1251
- const expiresAt = Math.floor(Date.now() / 1e3) + 300;
1252
- const paymentRequired = {
1253
- recipient,
1254
- amount: priceMinor.toString(),
1255
- currency: "USDC",
1256
- chainId,
1257
- facilitator,
1258
- nonce,
1259
- expiresAt,
1260
- description: options.description
1261
- };
1262
- reply.header("X-Payment-Required", JSON.stringify(paymentRequired));
1263
- reply.header("WWW-Authenticate", `X-402 ${Buffer.from(JSON.stringify(paymentRequired)).toString("base64")}`);
1264
- return reply.status(402).send({
1265
- error: "Payment required",
1266
- payment: paymentRequired
1267
- });
1268
- }
1269
- const result = await verifyX402Payment(paymentHeader, {
1270
- expectedAmount: priceMinor,
1271
- expectedRecipient: recipient,
1272
- chainId,
1273
- facilitator,
1274
- skipSettlement: options.testMode
1275
- });
1276
- if (!result.valid) {
1277
- return reply.status(402).send({
1278
- error: "Invalid payment",
1279
- reason: result.error
1280
- });
1281
- }
1282
- request.x402Payment = result;
1283
- if (result.txHash) {
1284
- reply.header("X-Payment-TxHash", result.txHash);
1285
- }
1286
- const receiptMode = options.receiptMode || "webhook";
1287
- if ((receiptMode === "jwt" || receiptMode === "both") && result.txHash && result.payer) {
1288
- try {
1289
- const receipt = await fetchPaymentReceipt({
1290
- txHash: result.txHash,
1291
- payer: result.payer,
1292
- recipient,
1293
- amount: priceMinor,
1294
- chainId,
1295
- apiUrl: options.mixrpayApiUrl
1296
- });
1297
- if (receipt) {
1298
- result.receipt = receipt;
1299
- reply.header("X-Payment-Receipt", receipt);
1300
- }
1301
- } catch (receiptError) {
1302
- request.log.warn({ err: receiptError }, "[x402] Failed to fetch JWT receipt");
1303
- }
1304
- }
1305
- if (options.onPayment) {
1306
- await options.onPayment(result);
1307
- }
1308
- };
1309
- }
1310
79
  export {
1311
- ChargesClient,
1312
- DEFAULT_FACILITATOR,
1313
- USDC_CONTRACTS,
1314
- clearJWKSCache,
1315
- createPaymentRequired,
1316
- mixrpay as expressMixrPay,
1317
- x402 as expressX402,
1318
- x4022 as fastifyX402,
1319
- generateNonce,
1320
- getDefaultJWKSUrl,
1321
- isReceiptExpired,
1322
- isValidAddress,
1323
- minorToUsd,
1324
- normalizeAddress,
1325
- parsePaymentReceipt,
1326
- parseSessionGrant,
1327
- parseX402Payment,
1328
- usdToMinor,
1329
80
  verifyPaymentReceipt,
1330
- verifySessionWebhook,
1331
- verifyX402Payment,
1332
- withMixrPay,
1333
- withX402,
1334
- x402Plugin
81
+ verifySessionWebhook
1335
82
  };
1336
83
  //# sourceMappingURL=index.mjs.map