@mixrpay/merchant-sdk 0.3.0 → 0.3.2
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/README.md +75 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/middleware/express.d.mts +1 -1
- package/dist/middleware/express.d.ts +1 -1
- package/dist/middleware/express.js +92 -36
- package/dist/middleware/express.js.map +1 -1
- package/dist/middleware/express.mjs +92 -36
- package/dist/middleware/express.mjs.map +1 -1
- package/dist/middleware/fastify.d.mts +1 -1
- package/dist/middleware/fastify.d.ts +1 -1
- package/dist/middleware/fastify.js +1 -1
- package/dist/middleware/fastify.js.map +1 -1
- package/dist/middleware/fastify.mjs +1 -1
- package/dist/middleware/fastify.mjs.map +1 -1
- package/dist/middleware/nextjs.d.mts +1 -1
- package/dist/middleware/nextjs.d.ts +1 -1
- package/dist/middleware/nextjs.js +92 -36
- package/dist/middleware/nextjs.js.map +1 -1
- package/dist/middleware/nextjs.mjs +92 -36
- package/dist/middleware/nextjs.mjs.map +1 -1
- package/dist/{types-jelXkTp9.d.mts → types-BwiuIaOu.d.mts} +2 -0
- package/dist/{types-jelXkTp9.d.ts → types-BwiuIaOu.d.ts} +2 -0
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -225,9 +225,35 @@ interface MixrPayment {
|
|
|
225
225
|
|
|
226
226
|
/** Error message if invalid */
|
|
227
227
|
error?: string;
|
|
228
|
+
|
|
229
|
+
/** Whether payment was pre-charged by the agent (session only) */
|
|
230
|
+
preCharged?: boolean;
|
|
228
231
|
}
|
|
229
232
|
```
|
|
230
233
|
|
|
234
|
+
### Pre-Charged Sessions
|
|
235
|
+
|
|
236
|
+
When AI agents use the Agent SDK, they can pre-charge before calling your API by passing `X-Mixr-Charged: <amount>`. The middleware detects this and validates the session without double-charging:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// Agent SDK calls your API with pre-charged session
|
|
240
|
+
// Headers: { 'X-Mixr-Session': 'sess_abc', 'X-Mixr-Charged': '0.05' }
|
|
241
|
+
|
|
242
|
+
app.post('/api/query', mixrpay({ priceUsd: 0.05 }), (req, res) => {
|
|
243
|
+
const { preCharged, amountUsd, payer } = req.mixrPayment!;
|
|
244
|
+
|
|
245
|
+
if (preCharged) {
|
|
246
|
+
// Agent pre-charged - session validated only, no additional charge made
|
|
247
|
+
console.log(`Pre-charged payment of $${amountUsd} from ${payer}`);
|
|
248
|
+
} else {
|
|
249
|
+
// Middleware charged the session
|
|
250
|
+
console.log(`Charged $${amountUsd} from ${payer}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
res.json({ result: 'success' });
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
231
257
|
## Advanced Usage
|
|
232
258
|
|
|
233
259
|
### Dynamic Pricing
|
|
@@ -301,6 +327,55 @@ if (result.valid) {
|
|
|
301
327
|
}
|
|
302
328
|
```
|
|
303
329
|
|
|
330
|
+
### Webhook Verification
|
|
331
|
+
|
|
332
|
+
Verify webhook signatures to ensure events are from MixrPay:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import { verifySessionWebhook } from '@mixrpay/merchant-sdk';
|
|
336
|
+
|
|
337
|
+
app.post('/webhooks/mixrpay', (req, res) => {
|
|
338
|
+
const signature = req.headers['x-mixrpay-signature'] as string;
|
|
339
|
+
const payload = JSON.stringify(req.body);
|
|
340
|
+
|
|
341
|
+
// Verify signature using your webhook secret from the dashboard
|
|
342
|
+
if (!verifySessionWebhook(payload, signature, process.env.WEBHOOK_SECRET!)) {
|
|
343
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const event = req.body;
|
|
347
|
+
|
|
348
|
+
switch (event.event) {
|
|
349
|
+
case 'session.created':
|
|
350
|
+
// New session authorized
|
|
351
|
+
console.log(`Session created: ${event.session_id} from ${event.user_wallet}`);
|
|
352
|
+
console.log(`Spending limit: $${event.spending_limit_usd}`);
|
|
353
|
+
break;
|
|
354
|
+
|
|
355
|
+
case 'session.charged':
|
|
356
|
+
// Successful charge against a session
|
|
357
|
+
console.log(`Charged $${event.amount_usd} from ${event.user_wallet}`);
|
|
358
|
+
console.log(`Remaining: $${event.remaining_usd}`);
|
|
359
|
+
// Update your database, trigger fulfillment, etc.
|
|
360
|
+
break;
|
|
361
|
+
|
|
362
|
+
case 'session.revoked':
|
|
363
|
+
// User revoked their session
|
|
364
|
+
console.log(`Session revoked: ${event.session_id}`);
|
|
365
|
+
break;
|
|
366
|
+
|
|
367
|
+
case 'session.expired':
|
|
368
|
+
// Session expired
|
|
369
|
+
console.log(`Session expired: ${event.session_id}`);
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
res.json({ received: true });
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Configure webhook URLs in your [MixrPay Dashboard](https://www.mixrpay.com/seller/developers).
|
|
378
|
+
|
|
304
379
|
## Testing
|
|
305
380
|
|
|
306
381
|
Enable test mode to accept test payments during development:
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { V as VerifyReceiptOptions, P as PaymentReceipt } from './types-
|
|
2
|
-
export { M as MixrPayOptions, a as MixrPayPaymentResult, b as PaymentMethod, c as PriceContext } from './types-
|
|
1
|
+
import { V as VerifyReceiptOptions, P as PaymentReceipt } from './types-BwiuIaOu.mjs';
|
|
2
|
+
export { M as MixrPayOptions, a as MixrPayPaymentResult, b as PaymentMethod, c as PriceContext } from './types-BwiuIaOu.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* MixrPay Merchant SDK - Payment Receipt Verification
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { V as VerifyReceiptOptions, P as PaymentReceipt } from './types-
|
|
2
|
-
export { M as MixrPayOptions, a as MixrPayPaymentResult, b as PaymentMethod, c as PriceContext } from './types-
|
|
1
|
+
import { V as VerifyReceiptOptions, P as PaymentReceipt } from './types-BwiuIaOu.js';
|
|
2
|
+
export { M as MixrPayOptions, a as MixrPayPaymentResult, b as PaymentMethod, c as PriceContext } from './types-BwiuIaOu.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* MixrPay Merchant SDK - Payment Receipt Verification
|
package/dist/index.js
CHANGED
|
@@ -37,7 +37,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
37
37
|
|
|
38
38
|
// src/receipt.ts
|
|
39
39
|
var jose = __toESM(require("jose"));
|
|
40
|
-
var DEFAULT_JWKS_URL = process.env.MIXRPAY_JWKS_URL || "https://mixrpay.com/.well-known/jwks";
|
|
40
|
+
var DEFAULT_JWKS_URL = process.env.MIXRPAY_JWKS_URL || "https://www.mixrpay.com/.well-known/jwks";
|
|
41
41
|
var JWKS_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
42
42
|
var jwksCache = /* @__PURE__ */ new Map();
|
|
43
43
|
async function getJWKS(jwksUrl) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/receipt.ts","../src/charges.ts"],"sourcesContent":["/**\n * MixrPay Merchant SDK\n * \n * Accept payments from AI agents and web apps with one middleware.\n * \n * @example Express\n * ```typescript\n * import { mixrpay } from '@mixrpay/merchant-sdk/express';\n * \n * app.post('/api/query', mixrpay({ priceUsd: 0.05 }), (req, res) => {\n * console.log(`Paid by ${req.mixrPayment?.payer}`);\n * res.json({ result: 'success' });\n * });\n * ```\n * \n * @example Next.js\n * ```typescript\n * import { withMixrPay } from '@mixrpay/merchant-sdk/nextjs';\n * \n * export const POST = withMixrPay({ priceUsd: 0.05 }, async (req, payment) => {\n * return NextResponse.json({ result: 'success' });\n * });\n * ```\n * \n * @packageDocumentation\n */\n\n// Receipt verification (for custom integrations)\nexport { verifyPaymentReceipt } from './receipt';\n\n// Session webhook verification\nexport { verifySessionWebhook } from './charges';\n\n// Types for TypeScript users\nexport type {\n MixrPayOptions,\n MixrPayPaymentResult,\n PaymentMethod,\n PriceContext,\n PaymentReceipt,\n} from './types';\n\nexport type {\n SessionGrant,\n} from './charges';\n\n// Widget element types (for TypeScript JSX support)\nexport type {} from './widget-elements';\n","/**\n * MixrPay Merchant SDK - Payment Receipt Verification\n * \n * Verify JWT payment receipts issued by MixrPay after successful x402 payments.\n * \n * @example\n * ```typescript\n * import { verifyPaymentReceipt } from '@mixrpay/merchant-sdk';\n * \n * const receipt = req.headers['x-payment-receipt'];\n * const payment = await verifyPaymentReceipt(receipt);\n * \n * console.log(`Payment received: $${payment.amountUsd} from ${payment.payer}`);\n * console.log(`Transaction: ${payment.txHash}`);\n * ```\n */\n\nimport * as jose from 'jose';\nimport type { PaymentReceipt, VerifyReceiptOptions } from './types';\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nconst DEFAULT_JWKS_URL = process.env.MIXRPAY_JWKS_URL || 'https://mixrpay.com/.well-known/jwks';\nconst JWKS_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour\n\n// =============================================================================\n// JWKS Cache\n// =============================================================================\n\ninterface CachedJWKS {\n jwks: jose.JSONWebKeySet;\n fetchedAt: number;\n}\n\nconst jwksCache = new Map<string, CachedJWKS>();\n\n/**\n * Fetch and cache JWKS from the specified URL.\n */\nasync function getJWKS(jwksUrl: string): Promise<jose.JSONWebKeySet> {\n const cached = jwksCache.get(jwksUrl);\n const now = Date.now();\n\n // Return cached JWKS if still valid\n if (cached && (now - cached.fetchedAt) < JWKS_CACHE_TTL_MS) {\n return cached.jwks;\n }\n\n // Fetch fresh JWKS\n const response = await fetch(jwksUrl);\n \n if (!response.ok) {\n throw new Error(`Failed to fetch JWKS from ${jwksUrl}: ${response.status} ${response.statusText}`);\n }\n\n const jwks = await response.json() as jose.JSONWebKeySet;\n\n // Validate JWKS structure\n if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {\n throw new Error('Invalid JWKS: missing or empty keys array');\n }\n\n // Cache the JWKS\n jwksCache.set(jwksUrl, { jwks, fetchedAt: now });\n\n return jwks;\n}\n\n/**\n * Create a JWKS key set from a fetched JWKS.\n */\nasync function createKeySet(jwksUrl: string): Promise<jose.JWTVerifyGetKey> {\n const jwks = await getJWKS(jwksUrl);\n return jose.createLocalJWKSet(jwks);\n}\n\n// =============================================================================\n// Receipt Verification\n// =============================================================================\n\n/**\n * Verify a JWT payment receipt from MixrPay.\n * \n * This function:\n * 1. Fetches the JWKS from MixrPay (cached for 1 hour)\n * 2. Verifies the JWT signature using RS256\n * 3. Validates standard JWT claims (exp, iat)\n * 4. Returns the typed payment receipt\n * \n * @param receipt - The JWT receipt string from X-Payment-Receipt header\n * @param options - Optional configuration (custom JWKS URL, issuer validation)\n * @returns Verified payment receipt\n * @throws Error if verification fails\n * \n * @example\n * ```typescript\n * // Basic usage\n * const payment = await verifyPaymentReceipt(receipt);\n * \n * // With custom JWKS URL (for testing or self-hosted)\n * const payment = await verifyPaymentReceipt(receipt, {\n * jwksUrl: 'https://your-mixrpay.com/.well-known/jwks'\n * });\n * ```\n */\nexport async function verifyPaymentReceipt(\n receipt: string,\n options?: VerifyReceiptOptions\n): Promise<PaymentReceipt> {\n const jwksUrl = options?.jwksUrl || DEFAULT_JWKS_URL;\n\n try {\n // Get the key set\n const keySet = await createKeySet(jwksUrl);\n\n // Build verification options\n const verifyOptions: jose.JWTVerifyOptions = {\n algorithms: ['RS256'],\n };\n\n // Add issuer validation if specified\n if (options?.issuer) {\n verifyOptions.issuer = options.issuer;\n }\n\n // Verify the JWT\n const { payload } = await jose.jwtVerify(receipt, keySet, verifyOptions);\n\n // Validate required payment fields\n const requiredFields = ['paymentId', 'amount', 'amountUsd', 'payer', 'recipient', 'chainId', 'txHash', 'settledAt'];\n for (const field of requiredFields) {\n if (!(field in payload)) {\n throw new Error(`Missing required field in receipt: ${field}`);\n }\n }\n\n // Return typed receipt\n return {\n paymentId: payload.paymentId as string,\n amount: payload.amount as string,\n amountUsd: payload.amountUsd as number,\n payer: payload.payer as string,\n recipient: payload.recipient as string,\n chainId: payload.chainId as number,\n txHash: payload.txHash as string,\n settledAt: payload.settledAt as string,\n issuedAt: new Date((payload.iat as number) * 1000),\n expiresAt: new Date((payload.exp as number) * 1000),\n };\n } catch (error) {\n if (error instanceof jose.errors.JWTExpired) {\n throw new Error('Payment receipt has expired');\n }\n if (error instanceof jose.errors.JWTClaimValidationFailed) {\n throw new Error(`Receipt validation failed: ${error.message}`);\n }\n if (error instanceof jose.errors.JWSSignatureVerificationFailed) {\n throw new Error('Invalid receipt signature');\n }\n throw error;\n }\n}\n\n/**\n * Parse a JWT payment receipt without verification.\n * \n * WARNING: This does NOT verify the signature. Use only for debugging\n * or when you've already verified the receipt elsewhere.\n * \n * @param receipt - The JWT receipt string\n * @returns Decoded payload (unverified)\n */\nexport function parsePaymentReceipt(receipt: string): PaymentReceipt {\n const decoded = jose.decodeJwt(receipt);\n\n return {\n paymentId: decoded.paymentId as string,\n amount: decoded.amount as string,\n amountUsd: decoded.amountUsd as number,\n payer: decoded.payer as string,\n recipient: decoded.recipient as string,\n chainId: decoded.chainId as number,\n txHash: decoded.txHash as string,\n settledAt: decoded.settledAt as string,\n issuedAt: decoded.iat ? new Date(decoded.iat * 1000) : undefined,\n expiresAt: decoded.exp ? new Date(decoded.exp * 1000) : undefined,\n };\n}\n\n/**\n * Check if a receipt is expired.\n * \n * @param receipt - The JWT receipt string or parsed PaymentReceipt\n * @returns true if expired, false otherwise\n */\nexport function isReceiptExpired(receipt: string | PaymentReceipt): boolean {\n const parsed = typeof receipt === 'string' ? parsePaymentReceipt(receipt) : receipt;\n \n if (!parsed.expiresAt) {\n return false; // No expiration = never expires\n }\n\n return parsed.expiresAt.getTime() < Date.now();\n}\n\n/**\n * Clear the JWKS cache.\n * Useful for testing or when keys have rotated.\n */\nexport function clearJWKSCache(): void {\n jwksCache.clear();\n}\n\n/**\n * Get the default JWKS URL.\n */\nexport function getDefaultJWKSUrl(): string {\n return DEFAULT_JWKS_URL;\n}\n\n","/**\n * MixrPay Charges Module\n * \n * Create and manage charges using session signers.\n * Session signers allow you to charge user wallets without\n * requiring approval for each transaction.\n */\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface CreateChargeParams {\n /** Session key ID granted by the user */\n sessionId: string;\n /** Amount to charge in USD (e.g., 0.05 for 5 cents) */\n amountUsd: number;\n /** Unique reference for idempotency (e.g., \"order_123\") */\n reference: string;\n /** Optional metadata */\n metadata?: Record<string, unknown>;\n}\n\nexport interface Charge {\n /** Unique charge ID */\n id: string;\n /** Current status */\n status: 'pending' | 'submitted' | 'confirmed' | 'failed';\n /** Amount in USD */\n amountUsd: number;\n /** Formatted USDC amount */\n amountUsdc: string;\n /** Transaction hash (if submitted) */\n txHash?: string;\n /** Block explorer URL */\n explorerUrl?: string;\n /** From wallet address */\n fromAddress: string;\n /** To wallet address */\n toAddress: string;\n /** Your reference */\n reference: string;\n /** When the charge was created */\n createdAt: string;\n /** When the charge was confirmed */\n confirmedAt?: string;\n /** Session name */\n sessionName?: string;\n /** User wallet address */\n userWallet?: string;\n /** Error message if failed */\n error?: string;\n}\n\nexport interface CreateChargeResult {\n success: boolean;\n charge?: Charge;\n /** True if this is a replay of an existing charge */\n idempotentReplay?: boolean;\n error?: string;\n details?: string;\n}\n\nexport interface SessionGrant {\n /** Session key ID */\n sessionId: string;\n /** User's wallet address */\n userWallet: string;\n /** Your merchant wallet address */\n merchantWallet: string;\n /** Spending limits */\n limits: {\n maxTotalUsd?: number;\n maxPerTxUsd?: number;\n expiresAt: string;\n };\n /** When the session was granted */\n grantedAt: string;\n}\n\n// Internal types for API responses\ninterface CreateChargeApiResponse {\n success: boolean;\n charge_id?: string;\n status?: string;\n amount_usd?: number;\n amount_usdc?: string;\n tx_hash?: string;\n explorer_url?: string;\n idempotent_replay?: boolean;\n error?: string;\n details?: string;\n}\n\ninterface ChargeApiResponse {\n id: string;\n status: string;\n amount_usd: number;\n amount_usdc: string;\n tx_hash?: string;\n explorer_url?: string;\n from_address: string;\n to_address: string;\n reference: string;\n created_at: string;\n confirmed_at?: string;\n session_name?: string;\n user_wallet?: string;\n}\n\nexport interface ChargesClientOptions {\n /** Your API key */\n apiKey: string;\n /** MixrPay API base URL */\n baseUrl?: string;\n}\n\n// =============================================================================\n// Client\n// =============================================================================\n\nexport class ChargesClient {\n private apiKey: string;\n private baseUrl: string;\n\n constructor(options: ChargesClientOptions) {\n this.apiKey = options.apiKey;\n this.baseUrl = options.baseUrl || process.env.MIXRPAY_BASE_URL || 'https://mixrpay.com';\n }\n\n /**\n * Create a new charge.\n * \n * @example\n * ```typescript\n * const result = await charges.create({\n * sessionId: 'sk_xxx',\n * amountUsd: 0.05,\n * reference: 'image_gen_123',\n * metadata: { feature: 'image_generation' }\n * });\n * \n * if (result.success) {\n * console.log(`Charged ${result.charge.amountUsdc}`);\n * console.log(`TX: ${result.charge.explorerUrl}`);\n * }\n * ```\n */\n async create(params: CreateChargeParams): Promise<CreateChargeResult> {\n const response = await fetch(`${this.baseUrl}/api/v1/charges`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({\n session_id: params.sessionId,\n amount_usd: params.amountUsd,\n reference: params.reference,\n metadata: params.metadata,\n }),\n });\n\n const data = await response.json() as CreateChargeApiResponse;\n\n if (!response.ok) {\n return {\n success: false,\n error: data.error,\n details: data.details,\n };\n }\n\n return {\n success: data.success,\n charge: data.charge_id ? {\n id: data.charge_id,\n status: data.status as Charge['status'],\n amountUsd: data.amount_usd ?? 0,\n amountUsdc: data.amount_usdc ?? '0',\n txHash: data.tx_hash,\n explorerUrl: data.explorer_url,\n fromAddress: '',\n toAddress: '',\n reference: params.reference,\n createdAt: new Date().toISOString(),\n } : undefined,\n idempotentReplay: data.idempotent_replay,\n error: data.error,\n };\n }\n\n /**\n * Get a charge by ID.\n * \n * @example\n * ```typescript\n * const charge = await charges.get('chg_xxx');\n * console.log(charge.status); // 'confirmed'\n * ```\n */\n async get(chargeId: string): Promise<Charge | null> {\n const response = await fetch(`${this.baseUrl}/api/v1/charges?id=${chargeId}`, {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n },\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n return null;\n }\n throw new Error(`Failed to get charge: ${response.statusText}`);\n }\n\n const data = await response.json() as ChargeApiResponse;\n\n return {\n id: data.id,\n status: data.status as Charge['status'],\n amountUsd: data.amount_usd,\n amountUsdc: data.amount_usdc,\n txHash: data.tx_hash,\n explorerUrl: data.explorer_url,\n fromAddress: data.from_address,\n toAddress: data.to_address,\n reference: data.reference,\n createdAt: data.created_at,\n confirmedAt: data.confirmed_at,\n sessionName: data.session_name,\n userWallet: data.user_wallet,\n };\n }\n\n /**\n * Get a charge by transaction hash.\n */\n async getByTxHash(txHash: string): Promise<Charge | null> {\n const response = await fetch(`${this.baseUrl}/api/v1/charges?tx_hash=${txHash}`, {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n },\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n return null;\n }\n throw new Error(`Failed to get charge: ${response.statusText}`);\n }\n\n const data = await response.json() as ChargeApiResponse;\n\n return {\n id: data.id,\n status: data.status as Charge['status'],\n amountUsd: data.amount_usd,\n amountUsdc: data.amount_usdc,\n txHash: data.tx_hash,\n explorerUrl: data.explorer_url,\n fromAddress: data.from_address,\n toAddress: data.to_address,\n reference: data.reference,\n createdAt: data.created_at,\n confirmedAt: data.confirmed_at,\n sessionName: data.session_name,\n userWallet: data.user_wallet,\n };\n }\n}\n\n// =============================================================================\n// Webhook Verification\n// =============================================================================\n\nimport crypto from 'crypto';\n\n/**\n * Verify a session.granted webhook signature.\n * \n * @example\n * ```typescript\n * const payload = req.body;\n * const signature = req.headers['x-mixrpay-signature'];\n * \n * if (verifySessionWebhook(JSON.stringify(payload), signature, webhookSecret)) {\n * const grant = parseSessionGrant(payload);\n * console.log(`User ${grant.userWallet} granted access`);\n * }\n * ```\n */\nexport function verifySessionWebhook(\n payload: string,\n signature: string,\n secret: string\n): boolean {\n const expectedSignature = crypto\n .createHmac('sha256', secret)\n .update(payload)\n .digest('hex');\n \n return crypto.timingSafeEqual(\n Buffer.from(signature),\n Buffer.from(expectedSignature)\n );\n}\n\n/**\n * Parse a session.granted webhook payload.\n */\nexport function parseSessionGrant(payload: {\n event: string;\n session_id: string;\n user_wallet: string;\n merchant_wallet: string;\n limits: {\n max_total_usd?: number;\n max_per_tx_usd?: number;\n expires_at: string;\n };\n granted_at: string;\n}): SessionGrant {\n if (payload.event !== 'session.granted') {\n throw new Error(`Unexpected event type: ${payload.event}`);\n }\n\n return {\n sessionId: payload.session_id,\n userWallet: payload.user_wallet,\n merchantWallet: payload.merchant_wallet,\n limits: {\n maxTotalUsd: payload.limits.max_total_usd,\n maxPerTxUsd: payload.limits.max_per_tx_usd,\n expiresAt: payload.limits.expires_at,\n },\n grantedAt: payload.granted_at,\n };\n}\n\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACiBA,WAAsB;AAOtB,IAAM,mBAAmB,QAAQ,IAAI,oBAAoB;AACzD,IAAM,oBAAoB,KAAK,KAAK;AAWpC,IAAM,YAAY,oBAAI,IAAwB;AAK9C,eAAe,QAAQ,SAA8C;AACnE,QAAM,SAAS,UAAU,IAAI,OAAO;AACpC,QAAM,MAAM,KAAK,IAAI;AAGrB,MAAI,UAAW,MAAM,OAAO,YAAa,mBAAmB;AAC1D,WAAO,OAAO;AAAA,EAChB;AAGA,QAAM,WAAW,MAAM,MAAM,OAAO;AAEpC,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,6BAA6B,OAAO,KAAK,SAAS,MAAM,IAAI,SAAS,UAAU,EAAE;AAAA,EACnG;AAEA,QAAM,OAAO,MAAM,SAAS,KAAK;AAGjC,MAAI,CAAC,KAAK,QAAQ,CAAC,MAAM,QAAQ,KAAK,IAAI,KAAK,KAAK,KAAK,WAAW,GAAG;AACrE,UAAM,IAAI,MAAM,2CAA2C;AAAA,EAC7D;AAGA,YAAU,IAAI,SAAS,EAAE,MAAM,WAAW,IAAI,CAAC;AAE/C,SAAO;AACT;AAKA,eAAe,aAAa,SAAgD;AAC1E,QAAM,OAAO,MAAM,QAAQ,OAAO;AAClC,SAAY,uBAAkB,IAAI;AACpC;AA+BA,eAAsB,qBACpB,SACA,SACyB;AACzB,QAAM,UAAU,SAAS,WAAW;AAEpC,MAAI;AAEF,UAAM,SAAS,MAAM,aAAa,OAAO;AAGzC,UAAM,gBAAuC;AAAA,MAC3C,YAAY,CAAC,OAAO;AAAA,IACtB;AAGA,QAAI,SAAS,QAAQ;AACnB,oBAAc,SAAS,QAAQ;AAAA,IACjC;AAGA,UAAM,EAAE,QAAQ,IAAI,MAAW,eAAU,SAAS,QAAQ,aAAa;AAGvE,UAAM,iBAAiB,CAAC,aAAa,UAAU,aAAa,SAAS,aAAa,WAAW,UAAU,WAAW;AAClH,eAAW,SAAS,gBAAgB;AAClC,UAAI,EAAE,SAAS,UAAU;AACvB,cAAM,IAAI,MAAM,sCAAsC,KAAK,EAAE;AAAA,MAC/D;AAAA,IACF;AAGA,WAAO;AAAA,MACL,WAAW,QAAQ;AAAA,MACnB,QAAQ,QAAQ;AAAA,MAChB,WAAW,QAAQ;AAAA,MACnB,OAAO,QAAQ;AAAA,MACf,WAAW,QAAQ;AAAA,MACnB,SAAS,QAAQ;AAAA,MACjB,QAAQ,QAAQ;AAAA,MAChB,WAAW,QAAQ;AAAA,MACnB,UAAU,IAAI,KAAM,QAAQ,MAAiB,GAAI;AAAA,MACjD,WAAW,IAAI,KAAM,QAAQ,MAAiB,GAAI;AAAA,IACpD;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAsB,YAAO,YAAY;AAC3C,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,QAAI,iBAAsB,YAAO,0BAA0B;AACzD,YAAM,IAAI,MAAM,8BAA8B,MAAM,OAAO,EAAE;AAAA,IAC/D;AACA,QAAI,iBAAsB,YAAO,gCAAgC;AAC/D,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AACA,UAAM;AAAA,EACR;AACF;;;ACgHA,oBAAmB;AAgBZ,SAAS,qBACd,SACA,WACA,QACS;AACT,QAAM,oBAAoB,cAAAA,QACvB,WAAW,UAAU,MAAM,EAC3B,OAAO,OAAO,EACd,OAAO,KAAK;AAEf,SAAO,cAAAA,QAAO;AAAA,IACZ,OAAO,KAAK,SAAS;AAAA,IACrB,OAAO,KAAK,iBAAiB;AAAA,EAC/B;AACF;","names":["crypto"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/receipt.ts","../src/charges.ts"],"sourcesContent":["/**\n * MixrPay Merchant SDK\n * \n * Accept payments from AI agents and web apps with one middleware.\n * \n * @example Express\n * ```typescript\n * import { mixrpay } from '@mixrpay/merchant-sdk/express';\n * \n * app.post('/api/query', mixrpay({ priceUsd: 0.05 }), (req, res) => {\n * console.log(`Paid by ${req.mixrPayment?.payer}`);\n * res.json({ result: 'success' });\n * });\n * ```\n * \n * @example Next.js\n * ```typescript\n * import { withMixrPay } from '@mixrpay/merchant-sdk/nextjs';\n * \n * export const POST = withMixrPay({ priceUsd: 0.05 }, async (req, payment) => {\n * return NextResponse.json({ result: 'success' });\n * });\n * ```\n * \n * @packageDocumentation\n */\n\n// Receipt verification (for custom integrations)\nexport { verifyPaymentReceipt } from './receipt';\n\n// Session webhook verification\nexport { verifySessionWebhook } from './charges';\n\n// Types for TypeScript users\nexport type {\n MixrPayOptions,\n MixrPayPaymentResult,\n PaymentMethod,\n PriceContext,\n PaymentReceipt,\n} from './types';\n\nexport type {\n SessionGrant,\n} from './charges';\n\n// Widget element types (for TypeScript JSX support)\nexport type {} from './widget-elements';\n","/**\n * MixrPay Merchant SDK - Payment Receipt Verification\n * \n * Verify JWT payment receipts issued by MixrPay after successful x402 payments.\n * \n * @example\n * ```typescript\n * import { verifyPaymentReceipt } from '@mixrpay/merchant-sdk';\n * \n * const receipt = req.headers['x-payment-receipt'];\n * const payment = await verifyPaymentReceipt(receipt);\n * \n * console.log(`Payment received: $${payment.amountUsd} from ${payment.payer}`);\n * console.log(`Transaction: ${payment.txHash}`);\n * ```\n */\n\nimport * as jose from 'jose';\nimport type { PaymentReceipt, VerifyReceiptOptions } from './types';\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nconst DEFAULT_JWKS_URL = process.env.MIXRPAY_JWKS_URL || 'https://www.mixrpay.com/.well-known/jwks';\nconst JWKS_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour\n\n// =============================================================================\n// JWKS Cache\n// =============================================================================\n\ninterface CachedJWKS {\n jwks: jose.JSONWebKeySet;\n fetchedAt: number;\n}\n\nconst jwksCache = new Map<string, CachedJWKS>();\n\n/**\n * Fetch and cache JWKS from the specified URL.\n */\nasync function getJWKS(jwksUrl: string): Promise<jose.JSONWebKeySet> {\n const cached = jwksCache.get(jwksUrl);\n const now = Date.now();\n\n // Return cached JWKS if still valid\n if (cached && (now - cached.fetchedAt) < JWKS_CACHE_TTL_MS) {\n return cached.jwks;\n }\n\n // Fetch fresh JWKS\n const response = await fetch(jwksUrl);\n \n if (!response.ok) {\n throw new Error(`Failed to fetch JWKS from ${jwksUrl}: ${response.status} ${response.statusText}`);\n }\n\n const jwks = await response.json() as jose.JSONWebKeySet;\n\n // Validate JWKS structure\n if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {\n throw new Error('Invalid JWKS: missing or empty keys array');\n }\n\n // Cache the JWKS\n jwksCache.set(jwksUrl, { jwks, fetchedAt: now });\n\n return jwks;\n}\n\n/**\n * Create a JWKS key set from a fetched JWKS.\n */\nasync function createKeySet(jwksUrl: string): Promise<jose.JWTVerifyGetKey> {\n const jwks = await getJWKS(jwksUrl);\n return jose.createLocalJWKSet(jwks);\n}\n\n// =============================================================================\n// Receipt Verification\n// =============================================================================\n\n/**\n * Verify a JWT payment receipt from MixrPay.\n * \n * This function:\n * 1. Fetches the JWKS from MixrPay (cached for 1 hour)\n * 2. Verifies the JWT signature using RS256\n * 3. Validates standard JWT claims (exp, iat)\n * 4. Returns the typed payment receipt\n * \n * @param receipt - The JWT receipt string from X-Payment-Receipt header\n * @param options - Optional configuration (custom JWKS URL, issuer validation)\n * @returns Verified payment receipt\n * @throws Error if verification fails\n * \n * @example\n * ```typescript\n * // Basic usage\n * const payment = await verifyPaymentReceipt(receipt);\n * \n * // With custom JWKS URL (for testing or self-hosted)\n * const payment = await verifyPaymentReceipt(receipt, {\n * jwksUrl: 'https://your-mixrpay.com/.well-known/jwks'\n * });\n * ```\n */\nexport async function verifyPaymentReceipt(\n receipt: string,\n options?: VerifyReceiptOptions\n): Promise<PaymentReceipt> {\n const jwksUrl = options?.jwksUrl || DEFAULT_JWKS_URL;\n\n try {\n // Get the key set\n const keySet = await createKeySet(jwksUrl);\n\n // Build verification options\n const verifyOptions: jose.JWTVerifyOptions = {\n algorithms: ['RS256'],\n };\n\n // Add issuer validation if specified\n if (options?.issuer) {\n verifyOptions.issuer = options.issuer;\n }\n\n // Verify the JWT\n const { payload } = await jose.jwtVerify(receipt, keySet, verifyOptions);\n\n // Validate required payment fields\n const requiredFields = ['paymentId', 'amount', 'amountUsd', 'payer', 'recipient', 'chainId', 'txHash', 'settledAt'];\n for (const field of requiredFields) {\n if (!(field in payload)) {\n throw new Error(`Missing required field in receipt: ${field}`);\n }\n }\n\n // Return typed receipt\n return {\n paymentId: payload.paymentId as string,\n amount: payload.amount as string,\n amountUsd: payload.amountUsd as number,\n payer: payload.payer as string,\n recipient: payload.recipient as string,\n chainId: payload.chainId as number,\n txHash: payload.txHash as string,\n settledAt: payload.settledAt as string,\n issuedAt: new Date((payload.iat as number) * 1000),\n expiresAt: new Date((payload.exp as number) * 1000),\n };\n } catch (error) {\n if (error instanceof jose.errors.JWTExpired) {\n throw new Error('Payment receipt has expired');\n }\n if (error instanceof jose.errors.JWTClaimValidationFailed) {\n throw new Error(`Receipt validation failed: ${error.message}`);\n }\n if (error instanceof jose.errors.JWSSignatureVerificationFailed) {\n throw new Error('Invalid receipt signature');\n }\n throw error;\n }\n}\n\n/**\n * Parse a JWT payment receipt without verification.\n * \n * WARNING: This does NOT verify the signature. Use only for debugging\n * or when you've already verified the receipt elsewhere.\n * \n * @param receipt - The JWT receipt string\n * @returns Decoded payload (unverified)\n */\nexport function parsePaymentReceipt(receipt: string): PaymentReceipt {\n const decoded = jose.decodeJwt(receipt);\n\n return {\n paymentId: decoded.paymentId as string,\n amount: decoded.amount as string,\n amountUsd: decoded.amountUsd as number,\n payer: decoded.payer as string,\n recipient: decoded.recipient as string,\n chainId: decoded.chainId as number,\n txHash: decoded.txHash as string,\n settledAt: decoded.settledAt as string,\n issuedAt: decoded.iat ? new Date(decoded.iat * 1000) : undefined,\n expiresAt: decoded.exp ? new Date(decoded.exp * 1000) : undefined,\n };\n}\n\n/**\n * Check if a receipt is expired.\n * \n * @param receipt - The JWT receipt string or parsed PaymentReceipt\n * @returns true if expired, false otherwise\n */\nexport function isReceiptExpired(receipt: string | PaymentReceipt): boolean {\n const parsed = typeof receipt === 'string' ? parsePaymentReceipt(receipt) : receipt;\n \n if (!parsed.expiresAt) {\n return false; // No expiration = never expires\n }\n\n return parsed.expiresAt.getTime() < Date.now();\n}\n\n/**\n * Clear the JWKS cache.\n * Useful for testing or when keys have rotated.\n */\nexport function clearJWKSCache(): void {\n jwksCache.clear();\n}\n\n/**\n * Get the default JWKS URL.\n */\nexport function getDefaultJWKSUrl(): string {\n return DEFAULT_JWKS_URL;\n}\n\n","/**\n * MixrPay Charges Module\n * \n * Create and manage charges using session signers.\n * Session signers allow you to charge user wallets without\n * requiring approval for each transaction.\n */\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface CreateChargeParams {\n /** Session key ID granted by the user */\n sessionId: string;\n /** Amount to charge in USD (e.g., 0.05 for 5 cents) */\n amountUsd: number;\n /** Unique reference for idempotency (e.g., \"order_123\") */\n reference: string;\n /** Optional metadata */\n metadata?: Record<string, unknown>;\n}\n\nexport interface Charge {\n /** Unique charge ID */\n id: string;\n /** Current status */\n status: 'pending' | 'submitted' | 'confirmed' | 'failed';\n /** Amount in USD */\n amountUsd: number;\n /** Formatted USDC amount */\n amountUsdc: string;\n /** Transaction hash (if submitted) */\n txHash?: string;\n /** Block explorer URL */\n explorerUrl?: string;\n /** From wallet address */\n fromAddress: string;\n /** To wallet address */\n toAddress: string;\n /** Your reference */\n reference: string;\n /** When the charge was created */\n createdAt: string;\n /** When the charge was confirmed */\n confirmedAt?: string;\n /** Session name */\n sessionName?: string;\n /** User wallet address */\n userWallet?: string;\n /** Error message if failed */\n error?: string;\n}\n\nexport interface CreateChargeResult {\n success: boolean;\n charge?: Charge;\n /** True if this is a replay of an existing charge */\n idempotentReplay?: boolean;\n error?: string;\n details?: string;\n}\n\nexport interface SessionGrant {\n /** Session key ID */\n sessionId: string;\n /** User's wallet address */\n userWallet: string;\n /** Your merchant wallet address */\n merchantWallet: string;\n /** Spending limits */\n limits: {\n maxTotalUsd?: number;\n maxPerTxUsd?: number;\n expiresAt: string;\n };\n /** When the session was granted */\n grantedAt: string;\n}\n\n// Internal types for API responses\ninterface CreateChargeApiResponse {\n success: boolean;\n charge_id?: string;\n status?: string;\n amount_usd?: number;\n amount_usdc?: string;\n tx_hash?: string;\n explorer_url?: string;\n idempotent_replay?: boolean;\n error?: string;\n details?: string;\n}\n\ninterface ChargeApiResponse {\n id: string;\n status: string;\n amount_usd: number;\n amount_usdc: string;\n tx_hash?: string;\n explorer_url?: string;\n from_address: string;\n to_address: string;\n reference: string;\n created_at: string;\n confirmed_at?: string;\n session_name?: string;\n user_wallet?: string;\n}\n\nexport interface ChargesClientOptions {\n /** Your API key */\n apiKey: string;\n /** MixrPay API base URL */\n baseUrl?: string;\n}\n\n// =============================================================================\n// Client\n// =============================================================================\n\nexport class ChargesClient {\n private apiKey: string;\n private baseUrl: string;\n\n constructor(options: ChargesClientOptions) {\n this.apiKey = options.apiKey;\n this.baseUrl = options.baseUrl || process.env.MIXRPAY_BASE_URL || 'https://www.mixrpay.com';\n }\n\n /**\n * Create a new charge.\n * \n * @example\n * ```typescript\n * const result = await charges.create({\n * sessionId: 'sk_xxx',\n * amountUsd: 0.05,\n * reference: 'image_gen_123',\n * metadata: { feature: 'image_generation' }\n * });\n * \n * if (result.success) {\n * console.log(`Charged ${result.charge.amountUsdc}`);\n * console.log(`TX: ${result.charge.explorerUrl}`);\n * }\n * ```\n */\n async create(params: CreateChargeParams): Promise<CreateChargeResult> {\n const response = await fetch(`${this.baseUrl}/api/v1/charges`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({\n session_id: params.sessionId,\n amount_usd: params.amountUsd,\n reference: params.reference,\n metadata: params.metadata,\n }),\n });\n\n const data = await response.json() as CreateChargeApiResponse;\n\n if (!response.ok) {\n return {\n success: false,\n error: data.error,\n details: data.details,\n };\n }\n\n return {\n success: data.success,\n charge: data.charge_id ? {\n id: data.charge_id,\n status: data.status as Charge['status'],\n amountUsd: data.amount_usd ?? 0,\n amountUsdc: data.amount_usdc ?? '0',\n txHash: data.tx_hash,\n explorerUrl: data.explorer_url,\n fromAddress: '',\n toAddress: '',\n reference: params.reference,\n createdAt: new Date().toISOString(),\n } : undefined,\n idempotentReplay: data.idempotent_replay,\n error: data.error,\n };\n }\n\n /**\n * Get a charge by ID.\n * \n * @example\n * ```typescript\n * const charge = await charges.get('chg_xxx');\n * console.log(charge.status); // 'confirmed'\n * ```\n */\n async get(chargeId: string): Promise<Charge | null> {\n const response = await fetch(`${this.baseUrl}/api/v1/charges?id=${chargeId}`, {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n },\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n return null;\n }\n throw new Error(`Failed to get charge: ${response.statusText}`);\n }\n\n const data = await response.json() as ChargeApiResponse;\n\n return {\n id: data.id,\n status: data.status as Charge['status'],\n amountUsd: data.amount_usd,\n amountUsdc: data.amount_usdc,\n txHash: data.tx_hash,\n explorerUrl: data.explorer_url,\n fromAddress: data.from_address,\n toAddress: data.to_address,\n reference: data.reference,\n createdAt: data.created_at,\n confirmedAt: data.confirmed_at,\n sessionName: data.session_name,\n userWallet: data.user_wallet,\n };\n }\n\n /**\n * Get a charge by transaction hash.\n */\n async getByTxHash(txHash: string): Promise<Charge | null> {\n const response = await fetch(`${this.baseUrl}/api/v1/charges?tx_hash=${txHash}`, {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n },\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n return null;\n }\n throw new Error(`Failed to get charge: ${response.statusText}`);\n }\n\n const data = await response.json() as ChargeApiResponse;\n\n return {\n id: data.id,\n status: data.status as Charge['status'],\n amountUsd: data.amount_usd,\n amountUsdc: data.amount_usdc,\n txHash: data.tx_hash,\n explorerUrl: data.explorer_url,\n fromAddress: data.from_address,\n toAddress: data.to_address,\n reference: data.reference,\n createdAt: data.created_at,\n confirmedAt: data.confirmed_at,\n sessionName: data.session_name,\n userWallet: data.user_wallet,\n };\n }\n}\n\n// =============================================================================\n// Webhook Verification\n// =============================================================================\n\nimport crypto from 'crypto';\n\n/**\n * Verify a session.granted webhook signature.\n * \n * @example\n * ```typescript\n * const payload = req.body;\n * const signature = req.headers['x-mixrpay-signature'];\n * \n * if (verifySessionWebhook(JSON.stringify(payload), signature, webhookSecret)) {\n * const grant = parseSessionGrant(payload);\n * console.log(`User ${grant.userWallet} granted access`);\n * }\n * ```\n */\nexport function verifySessionWebhook(\n payload: string,\n signature: string,\n secret: string\n): boolean {\n const expectedSignature = crypto\n .createHmac('sha256', secret)\n .update(payload)\n .digest('hex');\n \n return crypto.timingSafeEqual(\n Buffer.from(signature),\n Buffer.from(expectedSignature)\n );\n}\n\n/**\n * Parse a session.granted webhook payload.\n */\nexport function parseSessionGrant(payload: {\n event: string;\n session_id: string;\n user_wallet: string;\n merchant_wallet: string;\n limits: {\n max_total_usd?: number;\n max_per_tx_usd?: number;\n expires_at: string;\n };\n granted_at: string;\n}): SessionGrant {\n if (payload.event !== 'session.granted') {\n throw new Error(`Unexpected event type: ${payload.event}`);\n }\n\n return {\n sessionId: payload.session_id,\n userWallet: payload.user_wallet,\n merchantWallet: payload.merchant_wallet,\n limits: {\n maxTotalUsd: payload.limits.max_total_usd,\n maxPerTxUsd: payload.limits.max_per_tx_usd,\n expiresAt: payload.limits.expires_at,\n },\n grantedAt: payload.granted_at,\n };\n}\n\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACiBA,WAAsB;AAOtB,IAAM,mBAAmB,QAAQ,IAAI,oBAAoB;AACzD,IAAM,oBAAoB,KAAK,KAAK;AAWpC,IAAM,YAAY,oBAAI,IAAwB;AAK9C,eAAe,QAAQ,SAA8C;AACnE,QAAM,SAAS,UAAU,IAAI,OAAO;AACpC,QAAM,MAAM,KAAK,IAAI;AAGrB,MAAI,UAAW,MAAM,OAAO,YAAa,mBAAmB;AAC1D,WAAO,OAAO;AAAA,EAChB;AAGA,QAAM,WAAW,MAAM,MAAM,OAAO;AAEpC,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,6BAA6B,OAAO,KAAK,SAAS,MAAM,IAAI,SAAS,UAAU,EAAE;AAAA,EACnG;AAEA,QAAM,OAAO,MAAM,SAAS,KAAK;AAGjC,MAAI,CAAC,KAAK,QAAQ,CAAC,MAAM,QAAQ,KAAK,IAAI,KAAK,KAAK,KAAK,WAAW,GAAG;AACrE,UAAM,IAAI,MAAM,2CAA2C;AAAA,EAC7D;AAGA,YAAU,IAAI,SAAS,EAAE,MAAM,WAAW,IAAI,CAAC;AAE/C,SAAO;AACT;AAKA,eAAe,aAAa,SAAgD;AAC1E,QAAM,OAAO,MAAM,QAAQ,OAAO;AAClC,SAAY,uBAAkB,IAAI;AACpC;AA+BA,eAAsB,qBACpB,SACA,SACyB;AACzB,QAAM,UAAU,SAAS,WAAW;AAEpC,MAAI;AAEF,UAAM,SAAS,MAAM,aAAa,OAAO;AAGzC,UAAM,gBAAuC;AAAA,MAC3C,YAAY,CAAC,OAAO;AAAA,IACtB;AAGA,QAAI,SAAS,QAAQ;AACnB,oBAAc,SAAS,QAAQ;AAAA,IACjC;AAGA,UAAM,EAAE,QAAQ,IAAI,MAAW,eAAU,SAAS,QAAQ,aAAa;AAGvE,UAAM,iBAAiB,CAAC,aAAa,UAAU,aAAa,SAAS,aAAa,WAAW,UAAU,WAAW;AAClH,eAAW,SAAS,gBAAgB;AAClC,UAAI,EAAE,SAAS,UAAU;AACvB,cAAM,IAAI,MAAM,sCAAsC,KAAK,EAAE;AAAA,MAC/D;AAAA,IACF;AAGA,WAAO;AAAA,MACL,WAAW,QAAQ;AAAA,MACnB,QAAQ,QAAQ;AAAA,MAChB,WAAW,QAAQ;AAAA,MACnB,OAAO,QAAQ;AAAA,MACf,WAAW,QAAQ;AAAA,MACnB,SAAS,QAAQ;AAAA,MACjB,QAAQ,QAAQ;AAAA,MAChB,WAAW,QAAQ;AAAA,MACnB,UAAU,IAAI,KAAM,QAAQ,MAAiB,GAAI;AAAA,MACjD,WAAW,IAAI,KAAM,QAAQ,MAAiB,GAAI;AAAA,IACpD;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAsB,YAAO,YAAY;AAC3C,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,QAAI,iBAAsB,YAAO,0BAA0B;AACzD,YAAM,IAAI,MAAM,8BAA8B,MAAM,OAAO,EAAE;AAAA,IAC/D;AACA,QAAI,iBAAsB,YAAO,gCAAgC;AAC/D,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AACA,UAAM;AAAA,EACR;AACF;;;ACgHA,oBAAmB;AAgBZ,SAAS,qBACd,SACA,WACA,QACS;AACT,QAAM,oBAAoB,cAAAA,QACvB,WAAW,UAAU,MAAM,EAC3B,OAAO,OAAO,EACd,OAAO,KAAK;AAEf,SAAO,cAAAA,QAAO;AAAA,IACZ,OAAO,KAAK,SAAS;AAAA,IACrB,OAAO,KAAK,iBAAiB;AAAA,EAC/B;AACF;","names":["crypto"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/receipt.ts
|
|
2
2
|
import * as jose from "jose";
|
|
3
|
-
var DEFAULT_JWKS_URL = process.env.MIXRPAY_JWKS_URL || "https://mixrpay.com/.well-known/jwks";
|
|
3
|
+
var DEFAULT_JWKS_URL = process.env.MIXRPAY_JWKS_URL || "https://www.mixrpay.com/.well-known/jwks";
|
|
4
4
|
var JWKS_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
5
5
|
var jwksCache = /* @__PURE__ */ new Map();
|
|
6
6
|
async function getJWKS(jwksUrl) {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/receipt.ts","../src/charges.ts"],"sourcesContent":["/**\n * MixrPay Merchant SDK - Payment Receipt Verification\n * \n * Verify JWT payment receipts issued by MixrPay after successful x402 payments.\n * \n * @example\n * ```typescript\n * import { verifyPaymentReceipt } from '@mixrpay/merchant-sdk';\n * \n * const receipt = req.headers['x-payment-receipt'];\n * const payment = await verifyPaymentReceipt(receipt);\n * \n * console.log(`Payment received: $${payment.amountUsd} from ${payment.payer}`);\n * console.log(`Transaction: ${payment.txHash}`);\n * ```\n */\n\nimport * as jose from 'jose';\nimport type { PaymentReceipt, VerifyReceiptOptions } from './types';\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nconst DEFAULT_JWKS_URL = process.env.MIXRPAY_JWKS_URL || 'https://mixrpay.com/.well-known/jwks';\nconst JWKS_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour\n\n// =============================================================================\n// JWKS Cache\n// =============================================================================\n\ninterface CachedJWKS {\n jwks: jose.JSONWebKeySet;\n fetchedAt: number;\n}\n\nconst jwksCache = new Map<string, CachedJWKS>();\n\n/**\n * Fetch and cache JWKS from the specified URL.\n */\nasync function getJWKS(jwksUrl: string): Promise<jose.JSONWebKeySet> {\n const cached = jwksCache.get(jwksUrl);\n const now = Date.now();\n\n // Return cached JWKS if still valid\n if (cached && (now - cached.fetchedAt) < JWKS_CACHE_TTL_MS) {\n return cached.jwks;\n }\n\n // Fetch fresh JWKS\n const response = await fetch(jwksUrl);\n \n if (!response.ok) {\n throw new Error(`Failed to fetch JWKS from ${jwksUrl}: ${response.status} ${response.statusText}`);\n }\n\n const jwks = await response.json() as jose.JSONWebKeySet;\n\n // Validate JWKS structure\n if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {\n throw new Error('Invalid JWKS: missing or empty keys array');\n }\n\n // Cache the JWKS\n jwksCache.set(jwksUrl, { jwks, fetchedAt: now });\n\n return jwks;\n}\n\n/**\n * Create a JWKS key set from a fetched JWKS.\n */\nasync function createKeySet(jwksUrl: string): Promise<jose.JWTVerifyGetKey> {\n const jwks = await getJWKS(jwksUrl);\n return jose.createLocalJWKSet(jwks);\n}\n\n// =============================================================================\n// Receipt Verification\n// =============================================================================\n\n/**\n * Verify a JWT payment receipt from MixrPay.\n * \n * This function:\n * 1. Fetches the JWKS from MixrPay (cached for 1 hour)\n * 2. Verifies the JWT signature using RS256\n * 3. Validates standard JWT claims (exp, iat)\n * 4. Returns the typed payment receipt\n * \n * @param receipt - The JWT receipt string from X-Payment-Receipt header\n * @param options - Optional configuration (custom JWKS URL, issuer validation)\n * @returns Verified payment receipt\n * @throws Error if verification fails\n * \n * @example\n * ```typescript\n * // Basic usage\n * const payment = await verifyPaymentReceipt(receipt);\n * \n * // With custom JWKS URL (for testing or self-hosted)\n * const payment = await verifyPaymentReceipt(receipt, {\n * jwksUrl: 'https://your-mixrpay.com/.well-known/jwks'\n * });\n * ```\n */\nexport async function verifyPaymentReceipt(\n receipt: string,\n options?: VerifyReceiptOptions\n): Promise<PaymentReceipt> {\n const jwksUrl = options?.jwksUrl || DEFAULT_JWKS_URL;\n\n try {\n // Get the key set\n const keySet = await createKeySet(jwksUrl);\n\n // Build verification options\n const verifyOptions: jose.JWTVerifyOptions = {\n algorithms: ['RS256'],\n };\n\n // Add issuer validation if specified\n if (options?.issuer) {\n verifyOptions.issuer = options.issuer;\n }\n\n // Verify the JWT\n const { payload } = await jose.jwtVerify(receipt, keySet, verifyOptions);\n\n // Validate required payment fields\n const requiredFields = ['paymentId', 'amount', 'amountUsd', 'payer', 'recipient', 'chainId', 'txHash', 'settledAt'];\n for (const field of requiredFields) {\n if (!(field in payload)) {\n throw new Error(`Missing required field in receipt: ${field}`);\n }\n }\n\n // Return typed receipt\n return {\n paymentId: payload.paymentId as string,\n amount: payload.amount as string,\n amountUsd: payload.amountUsd as number,\n payer: payload.payer as string,\n recipient: payload.recipient as string,\n chainId: payload.chainId as number,\n txHash: payload.txHash as string,\n settledAt: payload.settledAt as string,\n issuedAt: new Date((payload.iat as number) * 1000),\n expiresAt: new Date((payload.exp as number) * 1000),\n };\n } catch (error) {\n if (error instanceof jose.errors.JWTExpired) {\n throw new Error('Payment receipt has expired');\n }\n if (error instanceof jose.errors.JWTClaimValidationFailed) {\n throw new Error(`Receipt validation failed: ${error.message}`);\n }\n if (error instanceof jose.errors.JWSSignatureVerificationFailed) {\n throw new Error('Invalid receipt signature');\n }\n throw error;\n }\n}\n\n/**\n * Parse a JWT payment receipt without verification.\n * \n * WARNING: This does NOT verify the signature. Use only for debugging\n * or when you've already verified the receipt elsewhere.\n * \n * @param receipt - The JWT receipt string\n * @returns Decoded payload (unverified)\n */\nexport function parsePaymentReceipt(receipt: string): PaymentReceipt {\n const decoded = jose.decodeJwt(receipt);\n\n return {\n paymentId: decoded.paymentId as string,\n amount: decoded.amount as string,\n amountUsd: decoded.amountUsd as number,\n payer: decoded.payer as string,\n recipient: decoded.recipient as string,\n chainId: decoded.chainId as number,\n txHash: decoded.txHash as string,\n settledAt: decoded.settledAt as string,\n issuedAt: decoded.iat ? new Date(decoded.iat * 1000) : undefined,\n expiresAt: decoded.exp ? new Date(decoded.exp * 1000) : undefined,\n };\n}\n\n/**\n * Check if a receipt is expired.\n * \n * @param receipt - The JWT receipt string or parsed PaymentReceipt\n * @returns true if expired, false otherwise\n */\nexport function isReceiptExpired(receipt: string | PaymentReceipt): boolean {\n const parsed = typeof receipt === 'string' ? parsePaymentReceipt(receipt) : receipt;\n \n if (!parsed.expiresAt) {\n return false; // No expiration = never expires\n }\n\n return parsed.expiresAt.getTime() < Date.now();\n}\n\n/**\n * Clear the JWKS cache.\n * Useful for testing or when keys have rotated.\n */\nexport function clearJWKSCache(): void {\n jwksCache.clear();\n}\n\n/**\n * Get the default JWKS URL.\n */\nexport function getDefaultJWKSUrl(): string {\n return DEFAULT_JWKS_URL;\n}\n\n","/**\n * MixrPay Charges Module\n * \n * Create and manage charges using session signers.\n * Session signers allow you to charge user wallets without\n * requiring approval for each transaction.\n */\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface CreateChargeParams {\n /** Session key ID granted by the user */\n sessionId: string;\n /** Amount to charge in USD (e.g., 0.05 for 5 cents) */\n amountUsd: number;\n /** Unique reference for idempotency (e.g., \"order_123\") */\n reference: string;\n /** Optional metadata */\n metadata?: Record<string, unknown>;\n}\n\nexport interface Charge {\n /** Unique charge ID */\n id: string;\n /** Current status */\n status: 'pending' | 'submitted' | 'confirmed' | 'failed';\n /** Amount in USD */\n amountUsd: number;\n /** Formatted USDC amount */\n amountUsdc: string;\n /** Transaction hash (if submitted) */\n txHash?: string;\n /** Block explorer URL */\n explorerUrl?: string;\n /** From wallet address */\n fromAddress: string;\n /** To wallet address */\n toAddress: string;\n /** Your reference */\n reference: string;\n /** When the charge was created */\n createdAt: string;\n /** When the charge was confirmed */\n confirmedAt?: string;\n /** Session name */\n sessionName?: string;\n /** User wallet address */\n userWallet?: string;\n /** Error message if failed */\n error?: string;\n}\n\nexport interface CreateChargeResult {\n success: boolean;\n charge?: Charge;\n /** True if this is a replay of an existing charge */\n idempotentReplay?: boolean;\n error?: string;\n details?: string;\n}\n\nexport interface SessionGrant {\n /** Session key ID */\n sessionId: string;\n /** User's wallet address */\n userWallet: string;\n /** Your merchant wallet address */\n merchantWallet: string;\n /** Spending limits */\n limits: {\n maxTotalUsd?: number;\n maxPerTxUsd?: number;\n expiresAt: string;\n };\n /** When the session was granted */\n grantedAt: string;\n}\n\n// Internal types for API responses\ninterface CreateChargeApiResponse {\n success: boolean;\n charge_id?: string;\n status?: string;\n amount_usd?: number;\n amount_usdc?: string;\n tx_hash?: string;\n explorer_url?: string;\n idempotent_replay?: boolean;\n error?: string;\n details?: string;\n}\n\ninterface ChargeApiResponse {\n id: string;\n status: string;\n amount_usd: number;\n amount_usdc: string;\n tx_hash?: string;\n explorer_url?: string;\n from_address: string;\n to_address: string;\n reference: string;\n created_at: string;\n confirmed_at?: string;\n session_name?: string;\n user_wallet?: string;\n}\n\nexport interface ChargesClientOptions {\n /** Your API key */\n apiKey: string;\n /** MixrPay API base URL */\n baseUrl?: string;\n}\n\n// =============================================================================\n// Client\n// =============================================================================\n\nexport class ChargesClient {\n private apiKey: string;\n private baseUrl: string;\n\n constructor(options: ChargesClientOptions) {\n this.apiKey = options.apiKey;\n this.baseUrl = options.baseUrl || process.env.MIXRPAY_BASE_URL || 'https://mixrpay.com';\n }\n\n /**\n * Create a new charge.\n * \n * @example\n * ```typescript\n * const result = await charges.create({\n * sessionId: 'sk_xxx',\n * amountUsd: 0.05,\n * reference: 'image_gen_123',\n * metadata: { feature: 'image_generation' }\n * });\n * \n * if (result.success) {\n * console.log(`Charged ${result.charge.amountUsdc}`);\n * console.log(`TX: ${result.charge.explorerUrl}`);\n * }\n * ```\n */\n async create(params: CreateChargeParams): Promise<CreateChargeResult> {\n const response = await fetch(`${this.baseUrl}/api/v1/charges`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({\n session_id: params.sessionId,\n amount_usd: params.amountUsd,\n reference: params.reference,\n metadata: params.metadata,\n }),\n });\n\n const data = await response.json() as CreateChargeApiResponse;\n\n if (!response.ok) {\n return {\n success: false,\n error: data.error,\n details: data.details,\n };\n }\n\n return {\n success: data.success,\n charge: data.charge_id ? {\n id: data.charge_id,\n status: data.status as Charge['status'],\n amountUsd: data.amount_usd ?? 0,\n amountUsdc: data.amount_usdc ?? '0',\n txHash: data.tx_hash,\n explorerUrl: data.explorer_url,\n fromAddress: '',\n toAddress: '',\n reference: params.reference,\n createdAt: new Date().toISOString(),\n } : undefined,\n idempotentReplay: data.idempotent_replay,\n error: data.error,\n };\n }\n\n /**\n * Get a charge by ID.\n * \n * @example\n * ```typescript\n * const charge = await charges.get('chg_xxx');\n * console.log(charge.status); // 'confirmed'\n * ```\n */\n async get(chargeId: string): Promise<Charge | null> {\n const response = await fetch(`${this.baseUrl}/api/v1/charges?id=${chargeId}`, {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n },\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n return null;\n }\n throw new Error(`Failed to get charge: ${response.statusText}`);\n }\n\n const data = await response.json() as ChargeApiResponse;\n\n return {\n id: data.id,\n status: data.status as Charge['status'],\n amountUsd: data.amount_usd,\n amountUsdc: data.amount_usdc,\n txHash: data.tx_hash,\n explorerUrl: data.explorer_url,\n fromAddress: data.from_address,\n toAddress: data.to_address,\n reference: data.reference,\n createdAt: data.created_at,\n confirmedAt: data.confirmed_at,\n sessionName: data.session_name,\n userWallet: data.user_wallet,\n };\n }\n\n /**\n * Get a charge by transaction hash.\n */\n async getByTxHash(txHash: string): Promise<Charge | null> {\n const response = await fetch(`${this.baseUrl}/api/v1/charges?tx_hash=${txHash}`, {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n },\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n return null;\n }\n throw new Error(`Failed to get charge: ${response.statusText}`);\n }\n\n const data = await response.json() as ChargeApiResponse;\n\n return {\n id: data.id,\n status: data.status as Charge['status'],\n amountUsd: data.amount_usd,\n amountUsdc: data.amount_usdc,\n txHash: data.tx_hash,\n explorerUrl: data.explorer_url,\n fromAddress: data.from_address,\n toAddress: data.to_address,\n reference: data.reference,\n createdAt: data.created_at,\n confirmedAt: data.confirmed_at,\n sessionName: data.session_name,\n userWallet: data.user_wallet,\n };\n }\n}\n\n// =============================================================================\n// Webhook Verification\n// =============================================================================\n\nimport crypto from 'crypto';\n\n/**\n * Verify a session.granted webhook signature.\n * \n * @example\n * ```typescript\n * const payload = req.body;\n * const signature = req.headers['x-mixrpay-signature'];\n * \n * if (verifySessionWebhook(JSON.stringify(payload), signature, webhookSecret)) {\n * const grant = parseSessionGrant(payload);\n * console.log(`User ${grant.userWallet} granted access`);\n * }\n * ```\n */\nexport function verifySessionWebhook(\n payload: string,\n signature: string,\n secret: string\n): boolean {\n const expectedSignature = crypto\n .createHmac('sha256', secret)\n .update(payload)\n .digest('hex');\n \n return crypto.timingSafeEqual(\n Buffer.from(signature),\n Buffer.from(expectedSignature)\n );\n}\n\n/**\n * Parse a session.granted webhook payload.\n */\nexport function parseSessionGrant(payload: {\n event: string;\n session_id: string;\n user_wallet: string;\n merchant_wallet: string;\n limits: {\n max_total_usd?: number;\n max_per_tx_usd?: number;\n expires_at: string;\n };\n granted_at: string;\n}): SessionGrant {\n if (payload.event !== 'session.granted') {\n throw new Error(`Unexpected event type: ${payload.event}`);\n }\n\n return {\n sessionId: payload.session_id,\n userWallet: payload.user_wallet,\n merchantWallet: payload.merchant_wallet,\n limits: {\n maxTotalUsd: payload.limits.max_total_usd,\n maxPerTxUsd: payload.limits.max_per_tx_usd,\n expiresAt: payload.limits.expires_at,\n },\n grantedAt: payload.granted_at,\n };\n}\n\n"],"mappings":";AAiBA,YAAY,UAAU;AAOtB,IAAM,mBAAmB,QAAQ,IAAI,oBAAoB;AACzD,IAAM,oBAAoB,KAAK,KAAK;AAWpC,IAAM,YAAY,oBAAI,IAAwB;AAK9C,eAAe,QAAQ,SAA8C;AACnE,QAAM,SAAS,UAAU,IAAI,OAAO;AACpC,QAAM,MAAM,KAAK,IAAI;AAGrB,MAAI,UAAW,MAAM,OAAO,YAAa,mBAAmB;AAC1D,WAAO,OAAO;AAAA,EAChB;AAGA,QAAM,WAAW,MAAM,MAAM,OAAO;AAEpC,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,6BAA6B,OAAO,KAAK,SAAS,MAAM,IAAI,SAAS,UAAU,EAAE;AAAA,EACnG;AAEA,QAAM,OAAO,MAAM,SAAS,KAAK;AAGjC,MAAI,CAAC,KAAK,QAAQ,CAAC,MAAM,QAAQ,KAAK,IAAI,KAAK,KAAK,KAAK,WAAW,GAAG;AACrE,UAAM,IAAI,MAAM,2CAA2C;AAAA,EAC7D;AAGA,YAAU,IAAI,SAAS,EAAE,MAAM,WAAW,IAAI,CAAC;AAE/C,SAAO;AACT;AAKA,eAAe,aAAa,SAAgD;AAC1E,QAAM,OAAO,MAAM,QAAQ,OAAO;AAClC,SAAY,uBAAkB,IAAI;AACpC;AA+BA,eAAsB,qBACpB,SACA,SACyB;AACzB,QAAM,UAAU,SAAS,WAAW;AAEpC,MAAI;AAEF,UAAM,SAAS,MAAM,aAAa,OAAO;AAGzC,UAAM,gBAAuC;AAAA,MAC3C,YAAY,CAAC,OAAO;AAAA,IACtB;AAGA,QAAI,SAAS,QAAQ;AACnB,oBAAc,SAAS,QAAQ;AAAA,IACjC;AAGA,UAAM,EAAE,QAAQ,IAAI,MAAW,eAAU,SAAS,QAAQ,aAAa;AAGvE,UAAM,iBAAiB,CAAC,aAAa,UAAU,aAAa,SAAS,aAAa,WAAW,UAAU,WAAW;AAClH,eAAW,SAAS,gBAAgB;AAClC,UAAI,EAAE,SAAS,UAAU;AACvB,cAAM,IAAI,MAAM,sCAAsC,KAAK,EAAE;AAAA,MAC/D;AAAA,IACF;AAGA,WAAO;AAAA,MACL,WAAW,QAAQ;AAAA,MACnB,QAAQ,QAAQ;AAAA,MAChB,WAAW,QAAQ;AAAA,MACnB,OAAO,QAAQ;AAAA,MACf,WAAW,QAAQ;AAAA,MACnB,SAAS,QAAQ;AAAA,MACjB,QAAQ,QAAQ;AAAA,MAChB,WAAW,QAAQ;AAAA,MACnB,UAAU,IAAI,KAAM,QAAQ,MAAiB,GAAI;AAAA,MACjD,WAAW,IAAI,KAAM,QAAQ,MAAiB,GAAI;AAAA,IACpD;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAsB,YAAO,YAAY;AAC3C,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,QAAI,iBAAsB,YAAO,0BAA0B;AACzD,YAAM,IAAI,MAAM,8BAA8B,MAAM,OAAO,EAAE;AAAA,IAC/D;AACA,QAAI,iBAAsB,YAAO,gCAAgC;AAC/D,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AACA,UAAM;AAAA,EACR;AACF;;;ACgHA,OAAO,YAAY;AAgBZ,SAAS,qBACd,SACA,WACA,QACS;AACT,QAAM,oBAAoB,OACvB,WAAW,UAAU,MAAM,EAC3B,OAAO,OAAO,EACd,OAAO,KAAK;AAEf,SAAO,OAAO;AAAA,IACZ,OAAO,KAAK,SAAS;AAAA,IACrB,OAAO,KAAK,iBAAiB;AAAA,EAC/B;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/receipt.ts","../src/charges.ts"],"sourcesContent":["/**\n * MixrPay Merchant SDK - Payment Receipt Verification\n * \n * Verify JWT payment receipts issued by MixrPay after successful x402 payments.\n * \n * @example\n * ```typescript\n * import { verifyPaymentReceipt } from '@mixrpay/merchant-sdk';\n * \n * const receipt = req.headers['x-payment-receipt'];\n * const payment = await verifyPaymentReceipt(receipt);\n * \n * console.log(`Payment received: $${payment.amountUsd} from ${payment.payer}`);\n * console.log(`Transaction: ${payment.txHash}`);\n * ```\n */\n\nimport * as jose from 'jose';\nimport type { PaymentReceipt, VerifyReceiptOptions } from './types';\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nconst DEFAULT_JWKS_URL = process.env.MIXRPAY_JWKS_URL || 'https://www.mixrpay.com/.well-known/jwks';\nconst JWKS_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour\n\n// =============================================================================\n// JWKS Cache\n// =============================================================================\n\ninterface CachedJWKS {\n jwks: jose.JSONWebKeySet;\n fetchedAt: number;\n}\n\nconst jwksCache = new Map<string, CachedJWKS>();\n\n/**\n * Fetch and cache JWKS from the specified URL.\n */\nasync function getJWKS(jwksUrl: string): Promise<jose.JSONWebKeySet> {\n const cached = jwksCache.get(jwksUrl);\n const now = Date.now();\n\n // Return cached JWKS if still valid\n if (cached && (now - cached.fetchedAt) < JWKS_CACHE_TTL_MS) {\n return cached.jwks;\n }\n\n // Fetch fresh JWKS\n const response = await fetch(jwksUrl);\n \n if (!response.ok) {\n throw new Error(`Failed to fetch JWKS from ${jwksUrl}: ${response.status} ${response.statusText}`);\n }\n\n const jwks = await response.json() as jose.JSONWebKeySet;\n\n // Validate JWKS structure\n if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {\n throw new Error('Invalid JWKS: missing or empty keys array');\n }\n\n // Cache the JWKS\n jwksCache.set(jwksUrl, { jwks, fetchedAt: now });\n\n return jwks;\n}\n\n/**\n * Create a JWKS key set from a fetched JWKS.\n */\nasync function createKeySet(jwksUrl: string): Promise<jose.JWTVerifyGetKey> {\n const jwks = await getJWKS(jwksUrl);\n return jose.createLocalJWKSet(jwks);\n}\n\n// =============================================================================\n// Receipt Verification\n// =============================================================================\n\n/**\n * Verify a JWT payment receipt from MixrPay.\n * \n * This function:\n * 1. Fetches the JWKS from MixrPay (cached for 1 hour)\n * 2. Verifies the JWT signature using RS256\n * 3. Validates standard JWT claims (exp, iat)\n * 4. Returns the typed payment receipt\n * \n * @param receipt - The JWT receipt string from X-Payment-Receipt header\n * @param options - Optional configuration (custom JWKS URL, issuer validation)\n * @returns Verified payment receipt\n * @throws Error if verification fails\n * \n * @example\n * ```typescript\n * // Basic usage\n * const payment = await verifyPaymentReceipt(receipt);\n * \n * // With custom JWKS URL (for testing or self-hosted)\n * const payment = await verifyPaymentReceipt(receipt, {\n * jwksUrl: 'https://your-mixrpay.com/.well-known/jwks'\n * });\n * ```\n */\nexport async function verifyPaymentReceipt(\n receipt: string,\n options?: VerifyReceiptOptions\n): Promise<PaymentReceipt> {\n const jwksUrl = options?.jwksUrl || DEFAULT_JWKS_URL;\n\n try {\n // Get the key set\n const keySet = await createKeySet(jwksUrl);\n\n // Build verification options\n const verifyOptions: jose.JWTVerifyOptions = {\n algorithms: ['RS256'],\n };\n\n // Add issuer validation if specified\n if (options?.issuer) {\n verifyOptions.issuer = options.issuer;\n }\n\n // Verify the JWT\n const { payload } = await jose.jwtVerify(receipt, keySet, verifyOptions);\n\n // Validate required payment fields\n const requiredFields = ['paymentId', 'amount', 'amountUsd', 'payer', 'recipient', 'chainId', 'txHash', 'settledAt'];\n for (const field of requiredFields) {\n if (!(field in payload)) {\n throw new Error(`Missing required field in receipt: ${field}`);\n }\n }\n\n // Return typed receipt\n return {\n paymentId: payload.paymentId as string,\n amount: payload.amount as string,\n amountUsd: payload.amountUsd as number,\n payer: payload.payer as string,\n recipient: payload.recipient as string,\n chainId: payload.chainId as number,\n txHash: payload.txHash as string,\n settledAt: payload.settledAt as string,\n issuedAt: new Date((payload.iat as number) * 1000),\n expiresAt: new Date((payload.exp as number) * 1000),\n };\n } catch (error) {\n if (error instanceof jose.errors.JWTExpired) {\n throw new Error('Payment receipt has expired');\n }\n if (error instanceof jose.errors.JWTClaimValidationFailed) {\n throw new Error(`Receipt validation failed: ${error.message}`);\n }\n if (error instanceof jose.errors.JWSSignatureVerificationFailed) {\n throw new Error('Invalid receipt signature');\n }\n throw error;\n }\n}\n\n/**\n * Parse a JWT payment receipt without verification.\n * \n * WARNING: This does NOT verify the signature. Use only for debugging\n * or when you've already verified the receipt elsewhere.\n * \n * @param receipt - The JWT receipt string\n * @returns Decoded payload (unverified)\n */\nexport function parsePaymentReceipt(receipt: string): PaymentReceipt {\n const decoded = jose.decodeJwt(receipt);\n\n return {\n paymentId: decoded.paymentId as string,\n amount: decoded.amount as string,\n amountUsd: decoded.amountUsd as number,\n payer: decoded.payer as string,\n recipient: decoded.recipient as string,\n chainId: decoded.chainId as number,\n txHash: decoded.txHash as string,\n settledAt: decoded.settledAt as string,\n issuedAt: decoded.iat ? new Date(decoded.iat * 1000) : undefined,\n expiresAt: decoded.exp ? new Date(decoded.exp * 1000) : undefined,\n };\n}\n\n/**\n * Check if a receipt is expired.\n * \n * @param receipt - The JWT receipt string or parsed PaymentReceipt\n * @returns true if expired, false otherwise\n */\nexport function isReceiptExpired(receipt: string | PaymentReceipt): boolean {\n const parsed = typeof receipt === 'string' ? parsePaymentReceipt(receipt) : receipt;\n \n if (!parsed.expiresAt) {\n return false; // No expiration = never expires\n }\n\n return parsed.expiresAt.getTime() < Date.now();\n}\n\n/**\n * Clear the JWKS cache.\n * Useful for testing or when keys have rotated.\n */\nexport function clearJWKSCache(): void {\n jwksCache.clear();\n}\n\n/**\n * Get the default JWKS URL.\n */\nexport function getDefaultJWKSUrl(): string {\n return DEFAULT_JWKS_URL;\n}\n\n","/**\n * MixrPay Charges Module\n * \n * Create and manage charges using session signers.\n * Session signers allow you to charge user wallets without\n * requiring approval for each transaction.\n */\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface CreateChargeParams {\n /** Session key ID granted by the user */\n sessionId: string;\n /** Amount to charge in USD (e.g., 0.05 for 5 cents) */\n amountUsd: number;\n /** Unique reference for idempotency (e.g., \"order_123\") */\n reference: string;\n /** Optional metadata */\n metadata?: Record<string, unknown>;\n}\n\nexport interface Charge {\n /** Unique charge ID */\n id: string;\n /** Current status */\n status: 'pending' | 'submitted' | 'confirmed' | 'failed';\n /** Amount in USD */\n amountUsd: number;\n /** Formatted USDC amount */\n amountUsdc: string;\n /** Transaction hash (if submitted) */\n txHash?: string;\n /** Block explorer URL */\n explorerUrl?: string;\n /** From wallet address */\n fromAddress: string;\n /** To wallet address */\n toAddress: string;\n /** Your reference */\n reference: string;\n /** When the charge was created */\n createdAt: string;\n /** When the charge was confirmed */\n confirmedAt?: string;\n /** Session name */\n sessionName?: string;\n /** User wallet address */\n userWallet?: string;\n /** Error message if failed */\n error?: string;\n}\n\nexport interface CreateChargeResult {\n success: boolean;\n charge?: Charge;\n /** True if this is a replay of an existing charge */\n idempotentReplay?: boolean;\n error?: string;\n details?: string;\n}\n\nexport interface SessionGrant {\n /** Session key ID */\n sessionId: string;\n /** User's wallet address */\n userWallet: string;\n /** Your merchant wallet address */\n merchantWallet: string;\n /** Spending limits */\n limits: {\n maxTotalUsd?: number;\n maxPerTxUsd?: number;\n expiresAt: string;\n };\n /** When the session was granted */\n grantedAt: string;\n}\n\n// Internal types for API responses\ninterface CreateChargeApiResponse {\n success: boolean;\n charge_id?: string;\n status?: string;\n amount_usd?: number;\n amount_usdc?: string;\n tx_hash?: string;\n explorer_url?: string;\n idempotent_replay?: boolean;\n error?: string;\n details?: string;\n}\n\ninterface ChargeApiResponse {\n id: string;\n status: string;\n amount_usd: number;\n amount_usdc: string;\n tx_hash?: string;\n explorer_url?: string;\n from_address: string;\n to_address: string;\n reference: string;\n created_at: string;\n confirmed_at?: string;\n session_name?: string;\n user_wallet?: string;\n}\n\nexport interface ChargesClientOptions {\n /** Your API key */\n apiKey: string;\n /** MixrPay API base URL */\n baseUrl?: string;\n}\n\n// =============================================================================\n// Client\n// =============================================================================\n\nexport class ChargesClient {\n private apiKey: string;\n private baseUrl: string;\n\n constructor(options: ChargesClientOptions) {\n this.apiKey = options.apiKey;\n this.baseUrl = options.baseUrl || process.env.MIXRPAY_BASE_URL || 'https://www.mixrpay.com';\n }\n\n /**\n * Create a new charge.\n * \n * @example\n * ```typescript\n * const result = await charges.create({\n * sessionId: 'sk_xxx',\n * amountUsd: 0.05,\n * reference: 'image_gen_123',\n * metadata: { feature: 'image_generation' }\n * });\n * \n * if (result.success) {\n * console.log(`Charged ${result.charge.amountUsdc}`);\n * console.log(`TX: ${result.charge.explorerUrl}`);\n * }\n * ```\n */\n async create(params: CreateChargeParams): Promise<CreateChargeResult> {\n const response = await fetch(`${this.baseUrl}/api/v1/charges`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({\n session_id: params.sessionId,\n amount_usd: params.amountUsd,\n reference: params.reference,\n metadata: params.metadata,\n }),\n });\n\n const data = await response.json() as CreateChargeApiResponse;\n\n if (!response.ok) {\n return {\n success: false,\n error: data.error,\n details: data.details,\n };\n }\n\n return {\n success: data.success,\n charge: data.charge_id ? {\n id: data.charge_id,\n status: data.status as Charge['status'],\n amountUsd: data.amount_usd ?? 0,\n amountUsdc: data.amount_usdc ?? '0',\n txHash: data.tx_hash,\n explorerUrl: data.explorer_url,\n fromAddress: '',\n toAddress: '',\n reference: params.reference,\n createdAt: new Date().toISOString(),\n } : undefined,\n idempotentReplay: data.idempotent_replay,\n error: data.error,\n };\n }\n\n /**\n * Get a charge by ID.\n * \n * @example\n * ```typescript\n * const charge = await charges.get('chg_xxx');\n * console.log(charge.status); // 'confirmed'\n * ```\n */\n async get(chargeId: string): Promise<Charge | null> {\n const response = await fetch(`${this.baseUrl}/api/v1/charges?id=${chargeId}`, {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n },\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n return null;\n }\n throw new Error(`Failed to get charge: ${response.statusText}`);\n }\n\n const data = await response.json() as ChargeApiResponse;\n\n return {\n id: data.id,\n status: data.status as Charge['status'],\n amountUsd: data.amount_usd,\n amountUsdc: data.amount_usdc,\n txHash: data.tx_hash,\n explorerUrl: data.explorer_url,\n fromAddress: data.from_address,\n toAddress: data.to_address,\n reference: data.reference,\n createdAt: data.created_at,\n confirmedAt: data.confirmed_at,\n sessionName: data.session_name,\n userWallet: data.user_wallet,\n };\n }\n\n /**\n * Get a charge by transaction hash.\n */\n async getByTxHash(txHash: string): Promise<Charge | null> {\n const response = await fetch(`${this.baseUrl}/api/v1/charges?tx_hash=${txHash}`, {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n },\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n return null;\n }\n throw new Error(`Failed to get charge: ${response.statusText}`);\n }\n\n const data = await response.json() as ChargeApiResponse;\n\n return {\n id: data.id,\n status: data.status as Charge['status'],\n amountUsd: data.amount_usd,\n amountUsdc: data.amount_usdc,\n txHash: data.tx_hash,\n explorerUrl: data.explorer_url,\n fromAddress: data.from_address,\n toAddress: data.to_address,\n reference: data.reference,\n createdAt: data.created_at,\n confirmedAt: data.confirmed_at,\n sessionName: data.session_name,\n userWallet: data.user_wallet,\n };\n }\n}\n\n// =============================================================================\n// Webhook Verification\n// =============================================================================\n\nimport crypto from 'crypto';\n\n/**\n * Verify a session.granted webhook signature.\n * \n * @example\n * ```typescript\n * const payload = req.body;\n * const signature = req.headers['x-mixrpay-signature'];\n * \n * if (verifySessionWebhook(JSON.stringify(payload), signature, webhookSecret)) {\n * const grant = parseSessionGrant(payload);\n * console.log(`User ${grant.userWallet} granted access`);\n * }\n * ```\n */\nexport function verifySessionWebhook(\n payload: string,\n signature: string,\n secret: string\n): boolean {\n const expectedSignature = crypto\n .createHmac('sha256', secret)\n .update(payload)\n .digest('hex');\n \n return crypto.timingSafeEqual(\n Buffer.from(signature),\n Buffer.from(expectedSignature)\n );\n}\n\n/**\n * Parse a session.granted webhook payload.\n */\nexport function parseSessionGrant(payload: {\n event: string;\n session_id: string;\n user_wallet: string;\n merchant_wallet: string;\n limits: {\n max_total_usd?: number;\n max_per_tx_usd?: number;\n expires_at: string;\n };\n granted_at: string;\n}): SessionGrant {\n if (payload.event !== 'session.granted') {\n throw new Error(`Unexpected event type: ${payload.event}`);\n }\n\n return {\n sessionId: payload.session_id,\n userWallet: payload.user_wallet,\n merchantWallet: payload.merchant_wallet,\n limits: {\n maxTotalUsd: payload.limits.max_total_usd,\n maxPerTxUsd: payload.limits.max_per_tx_usd,\n expiresAt: payload.limits.expires_at,\n },\n grantedAt: payload.granted_at,\n };\n}\n\n"],"mappings":";AAiBA,YAAY,UAAU;AAOtB,IAAM,mBAAmB,QAAQ,IAAI,oBAAoB;AACzD,IAAM,oBAAoB,KAAK,KAAK;AAWpC,IAAM,YAAY,oBAAI,IAAwB;AAK9C,eAAe,QAAQ,SAA8C;AACnE,QAAM,SAAS,UAAU,IAAI,OAAO;AACpC,QAAM,MAAM,KAAK,IAAI;AAGrB,MAAI,UAAW,MAAM,OAAO,YAAa,mBAAmB;AAC1D,WAAO,OAAO;AAAA,EAChB;AAGA,QAAM,WAAW,MAAM,MAAM,OAAO;AAEpC,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,6BAA6B,OAAO,KAAK,SAAS,MAAM,IAAI,SAAS,UAAU,EAAE;AAAA,EACnG;AAEA,QAAM,OAAO,MAAM,SAAS,KAAK;AAGjC,MAAI,CAAC,KAAK,QAAQ,CAAC,MAAM,QAAQ,KAAK,IAAI,KAAK,KAAK,KAAK,WAAW,GAAG;AACrE,UAAM,IAAI,MAAM,2CAA2C;AAAA,EAC7D;AAGA,YAAU,IAAI,SAAS,EAAE,MAAM,WAAW,IAAI,CAAC;AAE/C,SAAO;AACT;AAKA,eAAe,aAAa,SAAgD;AAC1E,QAAM,OAAO,MAAM,QAAQ,OAAO;AAClC,SAAY,uBAAkB,IAAI;AACpC;AA+BA,eAAsB,qBACpB,SACA,SACyB;AACzB,QAAM,UAAU,SAAS,WAAW;AAEpC,MAAI;AAEF,UAAM,SAAS,MAAM,aAAa,OAAO;AAGzC,UAAM,gBAAuC;AAAA,MAC3C,YAAY,CAAC,OAAO;AAAA,IACtB;AAGA,QAAI,SAAS,QAAQ;AACnB,oBAAc,SAAS,QAAQ;AAAA,IACjC;AAGA,UAAM,EAAE,QAAQ,IAAI,MAAW,eAAU,SAAS,QAAQ,aAAa;AAGvE,UAAM,iBAAiB,CAAC,aAAa,UAAU,aAAa,SAAS,aAAa,WAAW,UAAU,WAAW;AAClH,eAAW,SAAS,gBAAgB;AAClC,UAAI,EAAE,SAAS,UAAU;AACvB,cAAM,IAAI,MAAM,sCAAsC,KAAK,EAAE;AAAA,MAC/D;AAAA,IACF;AAGA,WAAO;AAAA,MACL,WAAW,QAAQ;AAAA,MACnB,QAAQ,QAAQ;AAAA,MAChB,WAAW,QAAQ;AAAA,MACnB,OAAO,QAAQ;AAAA,MACf,WAAW,QAAQ;AAAA,MACnB,SAAS,QAAQ;AAAA,MACjB,QAAQ,QAAQ;AAAA,MAChB,WAAW,QAAQ;AAAA,MACnB,UAAU,IAAI,KAAM,QAAQ,MAAiB,GAAI;AAAA,MACjD,WAAW,IAAI,KAAM,QAAQ,MAAiB,GAAI;AAAA,IACpD;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAsB,YAAO,YAAY;AAC3C,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,QAAI,iBAAsB,YAAO,0BAA0B;AACzD,YAAM,IAAI,MAAM,8BAA8B,MAAM,OAAO,EAAE;AAAA,IAC/D;AACA,QAAI,iBAAsB,YAAO,gCAAgC;AAC/D,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AACA,UAAM;AAAA,EACR;AACF;;;ACgHA,OAAO,YAAY;AAgBZ,SAAS,qBACd,SACA,WACA,QACS;AACT,QAAM,oBAAoB,OACvB,WAAW,UAAU,MAAM,EAC3B,OAAO,OAAO,EACd,OAAO,KAAK;AAEf,SAAO,OAAO;AAAA,IACZ,OAAO,KAAK,SAAS;AAAA,IACrB,OAAO,KAAK,iBAAiB;AAAA,EAC/B;AACF;","names":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
|
-
import { X as X402PaymentResult, a as MixrPayPaymentResult, d as X402Options, M as MixrPayOptions } from '../types-
|
|
2
|
+
import { X as X402PaymentResult, a as MixrPayPaymentResult, d as X402Options, M as MixrPayOptions } from '../types-BwiuIaOu.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* MixrPay Merchant SDK - Express Middleware
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
|
-
import { X as X402PaymentResult, a as MixrPayPaymentResult, d as X402Options, M as MixrPayOptions } from '../types-
|
|
2
|
+
import { X as X402PaymentResult, a as MixrPayPaymentResult, d as X402Options, M as MixrPayOptions } from '../types-BwiuIaOu.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* MixrPay Merchant SDK - Express Middleware
|
|
@@ -192,7 +192,7 @@ async function verifyX402Payment(paymentHeader, options) {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
// src/receipt-fetcher.ts
|
|
195
|
-
var DEFAULT_MIXRPAY_API_URL = process.env.MIXRPAY_BASE_URL || "https://mixrpay.com";
|
|
195
|
+
var DEFAULT_MIXRPAY_API_URL = process.env.MIXRPAY_BASE_URL || "https://www.mixrpay.com";
|
|
196
196
|
async function fetchPaymentReceipt(params) {
|
|
197
197
|
const apiUrl = params.apiUrl || DEFAULT_MIXRPAY_API_URL;
|
|
198
198
|
const endpoint = `${apiUrl}/api/v1/receipts`;
|
|
@@ -359,7 +359,7 @@ async function getFeaturePriceExpress(feature, baseUrl) {
|
|
|
359
359
|
function mixrpay(options) {
|
|
360
360
|
return async (req, res, next) => {
|
|
361
361
|
try {
|
|
362
|
-
const baseUrl = options.mixrpayApiUrl || process.env.MIXRPAY_API_URL || "https://mixrpay.com";
|
|
362
|
+
const baseUrl = options.mixrpayApiUrl || process.env.MIXRPAY_API_URL || "https://www.mixrpay.com";
|
|
363
363
|
const context = {
|
|
364
364
|
path: req.path,
|
|
365
365
|
method: req.method,
|
|
@@ -436,7 +436,7 @@ function mixrpay(options) {
|
|
|
436
436
|
};
|
|
437
437
|
}
|
|
438
438
|
async function verifySessionPayment(sessionId, priceUsd, options, req) {
|
|
439
|
-
const baseUrl = options.mixrpayApiUrl || process.env.MIXRPAY_API_URL || "https://mixrpay.com";
|
|
439
|
+
const baseUrl = options.mixrpayApiUrl || process.env.MIXRPAY_API_URL || "https://www.mixrpay.com";
|
|
440
440
|
const secretKey = process.env.MIXRPAY_SECRET_KEY;
|
|
441
441
|
if (!secretKey) {
|
|
442
442
|
return {
|
|
@@ -445,52 +445,108 @@ async function verifySessionPayment(sessionId, priceUsd, options, req) {
|
|
|
445
445
|
error: "MIXRPAY_SECRET_KEY not configured"
|
|
446
446
|
};
|
|
447
447
|
}
|
|
448
|
+
const preChargedHeader = req.headers["x-mixr-charged"];
|
|
449
|
+
const isPreCharged = preChargedHeader && preChargedHeader !== "0" && preChargedHeader.toLowerCase() !== "false";
|
|
448
450
|
try {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
"Authorization": `Bearer ${secretKey}`
|
|
454
|
-
},
|
|
455
|
-
body: JSON.stringify({
|
|
456
|
-
sessionId,
|
|
457
|
-
amountUsd: priceUsd,
|
|
458
|
-
feature: options.feature || req.headers["x-mixr-feature"],
|
|
459
|
-
idempotencyKey: req.headers["x-idempotency-key"]
|
|
460
|
-
})
|
|
461
|
-
});
|
|
462
|
-
if (!response.ok) {
|
|
463
|
-
const errorData = await response.json().catch(() => ({}));
|
|
464
|
-
return {
|
|
465
|
-
valid: false,
|
|
466
|
-
method: "session",
|
|
467
|
-
error: errorData.message || errorData.error || `Charge failed: ${response.status}`,
|
|
468
|
-
sessionId
|
|
469
|
-
};
|
|
451
|
+
if (isPreCharged) {
|
|
452
|
+
return await validatePreChargedSession(sessionId, priceUsd, preChargedHeader, baseUrl, secretKey, options);
|
|
453
|
+
} else {
|
|
454
|
+
return await chargeSession(sessionId, priceUsd, baseUrl, secretKey, options, req);
|
|
470
455
|
}
|
|
471
|
-
|
|
456
|
+
} catch (error) {
|
|
472
457
|
return {
|
|
473
|
-
valid:
|
|
458
|
+
valid: false,
|
|
474
459
|
method: "session",
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
txHash: data.txHash,
|
|
478
|
-
chargeId: data.chargeId,
|
|
479
|
-
sessionId,
|
|
480
|
-
feature: options.feature,
|
|
481
|
-
settledAt: data.settledAt ? new Date(data.settledAt) : /* @__PURE__ */ new Date()
|
|
460
|
+
error: `Session verification failed: ${error.message}`,
|
|
461
|
+
sessionId
|
|
482
462
|
};
|
|
483
|
-
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async function validatePreChargedSession(sessionId, expectedPriceUsd, chargedAmount, baseUrl, secretKey, options) {
|
|
466
|
+
const chargedUsd = parseFloat(chargedAmount);
|
|
467
|
+
if (isNaN(chargedUsd) || chargedUsd < expectedPriceUsd) {
|
|
484
468
|
return {
|
|
485
469
|
valid: false,
|
|
486
470
|
method: "session",
|
|
487
|
-
error: `
|
|
471
|
+
error: `Insufficient payment: charged $${chargedUsd}, required $${expectedPriceUsd}`,
|
|
472
|
+
sessionId
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const response = await fetch(`${baseUrl}/api/v2/session/validate`, {
|
|
476
|
+
method: "POST",
|
|
477
|
+
headers: {
|
|
478
|
+
"Content-Type": "application/json",
|
|
479
|
+
"Authorization": `Bearer ${secretKey}`
|
|
480
|
+
},
|
|
481
|
+
body: JSON.stringify({ session_id: sessionId })
|
|
482
|
+
});
|
|
483
|
+
if (!response.ok) {
|
|
484
|
+
const errorData = await response.json().catch(() => ({}));
|
|
485
|
+
return {
|
|
486
|
+
valid: false,
|
|
487
|
+
method: "session",
|
|
488
|
+
error: errorData.error || `Validation failed: ${response.status}`,
|
|
489
|
+
sessionId
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
const data = await response.json();
|
|
493
|
+
if (!data.valid) {
|
|
494
|
+
return {
|
|
495
|
+
valid: false,
|
|
496
|
+
method: "session",
|
|
497
|
+
error: data.error || data.code || "Session invalid",
|
|
488
498
|
sessionId
|
|
489
499
|
};
|
|
490
500
|
}
|
|
501
|
+
return {
|
|
502
|
+
valid: true,
|
|
503
|
+
method: "session",
|
|
504
|
+
payer: data.wallet_address,
|
|
505
|
+
amountUsd: chargedUsd,
|
|
506
|
+
sessionId,
|
|
507
|
+
feature: options.feature,
|
|
508
|
+
settledAt: /* @__PURE__ */ new Date(),
|
|
509
|
+
preCharged: true
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
async function chargeSession(sessionId, priceUsd, baseUrl, secretKey, options, req) {
|
|
513
|
+
const response = await fetch(`${baseUrl}/api/v2/charge`, {
|
|
514
|
+
method: "POST",
|
|
515
|
+
headers: {
|
|
516
|
+
"Content-Type": "application/json",
|
|
517
|
+
"Authorization": `Bearer ${secretKey}`
|
|
518
|
+
},
|
|
519
|
+
body: JSON.stringify({
|
|
520
|
+
session_id: sessionId,
|
|
521
|
+
price_usd: priceUsd,
|
|
522
|
+
feature: options.feature || req.headers["x-mixr-feature"],
|
|
523
|
+
idempotency_key: req.headers["x-idempotency-key"]
|
|
524
|
+
})
|
|
525
|
+
});
|
|
526
|
+
if (!response.ok) {
|
|
527
|
+
const errorData = await response.json().catch(() => ({}));
|
|
528
|
+
return {
|
|
529
|
+
valid: false,
|
|
530
|
+
method: "session",
|
|
531
|
+
error: errorData.message || errorData.error || `Charge failed: ${response.status}`,
|
|
532
|
+
sessionId
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
const data = await response.json();
|
|
536
|
+
return {
|
|
537
|
+
valid: true,
|
|
538
|
+
method: "session",
|
|
539
|
+
payer: data.payer || data.walletAddress,
|
|
540
|
+
amountUsd: data.charged_usd || priceUsd,
|
|
541
|
+
txHash: data.tx_hash,
|
|
542
|
+
chargeId: data.idempotency_key,
|
|
543
|
+
sessionId,
|
|
544
|
+
feature: options.feature,
|
|
545
|
+
settledAt: data.settledAt ? new Date(data.settledAt) : /* @__PURE__ */ new Date()
|
|
546
|
+
};
|
|
491
547
|
}
|
|
492
548
|
async function verifyWidgetPayment(paymentJwt, priceUsd, options, req) {
|
|
493
|
-
const baseUrl = options.mixrpayApiUrl || process.env.MIXRPAY_API_URL || "https://mixrpay.com";
|
|
549
|
+
const baseUrl = options.mixrpayApiUrl || process.env.MIXRPAY_API_URL || "https://www.mixrpay.com";
|
|
494
550
|
const secretKey = process.env.MIXRPAY_SECRET_KEY;
|
|
495
551
|
if (!secretKey) {
|
|
496
552
|
return {
|