moltspay 1.6.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +1 -0
  2. package/dist/cdp/index.js.map +1 -1
  3. package/dist/cdp/index.mjs.map +1 -1
  4. package/dist/{cdp-DeohBe1o.d.ts → cdp-B5PhDUTC.d.ts} +1 -1
  5. package/dist/{cdp-p_eHuQpb.d.mts → cdp-D-5Hg3y_.d.mts} +1 -1
  6. package/dist/chains/index.d.mts +37 -1
  7. package/dist/chains/index.d.ts +37 -1
  8. package/dist/chains/index.js +22 -0
  9. package/dist/chains/index.js.map +1 -1
  10. package/dist/chains/index.mjs +19 -0
  11. package/dist/chains/index.mjs.map +1 -1
  12. package/dist/cli/index.js +1466 -61
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/cli/index.mjs +1466 -61
  15. package/dist/cli/index.mjs.map +1 -1
  16. package/dist/client/index.d.mts +41 -0
  17. package/dist/client/index.d.ts +41 -0
  18. package/dist/client/index.js +824 -10
  19. package/dist/client/index.js.map +1 -1
  20. package/dist/client/index.mjs +824 -10
  21. package/dist/client/index.mjs.map +1 -1
  22. package/dist/client/web/index.d.mts +17 -1
  23. package/dist/client/web/index.mjs +11 -0
  24. package/dist/client/web/index.mjs.map +1 -1
  25. package/dist/facilitators/index.d.mts +161 -4
  26. package/dist/facilitators/index.d.ts +161 -4
  27. package/dist/facilitators/index.js +370 -0
  28. package/dist/facilitators/index.js.map +1 -1
  29. package/dist/facilitators/index.mjs +365 -0
  30. package/dist/facilitators/index.mjs.map +1 -1
  31. package/dist/index.d.mts +77 -2
  32. package/dist/index.d.ts +77 -2
  33. package/dist/index.js +1412 -31
  34. package/dist/index.js.map +1 -1
  35. package/dist/index.mjs +1406 -31
  36. package/dist/index.mjs.map +1 -1
  37. package/dist/mcp/index.js +828 -14
  38. package/dist/mcp/index.js.map +1 -1
  39. package/dist/mcp/index.mjs +828 -14
  40. package/dist/mcp/index.mjs.map +1 -1
  41. package/dist/{registry-OsEO2dOu.d.mts → registry-DIo0WNoO.d.mts} +8 -1
  42. package/dist/{registry-OsEO2dOu.d.ts → registry-DIo0WNoO.d.ts} +8 -1
  43. package/dist/server/index.d.mts +95 -2
  44. package/dist/server/index.d.ts +95 -2
  45. package/dist/server/index.js +554 -14
  46. package/dist/server/index.js.map +1 -1
  47. package/dist/server/index.mjs +554 -14
  48. package/dist/server/index.mjs.map +1 -1
  49. package/dist/verify/index.js.map +1 -1
  50. package/dist/verify/index.mjs.map +1 -1
  51. package/dist/wallet/index.js.map +1 -1
  52. package/dist/wallet/index.mjs.map +1 -1
  53. package/package.json +7 -1
  54. package/schemas/moltspay.services.schema.json +100 -6
  55. package/scripts/postinstall.js +91 -0
package/dist/index.mjs CHANGED
@@ -418,6 +418,10 @@ function listChains() {
418
418
  function getChainById(chainId) {
419
419
  return Object.values(CHAINS).find((c) => c.chainId === chainId);
420
420
  }
421
+ var ALIPAY_CHAIN_ID = "alipay";
422
+ function isAlipayChainId(id) {
423
+ return id === ALIPAY_CHAIN_ID;
424
+ }
421
425
  var ERC20_ABI = [
422
426
  "function balanceOf(address owner) view returns (uint256)",
423
427
  "function transfer(address to, uint256 amount) returns (bool)",
@@ -1237,6 +1241,374 @@ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amo
1237
1241
  return transaction;
1238
1242
  }
1239
1243
 
1244
+ // src/facilitators/alipay.ts
1245
+ import crypto2 from "crypto";
1246
+
1247
+ // src/facilitators/alipay/rsa2.ts
1248
+ import crypto from "crypto";
1249
+ function rsa2Sign(data, privateKeyPem) {
1250
+ const signer = crypto.createSign("RSA-SHA256");
1251
+ signer.update(data, "utf-8");
1252
+ signer.end();
1253
+ return signer.sign(privateKeyPem, "base64");
1254
+ }
1255
+
1256
+ // src/facilitators/alipay/encoding.ts
1257
+ function base64url(input) {
1258
+ return Buffer.from(input, "utf-8").toString("base64url");
1259
+ }
1260
+ function decodeBase64UrlWithPadFix(input) {
1261
+ const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
1262
+ const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
1263
+ return Buffer.from(padded, "base64").toString("utf-8");
1264
+ }
1265
+ function toPem(key, kind) {
1266
+ const trimmed = key.trim();
1267
+ if (trimmed.includes("-----BEGIN")) return trimmed;
1268
+ const label = kind === "PRIVATE" ? "PRIVATE KEY" : "PUBLIC KEY";
1269
+ const body = trimmed.replace(/\s+/g, "").match(/.{1,64}/g)?.join("\n") ?? "";
1270
+ return `-----BEGIN ${label}-----
1271
+ ${body}
1272
+ -----END ${label}-----
1273
+ `;
1274
+ }
1275
+
1276
+ // src/facilitators/alipay/openapi.ts
1277
+ function formatAlipayTimestamp(d = /* @__PURE__ */ new Date()) {
1278
+ const pad = (n) => String(n).padStart(2, "0");
1279
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
1280
+ }
1281
+ function buildSigningString(params) {
1282
+ return Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join("&");
1283
+ }
1284
+ function responseWrapperKey(method) {
1285
+ return `${method.replace(/\./g, "_")}_response`;
1286
+ }
1287
+ async function alipayOpenApiCall(method, bizContent, config) {
1288
+ const publicParams = {
1289
+ app_id: config.app_id,
1290
+ method,
1291
+ format: "JSON",
1292
+ charset: "utf-8",
1293
+ sign_type: config.sign_type ?? "RSA2",
1294
+ timestamp: formatAlipayTimestamp(),
1295
+ version: "1.0",
1296
+ biz_content: JSON.stringify(bizContent)
1297
+ };
1298
+ const signingString = buildSigningString(publicParams);
1299
+ const sign = rsa2Sign(signingString, config.private_key_pem);
1300
+ const body = new URLSearchParams({ ...publicParams, sign }).toString();
1301
+ const response = await fetch(config.gateway_url, {
1302
+ method: "POST",
1303
+ headers: {
1304
+ "Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
1305
+ },
1306
+ body
1307
+ });
1308
+ if (!response.ok) {
1309
+ const text = await response.text().catch(() => "<unreadable>");
1310
+ throw new Error(
1311
+ `Alipay Open API HTTP ${response.status} for ${method}: ${text.slice(0, 500)}`
1312
+ );
1313
+ }
1314
+ const json = await response.json();
1315
+ const wrapperKey = responseWrapperKey(method);
1316
+ const business = json[wrapperKey];
1317
+ if (business === void 0 || business === null || typeof business !== "object") {
1318
+ throw new Error(
1319
+ `Alipay Open API response missing "${wrapperKey}" wrapper for ${method}: ${JSON.stringify(json).slice(0, 500)}`
1320
+ );
1321
+ }
1322
+ return business;
1323
+ }
1324
+
1325
+ // src/facilitators/alipay.ts
1326
+ var ALIPAY_NETWORK = "alipay";
1327
+ var ALIPAY_SCHEME = "alipay-aipay";
1328
+ var ALIPAY_GATEWAY_PROD = "https://openapi.alipay.com/gateway.do";
1329
+ var ALIPAY_AMOUNT_REGEX = /^\d+(\.\d{1,2})?$/;
1330
+ var ALIPAY_PAY_BEFORE_MS = 30 * 60 * 1e3;
1331
+ var ALIPAY_SIGNING_FIELDS = [
1332
+ "amount",
1333
+ "currency",
1334
+ "goods_name",
1335
+ "out_trade_no",
1336
+ "pay_before",
1337
+ "resource_id",
1338
+ "seller_id",
1339
+ "service_id"
1340
+ ];
1341
+ var AlipayFacilitator = class extends BaseFacilitator {
1342
+ name = "alipay";
1343
+ displayName = "Alipay AI \u6536";
1344
+ supportedNetworks = [ALIPAY_NETWORK];
1345
+ config;
1346
+ constructor(config) {
1347
+ super();
1348
+ this.config = {
1349
+ gateway_url: ALIPAY_GATEWAY_PROD,
1350
+ sign_type: "RSA2",
1351
+ ...config
1352
+ };
1353
+ }
1354
+ /**
1355
+ * Build the 402 challenge for a service: signs the 8-field payload with
1356
+ * RSA2, packages the nested `{protocol, method}` JSON as Base64URL for
1357
+ * `Payment-Needed`, and emits the parallel x402 `accepts[]` entry.
1358
+ */
1359
+ async createPaymentRequirements(opts) {
1360
+ if (!ALIPAY_AMOUNT_REGEX.test(opts.priceCny)) {
1361
+ throw new Error(
1362
+ `AlipayFacilitator.createPaymentRequirements: priceCny "${opts.priceCny}" does not match /^\\d+(\\.\\d{1,2})?$/ (unit is \u5143, not \u5206; e.g. "1.00" not "100")`
1363
+ );
1364
+ }
1365
+ const now = /* @__PURE__ */ new Date();
1366
+ const outTradeNo = opts.outTradeNo ?? generateOutTradeNo();
1367
+ const payBefore = formatPayBefore(now);
1368
+ const signedFields = {
1369
+ amount: opts.priceCny,
1370
+ currency: "CNY",
1371
+ goods_name: opts.goodsName,
1372
+ out_trade_no: outTradeNo,
1373
+ pay_before: payBefore,
1374
+ resource_id: opts.resourceId,
1375
+ seller_id: this.config.seller_id,
1376
+ service_id: opts.serviceId
1377
+ };
1378
+ const signingString = ALIPAY_SIGNING_FIELDS.map((k) => `${k}=${signedFields[k]}`).join("&");
1379
+ const seller_signature = rsa2Sign(signingString, this.config.private_key_pem);
1380
+ const challenge = {
1381
+ protocol: {
1382
+ out_trade_no: outTradeNo,
1383
+ amount: signedFields.amount,
1384
+ currency: signedFields.currency,
1385
+ resource_id: signedFields.resource_id,
1386
+ pay_before: payBefore,
1387
+ seller_signature,
1388
+ seller_sign_type: this.config.sign_type ?? "RSA2",
1389
+ seller_unique_id: this.config.seller_id
1390
+ },
1391
+ method: {
1392
+ seller_name: this.config.seller_name,
1393
+ seller_id: this.config.seller_id,
1394
+ seller_app_id: this.config.app_id,
1395
+ goods_name: signedFields.goods_name,
1396
+ seller_unique_id_key: "seller_id",
1397
+ service_id: signedFields.service_id
1398
+ }
1399
+ };
1400
+ const paymentNeededHeader = base64url(JSON.stringify(challenge));
1401
+ const x402Accepts = {
1402
+ scheme: ALIPAY_SCHEME,
1403
+ network: ALIPAY_NETWORK,
1404
+ asset: "CNY",
1405
+ amount: opts.priceCny,
1406
+ payTo: this.config.seller_id,
1407
+ maxTimeoutSeconds: ALIPAY_PAY_BEFORE_MS / 1e3,
1408
+ extra: {
1409
+ payment_needed_header: paymentNeededHeader,
1410
+ out_trade_no: outTradeNo,
1411
+ pay_before: payBefore,
1412
+ service_id: signedFields.service_id
1413
+ }
1414
+ };
1415
+ return { x402Accepts, paymentNeededHeader };
1416
+ }
1417
+ /**
1418
+ * Verify a `Payment-Proof` by calling
1419
+ * `alipay.aipay.agent.payment.verify` against the Alipay Open API.
1420
+ *
1421
+ * The proof is conveyed via `paymentPayload.payload`, which may be:
1422
+ * - a raw Base64URL string (the `Payment-Proof` header value), or
1423
+ * - an object `{ paymentProof: string }` / `{ proofHeader: string }`.
1424
+ *
1425
+ * All failure modes (malformed payload, network errors, Alipay
1426
+ * `code != 10000`) return `{ valid: false, error }`. No exception
1427
+ * escapes, regardless of client-supplied input.
1428
+ */
1429
+ async verify(paymentPayload, _requirements) {
1430
+ try {
1431
+ const proofHeader = extractProofHeader(paymentPayload.payload);
1432
+ const decoded = decodeProof(proofHeader);
1433
+ const response = await alipayOpenApiCall(
1434
+ "alipay.aipay.agent.payment.verify",
1435
+ {
1436
+ payment_proof: decoded.protocol.payment_proof,
1437
+ trade_no: decoded.protocol.trade_no,
1438
+ client_session: decoded.method.client_session
1439
+ },
1440
+ this.getOpenApiConfig()
1441
+ );
1442
+ if (response.code !== "10000") {
1443
+ return {
1444
+ valid: false,
1445
+ error: `alipay verify ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
1446
+ details: {
1447
+ code: response.code,
1448
+ sub_code: response.sub_code,
1449
+ sub_msg: response.sub_msg
1450
+ }
1451
+ };
1452
+ }
1453
+ return {
1454
+ valid: true,
1455
+ details: {
1456
+ trade_no: response.trade_no ?? decoded.protocol.trade_no,
1457
+ amount: response.amount,
1458
+ out_trade_no: response.out_trade_no,
1459
+ resource_id: response.resource_id,
1460
+ active: response.active
1461
+ }
1462
+ };
1463
+ } catch (e) {
1464
+ return {
1465
+ valid: false,
1466
+ error: e instanceof Error ? e.message : String(e)
1467
+ };
1468
+ }
1469
+ }
1470
+ /**
1471
+ * Settle by calling `alipay.aipay.agent.fulfillment.confirm` after the
1472
+ * service resource has been returned to the buyer.
1473
+ *
1474
+ * Per the design (see ALIPAY-INTEGRATION-DESIGN.md §5.1, risk row
1475
+ * "履约确认失败"), this is **fire-and-forget**: the caller (registry /
1476
+ * server) is expected to log fulfillment failures but NOT roll back
1477
+ * the already-delivered resource.
1478
+ *
1479
+ * Re-decodes `paymentPayload.payload` to extract `trade_no` (verify
1480
+ * does the same; the redundant Base64URL decode is negligible).
1481
+ */
1482
+ async settle(paymentPayload, _requirements) {
1483
+ try {
1484
+ const proofHeader = extractProofHeader(paymentPayload.payload);
1485
+ const decoded = decodeProof(proofHeader);
1486
+ const tradeNo = decoded.protocol.trade_no;
1487
+ const response = await alipayOpenApiCall(
1488
+ "alipay.aipay.agent.fulfillment.confirm",
1489
+ { trade_no: tradeNo },
1490
+ this.getOpenApiConfig()
1491
+ );
1492
+ if (response.code !== "10000") {
1493
+ return {
1494
+ success: false,
1495
+ transaction: tradeNo,
1496
+ error: `alipay fulfillment ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
1497
+ status: "fulfillment_failed"
1498
+ };
1499
+ }
1500
+ return {
1501
+ success: true,
1502
+ transaction: tradeNo,
1503
+ status: "fulfilled"
1504
+ };
1505
+ } catch (e) {
1506
+ return {
1507
+ success: false,
1508
+ error: e instanceof Error ? e.message : String(e)
1509
+ };
1510
+ }
1511
+ }
1512
+ /**
1513
+ * Validate that the configured RSA keys parse and that the Open API
1514
+ * gateway is reachable. Does NOT make a real business API call (would
1515
+ * burn quota and could appear as a legitimate verify attempt in logs).
1516
+ */
1517
+ async healthCheck() {
1518
+ const start = Date.now();
1519
+ try {
1520
+ crypto2.createPrivateKey(this.config.private_key_pem);
1521
+ } catch (e) {
1522
+ return {
1523
+ healthy: false,
1524
+ error: `merchant private_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
1525
+ };
1526
+ }
1527
+ try {
1528
+ crypto2.createPublicKey(this.config.alipay_public_key_pem);
1529
+ } catch (e) {
1530
+ return {
1531
+ healthy: false,
1532
+ error: `alipay_public_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
1533
+ };
1534
+ }
1535
+ const gatewayUrl = this.config.gateway_url ?? ALIPAY_GATEWAY_PROD;
1536
+ const controller = new AbortController();
1537
+ const timeout = setTimeout(() => controller.abort(), 5e3);
1538
+ const response = await fetch(gatewayUrl, {
1539
+ method: "HEAD",
1540
+ signal: controller.signal
1541
+ }).catch(() => null);
1542
+ clearTimeout(timeout);
1543
+ const latencyMs = Date.now() - start;
1544
+ if (!response) {
1545
+ return { healthy: false, error: `gateway unreachable: ${gatewayUrl}`, latencyMs };
1546
+ }
1547
+ return { healthy: true, latencyMs };
1548
+ }
1549
+ /** Bundle the facilitator config into the shape openapi.ts wants. */
1550
+ getOpenApiConfig() {
1551
+ return {
1552
+ gateway_url: this.config.gateway_url ?? ALIPAY_GATEWAY_PROD,
1553
+ app_id: this.config.app_id,
1554
+ private_key_pem: this.config.private_key_pem,
1555
+ alipay_public_key_pem: this.config.alipay_public_key_pem,
1556
+ sign_type: this.config.sign_type
1557
+ };
1558
+ }
1559
+ };
1560
+ function generateOutTradeNo() {
1561
+ return "VID" + crypto2.randomBytes(22).toString("base64url").slice(0, 29);
1562
+ }
1563
+ function formatPayBefore(now) {
1564
+ const expiry = new Date(now.getTime() + ALIPAY_PAY_BEFORE_MS);
1565
+ return expiry.toISOString().replace(/\.\d{3}Z$/, "Z");
1566
+ }
1567
+ function extractProofHeader(payload) {
1568
+ if (typeof payload === "string") {
1569
+ if (payload.length === 0) {
1570
+ throw new Error("alipay Payment-Proof is empty");
1571
+ }
1572
+ return payload;
1573
+ }
1574
+ if (payload !== null && typeof payload === "object") {
1575
+ const obj = payload;
1576
+ const candidate = obj.paymentProof ?? obj.proofHeader;
1577
+ if (typeof candidate === "string" && candidate.length > 0) {
1578
+ return candidate;
1579
+ }
1580
+ }
1581
+ throw new Error(
1582
+ "alipay payment payload must be a Base64URL string or {paymentProof: string} / {proofHeader: string}"
1583
+ );
1584
+ }
1585
+ function decodeProof(proofHeader) {
1586
+ let parsed;
1587
+ try {
1588
+ parsed = JSON.parse(decodeBase64UrlWithPadFix(proofHeader));
1589
+ } catch (e) {
1590
+ throw new Error(
1591
+ `failed to decode Payment-Proof: ${e instanceof Error ? e.message : String(e)}`
1592
+ );
1593
+ }
1594
+ if (parsed === null || typeof parsed !== "object") {
1595
+ throw new Error("decoded Payment-Proof is not an object");
1596
+ }
1597
+ const obj = parsed;
1598
+ const protocol = obj.protocol;
1599
+ const method = obj.method;
1600
+ if (!protocol || typeof protocol.payment_proof !== "string") {
1601
+ throw new Error("decoded Payment-Proof missing protocol.payment_proof");
1602
+ }
1603
+ if (typeof protocol.trade_no !== "string") {
1604
+ throw new Error("decoded Payment-Proof missing protocol.trade_no");
1605
+ }
1606
+ if (!method || typeof method.client_session !== "string") {
1607
+ throw new Error("decoded Payment-Proof missing method.client_session");
1608
+ }
1609
+ return parsed;
1610
+ }
1611
+
1240
1612
  // src/facilitators/registry.ts
1241
1613
  import { Keypair as Keypair2 } from "@solana/web3.js";
1242
1614
  import bs58 from "bs58";
@@ -1261,6 +1633,7 @@ var FacilitatorRegistry = class {
1261
1633
  }
1262
1634
  return new SolanaFacilitator({ feePayerKeypair });
1263
1635
  });
1636
+ this.registerFactory("alipay", (config) => new AlipayFacilitator(config));
1264
1637
  this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
1265
1638
  }
1266
1639
  /**
@@ -1495,6 +1868,11 @@ var PAYMENT_RESPONSE_HEADER = "x-payment-response";
1495
1868
  var MPP_AUTH_HEADER = "authorization";
1496
1869
  var MPP_WWW_AUTH_HEADER = "www-authenticate";
1497
1870
  var MPP_RECEIPT_HEADER = "payment-receipt";
1871
+ var ALIPAY_PAYMENT_NEEDED_HEADER = "payment-needed";
1872
+ var ALIPAY_PAYMENT_PROOF_HEADER = "payment-proof";
1873
+ function headerSafe(value) {
1874
+ return String(value ?? "").replace(/[^\x20-\x7E]/g, (ch) => encodeURIComponent(ch)).replace(/"/g, "%22");
1875
+ }
1498
1876
  var TOKEN_ADDRESSES = {
1499
1877
  "eip155:8453": {
1500
1878
  USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
@@ -1630,6 +2008,8 @@ var MoltsPayServer = class {
1630
2008
  registry;
1631
2009
  networkId;
1632
2010
  useMainnet;
2011
+ /** Alipay AI 收 facilitator instance, set when `provider.alipay` is configured (2.0.0). */
2012
+ alipayFacilitator = null;
1633
2013
  constructor(servicesPath, options = {}) {
1634
2014
  loadEnvFile2();
1635
2015
  const content = readFileSync2(servicesPath, "utf-8");
@@ -1651,7 +2031,38 @@ var MoltsPayServer = class {
1651
2031
  cdp: { useMainnet: this.useMainnet }
1652
2032
  }
1653
2033
  };
2034
+ const providerAlipay = this.manifest.provider.alipay;
2035
+ if (providerAlipay) {
2036
+ try {
2037
+ const baseDir = path2.dirname(servicesPath);
2038
+ const resolvePem = (p, kind) => toPem(readFileSync2(path2.isAbsolute(p) ? p : path2.resolve(baseDir, p), "utf-8"), kind);
2039
+ const alipayFacilitatorConfig = {
2040
+ seller_id: providerAlipay.seller_id,
2041
+ app_id: providerAlipay.app_id,
2042
+ seller_name: providerAlipay.seller_name,
2043
+ service_id_default: providerAlipay.service_id_default,
2044
+ private_key_pem: resolvePem(providerAlipay.private_key_path, "PRIVATE"),
2045
+ alipay_public_key_pem: resolvePem(providerAlipay.alipay_public_key_path, "PUBLIC"),
2046
+ gateway_url: providerAlipay.gateway_url,
2047
+ sign_type: providerAlipay.sign_type
2048
+ };
2049
+ facilitatorConfig.config = {
2050
+ ...facilitatorConfig.config,
2051
+ alipay: alipayFacilitatorConfig
2052
+ };
2053
+ facilitatorConfig.fallback = facilitatorConfig.fallback || [];
2054
+ if (facilitatorConfig.primary !== "alipay" && !facilitatorConfig.fallback.includes("alipay")) {
2055
+ facilitatorConfig.fallback.push("alipay");
2056
+ }
2057
+ } catch (err) {
2058
+ throw new Error(`[MoltsPay] Alipay rail configured but key load failed: ${err.message}`);
2059
+ }
2060
+ }
1654
2061
  this.registry = new FacilitatorRegistry(facilitatorConfig);
2062
+ if (providerAlipay) {
2063
+ this.alipayFacilitator = this.registry.get("alipay");
2064
+ console.log(`[MoltsPay] Alipay AI \u6536 rail enabled (seller ${providerAlipay.seller_id})`);
2065
+ }
1655
2066
  const primaryFacilitator = this.registry.get(facilitatorConfig.primary);
1656
2067
  console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
1657
2068
  console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
@@ -1787,10 +2198,10 @@ var MoltsPayServer = class {
1787
2198
  writeCorsHeaders(res, origin) {
1788
2199
  res.setHeader("Access-Control-Allow-Origin", origin);
1789
2200
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1790
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
2201
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization, Payment-Proof");
1791
2202
  res.setHeader(
1792
2203
  "Access-Control-Expose-Headers",
1793
- "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt"
2204
+ "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt, Payment-Needed"
1794
2205
  );
1795
2206
  }
1796
2207
  /**
@@ -1817,7 +2228,8 @@ var MoltsPayServer = class {
1817
2228
  if (url.pathname === "/execute" && req.method === "POST") {
1818
2229
  const body = await this.readBody(req);
1819
2230
  const paymentHeader = req.headers[PAYMENT_HEADER];
1820
- return await this.handleExecute(body, paymentHeader, res);
2231
+ const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
2232
+ return await this.handleExecute(body, paymentHeader, res, proofHeader);
1821
2233
  }
1822
2234
  if (url.pathname === "/proxy" && req.method === "POST") {
1823
2235
  const clientIP = req.headers["x-real-ip"] || req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "";
@@ -1835,7 +2247,8 @@ var MoltsPayServer = class {
1835
2247
  const body = req.method === "POST" ? await this.readBody(req) : {};
1836
2248
  const authHeader = req.headers[MPP_AUTH_HEADER];
1837
2249
  const x402Header = req.headers[PAYMENT_HEADER];
1838
- return await this.handleMPPRequest(skill, body, authHeader, x402Header, res);
2250
+ const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
2251
+ return await this.handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader);
1839
2252
  }
1840
2253
  this.sendJson(res, 404, { error: "Not found" });
1841
2254
  } catch (err) {
@@ -1932,7 +2345,7 @@ var MoltsPayServer = class {
1932
2345
  /**
1933
2346
  * POST /execute - Execute service with x402 payment
1934
2347
  */
1935
- async handleExecute(body, paymentHeader, res) {
2348
+ async handleExecute(body, paymentHeader, res, proofHeader) {
1936
2349
  const { service, params } = body;
1937
2350
  if (!service) {
1938
2351
  return this.sendJson(res, 400, { error: "Missing service" });
@@ -1946,6 +2359,15 @@ var MoltsPayServer = class {
1946
2359
  return this.sendJson(res, 400, { error: `Missing required param: ${key}` });
1947
2360
  }
1948
2361
  }
2362
+ if (proofHeader) {
2363
+ const alipayPayment = {
2364
+ x402Version: X402_VERSION2,
2365
+ scheme: ALIPAY_SCHEME,
2366
+ network: ALIPAY_NETWORK,
2367
+ payload: proofHeader
2368
+ };
2369
+ return this.handleAlipayExecute(skill, params || {}, alipayPayment, res);
2370
+ }
1949
2371
  if (!paymentHeader) {
1950
2372
  return this.sendPaymentRequired(skill.config, res);
1951
2373
  }
@@ -1956,6 +2378,11 @@ var MoltsPayServer = class {
1956
2378
  } catch {
1957
2379
  return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
1958
2380
  }
2381
+ const payScheme = payment.accepted?.scheme || payment.scheme;
2382
+ const payNetwork = payment.accepted?.network || payment.network;
2383
+ if (payScheme === ALIPAY_SCHEME || (payNetwork ? isAlipayChainId(payNetwork) : false)) {
2384
+ return this.handleAlipayExecute(skill, params || {}, payment, res);
2385
+ }
1959
2386
  const validation = this.validatePayment(payment, skill.config);
1960
2387
  if (!validation.valid) {
1961
2388
  return this.sendJson(res, 402, { error: validation.error });
@@ -2048,13 +2475,111 @@ var MoltsPayServer = class {
2048
2475
  payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
2049
2476
  }, responseHeaders);
2050
2477
  }
2478
+ /**
2479
+ * Execute a service paid via the Alipay AI 收 fiat rail (2.0.0).
2480
+ *
2481
+ * Differs from the EVM/SVM path: no token detection, no EIP-3009/permit
2482
+ * validation. Verify hits the Alipay Open API (`payment.verify`). Settlement
2483
+ * (`fulfillment.confirm`) is FIRE-AND-FORGET per ALIPAY-INTEGRATION-DESIGN
2484
+ * §5.1: a confirm failure is logged but does NOT fail the already-delivered
2485
+ * response (the buyer's payment proof was already verified).
2486
+ */
2487
+ async handleAlipayExecute(skill, params, payment, res) {
2488
+ if (!this.alipayFacilitator) {
2489
+ return this.sendJson(res, 402, { error: "Alipay rail not configured on this server" });
2490
+ }
2491
+ const requirements = {
2492
+ scheme: ALIPAY_SCHEME,
2493
+ network: ALIPAY_NETWORK,
2494
+ asset: "CNY",
2495
+ amount: skill.config.alipay?.price_cny || "0",
2496
+ payTo: this.manifest.provider.alipay?.seller_id || "",
2497
+ maxTimeoutSeconds: 1800
2498
+ };
2499
+ console.log(`[MoltsPay] Verifying Alipay payment...`);
2500
+ const verifyResult = await this.registry.verify(payment, requirements);
2501
+ if (!verifyResult.valid) {
2502
+ return this.sendJson(res, 402, {
2503
+ error: `Payment verification failed: ${verifyResult.error}`,
2504
+ facilitator: verifyResult.facilitator
2505
+ });
2506
+ }
2507
+ console.log(`[MoltsPay] Alipay payment verified by ${verifyResult.facilitator}`);
2508
+ const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
2509
+ console.log(`[MoltsPay] Executing skill: ${skill.id} (timeout: ${timeoutSeconds}s)`);
2510
+ let result;
2511
+ try {
2512
+ result = await Promise.race([
2513
+ skill.handler(params),
2514
+ new Promise(
2515
+ (_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
2516
+ )
2517
+ ]);
2518
+ } catch (err) {
2519
+ console.error("[MoltsPay] Skill execution failed:", err.message);
2520
+ return this.sendJson(res, 500, {
2521
+ error: "Service execution failed",
2522
+ message: err.message
2523
+ });
2524
+ }
2525
+ let settlement;
2526
+ try {
2527
+ settlement = await this.registry.settle(payment, requirements);
2528
+ if (settlement.success) {
2529
+ console.log(`[MoltsPay] Alipay fulfillment confirmed: ${settlement.transaction}`);
2530
+ } else {
2531
+ console.error(`[MoltsPay] Alipay fulfillment confirm failed (non-fatal): ${settlement.error}`);
2532
+ }
2533
+ } catch (err) {
2534
+ console.error(`[MoltsPay] Alipay fulfillment confirm threw (non-fatal): ${err.message}`);
2535
+ settlement = { success: false, error: err.message, facilitator: "alipay" };
2536
+ }
2537
+ const responseHeaders = {};
2538
+ if (settlement.success) {
2539
+ responseHeaders[PAYMENT_RESPONSE_HEADER] = Buffer.from(JSON.stringify({
2540
+ success: true,
2541
+ transaction: settlement.transaction,
2542
+ network: ALIPAY_NETWORK,
2543
+ facilitator: settlement.facilitator
2544
+ })).toString("base64");
2545
+ }
2546
+ this.sendJson(res, 200, {
2547
+ success: true,
2548
+ result,
2549
+ payment: settlement.success ? { transaction: settlement.transaction, status: "fulfilled", facilitator: settlement.facilitator } : { status: "delivered_unconfirmed", error: settlement.error }
2550
+ }, responseHeaders);
2551
+ }
2552
+ /**
2553
+ * Build the Alipay 402 challenge for a service, or null when the alipay rail
2554
+ * isn't configured for this server or this service. Returns the x402
2555
+ * `accepts[]` entry plus the Base64URL `Payment-Needed` header value so the
2556
+ * 402 responders can dual-emit both the x402 and legacy alipay-bot formats.
2557
+ */
2558
+ async buildAlipayChallenge(config) {
2559
+ if (!this.alipayFacilitator || !config.alipay) return null;
2560
+ try {
2561
+ const req = await this.alipayFacilitator.createPaymentRequirements({
2562
+ serviceId: config.alipay.service_id || this.manifest.provider.alipay.service_id_default,
2563
+ priceCny: config.alipay.price_cny,
2564
+ goodsName: config.alipay.goods_name,
2565
+ resourceId: `/execute?service=${config.id}`
2566
+ });
2567
+ return { accepts: req.x402Accepts, paymentNeededHeader: req.paymentNeededHeader };
2568
+ } catch (err) {
2569
+ console.error(`[MoltsPay] Alipay challenge build failed for ${config.id}: ${err.message}`);
2570
+ return null;
2571
+ }
2572
+ }
2051
2573
  /**
2052
2574
  * Handle MPP (Machine Payments Protocol) request
2053
2575
  * Supports both x402 and MPP protocols on service endpoints
2054
2576
  */
2055
- async handleMPPRequest(skill, body, authHeader, x402Header, res) {
2577
+ async handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader) {
2056
2578
  const config = skill.config;
2057
2579
  const params = body || {};
2580
+ if (proofHeader) {
2581
+ return await this.handleExecute({ service: config.id, params }, void 0, res, proofHeader);
2582
+ }
2058
2583
  if (x402Header) {
2059
2584
  return await this.handleExecute({ service: config.id, params }, x402Header, res);
2060
2585
  }
@@ -2160,7 +2685,7 @@ var MoltsPayServer = class {
2160
2685
  /**
2161
2686
  * Return 402 with both x402 and MPP payment requirements
2162
2687
  */
2163
- sendMPPPaymentRequired(config, res) {
2688
+ async sendMPPPaymentRequired(config, res) {
2164
2689
  const acceptedTokens = getAcceptedCurrencies(config);
2165
2690
  const providerChains = this.getProviderChains();
2166
2691
  const accepts = [];
@@ -2171,6 +2696,10 @@ var MoltsPayServer = class {
2171
2696
  }
2172
2697
  }
2173
2698
  }
2699
+ const alipayChallenge = await this.buildAlipayChallenge(config);
2700
+ if (alipayChallenge) {
2701
+ accepts.push(alipayChallenge.accepts);
2702
+ }
2174
2703
  const x402PaymentRequired = {
2175
2704
  x402Version: X402_VERSION2,
2176
2705
  accepts,
@@ -2198,7 +2727,7 @@ var MoltsPayServer = class {
2198
2727
  };
2199
2728
  const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
2200
2729
  const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
2201
- mppWwwAuth = `Payment id="${challengeId}", realm="${this.manifest.provider.name}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
2730
+ mppWwwAuth = `Payment id="${challengeId}", realm="${headerSafe(this.manifest.provider.name)}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
2202
2731
  }
2203
2732
  const headers = {
2204
2733
  "Content-Type": "application/problem+json",
@@ -2207,6 +2736,9 @@ var MoltsPayServer = class {
2207
2736
  if (mppWwwAuth) {
2208
2737
  headers[MPP_WWW_AUTH_HEADER] = mppWwwAuth;
2209
2738
  }
2739
+ if (alipayChallenge) {
2740
+ headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
2741
+ }
2210
2742
  res.writeHead(402, headers);
2211
2743
  res.end(JSON.stringify({
2212
2744
  type: "https://paymentauth.org/problems/payment-required",
@@ -2233,7 +2765,7 @@ var MoltsPayServer = class {
2233
2765
  * Return 402 with x402 payment requirements (v2 format)
2234
2766
  * Includes requirements for all chains and all accepted currencies
2235
2767
  */
2236
- sendPaymentRequired(config, res) {
2768
+ async sendPaymentRequired(config, res) {
2237
2769
  const acceptedTokens = getAcceptedCurrencies(config);
2238
2770
  const providerChains = this.getProviderChains();
2239
2771
  const accepts = [];
@@ -2244,6 +2776,10 @@ var MoltsPayServer = class {
2244
2776
  }
2245
2777
  }
2246
2778
  }
2779
+ const alipayChallenge = await this.buildAlipayChallenge(config);
2780
+ if (alipayChallenge) {
2781
+ accepts.push(alipayChallenge.accepts);
2782
+ }
2247
2783
  const acceptedChains = providerChains.map((c) => {
2248
2784
  if (c.network === "eip155:8453") return "base";
2249
2785
  if (c.network === "eip155:137") return "polygon";
@@ -2261,10 +2797,14 @@ var MoltsPayServer = class {
2261
2797
  }
2262
2798
  };
2263
2799
  const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
2264
- res.writeHead(402, {
2800
+ const headers = {
2265
2801
  "Content-Type": "application/json",
2266
2802
  [PAYMENT_REQUIRED_HEADER]: encoded
2267
- });
2803
+ };
2804
+ if (alipayChallenge) {
2805
+ headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
2806
+ }
2807
+ res.writeHead(402, headers);
2268
2808
  res.end(JSON.stringify({
2269
2809
  error: "Payment required",
2270
2810
  message: `Service requires $${config.price} ${config.currency}`,
@@ -2371,12 +2911,12 @@ var MoltsPayServer = class {
2371
2911
  return accepted.includes(token);
2372
2912
  }
2373
2913
  async readBody(req) {
2374
- return new Promise((resolve, reject) => {
2914
+ return new Promise((resolve2, reject) => {
2375
2915
  let body = "";
2376
2916
  req.on("data", (chunk) => body += chunk);
2377
2917
  req.on("end", () => {
2378
2918
  try {
2379
- resolve(body ? JSON.parse(body) : {});
2919
+ resolve2(body ? JSON.parse(body) : {});
2380
2920
  } catch {
2381
2921
  reject(new Error("Invalid JSON"));
2382
2922
  }
@@ -2634,7 +3174,7 @@ var MoltsPayServer = class {
2634
3174
  };
2635
3175
  const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
2636
3176
  const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
2637
- const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
3177
+ const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
2638
3178
  res.writeHead(402, {
2639
3179
  "Content-Type": "application/problem+json",
2640
3180
  [MPP_WWW_AUTH_HEADER]: wwwAuth
@@ -2801,8 +3341,8 @@ var MoltsPayServer = class {
2801
3341
 
2802
3342
  // src/client/node/index.ts
2803
3343
  import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, statSync, chmodSync } from "fs";
2804
- import { homedir as homedir2 } from "os";
2805
- import { join as join4 } from "path";
3344
+ import { homedir as homedir3 } from "os";
3345
+ import { join as join5 } from "path";
2806
3346
  import { Wallet as Wallet2, ethers as ethers3 } from "ethers";
2807
3347
 
2808
3348
  // src/wallet/solana.ts
@@ -2861,6 +3401,16 @@ function networkToChainName(network) {
2861
3401
  // src/client/core/base64.ts
2862
3402
  var BufferCtor = globalThis.Buffer;
2863
3403
 
3404
+ // src/client/core/errors.ts
3405
+ var MoltsPayError = class extends Error {
3406
+ code;
3407
+ constructor(code, message) {
3408
+ super(message);
3409
+ this.name = "MoltsPayError";
3410
+ this.code = code;
3411
+ }
3412
+ };
3413
+
2864
3414
  // src/client/core/eip3009.ts
2865
3415
  var EIP3009_TYPES = {
2866
3416
  TransferWithAuthorization: [
@@ -2997,6 +3547,748 @@ function findChainByChainId(chainId) {
2997
3547
  return void 0;
2998
3548
  }
2999
3549
 
3550
+ // src/client/alipay/index.ts
3551
+ import { randomUUID } from "crypto";
3552
+ import { mkdir, writeFile } from "fs/promises";
3553
+ import { homedir as homedir2 } from "os";
3554
+ import { join as join4 } from "path";
3555
+
3556
+ // src/client/alipay/cli.ts
3557
+ import { spawn } from "child_process";
3558
+
3559
+ // src/client/alipay/log.ts
3560
+ var RANK = { off: 0, info: 1, debug: 2 };
3561
+ function normalizeLevel(v) {
3562
+ const s = (v ?? "").toLowerCase();
3563
+ return s === "info" || s === "debug" ? s : "off";
3564
+ }
3565
+ var level = normalizeLevel(process.env.MOLTSPAY_ALIPAY_LOG);
3566
+ var sink = (line) => {
3567
+ process.stderr.write(line + "\n");
3568
+ };
3569
+ function setAlipayLogLevel(next) {
3570
+ level = next;
3571
+ }
3572
+ function getAlipayLogLevel() {
3573
+ return level;
3574
+ }
3575
+ function setAlipayLogSink(fn) {
3576
+ sink = fn;
3577
+ }
3578
+ function enabledFor(want) {
3579
+ return RANK[level] >= RANK[want];
3580
+ }
3581
+ function render(event, fields) {
3582
+ const parts = ["[moltspay:alipay]", String(fields.ts), event];
3583
+ if (fields.flow) parts.push(`flow=${fields.flow}`);
3584
+ if (fields.step) parts.push(`step=${fields.step}`);
3585
+ if (typeof fields.ms === "number") parts.push(`${fields.ms}ms`);
3586
+ for (const [k, v] of Object.entries(fields)) {
3587
+ if (["ts", "level", "event", "flow", "step", "ms"].includes(k)) continue;
3588
+ parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
3589
+ }
3590
+ return parts.join(" ");
3591
+ }
3592
+ function emit(lvl, event, fields) {
3593
+ if (!enabledFor(lvl)) return;
3594
+ const full = { ts: (/* @__PURE__ */ new Date()).toISOString(), level: lvl, event, ...fields };
3595
+ try {
3596
+ sink(render(event, full), full);
3597
+ } catch {
3598
+ }
3599
+ }
3600
+ var alipayLog = {
3601
+ info: (event, fields = {}) => emit("info", event, fields),
3602
+ debug: (event, fields = {}) => emit("debug", event, fields),
3603
+ /** True if anything at all would be logged (cheap guard for hot paths). */
3604
+ enabled: () => level !== "off"
3605
+ };
3606
+ async function timeStep(step, flow, fn) {
3607
+ if (!alipayLog.enabled()) return fn();
3608
+ const t0 = Date.now();
3609
+ alipayLog.debug("step.start", { flow, step });
3610
+ try {
3611
+ const out = await fn();
3612
+ alipayLog.info("step.end", { flow, step, ms: Date.now() - t0, ok: true });
3613
+ return out;
3614
+ } catch (err) {
3615
+ alipayLog.info("step.end", {
3616
+ flow,
3617
+ step,
3618
+ ms: Date.now() - t0,
3619
+ ok: false,
3620
+ err: err instanceof Error ? err.message : String(err)
3621
+ });
3622
+ throw err;
3623
+ }
3624
+ }
3625
+
3626
+ // src/client/alipay/cli.ts
3627
+ var ALLOWED_ENV = /* @__PURE__ */ new Set([
3628
+ "AIPAY_OUTPUT_CHANNEL",
3629
+ "AIPAY_SESSION_ID",
3630
+ "AIPAY_FRAMEWORK",
3631
+ "AIPAY_MODEL",
3632
+ "AIPAY_OS",
3633
+ // Minimal survival set for spawn to find the binary and a home dir.
3634
+ "PATH",
3635
+ "HOME"
3636
+ ]);
3637
+ function filterEnv(env) {
3638
+ return Object.fromEntries(
3639
+ Object.entries(env).filter(([k]) => ALLOWED_ENV.has(k))
3640
+ );
3641
+ }
3642
+ function makeLineSplitter(onLine) {
3643
+ let buf = "";
3644
+ return (chunk) => {
3645
+ buf += chunk.toString("utf-8");
3646
+ let nl;
3647
+ while ((nl = buf.indexOf("\n")) !== -1) {
3648
+ onLine(buf.slice(0, nl));
3649
+ buf = buf.slice(nl + 1);
3650
+ }
3651
+ };
3652
+ }
3653
+ function profileEnv(step, flow) {
3654
+ const want = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE;
3655
+ const hook = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_HOOK;
3656
+ if (!want || !hook) return {};
3657
+ const steps = want.split(",").map((s) => s.trim());
3658
+ if (!steps.includes("all") && !steps.includes(step)) return {};
3659
+ const dir = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_DIR || "/tmp";
3660
+ const safe = `${flow ?? "noflow"}-${step}-${Date.now()}`.replace(/[^a-zA-Z0-9._-]/g, "_");
3661
+ const out = `${dir}/cli-profile-${safe}.json`;
3662
+ alipayLog.info("cli.profile", { flow, step, out });
3663
+ return {
3664
+ NODE_OPTIONS: `--require=${hook}`,
3665
+ MOLTSPAY_CLI_PROFILE_OUT: out
3666
+ };
3667
+ }
3668
+ var runCli = (args, opts = {}) => {
3669
+ const bin = opts.bin ?? "alipay-bot";
3670
+ const step = opts.step ?? args[0] ?? "(no-arg)";
3671
+ const startedAt = Date.now();
3672
+ const lines = [];
3673
+ const collect = (line) => {
3674
+ lines.push(line);
3675
+ alipayLog.debug("cli.line", {
3676
+ flow: opts.flow,
3677
+ step,
3678
+ ms: Date.now() - startedAt,
3679
+ preview: line.slice(0, 120)
3680
+ });
3681
+ opts.onLine?.(line);
3682
+ };
3683
+ alipayLog.debug("step.start", { flow: opts.flow, step });
3684
+ return new Promise((resolve2, reject) => {
3685
+ const child = spawn(bin, args, {
3686
+ env: { ...filterEnv(process.env), ...profileEnv(step, opts.flow), ...opts.env ?? {} }
3687
+ });
3688
+ if (opts.signal) {
3689
+ if (opts.signal.aborted) child.kill("SIGTERM");
3690
+ opts.signal.addEventListener("abort", () => child.kill("SIGTERM"), { once: true });
3691
+ }
3692
+ let sawByte = false;
3693
+ const onChunk = (stream) => (chunk) => {
3694
+ const ms = Date.now() - startedAt;
3695
+ if (!sawByte) {
3696
+ sawByte = true;
3697
+ alipayLog.debug("cli.firstbyte", { flow: opts.flow, step, ms });
3698
+ }
3699
+ alipayLog.debug("cli.chunk", { flow: opts.flow, step, ms, stream, bytes: chunk.length });
3700
+ };
3701
+ const onStdout = makeLineSplitter(collect);
3702
+ const onStderr = makeLineSplitter(collect);
3703
+ child.stdout?.on("data", onChunk("out"));
3704
+ child.stderr?.on("data", onChunk("err"));
3705
+ child.stdout?.on("data", onStdout);
3706
+ child.stderr?.on("data", onStderr);
3707
+ child.on("error", (err) => {
3708
+ alipayLog.info("cli.exit", {
3709
+ flow: opts.flow,
3710
+ step,
3711
+ ms: Date.now() - startedAt,
3712
+ ok: false,
3713
+ err: err.message
3714
+ });
3715
+ reject(err);
3716
+ });
3717
+ child.on("close", (code) => {
3718
+ alipayLog.info("cli.exit", {
3719
+ flow: opts.flow,
3720
+ step,
3721
+ ms: Date.now() - startedAt,
3722
+ ok: (code ?? 1) === 0,
3723
+ code: code ?? 1
3724
+ });
3725
+ resolve2({ exitCode: code ?? 1, lines });
3726
+ });
3727
+ });
3728
+ };
3729
+
3730
+ // src/client/alipay/install.ts
3731
+ import { execFile } from "child_process";
3732
+ import { promisify } from "util";
3733
+
3734
+ // src/client/alipay/errors.ts
3735
+ var AlipayCliNotFoundError = class extends MoltsPayError {
3736
+ constructor(message) {
3737
+ super("ALIPAY_CLI_NOT_FOUND", message);
3738
+ this.name = "AlipayCliNotFoundError";
3739
+ }
3740
+ };
3741
+ var AlipayCliVersionError = class extends MoltsPayError {
3742
+ constructor(message) {
3743
+ super("ALIPAY_CLI_VERSION", message);
3744
+ this.name = "AlipayCliVersionError";
3745
+ }
3746
+ };
3747
+ var NeedsWalletSetupError = class extends MoltsPayError {
3748
+ constructor(message = "Alipay wallet not set up. Run: moltspay alipay apply") {
3749
+ super("ALIPAY_NEEDS_WALLET_SETUP", message);
3750
+ this.name = "NeedsWalletSetupError";
3751
+ }
3752
+ };
3753
+ var AlipayPaymentRejectedError = class extends MoltsPayError {
3754
+ constructor(message) {
3755
+ super("ALIPAY_PAYMENT_REJECTED", message);
3756
+ this.name = "AlipayPaymentRejectedError";
3757
+ }
3758
+ };
3759
+ var AlipayPaymentTimeoutError = class extends MoltsPayError {
3760
+ constructor(message) {
3761
+ super("ALIPAY_PAYMENT_TIMEOUT", message);
3762
+ this.name = "AlipayPaymentTimeoutError";
3763
+ }
3764
+ };
3765
+ var AlipayProtocolError = class extends MoltsPayError {
3766
+ constructor(message) {
3767
+ super("ALIPAY_PROTOCOL", message);
3768
+ this.name = "AlipayProtocolError";
3769
+ }
3770
+ };
3771
+ var UnsupportedRailError = class extends MoltsPayError {
3772
+ constructor(rail, message) {
3773
+ super("UNSUPPORTED_RAIL", message ?? `Rail not supported by server: ${rail}`);
3774
+ this.rail = rail;
3775
+ this.name = "UnsupportedRailError";
3776
+ }
3777
+ };
3778
+
3779
+ // src/client/alipay/install.ts
3780
+ var execFileAsync = promisify(execFile);
3781
+ var MIN_CLI_VERSION = "0.3.15";
3782
+ function semverLt(a, b) {
3783
+ const pa = a.split(".").map((n) => parseInt(n, 10));
3784
+ const pb = b.split(".").map((n) => parseInt(n, 10));
3785
+ for (let i = 0; i < 3; i++) {
3786
+ const x = pa[i] ?? 0;
3787
+ const y = pb[i] ?? 0;
3788
+ if (x !== y) return x < y;
3789
+ }
3790
+ return false;
3791
+ }
3792
+ function parseVersion(stdout) {
3793
+ return stdout.match(/v?(\d+\.\d+\.\d+)/)?.[1] ?? null;
3794
+ }
3795
+ var defaultGetVersion = async () => {
3796
+ const { stdout } = await execFileAsync("alipay-bot", ["--version"]);
3797
+ return stdout;
3798
+ };
3799
+ async function ensureCliUncached(getVersion) {
3800
+ let stdout;
3801
+ try {
3802
+ stdout = await getVersion();
3803
+ } catch (e) {
3804
+ if (e?.code === "ENOENT") {
3805
+ throw new AlipayCliNotFoundError(
3806
+ "alipay-bot not installed. Run: npx -y @alipay/agent-payment install-cli"
3807
+ );
3808
+ }
3809
+ throw e;
3810
+ }
3811
+ const version = parseVersion(stdout);
3812
+ if (!version || semverLt(version, MIN_CLI_VERSION)) {
3813
+ throw new AlipayCliVersionError(
3814
+ `alipay-bot ${version ?? "?"} found, need \u2265 ${MIN_CLI_VERSION}. Run: npx -y @alipay/agent-payment@latest update`
3815
+ );
3816
+ }
3817
+ return version;
3818
+ }
3819
+ var cachedVersion = null;
3820
+ var inflight = null;
3821
+ async function ensureCli(getVersion = defaultGetVersion) {
3822
+ if (getVersion !== defaultGetVersion) {
3823
+ return ensureCliUncached(getVersion);
3824
+ }
3825
+ if (cachedVersion) return cachedVersion;
3826
+ if (!inflight) {
3827
+ inflight = ensureCliUncached(getVersion).then((v) => {
3828
+ cachedVersion = v;
3829
+ return v;
3830
+ }).finally(() => {
3831
+ inflight = null;
3832
+ });
3833
+ }
3834
+ return inflight;
3835
+ }
3836
+
3837
+ // src/client/alipay/poll.ts
3838
+ var POLL_INTERVAL_MS = 3e3;
3839
+ var POLL_MAX_INFLIGHT = 2;
3840
+ function resolveMaxInflight(override) {
3841
+ if (override !== void 0) return Math.max(1, Math.floor(override));
3842
+ const raw = process.env.MOLTSPAY_ALIPAY_POLL_MAX_INFLIGHT;
3843
+ if (raw !== void 0 && raw.trim() !== "") {
3844
+ const n = Number(raw);
3845
+ if (Number.isFinite(n) && n >= 1) return Math.floor(n);
3846
+ }
3847
+ return POLL_MAX_INFLIGHT;
3848
+ }
3849
+ function resolveLaunchGap(override) {
3850
+ if (override !== void 0) return Math.max(0, override);
3851
+ const raw = process.env.MOLTSPAY_ALIPAY_POLL_LAUNCH_MS;
3852
+ if (raw !== void 0 && raw.trim() !== "") {
3853
+ const n = Number(raw);
3854
+ if (Number.isFinite(n) && n >= 0) return n;
3855
+ }
3856
+ return POLL_INTERVAL_MS;
3857
+ }
3858
+ function parseStatus(lines) {
3859
+ const raw = lines.join("\n").trim();
3860
+ try {
3861
+ const json = JSON.parse(raw);
3862
+ if (json && typeof json === "object") {
3863
+ if (typeof json.success === "boolean") {
3864
+ if (json.success) return "paid";
3865
+ const err = String(json.errorCode ?? "").toUpperCase();
3866
+ if (/UNPAID|WAIT|PENDING|PROCESS|NOTPAY/.test(err)) return "pending";
3867
+ if (/CLOSED|CANCEL|FAIL|REJECT|REFUSE|TIMEOUT|EXPIRE/.test(err)) return "rejected";
3868
+ return "pending";
3869
+ }
3870
+ if (typeof json.code !== "undefined") {
3871
+ if (Number(json.code) === 200) return "paid";
3872
+ const msg = `${json.message ?? ""}${json.reason ?? ""}`;
3873
+ if (/关闭|失败|拒绝|取消|超时|已撤销/.test(msg)) return "rejected";
3874
+ return "pending";
3875
+ }
3876
+ if (typeof json.body === "string") {
3877
+ return classifyReportText(json.body);
3878
+ }
3879
+ }
3880
+ } catch {
3881
+ }
3882
+ return classifyReportText(raw);
3883
+ }
3884
+ function classifyReportText(s) {
3885
+ const text = s.toUpperCase();
3886
+ if (/查询支付状态成功并获取资源/.test(s) || /资源响应状态[^0-9\n]{0,8}200/.test(s) || /TRADE_SUCCESS|TRADE_FINISHED|"STATUS":\s*"FULFILLED"/.test(text)) return "paid";
3887
+ if (/TRADE_CLOSED|REJECTED|REFUSE|CANCEL/.test(text) || /交易关闭|支付失败|已拒绝|已取消|已撤销|交易超时/.test(s)) return "rejected";
3888
+ if (/WAIT_BUYER_PAY|UNPAID|PENDING|WAITING/.test(text) || /交易未支付|等待付款|待支付|未支付/.test(s)) return "pending";
3889
+ return "unknown";
3890
+ }
3891
+ function defaultSleep(ms, signal) {
3892
+ return new Promise((resolve2, reject) => {
3893
+ if (signal?.aborted) return reject(new Error("aborted"));
3894
+ const t = setTimeout(resolve2, ms);
3895
+ signal?.addEventListener(
3896
+ "abort",
3897
+ () => {
3898
+ clearTimeout(t);
3899
+ reject(new Error("aborted"));
3900
+ },
3901
+ { once: true }
3902
+ );
3903
+ });
3904
+ }
3905
+ var LAUNCH = /* @__PURE__ */ Symbol("launch");
3906
+ async function pollUntil(tradeNo, resourceUrl, opts) {
3907
+ const runner = opts.runner ?? runCli;
3908
+ const sleep = opts.sleep ?? defaultSleep;
3909
+ const now = opts.now ?? Date.now;
3910
+ const maxInflight = resolveMaxInflight(opts.maxInflight);
3911
+ const launchGap = resolveLaunchGap(opts.launchIntervalMs);
3912
+ const args = [
3913
+ "402-query-payment-status",
3914
+ "-t",
3915
+ tradeNo,
3916
+ "-r",
3917
+ resourceUrl,
3918
+ ...opts.method ? ["-m", opts.method] : [],
3919
+ ...opts.data ? ["-d", opts.data] : []
3920
+ ];
3921
+ const internal = new AbortController();
3922
+ const signal = opts.signal ? AbortSignal.any([opts.signal, internal.signal]) : internal.signal;
3923
+ let tick = 0;
3924
+ let lastLaunch = -Infinity;
3925
+ const inflight2 = /* @__PURE__ */ new Set();
3926
+ const launch = () => {
3927
+ tick += 1;
3928
+ const myTick = tick;
3929
+ lastLaunch = now();
3930
+ const run = (async () => {
3931
+ const { lines } = await runner(args, {
3932
+ signal,
3933
+ step: "query-payment-status",
3934
+ flow: tradeNo
3935
+ });
3936
+ const status = parseStatus(lines);
3937
+ alipayLog.debug("poll.tick", { flow: tradeNo, tick: myTick, status });
3938
+ return { status, lines };
3939
+ })();
3940
+ const tracked = run.finally(() => {
3941
+ inflight2.delete(tracked);
3942
+ });
3943
+ inflight2.add(tracked);
3944
+ };
3945
+ try {
3946
+ for (; ; ) {
3947
+ if (opts.signal?.aborted) throw new Error("aborted");
3948
+ const expired = now() >= opts.deadline;
3949
+ if (expired && inflight2.size === 0) {
3950
+ throw new AlipayPaymentTimeoutError(
3951
+ `Payment ${tradeNo} not completed before pay_before deadline`
3952
+ );
3953
+ }
3954
+ while (!expired && inflight2.size < maxInflight && now() - lastLaunch >= launchGap) {
3955
+ launch();
3956
+ }
3957
+ const waiters = [...inflight2];
3958
+ if (!expired && inflight2.size < maxInflight) {
3959
+ const untilNext = Math.max(0, launchGap - (now() - lastLaunch));
3960
+ waiters.push(sleep(untilNext, signal).then(() => LAUNCH, () => LAUNCH));
3961
+ }
3962
+ const winner = await Promise.race(waiters);
3963
+ if (winner === LAUNCH) continue;
3964
+ if (winner.status === "paid") return { status: "paid", lines: winner.lines };
3965
+ if (winner.status === "rejected") {
3966
+ throw new AlipayPaymentRejectedError(`Payment ${tradeNo} was rejected`);
3967
+ }
3968
+ }
3969
+ } finally {
3970
+ internal.abort();
3971
+ for (const p of inflight2) p.catch(() => void 0);
3972
+ }
3973
+ }
3974
+
3975
+ // src/client/alipay/index.ts
3976
+ var TRADE_NO_RE = /^\d{32}$/;
3977
+ function resolveSessionId(explicit, envSession) {
3978
+ return explicit ?? envSession ?? randomUUID();
3979
+ }
3980
+ function assertTradeNo(t) {
3981
+ if (!TRADE_NO_RE.test(t)) {
3982
+ throw new AlipayProtocolError(`invalid tradeNo (expect 32 digits): ${t}`);
3983
+ }
3984
+ }
3985
+ function parseTradeNo(lines) {
3986
+ for (const line of lines) {
3987
+ const labeled = line.match(/trade[_-]?no["'\s:=]+(\d{32})/i);
3988
+ if (labeled) return labeled[1];
3989
+ const bare = line.match(/\b(\d{32})\b/);
3990
+ if (bare) return bare[1];
3991
+ }
3992
+ return null;
3993
+ }
3994
+ function parsePaymentUrl(lines) {
3995
+ let paymentUrl;
3996
+ let shortenUrl;
3997
+ for (const line of lines) {
3998
+ const m = line.match(/(alipays?:\/\/\S+|https?:\/\/\S+)/i);
3999
+ if (!m) continue;
4000
+ const url = m[1].replace(/[)\]`>]+$/, "");
4001
+ if (/short|qr\.alipay|surl|\/s\//i.test(line) && !shortenUrl) shortenUrl = url;
4002
+ else if (!paymentUrl) paymentUrl = url;
4003
+ }
4004
+ if (!paymentUrl && shortenUrl) paymentUrl = shortenUrl;
4005
+ return { paymentUrl, shortenUrl };
4006
+ }
4007
+ function extractMedia(line) {
4008
+ const m = line.match(/^\s*MEDIA:\s*(.+?)\s*$/);
4009
+ return m ? m[1] : null;
4010
+ }
4011
+ function isWalletReady(lines) {
4012
+ const text = lines.join("\n").trim();
4013
+ try {
4014
+ const json = JSON.parse(text);
4015
+ if (json && typeof json.code !== "undefined") return Number(json.code) === 200;
4016
+ } catch {
4017
+ }
4018
+ return !/未开通|未开启|NOT[_\s-]*(OPEN|BOUND|SET)|NEEDS?[_\s-]*SETUP|NO[_\s-]*WALLET/i.test(text);
4019
+ }
4020
+ function extractResourceFromReport(report) {
4021
+ const label = report.search(/资源响应体/);
4022
+ const start = report.indexOf("{", label === -1 ? 0 : label);
4023
+ if (start === -1) return void 0;
4024
+ let depth = 0, inStr = false, esc = false;
4025
+ for (let i = start; i < report.length; i++) {
4026
+ const ch = report[i];
4027
+ if (inStr) {
4028
+ if (esc) esc = false;
4029
+ else if (ch === "\\") esc = true;
4030
+ else if (ch === '"') inStr = false;
4031
+ continue;
4032
+ }
4033
+ if (ch === '"') inStr = true;
4034
+ else if (ch === "{") depth++;
4035
+ else if (ch === "}" && --depth === 0) {
4036
+ const slice = report.slice(start, i + 1);
4037
+ try {
4038
+ const obj = JSON.parse(slice);
4039
+ const r = obj?.resourceResponse ?? obj?.result ?? obj?.data ?? obj?.body ?? obj;
4040
+ return typeof r === "string" ? r : JSON.stringify(r);
4041
+ } catch {
4042
+ return slice;
4043
+ }
4044
+ }
4045
+ }
4046
+ return void 0;
4047
+ }
4048
+ function extractBody(lines) {
4049
+ const text = lines.join("\n").trim();
4050
+ try {
4051
+ const json = JSON.parse(text);
4052
+ if (json && typeof json === "object") {
4053
+ if (json.resourceResponse !== void 0 && json.resourceResponse !== null) {
4054
+ const rr = json.resourceResponse;
4055
+ const r = typeof rr === "object" ? rr.result ?? rr.data ?? rr.body ?? rr : rr;
4056
+ return typeof r === "string" ? r : JSON.stringify(r);
4057
+ }
4058
+ if (typeof json.body === "string") {
4059
+ const inner = extractResourceFromReport(json.body);
4060
+ return inner ?? json.body;
4061
+ }
4062
+ const body = json.data ?? json.result ?? json.body ?? json.resource;
4063
+ if (body !== void 0) return typeof body === "string" ? body : JSON.stringify(body);
4064
+ return text;
4065
+ }
4066
+ } catch {
4067
+ }
4068
+ const idx = lines.findIndex((l) => /^\s*BODY:/.test(l));
4069
+ if (idx !== -1) {
4070
+ const first = lines[idx].replace(/^\s*BODY:\s*/, "");
4071
+ return [first, ...lines.slice(idx + 1)].join("\n").trim();
4072
+ }
4073
+ return lines.filter((l) => !/^\s*(MEDIA|STATUS|INFO):/.test(l)).join("\n").trim();
4074
+ }
4075
+ var DEFAULT_WALLET_TTL_MS = 10 * 60 * 1e3;
4076
+ function resolveWalletTtlMs() {
4077
+ const raw = process.env.MOLTSPAY_ALIPAY_WALLET_TTL_MS;
4078
+ if (raw === void 0 || raw.trim() === "") return DEFAULT_WALLET_TTL_MS;
4079
+ const n = Number(raw);
4080
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_WALLET_TTL_MS;
4081
+ }
4082
+ var walletReadyUntil = /* @__PURE__ */ new Map();
4083
+ function resetWalletCache() {
4084
+ walletReadyUntil.clear();
4085
+ }
4086
+ var DEFAULT_INTENT_TTL_MS = 10 * 60 * 1e3;
4087
+ function resolveIntentTtlMs() {
4088
+ const raw = process.env.MOLTSPAY_ALIPAY_INTENT_TTL_MS;
4089
+ if (raw === void 0 || raw.trim() === "") return DEFAULT_INTENT_TTL_MS;
4090
+ const n = Number(raw);
4091
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_INTENT_TTL_MS;
4092
+ }
4093
+ var intentDoneUntil = /* @__PURE__ */ new Map();
4094
+ function resetIntentCache() {
4095
+ intentDoneUntil.clear();
4096
+ }
4097
+ var AlipayClient = class {
4098
+ sessionId;
4099
+ configDir;
4100
+ framework;
4101
+ runner;
4102
+ getVersion;
4103
+ now;
4104
+ /** Only the default runner may use the process-level wallet cache. */
4105
+ walletCacheable;
4106
+ /** Only the default runner may use the process-level payment-intent cache. */
4107
+ intentCacheable;
4108
+ constructor(opts = {}) {
4109
+ this.sessionId = resolveSessionId(opts.sessionId, process.env.AIPAY_SESSION_ID);
4110
+ this.configDir = opts.configDir ?? join4(homedir2(), ".moltspay");
4111
+ this.framework = opts.framework ?? process.env.AIPAY_FRAMEWORK ?? "openclaw";
4112
+ this.runner = opts.runner ?? runCli;
4113
+ this.getVersion = opts.getVersion;
4114
+ this.now = opts.now ?? Date.now;
4115
+ this.walletCacheable = opts.cacheWallet ?? !opts.runner;
4116
+ this.intentCacheable = opts.cacheIntent ?? !opts.runner;
4117
+ }
4118
+ /**
4119
+ * Throws NeedsWalletSetupError unless alipay-bot reports an opened wallet.
4120
+ *
4121
+ * Skips the ~22s `check-wallet` spawn entirely when a prior call cached a
4122
+ * "ready" verdict within the TTL (see {@link DEFAULT_WALLET_TTL_MS}).
4123
+ */
4124
+ async checkWallet(signal, flow) {
4125
+ const ttlMs = resolveWalletTtlMs();
4126
+ const cacheKey = `${this.configDir}::${this.framework}`;
4127
+ const useCache = this.walletCacheable && ttlMs > 0;
4128
+ if (useCache) {
4129
+ const until = walletReadyUntil.get(cacheKey);
4130
+ if (until !== void 0 && this.now() < until) {
4131
+ alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: true, ttlMsLeft: until - this.now() });
4132
+ return;
4133
+ }
4134
+ }
4135
+ const { lines } = await this.runner(["check-wallet"], { signal, step: "check-wallet", flow });
4136
+ if (!isWalletReady(lines)) {
4137
+ if (this.walletCacheable) walletReadyUntil.delete(cacheKey);
4138
+ throw new NeedsWalletSetupError(
4139
+ "Alipay wallet not opened. Run: moltspay alipay apply (then: moltspay alipay bind)"
4140
+ );
4141
+ }
4142
+ if (useCache) {
4143
+ walletReadyUntil.set(cacheKey, this.now() + ttlMs);
4144
+ alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: false, cachedForMs: ttlMs });
4145
+ }
4146
+ }
4147
+ /** Run the full 8-step flow and resolve with the resource body. */
4148
+ async pay402(opts) {
4149
+ const { resourceUrl, requirement, signal } = opts;
4150
+ const extra = requirement.extra ?? {};
4151
+ const paymentNeededHeader = String(extra.payment_needed_header ?? "");
4152
+ if (!paymentNeededHeader) {
4153
+ throw new AlipayProtocolError("alipay requirement missing extra.payment_needed_header");
4154
+ }
4155
+ const flow = this.sessionId;
4156
+ const flowStart = this.now();
4157
+ alipayLog.info("flow.start", { flow, resource: resourceUrl });
4158
+ const media = [];
4159
+ const onLine = (line) => {
4160
+ const m = extractMedia(line);
4161
+ if (m) media.push(m);
4162
+ else opts.onLine?.(line);
4163
+ };
4164
+ const intentSummary = opts.intentSummary?.trim() || `\u652F\u4ED8 ${requirement.amount ?? ""} ${requirement.asset ?? "CNY"}`.trim();
4165
+ await timeStep("ensure-cli", flow, () => ensureCli(this.getVersion));
4166
+ const intentTtlMs = resolveIntentTtlMs();
4167
+ const intentKey = `${this.configDir}::${this.framework}`;
4168
+ const useIntentCache = this.intentCacheable && intentTtlMs > 0;
4169
+ const intentUntil = useIntentCache ? intentDoneUntil.get(intentKey) : void 0;
4170
+ if (intentUntil !== void 0 && this.now() < intentUntil) {
4171
+ alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: true, ttlMsLeft: intentUntil - this.now() });
4172
+ } else {
4173
+ await this.runner(
4174
+ [
4175
+ "payment-intent",
4176
+ "--session-id",
4177
+ this.sessionId,
4178
+ "--intent-summary",
4179
+ intentSummary,
4180
+ "--framework",
4181
+ this.framework
4182
+ ],
4183
+ { onLine, signal, step: "payment-intent", flow }
4184
+ );
4185
+ if (useIntentCache) {
4186
+ intentDoneUntil.set(intentKey, this.now() + intentTtlMs);
4187
+ alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: false, cachedForMs: intentTtlMs });
4188
+ }
4189
+ }
4190
+ await this.checkWallet(signal, flow);
4191
+ const reqId = String(extra.out_trade_no ?? randomUUID());
4192
+ const dir = join4(this.configDir, "alipay");
4193
+ await mkdir(dir, { recursive: true });
4194
+ const challengeFile = join4(dir, `402_${reqId}.txt`);
4195
+ await writeFile(challengeFile, paymentNeededHeader, "utf-8");
4196
+ const payArgs = [
4197
+ "402-buyer-pay",
4198
+ "-f",
4199
+ challengeFile,
4200
+ "-r",
4201
+ resourceUrl,
4202
+ "-s",
4203
+ this.sessionId,
4204
+ "-i",
4205
+ intentSummary,
4206
+ "-w",
4207
+ this.framework
4208
+ ];
4209
+ if (opts.method) payArgs.push("-m", opts.method);
4210
+ if (opts.data) payArgs.push("-d", opts.data);
4211
+ const payRun = await this.runner(payArgs, { onLine, signal, step: "402-buyer-pay", flow });
4212
+ const tradeNo = parseTradeNo(payRun.lines);
4213
+ if (!tradeNo) {
4214
+ throw new AlipayProtocolError("402-buyer-pay did not return a tradeNo");
4215
+ }
4216
+ assertTradeNo(tradeNo);
4217
+ const { paymentUrl, shortenUrl } = parsePaymentUrl(payRun.lines);
4218
+ if (paymentUrl) {
4219
+ alipayLog.info("flow.pending", { flow, tradeNo, ms: this.now() - flowStart });
4220
+ opts.onPaymentPending?.({ paymentUrl, shortenUrl, tradeNo });
4221
+ }
4222
+ const pendingAt = this.now();
4223
+ const windowMs = (requirement.maxTimeoutSeconds ?? 30 * 60) * 1e3;
4224
+ const deadline = this.now() + (opts.timeoutMs ?? windowMs);
4225
+ const poll = await pollUntil(tradeNo, resourceUrl, {
4226
+ // No onLine: the status-poll output embeds the resource and must not reach
4227
+ // the log stream — the body is surfaced via the return value below (§9.3).
4228
+ deadline,
4229
+ signal,
4230
+ runner: this.runner,
4231
+ now: this.now,
4232
+ // Re-fetch the resource the same way it was paid (POST + body), else the
4233
+ // status poll defaults to GET and 404s on a POST-only `/execute`.
4234
+ method: opts.method,
4235
+ data: opts.data
4236
+ });
4237
+ alipayLog.info("flow.settled", { flow, tradeNo, ms: this.now() - pendingAt });
4238
+ const body = extractBody(poll.lines);
4239
+ void this.runner(["402-buyer-fulfillment-ack", "-t", tradeNo], {
4240
+ onLine,
4241
+ signal,
4242
+ step: "402-buyer-fulfillment-ack",
4243
+ flow
4244
+ }).catch(() => void 0);
4245
+ return { body, payment: { tradeNo, outTradeNo: reqId }, media };
4246
+ }
4247
+ };
4248
+
4249
+ // src/client/alipay/router.ts
4250
+ var ALIPAY_RAIL = "alipay";
4251
+ function railOf(req) {
4252
+ if (req.scheme === "alipay-aipay" || req.network === ALIPAY_RAIL) return ALIPAY_RAIL;
4253
+ return networkToChainName(req.network) ?? req.network;
4254
+ }
4255
+ function findRail(accepts, rail) {
4256
+ const requirement = accepts.find((r) => railOf(r) === rail);
4257
+ return requirement ? { rail, requirement } : null;
4258
+ }
4259
+ function selectRail(input) {
4260
+ const { serverAccepts, explicitRail, preference, availability } = input;
4261
+ if (!serverAccepts || serverAccepts.length === 0) {
4262
+ throw new UnsupportedRailError(explicitRail ?? "unknown", "Server offered no payment options");
4263
+ }
4264
+ if (explicitRail) {
4265
+ const hit = findRail(serverAccepts, explicitRail);
4266
+ if (!hit) {
4267
+ const offered = serverAccepts.map(railOf);
4268
+ throw new UnsupportedRailError(
4269
+ explicitRail,
4270
+ `Server doesn't accept rail '${explicitRail}'. Offered: ${offered.join(", ")}`
4271
+ );
4272
+ }
4273
+ return hit;
4274
+ }
4275
+ if (preference) {
4276
+ for (const pref of preference) {
4277
+ const hit = findRail(serverAccepts, pref);
4278
+ if (hit) return hit;
4279
+ }
4280
+ }
4281
+ if (availability?.evmReady) {
4282
+ const evm = serverAccepts.find((r) => railOf(r) !== ALIPAY_RAIL);
4283
+ if (evm) return { rail: railOf(evm), requirement: evm };
4284
+ }
4285
+ if (availability?.alipayReady) {
4286
+ const hit = findRail(serverAccepts, ALIPAY_RAIL);
4287
+ if (hit) return hit;
4288
+ }
4289
+ return { rail: railOf(serverAccepts[0]), requirement: serverAccepts[0] };
4290
+ }
4291
+
3000
4292
  // src/client/node/index.ts
3001
4293
  var DEFAULT_CONFIG = {
3002
4294
  chain: "base",
@@ -3013,9 +4305,13 @@ var MoltsPayClient = class {
3013
4305
  signer = null;
3014
4306
  todaySpending = 0;
3015
4307
  lastSpendingReset = 0;
4308
+ railPreference;
4309
+ alipaySessionId;
3016
4310
  constructor(options = {}) {
3017
- this.configDir = options.configDir || join4(homedir2(), ".moltspay");
4311
+ this.configDir = options.configDir || join5(homedir3(), ".moltspay");
3018
4312
  this.config = this.loadConfig();
4313
+ this.railPreference = options.railPreference ?? this.config.railPreference;
4314
+ this.alipaySessionId = options.alipaySessionId;
3019
4315
  this.walletData = this.loadWallet();
3020
4316
  this.loadSpending();
3021
4317
  if (this.walletData) {
@@ -3093,6 +4389,9 @@ var MoltsPayClient = class {
3093
4389
  * @param options - Payment options (token selection)
3094
4390
  */
3095
4391
  async pay(serverUrl, service, params, options = {}) {
4392
+ if (options.rail === ALIPAY_RAIL) {
4393
+ return this.payViaAlipay(serverUrl, service, params, options);
4394
+ }
3096
4395
  if (!this.wallet || !this.walletData) {
3097
4396
  throw new Error("Client not initialized. Run: npx moltspay init");
3098
4397
  }
@@ -3284,6 +4583,76 @@ Please specify: --chain <chain_name>`
3284
4583
  console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
3285
4584
  return result.result || result;
3286
4585
  }
4586
+ /**
4587
+ * Pay for a service over the Alipay fiat rail (2.0.0).
4588
+ *
4589
+ * Unlike the crypto path this needs no EVM wallet — it shells out to
4590
+ * alipay-bot via {@link AlipayClient}. Flow: hit the resource with no
4591
+ * payment to get the 402 challenge, confirm the server actually offers the
4592
+ * alipay rail (selectRail), then run the 8-step state machine and return the
4593
+ * resource body.
4594
+ */
4595
+ async payViaAlipay(serverUrl, service, params, options) {
4596
+ const flow = this.alipaySessionId;
4597
+ let executeUrl = `${serverUrl}/execute`;
4598
+ try {
4599
+ const services = await timeStep(
4600
+ "discover-services",
4601
+ flow,
4602
+ () => this.getServices(serverUrl)
4603
+ );
4604
+ const svc = services.services?.find((s) => s.id === service);
4605
+ if (svc?.endpoint) executeUrl = `${serverUrl}${svc.endpoint}`;
4606
+ } catch {
4607
+ }
4608
+ const requestBody = options.rawData ? { service, ...params } : { service, params };
4609
+ const bodyJson = JSON.stringify(requestBody);
4610
+ const res = await timeStep(
4611
+ "challenge-402",
4612
+ flow,
4613
+ () => fetch(executeUrl, {
4614
+ method: "POST",
4615
+ headers: { "Content-Type": "application/json" },
4616
+ body: bodyJson
4617
+ })
4618
+ );
4619
+ if (res.status !== 402) {
4620
+ const data = await res.json().catch(() => ({}));
4621
+ if (res.ok && data.result) return data.result;
4622
+ throw new Error(data.error || `Expected 402, got ${res.status}`);
4623
+ }
4624
+ const header = res.headers.get(PAYMENT_REQUIRED_HEADER2);
4625
+ if (!header) throw new Error("Missing x-payment-required header on 402");
4626
+ const parsed = JSON.parse(Buffer.from(header, "base64").toString("utf-8"));
4627
+ const accepts = Array.isArray(parsed) ? parsed : parsed.accepts ?? [parsed];
4628
+ const { requirement } = selectRail({
4629
+ serverAccepts: accepts,
4630
+ explicitRail: ALIPAY_RAIL,
4631
+ preference: this.railPreference,
4632
+ availability: { evmReady: this.isInitialized }
4633
+ });
4634
+ const onLine = options.onLine ?? ((line) => process.stdout.write(line + "\n"));
4635
+ const alipay = new AlipayClient({
4636
+ sessionId: this.alipaySessionId,
4637
+ configDir: this.configDir
4638
+ });
4639
+ const result = await alipay.pay402({
4640
+ resourceUrl: executeUrl,
4641
+ requirement,
4642
+ method: "POST",
4643
+ data: bodyJson,
4644
+ onLine,
4645
+ onPaymentPending: options.onPaymentPending,
4646
+ timeoutMs: options.timeoutMs,
4647
+ signal: options.signal
4648
+ });
4649
+ try {
4650
+ const json = JSON.parse(result.body);
4651
+ return json.result ?? json;
4652
+ } catch {
4653
+ return { body: result.body, payment: result.payment, media: result.media };
4654
+ }
4655
+ }
3287
4656
  /**
3288
4657
  * Handle MPP (Machine Payments Protocol) payment flow
3289
4658
  * Called when pay() detects WWW-Authenticate header in 402 response
@@ -3624,7 +4993,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
3624
4993
  }
3625
4994
  // --- Config & Wallet Management ---
3626
4995
  loadConfig() {
3627
- const configPath = join4(this.configDir, "config.json");
4996
+ const configPath = join5(this.configDir, "config.json");
3628
4997
  if (existsSync4(configPath)) {
3629
4998
  const content = readFileSync4(configPath, "utf-8");
3630
4999
  return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
@@ -3633,14 +5002,14 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
3633
5002
  }
3634
5003
  saveConfig() {
3635
5004
  mkdirSync2(this.configDir, { recursive: true });
3636
- const configPath = join4(this.configDir, "config.json");
5005
+ const configPath = join5(this.configDir, "config.json");
3637
5006
  writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
3638
5007
  }
3639
5008
  /**
3640
5009
  * Load spending data from disk
3641
5010
  */
3642
5011
  loadSpending() {
3643
- const spendingPath = join4(this.configDir, "spending.json");
5012
+ const spendingPath = join5(this.configDir, "spending.json");
3644
5013
  if (existsSync4(spendingPath)) {
3645
5014
  try {
3646
5015
  const data = JSON.parse(readFileSync4(spendingPath, "utf-8"));
@@ -3663,7 +5032,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
3663
5032
  */
3664
5033
  saveSpending() {
3665
5034
  mkdirSync2(this.configDir, { recursive: true });
3666
- const spendingPath = join4(this.configDir, "spending.json");
5035
+ const spendingPath = join5(this.configDir, "spending.json");
3667
5036
  const data = {
3668
5037
  date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
3669
5038
  amount: this.todaySpending,
@@ -3672,7 +5041,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
3672
5041
  writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
3673
5042
  }
3674
5043
  loadWallet() {
3675
- const walletPath = join4(this.configDir, "wallet.json");
5044
+ const walletPath = join5(this.configDir, "wallet.json");
3676
5045
  if (existsSync4(walletPath)) {
3677
5046
  if (process.platform !== "win32") {
3678
5047
  try {
@@ -3702,7 +5071,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
3702
5071
  privateKey: wallet.privateKey,
3703
5072
  createdAt: Date.now()
3704
5073
  };
3705
- const walletPath = join4(configDir, "wallet.json");
5074
+ const walletPath = join5(configDir, "wallet.json");
3706
5075
  writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
3707
5076
  const config = {
3708
5077
  chain: options.chain,
@@ -3711,7 +5080,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
3711
5080
  maxPerDay: options.maxPerDay
3712
5081
  }
3713
5082
  };
3714
- const configPath = join4(configDir, "config.json");
5083
+ const configPath = join5(configDir, "config.json");
3715
5084
  writeFileSync2(configPath, JSON.stringify(config, null, 2));
3716
5085
  return { address: wallet.address, configDir };
3717
5086
  }
@@ -3925,9 +5294,9 @@ import { ethers as ethers4 } from "ethers";
3925
5294
  // src/wallet/createWallet.ts
3926
5295
  import { ethers as ethers5 } from "ethers";
3927
5296
  import { writeFileSync as writeFileSync3, readFileSync as readFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
3928
- import { join as join5, dirname } from "path";
5297
+ import { join as join6, dirname as dirname2 } from "path";
3929
5298
  import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
3930
- var DEFAULT_STORAGE_DIR = join5(process.env.HOME || "~", ".moltspay");
5299
+ var DEFAULT_STORAGE_DIR = join6(process.env.HOME || "~", ".moltspay");
3931
5300
  var DEFAULT_STORAGE_FILE = "wallet.json";
3932
5301
  function encryptPrivateKey(privateKey, password) {
3933
5302
  const salt = randomBytes(16);
@@ -3950,7 +5319,7 @@ function decryptPrivateKey(encrypted, password, iv, salt) {
3950
5319
  return decrypted;
3951
5320
  }
3952
5321
  function createWallet(options = {}) {
3953
- const storagePath = options.storagePath || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
5322
+ const storagePath = options.storagePath || join6(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
3954
5323
  if (existsSync5(storagePath) && !options.overwrite) {
3955
5324
  try {
3956
5325
  const existing = JSON.parse(readFileSync5(storagePath, "utf8"));
@@ -3984,7 +5353,7 @@ function createWallet(options = {}) {
3984
5353
  } else {
3985
5354
  walletData.privateKey = wallet.privateKey;
3986
5355
  }
3987
- const dir = dirname(storagePath);
5356
+ const dir = dirname2(storagePath);
3988
5357
  if (!existsSync5(dir)) {
3989
5358
  mkdirSync3(dir, { recursive: true });
3990
5359
  }
@@ -4003,7 +5372,7 @@ function createWallet(options = {}) {
4003
5372
  }
4004
5373
  }
4005
5374
  function loadWallet(options = {}) {
4006
- const storagePath = options.storagePath || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
5375
+ const storagePath = options.storagePath || join6(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
4007
5376
  if (!existsSync5(storagePath)) {
4008
5377
  return { success: false, error: "Wallet not found. Run createWallet() first." };
4009
5378
  }
@@ -4023,7 +5392,7 @@ function loadWallet(options = {}) {
4023
5392
  }
4024
5393
  }
4025
5394
  function getWalletAddress(storagePath) {
4026
- const path4 = storagePath || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
5395
+ const path4 = storagePath || join6(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
4027
5396
  if (!existsSync5(path4)) {
4028
5397
  return null;
4029
5398
  }
@@ -4035,7 +5404,7 @@ function getWalletAddress(storagePath) {
4035
5404
  }
4036
5405
  }
4037
5406
  function walletExists(storagePath) {
4038
- const path4 = storagePath || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
5407
+ const path4 = storagePath || join6(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
4039
5408
  return existsSync5(path4);
4040
5409
  }
4041
5410
 
@@ -4400,8 +5769,10 @@ export {
4400
5769
  FacilitatorRegistry,
4401
5770
  MoltsPayClient,
4402
5771
  MoltsPayServer,
5772
+ alipayLog,
4403
5773
  createRegistry,
4404
5774
  createWallet,
5775
+ getAlipayLogLevel,
4405
5776
  getCDPWalletAddress,
4406
5777
  getChain,
4407
5778
  getChainById,
@@ -4413,6 +5784,10 @@ export {
4413
5784
  listChains,
4414
5785
  loadCDPWallet,
4415
5786
  loadWallet,
5787
+ resetIntentCache,
5788
+ resetWalletCache,
5789
+ setAlipayLogLevel,
5790
+ setAlipayLogSink,
4416
5791
  verifyPayment,
4417
5792
  waitForTransaction,
4418
5793
  walletExists