moltspay 0.9.1 → 0.9.3

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/.env.example CHANGED
@@ -23,6 +23,15 @@ FACILITATOR_PRIMARY=cdp
23
23
  # Options: failover | cheapest | fastest | random | roundrobin
24
24
  FACILITATOR_STRATEGY=failover
25
25
 
26
+ # ===========================================
27
+ # Proxy Endpoint (v0.9.2+)
28
+ # ===========================================
29
+ # IP whitelist for /proxy endpoint (comma-separated)
30
+ # Used by external services like moltspay-creators to delegate payment handling
31
+ # If not set, /proxy endpoint is disabled (secure by default)
32
+ # Example: 127.0.0.1,::1,203.0.113.50
33
+ PROXY_ALLOWED_IPS=127.0.0.1,::1
34
+
26
35
  # ===========================================
27
36
  # CDP Facilitator (Coinbase)
28
37
  # ===========================================
package/dist/cli/index.js CHANGED
@@ -985,6 +985,7 @@ var MoltsPayServer = class {
985
985
  console.log(`[MoltsPay] Endpoints:`);
986
986
  console.log(` GET /services - List available services`);
987
987
  console.log(` POST /execute - Execute service (x402 payment)`);
988
+ console.log(` POST /proxy - Proxy payment for external services`);
988
989
  console.log(` GET /health - Health check (incl. facilitators)`);
989
990
  });
990
991
  }
@@ -1014,6 +1015,15 @@ var MoltsPayServer = class {
1014
1015
  const paymentHeader = req.headers[PAYMENT_HEADER2];
1015
1016
  return await this.handleExecute(body, paymentHeader, res);
1016
1017
  }
1018
+ if (url.pathname === "/proxy" && req.method === "POST") {
1019
+ const clientIP = req.headers["x-real-ip"] || req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "";
1020
+ if (!this.isProxyAllowed(clientIP)) {
1021
+ return this.sendJson(res, 403, { error: "Forbidden: IP not allowed" });
1022
+ }
1023
+ const body = await this.readBody(req);
1024
+ const paymentHeader = req.headers[PAYMENT_HEADER2];
1025
+ return await this.handleProxy(body, paymentHeader, res);
1026
+ }
1017
1027
  this.sendJson(res, 404, { error: "Not found" });
1018
1028
  } catch (err) {
1019
1029
  console.error("[MoltsPay] Error:", err);
@@ -1223,6 +1233,217 @@ var MoltsPayServer = class {
1223
1233
  res.writeHead(status, headers);
1224
1234
  res.end(JSON.stringify(data, null, 2));
1225
1235
  }
1236
+ /**
1237
+ * Check if IP is allowed for /proxy endpoint
1238
+ */
1239
+ isProxyAllowed(clientIP) {
1240
+ const allowedIPs = process.env.PROXY_ALLOWED_IPS?.split(",").map((ip) => ip.trim()) || [];
1241
+ if (allowedIPs.length === 0) {
1242
+ console.log(`[MoltsPay] /proxy denied: no PROXY_ALLOWED_IPS configured`);
1243
+ return false;
1244
+ }
1245
+ const normalizedIP = clientIP === "::1" ? "127.0.0.1" : clientIP.replace("::ffff:", "");
1246
+ const allowed = allowedIPs.includes(normalizedIP) || allowedIPs.includes(clientIP);
1247
+ if (!allowed) {
1248
+ console.log(`[MoltsPay] /proxy denied for IP: ${clientIP} (normalized: ${normalizedIP})`);
1249
+ }
1250
+ return allowed;
1251
+ }
1252
+ /**
1253
+ * POST /proxy - Handle payment for external services (moltspay-creators)
1254
+ *
1255
+ * This endpoint allows other services to delegate x402 payment handling.
1256
+ * It does NOT execute any skill - just handles payment verification/settlement.
1257
+ *
1258
+ * Request body:
1259
+ * { wallet, amount, currency, chain, memo, serviceId, description }
1260
+ *
1261
+ * Without X-Payment header: returns 402 with payment requirements
1262
+ * With X-Payment header: verifies payment and returns result
1263
+ */
1264
+ async handleProxy(body, paymentHeader, res) {
1265
+ const { wallet, amount, currency, chain, memo, serviceId, description } = body;
1266
+ if (!wallet || !amount) {
1267
+ return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
1268
+ }
1269
+ if (!/^0x[a-fA-F0-9]{40}$/.test(wallet)) {
1270
+ return this.sendJson(res, 400, { error: "Invalid wallet address format" });
1271
+ }
1272
+ const amountNum = parseFloat(amount);
1273
+ if (isNaN(amountNum) || amountNum <= 0) {
1274
+ return this.sendJson(res, 400, { error: "Invalid amount" });
1275
+ }
1276
+ const proxyConfig = {
1277
+ id: serviceId || "proxy",
1278
+ name: description || "Proxy Payment",
1279
+ description: description || "",
1280
+ price: amountNum,
1281
+ currency: currency || "USDC",
1282
+ function: "",
1283
+ // Not used
1284
+ input: {},
1285
+ output: {}
1286
+ };
1287
+ const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet);
1288
+ if (!paymentHeader) {
1289
+ return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, res);
1290
+ }
1291
+ let payment;
1292
+ try {
1293
+ const decoded = Buffer.from(paymentHeader, "base64").toString("utf-8");
1294
+ payment = JSON.parse(decoded);
1295
+ } catch {
1296
+ return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
1297
+ }
1298
+ if (payment.x402Version !== X402_VERSION3) {
1299
+ return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
1300
+ }
1301
+ const scheme = payment.accepted?.scheme || payment.scheme;
1302
+ const network = payment.accepted?.network || payment.network;
1303
+ if (scheme !== "exact") {
1304
+ return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
1305
+ }
1306
+ if (network !== this.networkId) {
1307
+ return this.sendJson(res, 402, { error: `Network mismatch: expected ${this.networkId}, got ${network}` });
1308
+ }
1309
+ console.log(`[MoltsPay] /proxy: Verifying payment for ${wallet}...`);
1310
+ const verifyResult = await this.registry.verify(payment, requirements);
1311
+ if (!verifyResult.valid) {
1312
+ return this.sendJson(res, 402, {
1313
+ success: false,
1314
+ error: `Payment verification failed: ${verifyResult.error}`,
1315
+ facilitator: verifyResult.facilitator
1316
+ });
1317
+ }
1318
+ console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
1319
+ const { execute, service, params } = body;
1320
+ if (execute && service) {
1321
+ console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
1322
+ const skill = this.skills.get(service);
1323
+ if (!skill) {
1324
+ console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
1325
+ return this.sendJson(res, 404, {
1326
+ success: false,
1327
+ paymentSettled: false,
1328
+ error: `Service not found: ${service}`
1329
+ });
1330
+ }
1331
+ let result;
1332
+ try {
1333
+ result = await skill.handler(params || {});
1334
+ console.log(`[MoltsPay] /proxy: Skill succeeded, now settling payment...`);
1335
+ } catch (err) {
1336
+ console.error(`[MoltsPay] /proxy: Skill failed: ${err.message} - NOT settling`);
1337
+ return this.sendJson(res, 500, {
1338
+ success: false,
1339
+ paymentSettled: false,
1340
+ error: `Service execution failed: ${err.message}`
1341
+ });
1342
+ }
1343
+ let settlement2 = null;
1344
+ try {
1345
+ settlement2 = await this.registry.settle(payment, requirements);
1346
+ console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
1347
+ } catch (err) {
1348
+ console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
1349
+ return this.sendJson(res, 200, {
1350
+ success: true,
1351
+ verified: true,
1352
+ settled: false,
1353
+ settlementError: err.message,
1354
+ from: payment.payload?.authorization?.from,
1355
+ // Buyer's wallet address
1356
+ paidTo: wallet,
1357
+ amount: amountNum,
1358
+ currency: currency || "USDC",
1359
+ memo,
1360
+ result
1361
+ });
1362
+ }
1363
+ return this.sendJson(res, 200, {
1364
+ success: true,
1365
+ verified: true,
1366
+ settled: settlement2?.success || false,
1367
+ txHash: settlement2?.transaction,
1368
+ from: payment.payload?.authorization?.from,
1369
+ // Buyer's wallet address
1370
+ paidTo: wallet,
1371
+ amount: amountNum,
1372
+ currency: currency || "USDC",
1373
+ facilitator: settlement2?.facilitator,
1374
+ memo,
1375
+ result
1376
+ });
1377
+ }
1378
+ console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
1379
+ let settlement = null;
1380
+ try {
1381
+ settlement = await this.registry.settle(payment, requirements);
1382
+ console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
1383
+ } catch (err) {
1384
+ console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
1385
+ return this.sendJson(res, 500, {
1386
+ success: false,
1387
+ error: `Settlement failed: ${err.message}`
1388
+ });
1389
+ }
1390
+ this.sendJson(res, 200, {
1391
+ success: true,
1392
+ verified: true,
1393
+ settled: settlement?.success || false,
1394
+ txHash: settlement?.transaction,
1395
+ from: payment.payload?.authorization?.from,
1396
+ // Buyer's wallet address
1397
+ paidTo: wallet,
1398
+ amount: amountNum,
1399
+ currency: currency || "USDC",
1400
+ facilitator: settlement?.facilitator,
1401
+ memo
1402
+ });
1403
+ }
1404
+ /**
1405
+ * Build payment requirements for proxy endpoint (uses provided wallet)
1406
+ */
1407
+ buildProxyPaymentRequirements(config, wallet) {
1408
+ const amountInUnits = Math.floor(config.price * 1e6).toString();
1409
+ const usdcAddress = USDC_ADDRESSES[this.networkId];
1410
+ return {
1411
+ scheme: "exact",
1412
+ network: this.networkId,
1413
+ asset: usdcAddress,
1414
+ amount: amountInUnits,
1415
+ payTo: wallet,
1416
+ // Use provided wallet, not manifest
1417
+ maxTimeoutSeconds: 300,
1418
+ extra: USDC_DOMAIN
1419
+ };
1420
+ }
1421
+ /**
1422
+ * Return 402 with x402 payment requirements for proxy endpoint
1423
+ */
1424
+ sendProxyPaymentRequired(config, wallet, memo, res) {
1425
+ const requirements = this.buildProxyPaymentRequirements(config, wallet);
1426
+ const paymentRequired = {
1427
+ x402Version: X402_VERSION3,
1428
+ accepts: [requirements],
1429
+ resource: {
1430
+ url: `/proxy`,
1431
+ description: `${config.name} - $${config.price} ${config.currency}`,
1432
+ mimeType: "application/json",
1433
+ memo
1434
+ }
1435
+ };
1436
+ const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
1437
+ res.writeHead(402, {
1438
+ "Content-Type": "application/json",
1439
+ [PAYMENT_REQUIRED_HEADER2]: encoded
1440
+ });
1441
+ res.end(JSON.stringify({
1442
+ error: "Payment required",
1443
+ message: `Payment requires $${config.price} ${config.currency}`,
1444
+ x402: paymentRequired
1445
+ }, null, 2));
1446
+ }
1226
1447
  };
1227
1448
 
1228
1449
  // src/cli/index.ts