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