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.
- package/README.md +1 -0
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/{cdp-DeohBe1o.d.ts → cdp-B5PhDUTC.d.ts} +1 -1
- package/dist/{cdp-p_eHuQpb.d.mts → cdp-D-5Hg3y_.d.mts} +1 -1
- package/dist/chains/index.d.mts +37 -1
- package/dist/chains/index.d.ts +37 -1
- package/dist/chains/index.js +22 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +19 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +1466 -61
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1466 -61
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +41 -0
- package/dist/client/index.d.ts +41 -0
- package/dist/client/index.js +824 -10
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +824 -10
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/web/index.d.mts +17 -1
- package/dist/client/web/index.mjs +11 -0
- package/dist/client/web/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +161 -4
- package/dist/facilitators/index.d.ts +161 -4
- package/dist/facilitators/index.js +370 -0
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +365 -0
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/index.d.mts +77 -2
- package/dist/index.d.ts +77 -2
- package/dist/index.js +1412 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1406 -31
- package/dist/index.mjs.map +1 -1
- package/dist/mcp/index.js +828 -14
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/index.mjs +828 -14
- package/dist/mcp/index.mjs.map +1 -1
- package/dist/{registry-OsEO2dOu.d.mts → registry-DIo0WNoO.d.mts} +8 -1
- package/dist/{registry-OsEO2dOu.d.ts → registry-DIo0WNoO.d.ts} +8 -1
- package/dist/server/index.d.mts +95 -2
- package/dist/server/index.d.ts +95 -2
- package/dist/server/index.js +554 -14
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +554 -14
- package/dist/server/index.mjs.map +1 -1
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +7 -1
- package/schemas/moltspay.services.schema.json +100 -6
- package/scripts/postinstall.js +91 -0
package/dist/index.js
CHANGED
|
@@ -38,8 +38,10 @@ __export(index_exports, {
|
|
|
38
38
|
FacilitatorRegistry: () => FacilitatorRegistry,
|
|
39
39
|
MoltsPayClient: () => MoltsPayClient,
|
|
40
40
|
MoltsPayServer: () => MoltsPayServer,
|
|
41
|
+
alipayLog: () => alipayLog,
|
|
41
42
|
createRegistry: () => createRegistry,
|
|
42
43
|
createWallet: () => createWallet,
|
|
44
|
+
getAlipayLogLevel: () => getAlipayLogLevel,
|
|
43
45
|
getCDPWalletAddress: () => getCDPWalletAddress,
|
|
44
46
|
getChain: () => getChain,
|
|
45
47
|
getChainById: () => getChainById,
|
|
@@ -51,6 +53,10 @@ __export(index_exports, {
|
|
|
51
53
|
listChains: () => listChains,
|
|
52
54
|
loadCDPWallet: () => loadCDPWallet,
|
|
53
55
|
loadWallet: () => loadWallet,
|
|
56
|
+
resetIntentCache: () => resetIntentCache,
|
|
57
|
+
resetWalletCache: () => resetWalletCache,
|
|
58
|
+
setAlipayLogLevel: () => setAlipayLogLevel,
|
|
59
|
+
setAlipayLogSink: () => setAlipayLogSink,
|
|
54
60
|
verifyPayment: () => verifyPayment,
|
|
55
61
|
waitForTransaction: () => waitForTransaction,
|
|
56
62
|
walletExists: () => walletExists
|
|
@@ -470,6 +476,10 @@ function listChains() {
|
|
|
470
476
|
function getChainById(chainId) {
|
|
471
477
|
return Object.values(CHAINS).find((c) => c.chainId === chainId);
|
|
472
478
|
}
|
|
479
|
+
var ALIPAY_CHAIN_ID = "alipay";
|
|
480
|
+
function isAlipayChainId(id) {
|
|
481
|
+
return id === ALIPAY_CHAIN_ID;
|
|
482
|
+
}
|
|
473
483
|
var ERC20_ABI = [
|
|
474
484
|
"function balanceOf(address owner) view returns (uint256)",
|
|
475
485
|
"function transfer(address to, uint256 amount) returns (bool)",
|
|
@@ -1279,6 +1289,374 @@ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amo
|
|
|
1279
1289
|
return transaction;
|
|
1280
1290
|
}
|
|
1281
1291
|
|
|
1292
|
+
// src/facilitators/alipay.ts
|
|
1293
|
+
var import_node_crypto2 = __toESM(require("crypto"));
|
|
1294
|
+
|
|
1295
|
+
// src/facilitators/alipay/rsa2.ts
|
|
1296
|
+
var import_node_crypto = __toESM(require("crypto"));
|
|
1297
|
+
function rsa2Sign(data, privateKeyPem) {
|
|
1298
|
+
const signer = import_node_crypto.default.createSign("RSA-SHA256");
|
|
1299
|
+
signer.update(data, "utf-8");
|
|
1300
|
+
signer.end();
|
|
1301
|
+
return signer.sign(privateKeyPem, "base64");
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// src/facilitators/alipay/encoding.ts
|
|
1305
|
+
function base64url(input) {
|
|
1306
|
+
return Buffer.from(input, "utf-8").toString("base64url");
|
|
1307
|
+
}
|
|
1308
|
+
function decodeBase64UrlWithPadFix(input) {
|
|
1309
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
1310
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
1311
|
+
return Buffer.from(padded, "base64").toString("utf-8");
|
|
1312
|
+
}
|
|
1313
|
+
function toPem(key, kind) {
|
|
1314
|
+
const trimmed = key.trim();
|
|
1315
|
+
if (trimmed.includes("-----BEGIN")) return trimmed;
|
|
1316
|
+
const label = kind === "PRIVATE" ? "PRIVATE KEY" : "PUBLIC KEY";
|
|
1317
|
+
const body = trimmed.replace(/\s+/g, "").match(/.{1,64}/g)?.join("\n") ?? "";
|
|
1318
|
+
return `-----BEGIN ${label}-----
|
|
1319
|
+
${body}
|
|
1320
|
+
-----END ${label}-----
|
|
1321
|
+
`;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// src/facilitators/alipay/openapi.ts
|
|
1325
|
+
function formatAlipayTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
1326
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1327
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
1328
|
+
}
|
|
1329
|
+
function buildSigningString(params) {
|
|
1330
|
+
return Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join("&");
|
|
1331
|
+
}
|
|
1332
|
+
function responseWrapperKey(method) {
|
|
1333
|
+
return `${method.replace(/\./g, "_")}_response`;
|
|
1334
|
+
}
|
|
1335
|
+
async function alipayOpenApiCall(method, bizContent, config) {
|
|
1336
|
+
const publicParams = {
|
|
1337
|
+
app_id: config.app_id,
|
|
1338
|
+
method,
|
|
1339
|
+
format: "JSON",
|
|
1340
|
+
charset: "utf-8",
|
|
1341
|
+
sign_type: config.sign_type ?? "RSA2",
|
|
1342
|
+
timestamp: formatAlipayTimestamp(),
|
|
1343
|
+
version: "1.0",
|
|
1344
|
+
biz_content: JSON.stringify(bizContent)
|
|
1345
|
+
};
|
|
1346
|
+
const signingString = buildSigningString(publicParams);
|
|
1347
|
+
const sign = rsa2Sign(signingString, config.private_key_pem);
|
|
1348
|
+
const body = new URLSearchParams({ ...publicParams, sign }).toString();
|
|
1349
|
+
const response = await fetch(config.gateway_url, {
|
|
1350
|
+
method: "POST",
|
|
1351
|
+
headers: {
|
|
1352
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
|
|
1353
|
+
},
|
|
1354
|
+
body
|
|
1355
|
+
});
|
|
1356
|
+
if (!response.ok) {
|
|
1357
|
+
const text = await response.text().catch(() => "<unreadable>");
|
|
1358
|
+
throw new Error(
|
|
1359
|
+
`Alipay Open API HTTP ${response.status} for ${method}: ${text.slice(0, 500)}`
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
const json = await response.json();
|
|
1363
|
+
const wrapperKey = responseWrapperKey(method);
|
|
1364
|
+
const business = json[wrapperKey];
|
|
1365
|
+
if (business === void 0 || business === null || typeof business !== "object") {
|
|
1366
|
+
throw new Error(
|
|
1367
|
+
`Alipay Open API response missing "${wrapperKey}" wrapper for ${method}: ${JSON.stringify(json).slice(0, 500)}`
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
return business;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// src/facilitators/alipay.ts
|
|
1374
|
+
var ALIPAY_NETWORK = "alipay";
|
|
1375
|
+
var ALIPAY_SCHEME = "alipay-aipay";
|
|
1376
|
+
var ALIPAY_GATEWAY_PROD = "https://openapi.alipay.com/gateway.do";
|
|
1377
|
+
var ALIPAY_AMOUNT_REGEX = /^\d+(\.\d{1,2})?$/;
|
|
1378
|
+
var ALIPAY_PAY_BEFORE_MS = 30 * 60 * 1e3;
|
|
1379
|
+
var ALIPAY_SIGNING_FIELDS = [
|
|
1380
|
+
"amount",
|
|
1381
|
+
"currency",
|
|
1382
|
+
"goods_name",
|
|
1383
|
+
"out_trade_no",
|
|
1384
|
+
"pay_before",
|
|
1385
|
+
"resource_id",
|
|
1386
|
+
"seller_id",
|
|
1387
|
+
"service_id"
|
|
1388
|
+
];
|
|
1389
|
+
var AlipayFacilitator = class extends BaseFacilitator {
|
|
1390
|
+
name = "alipay";
|
|
1391
|
+
displayName = "Alipay AI \u6536";
|
|
1392
|
+
supportedNetworks = [ALIPAY_NETWORK];
|
|
1393
|
+
config;
|
|
1394
|
+
constructor(config) {
|
|
1395
|
+
super();
|
|
1396
|
+
this.config = {
|
|
1397
|
+
gateway_url: ALIPAY_GATEWAY_PROD,
|
|
1398
|
+
sign_type: "RSA2",
|
|
1399
|
+
...config
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Build the 402 challenge for a service: signs the 8-field payload with
|
|
1404
|
+
* RSA2, packages the nested `{protocol, method}` JSON as Base64URL for
|
|
1405
|
+
* `Payment-Needed`, and emits the parallel x402 `accepts[]` entry.
|
|
1406
|
+
*/
|
|
1407
|
+
async createPaymentRequirements(opts) {
|
|
1408
|
+
if (!ALIPAY_AMOUNT_REGEX.test(opts.priceCny)) {
|
|
1409
|
+
throw new Error(
|
|
1410
|
+
`AlipayFacilitator.createPaymentRequirements: priceCny "${opts.priceCny}" does not match /^\\d+(\\.\\d{1,2})?$/ (unit is \u5143, not \u5206; e.g. "1.00" not "100")`
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
const now = /* @__PURE__ */ new Date();
|
|
1414
|
+
const outTradeNo = opts.outTradeNo ?? generateOutTradeNo();
|
|
1415
|
+
const payBefore = formatPayBefore(now);
|
|
1416
|
+
const signedFields = {
|
|
1417
|
+
amount: opts.priceCny,
|
|
1418
|
+
currency: "CNY",
|
|
1419
|
+
goods_name: opts.goodsName,
|
|
1420
|
+
out_trade_no: outTradeNo,
|
|
1421
|
+
pay_before: payBefore,
|
|
1422
|
+
resource_id: opts.resourceId,
|
|
1423
|
+
seller_id: this.config.seller_id,
|
|
1424
|
+
service_id: opts.serviceId
|
|
1425
|
+
};
|
|
1426
|
+
const signingString = ALIPAY_SIGNING_FIELDS.map((k) => `${k}=${signedFields[k]}`).join("&");
|
|
1427
|
+
const seller_signature = rsa2Sign(signingString, this.config.private_key_pem);
|
|
1428
|
+
const challenge = {
|
|
1429
|
+
protocol: {
|
|
1430
|
+
out_trade_no: outTradeNo,
|
|
1431
|
+
amount: signedFields.amount,
|
|
1432
|
+
currency: signedFields.currency,
|
|
1433
|
+
resource_id: signedFields.resource_id,
|
|
1434
|
+
pay_before: payBefore,
|
|
1435
|
+
seller_signature,
|
|
1436
|
+
seller_sign_type: this.config.sign_type ?? "RSA2",
|
|
1437
|
+
seller_unique_id: this.config.seller_id
|
|
1438
|
+
},
|
|
1439
|
+
method: {
|
|
1440
|
+
seller_name: this.config.seller_name,
|
|
1441
|
+
seller_id: this.config.seller_id,
|
|
1442
|
+
seller_app_id: this.config.app_id,
|
|
1443
|
+
goods_name: signedFields.goods_name,
|
|
1444
|
+
seller_unique_id_key: "seller_id",
|
|
1445
|
+
service_id: signedFields.service_id
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
const paymentNeededHeader = base64url(JSON.stringify(challenge));
|
|
1449
|
+
const x402Accepts = {
|
|
1450
|
+
scheme: ALIPAY_SCHEME,
|
|
1451
|
+
network: ALIPAY_NETWORK,
|
|
1452
|
+
asset: "CNY",
|
|
1453
|
+
amount: opts.priceCny,
|
|
1454
|
+
payTo: this.config.seller_id,
|
|
1455
|
+
maxTimeoutSeconds: ALIPAY_PAY_BEFORE_MS / 1e3,
|
|
1456
|
+
extra: {
|
|
1457
|
+
payment_needed_header: paymentNeededHeader,
|
|
1458
|
+
out_trade_no: outTradeNo,
|
|
1459
|
+
pay_before: payBefore,
|
|
1460
|
+
service_id: signedFields.service_id
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
return { x402Accepts, paymentNeededHeader };
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Verify a `Payment-Proof` by calling
|
|
1467
|
+
* `alipay.aipay.agent.payment.verify` against the Alipay Open API.
|
|
1468
|
+
*
|
|
1469
|
+
* The proof is conveyed via `paymentPayload.payload`, which may be:
|
|
1470
|
+
* - a raw Base64URL string (the `Payment-Proof` header value), or
|
|
1471
|
+
* - an object `{ paymentProof: string }` / `{ proofHeader: string }`.
|
|
1472
|
+
*
|
|
1473
|
+
* All failure modes (malformed payload, network errors, Alipay
|
|
1474
|
+
* `code != 10000`) return `{ valid: false, error }`. No exception
|
|
1475
|
+
* escapes, regardless of client-supplied input.
|
|
1476
|
+
*/
|
|
1477
|
+
async verify(paymentPayload, _requirements) {
|
|
1478
|
+
try {
|
|
1479
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
1480
|
+
const decoded = decodeProof(proofHeader);
|
|
1481
|
+
const response = await alipayOpenApiCall(
|
|
1482
|
+
"alipay.aipay.agent.payment.verify",
|
|
1483
|
+
{
|
|
1484
|
+
payment_proof: decoded.protocol.payment_proof,
|
|
1485
|
+
trade_no: decoded.protocol.trade_no,
|
|
1486
|
+
client_session: decoded.method.client_session
|
|
1487
|
+
},
|
|
1488
|
+
this.getOpenApiConfig()
|
|
1489
|
+
);
|
|
1490
|
+
if (response.code !== "10000") {
|
|
1491
|
+
return {
|
|
1492
|
+
valid: false,
|
|
1493
|
+
error: `alipay verify ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
1494
|
+
details: {
|
|
1495
|
+
code: response.code,
|
|
1496
|
+
sub_code: response.sub_code,
|
|
1497
|
+
sub_msg: response.sub_msg
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
return {
|
|
1502
|
+
valid: true,
|
|
1503
|
+
details: {
|
|
1504
|
+
trade_no: response.trade_no ?? decoded.protocol.trade_no,
|
|
1505
|
+
amount: response.amount,
|
|
1506
|
+
out_trade_no: response.out_trade_no,
|
|
1507
|
+
resource_id: response.resource_id,
|
|
1508
|
+
active: response.active
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
} catch (e) {
|
|
1512
|
+
return {
|
|
1513
|
+
valid: false,
|
|
1514
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Settle by calling `alipay.aipay.agent.fulfillment.confirm` after the
|
|
1520
|
+
* service resource has been returned to the buyer.
|
|
1521
|
+
*
|
|
1522
|
+
* Per the design (see ALIPAY-INTEGRATION-DESIGN.md §5.1, risk row
|
|
1523
|
+
* "履约确认失败"), this is **fire-and-forget**: the caller (registry /
|
|
1524
|
+
* server) is expected to log fulfillment failures but NOT roll back
|
|
1525
|
+
* the already-delivered resource.
|
|
1526
|
+
*
|
|
1527
|
+
* Re-decodes `paymentPayload.payload` to extract `trade_no` (verify
|
|
1528
|
+
* does the same; the redundant Base64URL decode is negligible).
|
|
1529
|
+
*/
|
|
1530
|
+
async settle(paymentPayload, _requirements) {
|
|
1531
|
+
try {
|
|
1532
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
1533
|
+
const decoded = decodeProof(proofHeader);
|
|
1534
|
+
const tradeNo = decoded.protocol.trade_no;
|
|
1535
|
+
const response = await alipayOpenApiCall(
|
|
1536
|
+
"alipay.aipay.agent.fulfillment.confirm",
|
|
1537
|
+
{ trade_no: tradeNo },
|
|
1538
|
+
this.getOpenApiConfig()
|
|
1539
|
+
);
|
|
1540
|
+
if (response.code !== "10000") {
|
|
1541
|
+
return {
|
|
1542
|
+
success: false,
|
|
1543
|
+
transaction: tradeNo,
|
|
1544
|
+
error: `alipay fulfillment ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
1545
|
+
status: "fulfillment_failed"
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
return {
|
|
1549
|
+
success: true,
|
|
1550
|
+
transaction: tradeNo,
|
|
1551
|
+
status: "fulfilled"
|
|
1552
|
+
};
|
|
1553
|
+
} catch (e) {
|
|
1554
|
+
return {
|
|
1555
|
+
success: false,
|
|
1556
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Validate that the configured RSA keys parse and that the Open API
|
|
1562
|
+
* gateway is reachable. Does NOT make a real business API call (would
|
|
1563
|
+
* burn quota and could appear as a legitimate verify attempt in logs).
|
|
1564
|
+
*/
|
|
1565
|
+
async healthCheck() {
|
|
1566
|
+
const start = Date.now();
|
|
1567
|
+
try {
|
|
1568
|
+
import_node_crypto2.default.createPrivateKey(this.config.private_key_pem);
|
|
1569
|
+
} catch (e) {
|
|
1570
|
+
return {
|
|
1571
|
+
healthy: false,
|
|
1572
|
+
error: `merchant private_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
try {
|
|
1576
|
+
import_node_crypto2.default.createPublicKey(this.config.alipay_public_key_pem);
|
|
1577
|
+
} catch (e) {
|
|
1578
|
+
return {
|
|
1579
|
+
healthy: false,
|
|
1580
|
+
error: `alipay_public_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
const gatewayUrl = this.config.gateway_url ?? ALIPAY_GATEWAY_PROD;
|
|
1584
|
+
const controller = new AbortController();
|
|
1585
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
1586
|
+
const response = await fetch(gatewayUrl, {
|
|
1587
|
+
method: "HEAD",
|
|
1588
|
+
signal: controller.signal
|
|
1589
|
+
}).catch(() => null);
|
|
1590
|
+
clearTimeout(timeout);
|
|
1591
|
+
const latencyMs = Date.now() - start;
|
|
1592
|
+
if (!response) {
|
|
1593
|
+
return { healthy: false, error: `gateway unreachable: ${gatewayUrl}`, latencyMs };
|
|
1594
|
+
}
|
|
1595
|
+
return { healthy: true, latencyMs };
|
|
1596
|
+
}
|
|
1597
|
+
/** Bundle the facilitator config into the shape openapi.ts wants. */
|
|
1598
|
+
getOpenApiConfig() {
|
|
1599
|
+
return {
|
|
1600
|
+
gateway_url: this.config.gateway_url ?? ALIPAY_GATEWAY_PROD,
|
|
1601
|
+
app_id: this.config.app_id,
|
|
1602
|
+
private_key_pem: this.config.private_key_pem,
|
|
1603
|
+
alipay_public_key_pem: this.config.alipay_public_key_pem,
|
|
1604
|
+
sign_type: this.config.sign_type
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
function generateOutTradeNo() {
|
|
1609
|
+
return "VID" + import_node_crypto2.default.randomBytes(22).toString("base64url").slice(0, 29);
|
|
1610
|
+
}
|
|
1611
|
+
function formatPayBefore(now) {
|
|
1612
|
+
const expiry = new Date(now.getTime() + ALIPAY_PAY_BEFORE_MS);
|
|
1613
|
+
return expiry.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1614
|
+
}
|
|
1615
|
+
function extractProofHeader(payload) {
|
|
1616
|
+
if (typeof payload === "string") {
|
|
1617
|
+
if (payload.length === 0) {
|
|
1618
|
+
throw new Error("alipay Payment-Proof is empty");
|
|
1619
|
+
}
|
|
1620
|
+
return payload;
|
|
1621
|
+
}
|
|
1622
|
+
if (payload !== null && typeof payload === "object") {
|
|
1623
|
+
const obj = payload;
|
|
1624
|
+
const candidate = obj.paymentProof ?? obj.proofHeader;
|
|
1625
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
1626
|
+
return candidate;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
throw new Error(
|
|
1630
|
+
"alipay payment payload must be a Base64URL string or {paymentProof: string} / {proofHeader: string}"
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
function decodeProof(proofHeader) {
|
|
1634
|
+
let parsed;
|
|
1635
|
+
try {
|
|
1636
|
+
parsed = JSON.parse(decodeBase64UrlWithPadFix(proofHeader));
|
|
1637
|
+
} catch (e) {
|
|
1638
|
+
throw new Error(
|
|
1639
|
+
`failed to decode Payment-Proof: ${e instanceof Error ? e.message : String(e)}`
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1643
|
+
throw new Error("decoded Payment-Proof is not an object");
|
|
1644
|
+
}
|
|
1645
|
+
const obj = parsed;
|
|
1646
|
+
const protocol = obj.protocol;
|
|
1647
|
+
const method = obj.method;
|
|
1648
|
+
if (!protocol || typeof protocol.payment_proof !== "string") {
|
|
1649
|
+
throw new Error("decoded Payment-Proof missing protocol.payment_proof");
|
|
1650
|
+
}
|
|
1651
|
+
if (typeof protocol.trade_no !== "string") {
|
|
1652
|
+
throw new Error("decoded Payment-Proof missing protocol.trade_no");
|
|
1653
|
+
}
|
|
1654
|
+
if (!method || typeof method.client_session !== "string") {
|
|
1655
|
+
throw new Error("decoded Payment-Proof missing method.client_session");
|
|
1656
|
+
}
|
|
1657
|
+
return parsed;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1282
1660
|
// src/facilitators/registry.ts
|
|
1283
1661
|
var import_web33 = require("@solana/web3.js");
|
|
1284
1662
|
var import_bs58 = __toESM(require("bs58"));
|
|
@@ -1303,6 +1681,7 @@ var FacilitatorRegistry = class {
|
|
|
1303
1681
|
}
|
|
1304
1682
|
return new SolanaFacilitator({ feePayerKeypair });
|
|
1305
1683
|
});
|
|
1684
|
+
this.registerFactory("alipay", (config) => new AlipayFacilitator(config));
|
|
1306
1685
|
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
1307
1686
|
}
|
|
1308
1687
|
/**
|
|
@@ -1537,6 +1916,11 @@ var PAYMENT_RESPONSE_HEADER = "x-payment-response";
|
|
|
1537
1916
|
var MPP_AUTH_HEADER = "authorization";
|
|
1538
1917
|
var MPP_WWW_AUTH_HEADER = "www-authenticate";
|
|
1539
1918
|
var MPP_RECEIPT_HEADER = "payment-receipt";
|
|
1919
|
+
var ALIPAY_PAYMENT_NEEDED_HEADER = "payment-needed";
|
|
1920
|
+
var ALIPAY_PAYMENT_PROOF_HEADER = "payment-proof";
|
|
1921
|
+
function headerSafe(value) {
|
|
1922
|
+
return String(value ?? "").replace(/[^\x20-\x7E]/g, (ch) => encodeURIComponent(ch)).replace(/"/g, "%22");
|
|
1923
|
+
}
|
|
1540
1924
|
var TOKEN_ADDRESSES = {
|
|
1541
1925
|
"eip155:8453": {
|
|
1542
1926
|
USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
@@ -1672,6 +2056,8 @@ var MoltsPayServer = class {
|
|
|
1672
2056
|
registry;
|
|
1673
2057
|
networkId;
|
|
1674
2058
|
useMainnet;
|
|
2059
|
+
/** Alipay AI 收 facilitator instance, set when `provider.alipay` is configured (2.0.0). */
|
|
2060
|
+
alipayFacilitator = null;
|
|
1675
2061
|
constructor(servicesPath, options = {}) {
|
|
1676
2062
|
loadEnvFile2();
|
|
1677
2063
|
const content = (0, import_fs2.readFileSync)(servicesPath, "utf-8");
|
|
@@ -1693,7 +2079,38 @@ var MoltsPayServer = class {
|
|
|
1693
2079
|
cdp: { useMainnet: this.useMainnet }
|
|
1694
2080
|
}
|
|
1695
2081
|
};
|
|
2082
|
+
const providerAlipay = this.manifest.provider.alipay;
|
|
2083
|
+
if (providerAlipay) {
|
|
2084
|
+
try {
|
|
2085
|
+
const baseDir = path2.dirname(servicesPath);
|
|
2086
|
+
const resolvePem = (p, kind) => toPem((0, import_fs2.readFileSync)(path2.isAbsolute(p) ? p : path2.resolve(baseDir, p), "utf-8"), kind);
|
|
2087
|
+
const alipayFacilitatorConfig = {
|
|
2088
|
+
seller_id: providerAlipay.seller_id,
|
|
2089
|
+
app_id: providerAlipay.app_id,
|
|
2090
|
+
seller_name: providerAlipay.seller_name,
|
|
2091
|
+
service_id_default: providerAlipay.service_id_default,
|
|
2092
|
+
private_key_pem: resolvePem(providerAlipay.private_key_path, "PRIVATE"),
|
|
2093
|
+
alipay_public_key_pem: resolvePem(providerAlipay.alipay_public_key_path, "PUBLIC"),
|
|
2094
|
+
gateway_url: providerAlipay.gateway_url,
|
|
2095
|
+
sign_type: providerAlipay.sign_type
|
|
2096
|
+
};
|
|
2097
|
+
facilitatorConfig.config = {
|
|
2098
|
+
...facilitatorConfig.config,
|
|
2099
|
+
alipay: alipayFacilitatorConfig
|
|
2100
|
+
};
|
|
2101
|
+
facilitatorConfig.fallback = facilitatorConfig.fallback || [];
|
|
2102
|
+
if (facilitatorConfig.primary !== "alipay" && !facilitatorConfig.fallback.includes("alipay")) {
|
|
2103
|
+
facilitatorConfig.fallback.push("alipay");
|
|
2104
|
+
}
|
|
2105
|
+
} catch (err) {
|
|
2106
|
+
throw new Error(`[MoltsPay] Alipay rail configured but key load failed: ${err.message}`);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
1696
2109
|
this.registry = new FacilitatorRegistry(facilitatorConfig);
|
|
2110
|
+
if (providerAlipay) {
|
|
2111
|
+
this.alipayFacilitator = this.registry.get("alipay");
|
|
2112
|
+
console.log(`[MoltsPay] Alipay AI \u6536 rail enabled (seller ${providerAlipay.seller_id})`);
|
|
2113
|
+
}
|
|
1697
2114
|
const primaryFacilitator = this.registry.get(facilitatorConfig.primary);
|
|
1698
2115
|
console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
|
|
1699
2116
|
console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
|
|
@@ -1829,10 +2246,10 @@ var MoltsPayServer = class {
|
|
|
1829
2246
|
writeCorsHeaders(res, origin) {
|
|
1830
2247
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
1831
2248
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1832
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
|
|
2249
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization, Payment-Proof");
|
|
1833
2250
|
res.setHeader(
|
|
1834
2251
|
"Access-Control-Expose-Headers",
|
|
1835
|
-
"X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt"
|
|
2252
|
+
"X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt, Payment-Needed"
|
|
1836
2253
|
);
|
|
1837
2254
|
}
|
|
1838
2255
|
/**
|
|
@@ -1859,7 +2276,8 @@ var MoltsPayServer = class {
|
|
|
1859
2276
|
if (url.pathname === "/execute" && req.method === "POST") {
|
|
1860
2277
|
const body = await this.readBody(req);
|
|
1861
2278
|
const paymentHeader = req.headers[PAYMENT_HEADER];
|
|
1862
|
-
|
|
2279
|
+
const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
|
|
2280
|
+
return await this.handleExecute(body, paymentHeader, res, proofHeader);
|
|
1863
2281
|
}
|
|
1864
2282
|
if (url.pathname === "/proxy" && req.method === "POST") {
|
|
1865
2283
|
const clientIP = req.headers["x-real-ip"] || req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "";
|
|
@@ -1877,7 +2295,8 @@ var MoltsPayServer = class {
|
|
|
1877
2295
|
const body = req.method === "POST" ? await this.readBody(req) : {};
|
|
1878
2296
|
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
1879
2297
|
const x402Header = req.headers[PAYMENT_HEADER];
|
|
1880
|
-
|
|
2298
|
+
const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
|
|
2299
|
+
return await this.handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader);
|
|
1881
2300
|
}
|
|
1882
2301
|
this.sendJson(res, 404, { error: "Not found" });
|
|
1883
2302
|
} catch (err) {
|
|
@@ -1974,7 +2393,7 @@ var MoltsPayServer = class {
|
|
|
1974
2393
|
/**
|
|
1975
2394
|
* POST /execute - Execute service with x402 payment
|
|
1976
2395
|
*/
|
|
1977
|
-
async handleExecute(body, paymentHeader, res) {
|
|
2396
|
+
async handleExecute(body, paymentHeader, res, proofHeader) {
|
|
1978
2397
|
const { service, params } = body;
|
|
1979
2398
|
if (!service) {
|
|
1980
2399
|
return this.sendJson(res, 400, { error: "Missing service" });
|
|
@@ -1988,6 +2407,15 @@ var MoltsPayServer = class {
|
|
|
1988
2407
|
return this.sendJson(res, 400, { error: `Missing required param: ${key}` });
|
|
1989
2408
|
}
|
|
1990
2409
|
}
|
|
2410
|
+
if (proofHeader) {
|
|
2411
|
+
const alipayPayment = {
|
|
2412
|
+
x402Version: X402_VERSION2,
|
|
2413
|
+
scheme: ALIPAY_SCHEME,
|
|
2414
|
+
network: ALIPAY_NETWORK,
|
|
2415
|
+
payload: proofHeader
|
|
2416
|
+
};
|
|
2417
|
+
return this.handleAlipayExecute(skill, params || {}, alipayPayment, res);
|
|
2418
|
+
}
|
|
1991
2419
|
if (!paymentHeader) {
|
|
1992
2420
|
return this.sendPaymentRequired(skill.config, res);
|
|
1993
2421
|
}
|
|
@@ -1998,6 +2426,11 @@ var MoltsPayServer = class {
|
|
|
1998
2426
|
} catch {
|
|
1999
2427
|
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
2000
2428
|
}
|
|
2429
|
+
const payScheme = payment.accepted?.scheme || payment.scheme;
|
|
2430
|
+
const payNetwork = payment.accepted?.network || payment.network;
|
|
2431
|
+
if (payScheme === ALIPAY_SCHEME || (payNetwork ? isAlipayChainId(payNetwork) : false)) {
|
|
2432
|
+
return this.handleAlipayExecute(skill, params || {}, payment, res);
|
|
2433
|
+
}
|
|
2001
2434
|
const validation = this.validatePayment(payment, skill.config);
|
|
2002
2435
|
if (!validation.valid) {
|
|
2003
2436
|
return this.sendJson(res, 402, { error: validation.error });
|
|
@@ -2090,13 +2523,111 @@ var MoltsPayServer = class {
|
|
|
2090
2523
|
payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
|
|
2091
2524
|
}, responseHeaders);
|
|
2092
2525
|
}
|
|
2526
|
+
/**
|
|
2527
|
+
* Execute a service paid via the Alipay AI 收 fiat rail (2.0.0).
|
|
2528
|
+
*
|
|
2529
|
+
* Differs from the EVM/SVM path: no token detection, no EIP-3009/permit
|
|
2530
|
+
* validation. Verify hits the Alipay Open API (`payment.verify`). Settlement
|
|
2531
|
+
* (`fulfillment.confirm`) is FIRE-AND-FORGET per ALIPAY-INTEGRATION-DESIGN
|
|
2532
|
+
* §5.1: a confirm failure is logged but does NOT fail the already-delivered
|
|
2533
|
+
* response (the buyer's payment proof was already verified).
|
|
2534
|
+
*/
|
|
2535
|
+
async handleAlipayExecute(skill, params, payment, res) {
|
|
2536
|
+
if (!this.alipayFacilitator) {
|
|
2537
|
+
return this.sendJson(res, 402, { error: "Alipay rail not configured on this server" });
|
|
2538
|
+
}
|
|
2539
|
+
const requirements = {
|
|
2540
|
+
scheme: ALIPAY_SCHEME,
|
|
2541
|
+
network: ALIPAY_NETWORK,
|
|
2542
|
+
asset: "CNY",
|
|
2543
|
+
amount: skill.config.alipay?.price_cny || "0",
|
|
2544
|
+
payTo: this.manifest.provider.alipay?.seller_id || "",
|
|
2545
|
+
maxTimeoutSeconds: 1800
|
|
2546
|
+
};
|
|
2547
|
+
console.log(`[MoltsPay] Verifying Alipay payment...`);
|
|
2548
|
+
const verifyResult = await this.registry.verify(payment, requirements);
|
|
2549
|
+
if (!verifyResult.valid) {
|
|
2550
|
+
return this.sendJson(res, 402, {
|
|
2551
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
2552
|
+
facilitator: verifyResult.facilitator
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
console.log(`[MoltsPay] Alipay payment verified by ${verifyResult.facilitator}`);
|
|
2556
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
2557
|
+
console.log(`[MoltsPay] Executing skill: ${skill.id} (timeout: ${timeoutSeconds}s)`);
|
|
2558
|
+
let result;
|
|
2559
|
+
try {
|
|
2560
|
+
result = await Promise.race([
|
|
2561
|
+
skill.handler(params),
|
|
2562
|
+
new Promise(
|
|
2563
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
2564
|
+
)
|
|
2565
|
+
]);
|
|
2566
|
+
} catch (err) {
|
|
2567
|
+
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
2568
|
+
return this.sendJson(res, 500, {
|
|
2569
|
+
error: "Service execution failed",
|
|
2570
|
+
message: err.message
|
|
2571
|
+
});
|
|
2572
|
+
}
|
|
2573
|
+
let settlement;
|
|
2574
|
+
try {
|
|
2575
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2576
|
+
if (settlement.success) {
|
|
2577
|
+
console.log(`[MoltsPay] Alipay fulfillment confirmed: ${settlement.transaction}`);
|
|
2578
|
+
} else {
|
|
2579
|
+
console.error(`[MoltsPay] Alipay fulfillment confirm failed (non-fatal): ${settlement.error}`);
|
|
2580
|
+
}
|
|
2581
|
+
} catch (err) {
|
|
2582
|
+
console.error(`[MoltsPay] Alipay fulfillment confirm threw (non-fatal): ${err.message}`);
|
|
2583
|
+
settlement = { success: false, error: err.message, facilitator: "alipay" };
|
|
2584
|
+
}
|
|
2585
|
+
const responseHeaders = {};
|
|
2586
|
+
if (settlement.success) {
|
|
2587
|
+
responseHeaders[PAYMENT_RESPONSE_HEADER] = Buffer.from(JSON.stringify({
|
|
2588
|
+
success: true,
|
|
2589
|
+
transaction: settlement.transaction,
|
|
2590
|
+
network: ALIPAY_NETWORK,
|
|
2591
|
+
facilitator: settlement.facilitator
|
|
2592
|
+
})).toString("base64");
|
|
2593
|
+
}
|
|
2594
|
+
this.sendJson(res, 200, {
|
|
2595
|
+
success: true,
|
|
2596
|
+
result,
|
|
2597
|
+
payment: settlement.success ? { transaction: settlement.transaction, status: "fulfilled", facilitator: settlement.facilitator } : { status: "delivered_unconfirmed", error: settlement.error }
|
|
2598
|
+
}, responseHeaders);
|
|
2599
|
+
}
|
|
2600
|
+
/**
|
|
2601
|
+
* Build the Alipay 402 challenge for a service, or null when the alipay rail
|
|
2602
|
+
* isn't configured for this server or this service. Returns the x402
|
|
2603
|
+
* `accepts[]` entry plus the Base64URL `Payment-Needed` header value so the
|
|
2604
|
+
* 402 responders can dual-emit both the x402 and legacy alipay-bot formats.
|
|
2605
|
+
*/
|
|
2606
|
+
async buildAlipayChallenge(config) {
|
|
2607
|
+
if (!this.alipayFacilitator || !config.alipay) return null;
|
|
2608
|
+
try {
|
|
2609
|
+
const req = await this.alipayFacilitator.createPaymentRequirements({
|
|
2610
|
+
serviceId: config.alipay.service_id || this.manifest.provider.alipay.service_id_default,
|
|
2611
|
+
priceCny: config.alipay.price_cny,
|
|
2612
|
+
goodsName: config.alipay.goods_name,
|
|
2613
|
+
resourceId: `/execute?service=${config.id}`
|
|
2614
|
+
});
|
|
2615
|
+
return { accepts: req.x402Accepts, paymentNeededHeader: req.paymentNeededHeader };
|
|
2616
|
+
} catch (err) {
|
|
2617
|
+
console.error(`[MoltsPay] Alipay challenge build failed for ${config.id}: ${err.message}`);
|
|
2618
|
+
return null;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2093
2621
|
/**
|
|
2094
2622
|
* Handle MPP (Machine Payments Protocol) request
|
|
2095
2623
|
* Supports both x402 and MPP protocols on service endpoints
|
|
2096
2624
|
*/
|
|
2097
|
-
async handleMPPRequest(skill, body, authHeader, x402Header, res) {
|
|
2625
|
+
async handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader) {
|
|
2098
2626
|
const config = skill.config;
|
|
2099
2627
|
const params = body || {};
|
|
2628
|
+
if (proofHeader) {
|
|
2629
|
+
return await this.handleExecute({ service: config.id, params }, void 0, res, proofHeader);
|
|
2630
|
+
}
|
|
2100
2631
|
if (x402Header) {
|
|
2101
2632
|
return await this.handleExecute({ service: config.id, params }, x402Header, res);
|
|
2102
2633
|
}
|
|
@@ -2202,7 +2733,7 @@ var MoltsPayServer = class {
|
|
|
2202
2733
|
/**
|
|
2203
2734
|
* Return 402 with both x402 and MPP payment requirements
|
|
2204
2735
|
*/
|
|
2205
|
-
sendMPPPaymentRequired(config, res) {
|
|
2736
|
+
async sendMPPPaymentRequired(config, res) {
|
|
2206
2737
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
2207
2738
|
const providerChains = this.getProviderChains();
|
|
2208
2739
|
const accepts = [];
|
|
@@ -2213,6 +2744,10 @@ var MoltsPayServer = class {
|
|
|
2213
2744
|
}
|
|
2214
2745
|
}
|
|
2215
2746
|
}
|
|
2747
|
+
const alipayChallenge = await this.buildAlipayChallenge(config);
|
|
2748
|
+
if (alipayChallenge) {
|
|
2749
|
+
accepts.push(alipayChallenge.accepts);
|
|
2750
|
+
}
|
|
2216
2751
|
const x402PaymentRequired = {
|
|
2217
2752
|
x402Version: X402_VERSION2,
|
|
2218
2753
|
accepts,
|
|
@@ -2240,7 +2775,7 @@ var MoltsPayServer = class {
|
|
|
2240
2775
|
};
|
|
2241
2776
|
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
2242
2777
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
2243
|
-
mppWwwAuth = `Payment id="${challengeId}", realm="${this.manifest.provider.name}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
2778
|
+
mppWwwAuth = `Payment id="${challengeId}", realm="${headerSafe(this.manifest.provider.name)}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
|
|
2244
2779
|
}
|
|
2245
2780
|
const headers = {
|
|
2246
2781
|
"Content-Type": "application/problem+json",
|
|
@@ -2249,6 +2784,9 @@ var MoltsPayServer = class {
|
|
|
2249
2784
|
if (mppWwwAuth) {
|
|
2250
2785
|
headers[MPP_WWW_AUTH_HEADER] = mppWwwAuth;
|
|
2251
2786
|
}
|
|
2787
|
+
if (alipayChallenge) {
|
|
2788
|
+
headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
|
|
2789
|
+
}
|
|
2252
2790
|
res.writeHead(402, headers);
|
|
2253
2791
|
res.end(JSON.stringify({
|
|
2254
2792
|
type: "https://paymentauth.org/problems/payment-required",
|
|
@@ -2275,7 +2813,7 @@ var MoltsPayServer = class {
|
|
|
2275
2813
|
* Return 402 with x402 payment requirements (v2 format)
|
|
2276
2814
|
* Includes requirements for all chains and all accepted currencies
|
|
2277
2815
|
*/
|
|
2278
|
-
sendPaymentRequired(config, res) {
|
|
2816
|
+
async sendPaymentRequired(config, res) {
|
|
2279
2817
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
2280
2818
|
const providerChains = this.getProviderChains();
|
|
2281
2819
|
const accepts = [];
|
|
@@ -2286,6 +2824,10 @@ var MoltsPayServer = class {
|
|
|
2286
2824
|
}
|
|
2287
2825
|
}
|
|
2288
2826
|
}
|
|
2827
|
+
const alipayChallenge = await this.buildAlipayChallenge(config);
|
|
2828
|
+
if (alipayChallenge) {
|
|
2829
|
+
accepts.push(alipayChallenge.accepts);
|
|
2830
|
+
}
|
|
2289
2831
|
const acceptedChains = providerChains.map((c) => {
|
|
2290
2832
|
if (c.network === "eip155:8453") return "base";
|
|
2291
2833
|
if (c.network === "eip155:137") return "polygon";
|
|
@@ -2303,10 +2845,14 @@ var MoltsPayServer = class {
|
|
|
2303
2845
|
}
|
|
2304
2846
|
};
|
|
2305
2847
|
const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
|
|
2306
|
-
|
|
2848
|
+
const headers = {
|
|
2307
2849
|
"Content-Type": "application/json",
|
|
2308
2850
|
[PAYMENT_REQUIRED_HEADER]: encoded
|
|
2309
|
-
}
|
|
2851
|
+
};
|
|
2852
|
+
if (alipayChallenge) {
|
|
2853
|
+
headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
|
|
2854
|
+
}
|
|
2855
|
+
res.writeHead(402, headers);
|
|
2310
2856
|
res.end(JSON.stringify({
|
|
2311
2857
|
error: "Payment required",
|
|
2312
2858
|
message: `Service requires $${config.price} ${config.currency}`,
|
|
@@ -2413,12 +2959,12 @@ var MoltsPayServer = class {
|
|
|
2413
2959
|
return accepted.includes(token);
|
|
2414
2960
|
}
|
|
2415
2961
|
async readBody(req) {
|
|
2416
|
-
return new Promise((
|
|
2962
|
+
return new Promise((resolve2, reject) => {
|
|
2417
2963
|
let body = "";
|
|
2418
2964
|
req.on("data", (chunk) => body += chunk);
|
|
2419
2965
|
req.on("end", () => {
|
|
2420
2966
|
try {
|
|
2421
|
-
|
|
2967
|
+
resolve2(body ? JSON.parse(body) : {});
|
|
2422
2968
|
} catch {
|
|
2423
2969
|
reject(new Error("Invalid JSON"));
|
|
2424
2970
|
}
|
|
@@ -2676,7 +3222,7 @@ var MoltsPayServer = class {
|
|
|
2676
3222
|
};
|
|
2677
3223
|
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
2678
3224
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
2679
|
-
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
3225
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
|
|
2680
3226
|
res.writeHead(402, {
|
|
2681
3227
|
"Content-Type": "application/problem+json",
|
|
2682
3228
|
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
@@ -2843,8 +3389,8 @@ var MoltsPayServer = class {
|
|
|
2843
3389
|
|
|
2844
3390
|
// src/client/node/index.ts
|
|
2845
3391
|
var import_fs4 = require("fs");
|
|
2846
|
-
var
|
|
2847
|
-
var
|
|
3392
|
+
var import_os3 = require("os");
|
|
3393
|
+
var import_path3 = require("path");
|
|
2848
3394
|
var import_ethers3 = require("ethers");
|
|
2849
3395
|
|
|
2850
3396
|
// src/wallet/solana.ts
|
|
@@ -2903,6 +3449,16 @@ function networkToChainName(network) {
|
|
|
2903
3449
|
// src/client/core/base64.ts
|
|
2904
3450
|
var BufferCtor = globalThis.Buffer;
|
|
2905
3451
|
|
|
3452
|
+
// src/client/core/errors.ts
|
|
3453
|
+
var MoltsPayError = class extends Error {
|
|
3454
|
+
code;
|
|
3455
|
+
constructor(code, message) {
|
|
3456
|
+
super(message);
|
|
3457
|
+
this.name = "MoltsPayError";
|
|
3458
|
+
this.code = code;
|
|
3459
|
+
}
|
|
3460
|
+
};
|
|
3461
|
+
|
|
2906
3462
|
// src/client/core/eip3009.ts
|
|
2907
3463
|
var EIP3009_TYPES = {
|
|
2908
3464
|
TransferWithAuthorization: [
|
|
@@ -3039,6 +3595,748 @@ function findChainByChainId(chainId) {
|
|
|
3039
3595
|
return void 0;
|
|
3040
3596
|
}
|
|
3041
3597
|
|
|
3598
|
+
// src/client/alipay/index.ts
|
|
3599
|
+
var import_node_crypto3 = require("crypto");
|
|
3600
|
+
var import_promises = require("fs/promises");
|
|
3601
|
+
var import_os2 = require("os");
|
|
3602
|
+
var import_path2 = require("path");
|
|
3603
|
+
|
|
3604
|
+
// src/client/alipay/cli.ts
|
|
3605
|
+
var import_child_process = require("child_process");
|
|
3606
|
+
|
|
3607
|
+
// src/client/alipay/log.ts
|
|
3608
|
+
var RANK = { off: 0, info: 1, debug: 2 };
|
|
3609
|
+
function normalizeLevel(v) {
|
|
3610
|
+
const s = (v ?? "").toLowerCase();
|
|
3611
|
+
return s === "info" || s === "debug" ? s : "off";
|
|
3612
|
+
}
|
|
3613
|
+
var level = normalizeLevel(process.env.MOLTSPAY_ALIPAY_LOG);
|
|
3614
|
+
var sink = (line) => {
|
|
3615
|
+
process.stderr.write(line + "\n");
|
|
3616
|
+
};
|
|
3617
|
+
function setAlipayLogLevel(next) {
|
|
3618
|
+
level = next;
|
|
3619
|
+
}
|
|
3620
|
+
function getAlipayLogLevel() {
|
|
3621
|
+
return level;
|
|
3622
|
+
}
|
|
3623
|
+
function setAlipayLogSink(fn) {
|
|
3624
|
+
sink = fn;
|
|
3625
|
+
}
|
|
3626
|
+
function enabledFor(want) {
|
|
3627
|
+
return RANK[level] >= RANK[want];
|
|
3628
|
+
}
|
|
3629
|
+
function render(event, fields) {
|
|
3630
|
+
const parts = ["[moltspay:alipay]", String(fields.ts), event];
|
|
3631
|
+
if (fields.flow) parts.push(`flow=${fields.flow}`);
|
|
3632
|
+
if (fields.step) parts.push(`step=${fields.step}`);
|
|
3633
|
+
if (typeof fields.ms === "number") parts.push(`${fields.ms}ms`);
|
|
3634
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
3635
|
+
if (["ts", "level", "event", "flow", "step", "ms"].includes(k)) continue;
|
|
3636
|
+
parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
3637
|
+
}
|
|
3638
|
+
return parts.join(" ");
|
|
3639
|
+
}
|
|
3640
|
+
function emit(lvl, event, fields) {
|
|
3641
|
+
if (!enabledFor(lvl)) return;
|
|
3642
|
+
const full = { ts: (/* @__PURE__ */ new Date()).toISOString(), level: lvl, event, ...fields };
|
|
3643
|
+
try {
|
|
3644
|
+
sink(render(event, full), full);
|
|
3645
|
+
} catch {
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
var alipayLog = {
|
|
3649
|
+
info: (event, fields = {}) => emit("info", event, fields),
|
|
3650
|
+
debug: (event, fields = {}) => emit("debug", event, fields),
|
|
3651
|
+
/** True if anything at all would be logged (cheap guard for hot paths). */
|
|
3652
|
+
enabled: () => level !== "off"
|
|
3653
|
+
};
|
|
3654
|
+
async function timeStep(step, flow, fn) {
|
|
3655
|
+
if (!alipayLog.enabled()) return fn();
|
|
3656
|
+
const t0 = Date.now();
|
|
3657
|
+
alipayLog.debug("step.start", { flow, step });
|
|
3658
|
+
try {
|
|
3659
|
+
const out = await fn();
|
|
3660
|
+
alipayLog.info("step.end", { flow, step, ms: Date.now() - t0, ok: true });
|
|
3661
|
+
return out;
|
|
3662
|
+
} catch (err) {
|
|
3663
|
+
alipayLog.info("step.end", {
|
|
3664
|
+
flow,
|
|
3665
|
+
step,
|
|
3666
|
+
ms: Date.now() - t0,
|
|
3667
|
+
ok: false,
|
|
3668
|
+
err: err instanceof Error ? err.message : String(err)
|
|
3669
|
+
});
|
|
3670
|
+
throw err;
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
// src/client/alipay/cli.ts
|
|
3675
|
+
var ALLOWED_ENV = /* @__PURE__ */ new Set([
|
|
3676
|
+
"AIPAY_OUTPUT_CHANNEL",
|
|
3677
|
+
"AIPAY_SESSION_ID",
|
|
3678
|
+
"AIPAY_FRAMEWORK",
|
|
3679
|
+
"AIPAY_MODEL",
|
|
3680
|
+
"AIPAY_OS",
|
|
3681
|
+
// Minimal survival set for spawn to find the binary and a home dir.
|
|
3682
|
+
"PATH",
|
|
3683
|
+
"HOME"
|
|
3684
|
+
]);
|
|
3685
|
+
function filterEnv(env) {
|
|
3686
|
+
return Object.fromEntries(
|
|
3687
|
+
Object.entries(env).filter(([k]) => ALLOWED_ENV.has(k))
|
|
3688
|
+
);
|
|
3689
|
+
}
|
|
3690
|
+
function makeLineSplitter(onLine) {
|
|
3691
|
+
let buf = "";
|
|
3692
|
+
return (chunk) => {
|
|
3693
|
+
buf += chunk.toString("utf-8");
|
|
3694
|
+
let nl;
|
|
3695
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
3696
|
+
onLine(buf.slice(0, nl));
|
|
3697
|
+
buf = buf.slice(nl + 1);
|
|
3698
|
+
}
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
function profileEnv(step, flow) {
|
|
3702
|
+
const want = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE;
|
|
3703
|
+
const hook = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_HOOK;
|
|
3704
|
+
if (!want || !hook) return {};
|
|
3705
|
+
const steps = want.split(",").map((s) => s.trim());
|
|
3706
|
+
if (!steps.includes("all") && !steps.includes(step)) return {};
|
|
3707
|
+
const dir = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_DIR || "/tmp";
|
|
3708
|
+
const safe = `${flow ?? "noflow"}-${step}-${Date.now()}`.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3709
|
+
const out = `${dir}/cli-profile-${safe}.json`;
|
|
3710
|
+
alipayLog.info("cli.profile", { flow, step, out });
|
|
3711
|
+
return {
|
|
3712
|
+
NODE_OPTIONS: `--require=${hook}`,
|
|
3713
|
+
MOLTSPAY_CLI_PROFILE_OUT: out
|
|
3714
|
+
};
|
|
3715
|
+
}
|
|
3716
|
+
var runCli = (args, opts = {}) => {
|
|
3717
|
+
const bin = opts.bin ?? "alipay-bot";
|
|
3718
|
+
const step = opts.step ?? args[0] ?? "(no-arg)";
|
|
3719
|
+
const startedAt = Date.now();
|
|
3720
|
+
const lines = [];
|
|
3721
|
+
const collect = (line) => {
|
|
3722
|
+
lines.push(line);
|
|
3723
|
+
alipayLog.debug("cli.line", {
|
|
3724
|
+
flow: opts.flow,
|
|
3725
|
+
step,
|
|
3726
|
+
ms: Date.now() - startedAt,
|
|
3727
|
+
preview: line.slice(0, 120)
|
|
3728
|
+
});
|
|
3729
|
+
opts.onLine?.(line);
|
|
3730
|
+
};
|
|
3731
|
+
alipayLog.debug("step.start", { flow: opts.flow, step });
|
|
3732
|
+
return new Promise((resolve2, reject) => {
|
|
3733
|
+
const child = (0, import_child_process.spawn)(bin, args, {
|
|
3734
|
+
env: { ...filterEnv(process.env), ...profileEnv(step, opts.flow), ...opts.env ?? {} }
|
|
3735
|
+
});
|
|
3736
|
+
if (opts.signal) {
|
|
3737
|
+
if (opts.signal.aborted) child.kill("SIGTERM");
|
|
3738
|
+
opts.signal.addEventListener("abort", () => child.kill("SIGTERM"), { once: true });
|
|
3739
|
+
}
|
|
3740
|
+
let sawByte = false;
|
|
3741
|
+
const onChunk = (stream) => (chunk) => {
|
|
3742
|
+
const ms = Date.now() - startedAt;
|
|
3743
|
+
if (!sawByte) {
|
|
3744
|
+
sawByte = true;
|
|
3745
|
+
alipayLog.debug("cli.firstbyte", { flow: opts.flow, step, ms });
|
|
3746
|
+
}
|
|
3747
|
+
alipayLog.debug("cli.chunk", { flow: opts.flow, step, ms, stream, bytes: chunk.length });
|
|
3748
|
+
};
|
|
3749
|
+
const onStdout = makeLineSplitter(collect);
|
|
3750
|
+
const onStderr = makeLineSplitter(collect);
|
|
3751
|
+
child.stdout?.on("data", onChunk("out"));
|
|
3752
|
+
child.stderr?.on("data", onChunk("err"));
|
|
3753
|
+
child.stdout?.on("data", onStdout);
|
|
3754
|
+
child.stderr?.on("data", onStderr);
|
|
3755
|
+
child.on("error", (err) => {
|
|
3756
|
+
alipayLog.info("cli.exit", {
|
|
3757
|
+
flow: opts.flow,
|
|
3758
|
+
step,
|
|
3759
|
+
ms: Date.now() - startedAt,
|
|
3760
|
+
ok: false,
|
|
3761
|
+
err: err.message
|
|
3762
|
+
});
|
|
3763
|
+
reject(err);
|
|
3764
|
+
});
|
|
3765
|
+
child.on("close", (code) => {
|
|
3766
|
+
alipayLog.info("cli.exit", {
|
|
3767
|
+
flow: opts.flow,
|
|
3768
|
+
step,
|
|
3769
|
+
ms: Date.now() - startedAt,
|
|
3770
|
+
ok: (code ?? 1) === 0,
|
|
3771
|
+
code: code ?? 1
|
|
3772
|
+
});
|
|
3773
|
+
resolve2({ exitCode: code ?? 1, lines });
|
|
3774
|
+
});
|
|
3775
|
+
});
|
|
3776
|
+
};
|
|
3777
|
+
|
|
3778
|
+
// src/client/alipay/install.ts
|
|
3779
|
+
var import_child_process2 = require("child_process");
|
|
3780
|
+
var import_util = require("util");
|
|
3781
|
+
|
|
3782
|
+
// src/client/alipay/errors.ts
|
|
3783
|
+
var AlipayCliNotFoundError = class extends MoltsPayError {
|
|
3784
|
+
constructor(message) {
|
|
3785
|
+
super("ALIPAY_CLI_NOT_FOUND", message);
|
|
3786
|
+
this.name = "AlipayCliNotFoundError";
|
|
3787
|
+
}
|
|
3788
|
+
};
|
|
3789
|
+
var AlipayCliVersionError = class extends MoltsPayError {
|
|
3790
|
+
constructor(message) {
|
|
3791
|
+
super("ALIPAY_CLI_VERSION", message);
|
|
3792
|
+
this.name = "AlipayCliVersionError";
|
|
3793
|
+
}
|
|
3794
|
+
};
|
|
3795
|
+
var NeedsWalletSetupError = class extends MoltsPayError {
|
|
3796
|
+
constructor(message = "Alipay wallet not set up. Run: moltspay alipay apply") {
|
|
3797
|
+
super("ALIPAY_NEEDS_WALLET_SETUP", message);
|
|
3798
|
+
this.name = "NeedsWalletSetupError";
|
|
3799
|
+
}
|
|
3800
|
+
};
|
|
3801
|
+
var AlipayPaymentRejectedError = class extends MoltsPayError {
|
|
3802
|
+
constructor(message) {
|
|
3803
|
+
super("ALIPAY_PAYMENT_REJECTED", message);
|
|
3804
|
+
this.name = "AlipayPaymentRejectedError";
|
|
3805
|
+
}
|
|
3806
|
+
};
|
|
3807
|
+
var AlipayPaymentTimeoutError = class extends MoltsPayError {
|
|
3808
|
+
constructor(message) {
|
|
3809
|
+
super("ALIPAY_PAYMENT_TIMEOUT", message);
|
|
3810
|
+
this.name = "AlipayPaymentTimeoutError";
|
|
3811
|
+
}
|
|
3812
|
+
};
|
|
3813
|
+
var AlipayProtocolError = class extends MoltsPayError {
|
|
3814
|
+
constructor(message) {
|
|
3815
|
+
super("ALIPAY_PROTOCOL", message);
|
|
3816
|
+
this.name = "AlipayProtocolError";
|
|
3817
|
+
}
|
|
3818
|
+
};
|
|
3819
|
+
var UnsupportedRailError = class extends MoltsPayError {
|
|
3820
|
+
constructor(rail, message) {
|
|
3821
|
+
super("UNSUPPORTED_RAIL", message ?? `Rail not supported by server: ${rail}`);
|
|
3822
|
+
this.rail = rail;
|
|
3823
|
+
this.name = "UnsupportedRailError";
|
|
3824
|
+
}
|
|
3825
|
+
};
|
|
3826
|
+
|
|
3827
|
+
// src/client/alipay/install.ts
|
|
3828
|
+
var execFileAsync = (0, import_util.promisify)(import_child_process2.execFile);
|
|
3829
|
+
var MIN_CLI_VERSION = "0.3.15";
|
|
3830
|
+
function semverLt(a, b) {
|
|
3831
|
+
const pa = a.split(".").map((n) => parseInt(n, 10));
|
|
3832
|
+
const pb = b.split(".").map((n) => parseInt(n, 10));
|
|
3833
|
+
for (let i = 0; i < 3; i++) {
|
|
3834
|
+
const x = pa[i] ?? 0;
|
|
3835
|
+
const y = pb[i] ?? 0;
|
|
3836
|
+
if (x !== y) return x < y;
|
|
3837
|
+
}
|
|
3838
|
+
return false;
|
|
3839
|
+
}
|
|
3840
|
+
function parseVersion(stdout) {
|
|
3841
|
+
return stdout.match(/v?(\d+\.\d+\.\d+)/)?.[1] ?? null;
|
|
3842
|
+
}
|
|
3843
|
+
var defaultGetVersion = async () => {
|
|
3844
|
+
const { stdout } = await execFileAsync("alipay-bot", ["--version"]);
|
|
3845
|
+
return stdout;
|
|
3846
|
+
};
|
|
3847
|
+
async function ensureCliUncached(getVersion) {
|
|
3848
|
+
let stdout;
|
|
3849
|
+
try {
|
|
3850
|
+
stdout = await getVersion();
|
|
3851
|
+
} catch (e) {
|
|
3852
|
+
if (e?.code === "ENOENT") {
|
|
3853
|
+
throw new AlipayCliNotFoundError(
|
|
3854
|
+
"alipay-bot not installed. Run: npx -y @alipay/agent-payment install-cli"
|
|
3855
|
+
);
|
|
3856
|
+
}
|
|
3857
|
+
throw e;
|
|
3858
|
+
}
|
|
3859
|
+
const version = parseVersion(stdout);
|
|
3860
|
+
if (!version || semverLt(version, MIN_CLI_VERSION)) {
|
|
3861
|
+
throw new AlipayCliVersionError(
|
|
3862
|
+
`alipay-bot ${version ?? "?"} found, need \u2265 ${MIN_CLI_VERSION}. Run: npx -y @alipay/agent-payment@latest update`
|
|
3863
|
+
);
|
|
3864
|
+
}
|
|
3865
|
+
return version;
|
|
3866
|
+
}
|
|
3867
|
+
var cachedVersion = null;
|
|
3868
|
+
var inflight = null;
|
|
3869
|
+
async function ensureCli(getVersion = defaultGetVersion) {
|
|
3870
|
+
if (getVersion !== defaultGetVersion) {
|
|
3871
|
+
return ensureCliUncached(getVersion);
|
|
3872
|
+
}
|
|
3873
|
+
if (cachedVersion) return cachedVersion;
|
|
3874
|
+
if (!inflight) {
|
|
3875
|
+
inflight = ensureCliUncached(getVersion).then((v) => {
|
|
3876
|
+
cachedVersion = v;
|
|
3877
|
+
return v;
|
|
3878
|
+
}).finally(() => {
|
|
3879
|
+
inflight = null;
|
|
3880
|
+
});
|
|
3881
|
+
}
|
|
3882
|
+
return inflight;
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3885
|
+
// src/client/alipay/poll.ts
|
|
3886
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
3887
|
+
var POLL_MAX_INFLIGHT = 2;
|
|
3888
|
+
function resolveMaxInflight(override) {
|
|
3889
|
+
if (override !== void 0) return Math.max(1, Math.floor(override));
|
|
3890
|
+
const raw = process.env.MOLTSPAY_ALIPAY_POLL_MAX_INFLIGHT;
|
|
3891
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
3892
|
+
const n = Number(raw);
|
|
3893
|
+
if (Number.isFinite(n) && n >= 1) return Math.floor(n);
|
|
3894
|
+
}
|
|
3895
|
+
return POLL_MAX_INFLIGHT;
|
|
3896
|
+
}
|
|
3897
|
+
function resolveLaunchGap(override) {
|
|
3898
|
+
if (override !== void 0) return Math.max(0, override);
|
|
3899
|
+
const raw = process.env.MOLTSPAY_ALIPAY_POLL_LAUNCH_MS;
|
|
3900
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
3901
|
+
const n = Number(raw);
|
|
3902
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
3903
|
+
}
|
|
3904
|
+
return POLL_INTERVAL_MS;
|
|
3905
|
+
}
|
|
3906
|
+
function parseStatus(lines) {
|
|
3907
|
+
const raw = lines.join("\n").trim();
|
|
3908
|
+
try {
|
|
3909
|
+
const json = JSON.parse(raw);
|
|
3910
|
+
if (json && typeof json === "object") {
|
|
3911
|
+
if (typeof json.success === "boolean") {
|
|
3912
|
+
if (json.success) return "paid";
|
|
3913
|
+
const err = String(json.errorCode ?? "").toUpperCase();
|
|
3914
|
+
if (/UNPAID|WAIT|PENDING|PROCESS|NOTPAY/.test(err)) return "pending";
|
|
3915
|
+
if (/CLOSED|CANCEL|FAIL|REJECT|REFUSE|TIMEOUT|EXPIRE/.test(err)) return "rejected";
|
|
3916
|
+
return "pending";
|
|
3917
|
+
}
|
|
3918
|
+
if (typeof json.code !== "undefined") {
|
|
3919
|
+
if (Number(json.code) === 200) return "paid";
|
|
3920
|
+
const msg = `${json.message ?? ""}${json.reason ?? ""}`;
|
|
3921
|
+
if (/关闭|失败|拒绝|取消|超时|已撤销/.test(msg)) return "rejected";
|
|
3922
|
+
return "pending";
|
|
3923
|
+
}
|
|
3924
|
+
if (typeof json.body === "string") {
|
|
3925
|
+
return classifyReportText(json.body);
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
} catch {
|
|
3929
|
+
}
|
|
3930
|
+
return classifyReportText(raw);
|
|
3931
|
+
}
|
|
3932
|
+
function classifyReportText(s) {
|
|
3933
|
+
const text = s.toUpperCase();
|
|
3934
|
+
if (/查询支付状态成功并获取资源/.test(s) || /资源响应状态[^0-9\n]{0,8}200/.test(s) || /TRADE_SUCCESS|TRADE_FINISHED|"STATUS":\s*"FULFILLED"/.test(text)) return "paid";
|
|
3935
|
+
if (/TRADE_CLOSED|REJECTED|REFUSE|CANCEL/.test(text) || /交易关闭|支付失败|已拒绝|已取消|已撤销|交易超时/.test(s)) return "rejected";
|
|
3936
|
+
if (/WAIT_BUYER_PAY|UNPAID|PENDING|WAITING/.test(text) || /交易未支付|等待付款|待支付|未支付/.test(s)) return "pending";
|
|
3937
|
+
return "unknown";
|
|
3938
|
+
}
|
|
3939
|
+
function defaultSleep(ms, signal) {
|
|
3940
|
+
return new Promise((resolve2, reject) => {
|
|
3941
|
+
if (signal?.aborted) return reject(new Error("aborted"));
|
|
3942
|
+
const t = setTimeout(resolve2, ms);
|
|
3943
|
+
signal?.addEventListener(
|
|
3944
|
+
"abort",
|
|
3945
|
+
() => {
|
|
3946
|
+
clearTimeout(t);
|
|
3947
|
+
reject(new Error("aborted"));
|
|
3948
|
+
},
|
|
3949
|
+
{ once: true }
|
|
3950
|
+
);
|
|
3951
|
+
});
|
|
3952
|
+
}
|
|
3953
|
+
var LAUNCH = /* @__PURE__ */ Symbol("launch");
|
|
3954
|
+
async function pollUntil(tradeNo, resourceUrl, opts) {
|
|
3955
|
+
const runner = opts.runner ?? runCli;
|
|
3956
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
3957
|
+
const now = opts.now ?? Date.now;
|
|
3958
|
+
const maxInflight = resolveMaxInflight(opts.maxInflight);
|
|
3959
|
+
const launchGap = resolveLaunchGap(opts.launchIntervalMs);
|
|
3960
|
+
const args = [
|
|
3961
|
+
"402-query-payment-status",
|
|
3962
|
+
"-t",
|
|
3963
|
+
tradeNo,
|
|
3964
|
+
"-r",
|
|
3965
|
+
resourceUrl,
|
|
3966
|
+
...opts.method ? ["-m", opts.method] : [],
|
|
3967
|
+
...opts.data ? ["-d", opts.data] : []
|
|
3968
|
+
];
|
|
3969
|
+
const internal = new AbortController();
|
|
3970
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, internal.signal]) : internal.signal;
|
|
3971
|
+
let tick = 0;
|
|
3972
|
+
let lastLaunch = -Infinity;
|
|
3973
|
+
const inflight2 = /* @__PURE__ */ new Set();
|
|
3974
|
+
const launch = () => {
|
|
3975
|
+
tick += 1;
|
|
3976
|
+
const myTick = tick;
|
|
3977
|
+
lastLaunch = now();
|
|
3978
|
+
const run = (async () => {
|
|
3979
|
+
const { lines } = await runner(args, {
|
|
3980
|
+
signal,
|
|
3981
|
+
step: "query-payment-status",
|
|
3982
|
+
flow: tradeNo
|
|
3983
|
+
});
|
|
3984
|
+
const status = parseStatus(lines);
|
|
3985
|
+
alipayLog.debug("poll.tick", { flow: tradeNo, tick: myTick, status });
|
|
3986
|
+
return { status, lines };
|
|
3987
|
+
})();
|
|
3988
|
+
const tracked = run.finally(() => {
|
|
3989
|
+
inflight2.delete(tracked);
|
|
3990
|
+
});
|
|
3991
|
+
inflight2.add(tracked);
|
|
3992
|
+
};
|
|
3993
|
+
try {
|
|
3994
|
+
for (; ; ) {
|
|
3995
|
+
if (opts.signal?.aborted) throw new Error("aborted");
|
|
3996
|
+
const expired = now() >= opts.deadline;
|
|
3997
|
+
if (expired && inflight2.size === 0) {
|
|
3998
|
+
throw new AlipayPaymentTimeoutError(
|
|
3999
|
+
`Payment ${tradeNo} not completed before pay_before deadline`
|
|
4000
|
+
);
|
|
4001
|
+
}
|
|
4002
|
+
while (!expired && inflight2.size < maxInflight && now() - lastLaunch >= launchGap) {
|
|
4003
|
+
launch();
|
|
4004
|
+
}
|
|
4005
|
+
const waiters = [...inflight2];
|
|
4006
|
+
if (!expired && inflight2.size < maxInflight) {
|
|
4007
|
+
const untilNext = Math.max(0, launchGap - (now() - lastLaunch));
|
|
4008
|
+
waiters.push(sleep(untilNext, signal).then(() => LAUNCH, () => LAUNCH));
|
|
4009
|
+
}
|
|
4010
|
+
const winner = await Promise.race(waiters);
|
|
4011
|
+
if (winner === LAUNCH) continue;
|
|
4012
|
+
if (winner.status === "paid") return { status: "paid", lines: winner.lines };
|
|
4013
|
+
if (winner.status === "rejected") {
|
|
4014
|
+
throw new AlipayPaymentRejectedError(`Payment ${tradeNo} was rejected`);
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
} finally {
|
|
4018
|
+
internal.abort();
|
|
4019
|
+
for (const p of inflight2) p.catch(() => void 0);
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
// src/client/alipay/index.ts
|
|
4024
|
+
var TRADE_NO_RE = /^\d{32}$/;
|
|
4025
|
+
function resolveSessionId(explicit, envSession) {
|
|
4026
|
+
return explicit ?? envSession ?? (0, import_node_crypto3.randomUUID)();
|
|
4027
|
+
}
|
|
4028
|
+
function assertTradeNo(t) {
|
|
4029
|
+
if (!TRADE_NO_RE.test(t)) {
|
|
4030
|
+
throw new AlipayProtocolError(`invalid tradeNo (expect 32 digits): ${t}`);
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
function parseTradeNo(lines) {
|
|
4034
|
+
for (const line of lines) {
|
|
4035
|
+
const labeled = line.match(/trade[_-]?no["'\s:=]+(\d{32})/i);
|
|
4036
|
+
if (labeled) return labeled[1];
|
|
4037
|
+
const bare = line.match(/\b(\d{32})\b/);
|
|
4038
|
+
if (bare) return bare[1];
|
|
4039
|
+
}
|
|
4040
|
+
return null;
|
|
4041
|
+
}
|
|
4042
|
+
function parsePaymentUrl(lines) {
|
|
4043
|
+
let paymentUrl;
|
|
4044
|
+
let shortenUrl;
|
|
4045
|
+
for (const line of lines) {
|
|
4046
|
+
const m = line.match(/(alipays?:\/\/\S+|https?:\/\/\S+)/i);
|
|
4047
|
+
if (!m) continue;
|
|
4048
|
+
const url = m[1].replace(/[)\]`>]+$/, "");
|
|
4049
|
+
if (/short|qr\.alipay|surl|\/s\//i.test(line) && !shortenUrl) shortenUrl = url;
|
|
4050
|
+
else if (!paymentUrl) paymentUrl = url;
|
|
4051
|
+
}
|
|
4052
|
+
if (!paymentUrl && shortenUrl) paymentUrl = shortenUrl;
|
|
4053
|
+
return { paymentUrl, shortenUrl };
|
|
4054
|
+
}
|
|
4055
|
+
function extractMedia(line) {
|
|
4056
|
+
const m = line.match(/^\s*MEDIA:\s*(.+?)\s*$/);
|
|
4057
|
+
return m ? m[1] : null;
|
|
4058
|
+
}
|
|
4059
|
+
function isWalletReady(lines) {
|
|
4060
|
+
const text = lines.join("\n").trim();
|
|
4061
|
+
try {
|
|
4062
|
+
const json = JSON.parse(text);
|
|
4063
|
+
if (json && typeof json.code !== "undefined") return Number(json.code) === 200;
|
|
4064
|
+
} catch {
|
|
4065
|
+
}
|
|
4066
|
+
return !/未开通|未开启|NOT[_\s-]*(OPEN|BOUND|SET)|NEEDS?[_\s-]*SETUP|NO[_\s-]*WALLET/i.test(text);
|
|
4067
|
+
}
|
|
4068
|
+
function extractResourceFromReport(report) {
|
|
4069
|
+
const label = report.search(/资源响应体/);
|
|
4070
|
+
const start = report.indexOf("{", label === -1 ? 0 : label);
|
|
4071
|
+
if (start === -1) return void 0;
|
|
4072
|
+
let depth = 0, inStr = false, esc = false;
|
|
4073
|
+
for (let i = start; i < report.length; i++) {
|
|
4074
|
+
const ch = report[i];
|
|
4075
|
+
if (inStr) {
|
|
4076
|
+
if (esc) esc = false;
|
|
4077
|
+
else if (ch === "\\") esc = true;
|
|
4078
|
+
else if (ch === '"') inStr = false;
|
|
4079
|
+
continue;
|
|
4080
|
+
}
|
|
4081
|
+
if (ch === '"') inStr = true;
|
|
4082
|
+
else if (ch === "{") depth++;
|
|
4083
|
+
else if (ch === "}" && --depth === 0) {
|
|
4084
|
+
const slice = report.slice(start, i + 1);
|
|
4085
|
+
try {
|
|
4086
|
+
const obj = JSON.parse(slice);
|
|
4087
|
+
const r = obj?.resourceResponse ?? obj?.result ?? obj?.data ?? obj?.body ?? obj;
|
|
4088
|
+
return typeof r === "string" ? r : JSON.stringify(r);
|
|
4089
|
+
} catch {
|
|
4090
|
+
return slice;
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
return void 0;
|
|
4095
|
+
}
|
|
4096
|
+
function extractBody(lines) {
|
|
4097
|
+
const text = lines.join("\n").trim();
|
|
4098
|
+
try {
|
|
4099
|
+
const json = JSON.parse(text);
|
|
4100
|
+
if (json && typeof json === "object") {
|
|
4101
|
+
if (json.resourceResponse !== void 0 && json.resourceResponse !== null) {
|
|
4102
|
+
const rr = json.resourceResponse;
|
|
4103
|
+
const r = typeof rr === "object" ? rr.result ?? rr.data ?? rr.body ?? rr : rr;
|
|
4104
|
+
return typeof r === "string" ? r : JSON.stringify(r);
|
|
4105
|
+
}
|
|
4106
|
+
if (typeof json.body === "string") {
|
|
4107
|
+
const inner = extractResourceFromReport(json.body);
|
|
4108
|
+
return inner ?? json.body;
|
|
4109
|
+
}
|
|
4110
|
+
const body = json.data ?? json.result ?? json.body ?? json.resource;
|
|
4111
|
+
if (body !== void 0) return typeof body === "string" ? body : JSON.stringify(body);
|
|
4112
|
+
return text;
|
|
4113
|
+
}
|
|
4114
|
+
} catch {
|
|
4115
|
+
}
|
|
4116
|
+
const idx = lines.findIndex((l) => /^\s*BODY:/.test(l));
|
|
4117
|
+
if (idx !== -1) {
|
|
4118
|
+
const first = lines[idx].replace(/^\s*BODY:\s*/, "");
|
|
4119
|
+
return [first, ...lines.slice(idx + 1)].join("\n").trim();
|
|
4120
|
+
}
|
|
4121
|
+
return lines.filter((l) => !/^\s*(MEDIA|STATUS|INFO):/.test(l)).join("\n").trim();
|
|
4122
|
+
}
|
|
4123
|
+
var DEFAULT_WALLET_TTL_MS = 10 * 60 * 1e3;
|
|
4124
|
+
function resolveWalletTtlMs() {
|
|
4125
|
+
const raw = process.env.MOLTSPAY_ALIPAY_WALLET_TTL_MS;
|
|
4126
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_WALLET_TTL_MS;
|
|
4127
|
+
const n = Number(raw);
|
|
4128
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_WALLET_TTL_MS;
|
|
4129
|
+
}
|
|
4130
|
+
var walletReadyUntil = /* @__PURE__ */ new Map();
|
|
4131
|
+
function resetWalletCache() {
|
|
4132
|
+
walletReadyUntil.clear();
|
|
4133
|
+
}
|
|
4134
|
+
var DEFAULT_INTENT_TTL_MS = 10 * 60 * 1e3;
|
|
4135
|
+
function resolveIntentTtlMs() {
|
|
4136
|
+
const raw = process.env.MOLTSPAY_ALIPAY_INTENT_TTL_MS;
|
|
4137
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_INTENT_TTL_MS;
|
|
4138
|
+
const n = Number(raw);
|
|
4139
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_INTENT_TTL_MS;
|
|
4140
|
+
}
|
|
4141
|
+
var intentDoneUntil = /* @__PURE__ */ new Map();
|
|
4142
|
+
function resetIntentCache() {
|
|
4143
|
+
intentDoneUntil.clear();
|
|
4144
|
+
}
|
|
4145
|
+
var AlipayClient = class {
|
|
4146
|
+
sessionId;
|
|
4147
|
+
configDir;
|
|
4148
|
+
framework;
|
|
4149
|
+
runner;
|
|
4150
|
+
getVersion;
|
|
4151
|
+
now;
|
|
4152
|
+
/** Only the default runner may use the process-level wallet cache. */
|
|
4153
|
+
walletCacheable;
|
|
4154
|
+
/** Only the default runner may use the process-level payment-intent cache. */
|
|
4155
|
+
intentCacheable;
|
|
4156
|
+
constructor(opts = {}) {
|
|
4157
|
+
this.sessionId = resolveSessionId(opts.sessionId, process.env.AIPAY_SESSION_ID);
|
|
4158
|
+
this.configDir = opts.configDir ?? (0, import_path2.join)((0, import_os2.homedir)(), ".moltspay");
|
|
4159
|
+
this.framework = opts.framework ?? process.env.AIPAY_FRAMEWORK ?? "openclaw";
|
|
4160
|
+
this.runner = opts.runner ?? runCli;
|
|
4161
|
+
this.getVersion = opts.getVersion;
|
|
4162
|
+
this.now = opts.now ?? Date.now;
|
|
4163
|
+
this.walletCacheable = opts.cacheWallet ?? !opts.runner;
|
|
4164
|
+
this.intentCacheable = opts.cacheIntent ?? !opts.runner;
|
|
4165
|
+
}
|
|
4166
|
+
/**
|
|
4167
|
+
* Throws NeedsWalletSetupError unless alipay-bot reports an opened wallet.
|
|
4168
|
+
*
|
|
4169
|
+
* Skips the ~22s `check-wallet` spawn entirely when a prior call cached a
|
|
4170
|
+
* "ready" verdict within the TTL (see {@link DEFAULT_WALLET_TTL_MS}).
|
|
4171
|
+
*/
|
|
4172
|
+
async checkWallet(signal, flow) {
|
|
4173
|
+
const ttlMs = resolveWalletTtlMs();
|
|
4174
|
+
const cacheKey = `${this.configDir}::${this.framework}`;
|
|
4175
|
+
const useCache = this.walletCacheable && ttlMs > 0;
|
|
4176
|
+
if (useCache) {
|
|
4177
|
+
const until = walletReadyUntil.get(cacheKey);
|
|
4178
|
+
if (until !== void 0 && this.now() < until) {
|
|
4179
|
+
alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: true, ttlMsLeft: until - this.now() });
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
const { lines } = await this.runner(["check-wallet"], { signal, step: "check-wallet", flow });
|
|
4184
|
+
if (!isWalletReady(lines)) {
|
|
4185
|
+
if (this.walletCacheable) walletReadyUntil.delete(cacheKey);
|
|
4186
|
+
throw new NeedsWalletSetupError(
|
|
4187
|
+
"Alipay wallet not opened. Run: moltspay alipay apply (then: moltspay alipay bind)"
|
|
4188
|
+
);
|
|
4189
|
+
}
|
|
4190
|
+
if (useCache) {
|
|
4191
|
+
walletReadyUntil.set(cacheKey, this.now() + ttlMs);
|
|
4192
|
+
alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: false, cachedForMs: ttlMs });
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
/** Run the full 8-step flow and resolve with the resource body. */
|
|
4196
|
+
async pay402(opts) {
|
|
4197
|
+
const { resourceUrl, requirement, signal } = opts;
|
|
4198
|
+
const extra = requirement.extra ?? {};
|
|
4199
|
+
const paymentNeededHeader = String(extra.payment_needed_header ?? "");
|
|
4200
|
+
if (!paymentNeededHeader) {
|
|
4201
|
+
throw new AlipayProtocolError("alipay requirement missing extra.payment_needed_header");
|
|
4202
|
+
}
|
|
4203
|
+
const flow = this.sessionId;
|
|
4204
|
+
const flowStart = this.now();
|
|
4205
|
+
alipayLog.info("flow.start", { flow, resource: resourceUrl });
|
|
4206
|
+
const media = [];
|
|
4207
|
+
const onLine = (line) => {
|
|
4208
|
+
const m = extractMedia(line);
|
|
4209
|
+
if (m) media.push(m);
|
|
4210
|
+
else opts.onLine?.(line);
|
|
4211
|
+
};
|
|
4212
|
+
const intentSummary = opts.intentSummary?.trim() || `\u652F\u4ED8 ${requirement.amount ?? ""} ${requirement.asset ?? "CNY"}`.trim();
|
|
4213
|
+
await timeStep("ensure-cli", flow, () => ensureCli(this.getVersion));
|
|
4214
|
+
const intentTtlMs = resolveIntentTtlMs();
|
|
4215
|
+
const intentKey = `${this.configDir}::${this.framework}`;
|
|
4216
|
+
const useIntentCache = this.intentCacheable && intentTtlMs > 0;
|
|
4217
|
+
const intentUntil = useIntentCache ? intentDoneUntil.get(intentKey) : void 0;
|
|
4218
|
+
if (intentUntil !== void 0 && this.now() < intentUntil) {
|
|
4219
|
+
alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: true, ttlMsLeft: intentUntil - this.now() });
|
|
4220
|
+
} else {
|
|
4221
|
+
await this.runner(
|
|
4222
|
+
[
|
|
4223
|
+
"payment-intent",
|
|
4224
|
+
"--session-id",
|
|
4225
|
+
this.sessionId,
|
|
4226
|
+
"--intent-summary",
|
|
4227
|
+
intentSummary,
|
|
4228
|
+
"--framework",
|
|
4229
|
+
this.framework
|
|
4230
|
+
],
|
|
4231
|
+
{ onLine, signal, step: "payment-intent", flow }
|
|
4232
|
+
);
|
|
4233
|
+
if (useIntentCache) {
|
|
4234
|
+
intentDoneUntil.set(intentKey, this.now() + intentTtlMs);
|
|
4235
|
+
alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: false, cachedForMs: intentTtlMs });
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
await this.checkWallet(signal, flow);
|
|
4239
|
+
const reqId = String(extra.out_trade_no ?? (0, import_node_crypto3.randomUUID)());
|
|
4240
|
+
const dir = (0, import_path2.join)(this.configDir, "alipay");
|
|
4241
|
+
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
4242
|
+
const challengeFile = (0, import_path2.join)(dir, `402_${reqId}.txt`);
|
|
4243
|
+
await (0, import_promises.writeFile)(challengeFile, paymentNeededHeader, "utf-8");
|
|
4244
|
+
const payArgs = [
|
|
4245
|
+
"402-buyer-pay",
|
|
4246
|
+
"-f",
|
|
4247
|
+
challengeFile,
|
|
4248
|
+
"-r",
|
|
4249
|
+
resourceUrl,
|
|
4250
|
+
"-s",
|
|
4251
|
+
this.sessionId,
|
|
4252
|
+
"-i",
|
|
4253
|
+
intentSummary,
|
|
4254
|
+
"-w",
|
|
4255
|
+
this.framework
|
|
4256
|
+
];
|
|
4257
|
+
if (opts.method) payArgs.push("-m", opts.method);
|
|
4258
|
+
if (opts.data) payArgs.push("-d", opts.data);
|
|
4259
|
+
const payRun = await this.runner(payArgs, { onLine, signal, step: "402-buyer-pay", flow });
|
|
4260
|
+
const tradeNo = parseTradeNo(payRun.lines);
|
|
4261
|
+
if (!tradeNo) {
|
|
4262
|
+
throw new AlipayProtocolError("402-buyer-pay did not return a tradeNo");
|
|
4263
|
+
}
|
|
4264
|
+
assertTradeNo(tradeNo);
|
|
4265
|
+
const { paymentUrl, shortenUrl } = parsePaymentUrl(payRun.lines);
|
|
4266
|
+
if (paymentUrl) {
|
|
4267
|
+
alipayLog.info("flow.pending", { flow, tradeNo, ms: this.now() - flowStart });
|
|
4268
|
+
opts.onPaymentPending?.({ paymentUrl, shortenUrl, tradeNo });
|
|
4269
|
+
}
|
|
4270
|
+
const pendingAt = this.now();
|
|
4271
|
+
const windowMs = (requirement.maxTimeoutSeconds ?? 30 * 60) * 1e3;
|
|
4272
|
+
const deadline = this.now() + (opts.timeoutMs ?? windowMs);
|
|
4273
|
+
const poll = await pollUntil(tradeNo, resourceUrl, {
|
|
4274
|
+
// No onLine: the status-poll output embeds the resource and must not reach
|
|
4275
|
+
// the log stream — the body is surfaced via the return value below (§9.3).
|
|
4276
|
+
deadline,
|
|
4277
|
+
signal,
|
|
4278
|
+
runner: this.runner,
|
|
4279
|
+
now: this.now,
|
|
4280
|
+
// Re-fetch the resource the same way it was paid (POST + body), else the
|
|
4281
|
+
// status poll defaults to GET and 404s on a POST-only `/execute`.
|
|
4282
|
+
method: opts.method,
|
|
4283
|
+
data: opts.data
|
|
4284
|
+
});
|
|
4285
|
+
alipayLog.info("flow.settled", { flow, tradeNo, ms: this.now() - pendingAt });
|
|
4286
|
+
const body = extractBody(poll.lines);
|
|
4287
|
+
void this.runner(["402-buyer-fulfillment-ack", "-t", tradeNo], {
|
|
4288
|
+
onLine,
|
|
4289
|
+
signal,
|
|
4290
|
+
step: "402-buyer-fulfillment-ack",
|
|
4291
|
+
flow
|
|
4292
|
+
}).catch(() => void 0);
|
|
4293
|
+
return { body, payment: { tradeNo, outTradeNo: reqId }, media };
|
|
4294
|
+
}
|
|
4295
|
+
};
|
|
4296
|
+
|
|
4297
|
+
// src/client/alipay/router.ts
|
|
4298
|
+
var ALIPAY_RAIL = "alipay";
|
|
4299
|
+
function railOf(req) {
|
|
4300
|
+
if (req.scheme === "alipay-aipay" || req.network === ALIPAY_RAIL) return ALIPAY_RAIL;
|
|
4301
|
+
return networkToChainName(req.network) ?? req.network;
|
|
4302
|
+
}
|
|
4303
|
+
function findRail(accepts, rail) {
|
|
4304
|
+
const requirement = accepts.find((r) => railOf(r) === rail);
|
|
4305
|
+
return requirement ? { rail, requirement } : null;
|
|
4306
|
+
}
|
|
4307
|
+
function selectRail(input) {
|
|
4308
|
+
const { serverAccepts, explicitRail, preference, availability } = input;
|
|
4309
|
+
if (!serverAccepts || serverAccepts.length === 0) {
|
|
4310
|
+
throw new UnsupportedRailError(explicitRail ?? "unknown", "Server offered no payment options");
|
|
4311
|
+
}
|
|
4312
|
+
if (explicitRail) {
|
|
4313
|
+
const hit = findRail(serverAccepts, explicitRail);
|
|
4314
|
+
if (!hit) {
|
|
4315
|
+
const offered = serverAccepts.map(railOf);
|
|
4316
|
+
throw new UnsupportedRailError(
|
|
4317
|
+
explicitRail,
|
|
4318
|
+
`Server doesn't accept rail '${explicitRail}'. Offered: ${offered.join(", ")}`
|
|
4319
|
+
);
|
|
4320
|
+
}
|
|
4321
|
+
return hit;
|
|
4322
|
+
}
|
|
4323
|
+
if (preference) {
|
|
4324
|
+
for (const pref of preference) {
|
|
4325
|
+
const hit = findRail(serverAccepts, pref);
|
|
4326
|
+
if (hit) return hit;
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
if (availability?.evmReady) {
|
|
4330
|
+
const evm = serverAccepts.find((r) => railOf(r) !== ALIPAY_RAIL);
|
|
4331
|
+
if (evm) return { rail: railOf(evm), requirement: evm };
|
|
4332
|
+
}
|
|
4333
|
+
if (availability?.alipayReady) {
|
|
4334
|
+
const hit = findRail(serverAccepts, ALIPAY_RAIL);
|
|
4335
|
+
if (hit) return hit;
|
|
4336
|
+
}
|
|
4337
|
+
return { rail: railOf(serverAccepts[0]), requirement: serverAccepts[0] };
|
|
4338
|
+
}
|
|
4339
|
+
|
|
3042
4340
|
// src/client/node/index.ts
|
|
3043
4341
|
var DEFAULT_CONFIG = {
|
|
3044
4342
|
chain: "base",
|
|
@@ -3055,9 +4353,13 @@ var MoltsPayClient = class {
|
|
|
3055
4353
|
signer = null;
|
|
3056
4354
|
todaySpending = 0;
|
|
3057
4355
|
lastSpendingReset = 0;
|
|
4356
|
+
railPreference;
|
|
4357
|
+
alipaySessionId;
|
|
3058
4358
|
constructor(options = {}) {
|
|
3059
|
-
this.configDir = options.configDir || (0,
|
|
4359
|
+
this.configDir = options.configDir || (0, import_path3.join)((0, import_os3.homedir)(), ".moltspay");
|
|
3060
4360
|
this.config = this.loadConfig();
|
|
4361
|
+
this.railPreference = options.railPreference ?? this.config.railPreference;
|
|
4362
|
+
this.alipaySessionId = options.alipaySessionId;
|
|
3061
4363
|
this.walletData = this.loadWallet();
|
|
3062
4364
|
this.loadSpending();
|
|
3063
4365
|
if (this.walletData) {
|
|
@@ -3135,6 +4437,9 @@ var MoltsPayClient = class {
|
|
|
3135
4437
|
* @param options - Payment options (token selection)
|
|
3136
4438
|
*/
|
|
3137
4439
|
async pay(serverUrl, service, params, options = {}) {
|
|
4440
|
+
if (options.rail === ALIPAY_RAIL) {
|
|
4441
|
+
return this.payViaAlipay(serverUrl, service, params, options);
|
|
4442
|
+
}
|
|
3138
4443
|
if (!this.wallet || !this.walletData) {
|
|
3139
4444
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
3140
4445
|
}
|
|
@@ -3326,6 +4631,76 @@ Please specify: --chain <chain_name>`
|
|
|
3326
4631
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
3327
4632
|
return result.result || result;
|
|
3328
4633
|
}
|
|
4634
|
+
/**
|
|
4635
|
+
* Pay for a service over the Alipay fiat rail (2.0.0).
|
|
4636
|
+
*
|
|
4637
|
+
* Unlike the crypto path this needs no EVM wallet — it shells out to
|
|
4638
|
+
* alipay-bot via {@link AlipayClient}. Flow: hit the resource with no
|
|
4639
|
+
* payment to get the 402 challenge, confirm the server actually offers the
|
|
4640
|
+
* alipay rail (selectRail), then run the 8-step state machine and return the
|
|
4641
|
+
* resource body.
|
|
4642
|
+
*/
|
|
4643
|
+
async payViaAlipay(serverUrl, service, params, options) {
|
|
4644
|
+
const flow = this.alipaySessionId;
|
|
4645
|
+
let executeUrl = `${serverUrl}/execute`;
|
|
4646
|
+
try {
|
|
4647
|
+
const services = await timeStep(
|
|
4648
|
+
"discover-services",
|
|
4649
|
+
flow,
|
|
4650
|
+
() => this.getServices(serverUrl)
|
|
4651
|
+
);
|
|
4652
|
+
const svc = services.services?.find((s) => s.id === service);
|
|
4653
|
+
if (svc?.endpoint) executeUrl = `${serverUrl}${svc.endpoint}`;
|
|
4654
|
+
} catch {
|
|
4655
|
+
}
|
|
4656
|
+
const requestBody = options.rawData ? { service, ...params } : { service, params };
|
|
4657
|
+
const bodyJson = JSON.stringify(requestBody);
|
|
4658
|
+
const res = await timeStep(
|
|
4659
|
+
"challenge-402",
|
|
4660
|
+
flow,
|
|
4661
|
+
() => fetch(executeUrl, {
|
|
4662
|
+
method: "POST",
|
|
4663
|
+
headers: { "Content-Type": "application/json" },
|
|
4664
|
+
body: bodyJson
|
|
4665
|
+
})
|
|
4666
|
+
);
|
|
4667
|
+
if (res.status !== 402) {
|
|
4668
|
+
const data = await res.json().catch(() => ({}));
|
|
4669
|
+
if (res.ok && data.result) return data.result;
|
|
4670
|
+
throw new Error(data.error || `Expected 402, got ${res.status}`);
|
|
4671
|
+
}
|
|
4672
|
+
const header = res.headers.get(PAYMENT_REQUIRED_HEADER2);
|
|
4673
|
+
if (!header) throw new Error("Missing x-payment-required header on 402");
|
|
4674
|
+
const parsed = JSON.parse(Buffer.from(header, "base64").toString("utf-8"));
|
|
4675
|
+
const accepts = Array.isArray(parsed) ? parsed : parsed.accepts ?? [parsed];
|
|
4676
|
+
const { requirement } = selectRail({
|
|
4677
|
+
serverAccepts: accepts,
|
|
4678
|
+
explicitRail: ALIPAY_RAIL,
|
|
4679
|
+
preference: this.railPreference,
|
|
4680
|
+
availability: { evmReady: this.isInitialized }
|
|
4681
|
+
});
|
|
4682
|
+
const onLine = options.onLine ?? ((line) => process.stdout.write(line + "\n"));
|
|
4683
|
+
const alipay = new AlipayClient({
|
|
4684
|
+
sessionId: this.alipaySessionId,
|
|
4685
|
+
configDir: this.configDir
|
|
4686
|
+
});
|
|
4687
|
+
const result = await alipay.pay402({
|
|
4688
|
+
resourceUrl: executeUrl,
|
|
4689
|
+
requirement,
|
|
4690
|
+
method: "POST",
|
|
4691
|
+
data: bodyJson,
|
|
4692
|
+
onLine,
|
|
4693
|
+
onPaymentPending: options.onPaymentPending,
|
|
4694
|
+
timeoutMs: options.timeoutMs,
|
|
4695
|
+
signal: options.signal
|
|
4696
|
+
});
|
|
4697
|
+
try {
|
|
4698
|
+
const json = JSON.parse(result.body);
|
|
4699
|
+
return json.result ?? json;
|
|
4700
|
+
} catch {
|
|
4701
|
+
return { body: result.body, payment: result.payment, media: result.media };
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
3329
4704
|
/**
|
|
3330
4705
|
* Handle MPP (Machine Payments Protocol) payment flow
|
|
3331
4706
|
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
@@ -3666,7 +5041,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3666
5041
|
}
|
|
3667
5042
|
// --- Config & Wallet Management ---
|
|
3668
5043
|
loadConfig() {
|
|
3669
|
-
const configPath = (0,
|
|
5044
|
+
const configPath = (0, import_path3.join)(this.configDir, "config.json");
|
|
3670
5045
|
if ((0, import_fs4.existsSync)(configPath)) {
|
|
3671
5046
|
const content = (0, import_fs4.readFileSync)(configPath, "utf-8");
|
|
3672
5047
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
@@ -3675,14 +5050,14 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3675
5050
|
}
|
|
3676
5051
|
saveConfig() {
|
|
3677
5052
|
(0, import_fs4.mkdirSync)(this.configDir, { recursive: true });
|
|
3678
|
-
const configPath = (0,
|
|
5053
|
+
const configPath = (0, import_path3.join)(this.configDir, "config.json");
|
|
3679
5054
|
(0, import_fs4.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
|
|
3680
5055
|
}
|
|
3681
5056
|
/**
|
|
3682
5057
|
* Load spending data from disk
|
|
3683
5058
|
*/
|
|
3684
5059
|
loadSpending() {
|
|
3685
|
-
const spendingPath = (0,
|
|
5060
|
+
const spendingPath = (0, import_path3.join)(this.configDir, "spending.json");
|
|
3686
5061
|
if ((0, import_fs4.existsSync)(spendingPath)) {
|
|
3687
5062
|
try {
|
|
3688
5063
|
const data = JSON.parse((0, import_fs4.readFileSync)(spendingPath, "utf-8"));
|
|
@@ -3705,7 +5080,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3705
5080
|
*/
|
|
3706
5081
|
saveSpending() {
|
|
3707
5082
|
(0, import_fs4.mkdirSync)(this.configDir, { recursive: true });
|
|
3708
|
-
const spendingPath = (0,
|
|
5083
|
+
const spendingPath = (0, import_path3.join)(this.configDir, "spending.json");
|
|
3709
5084
|
const data = {
|
|
3710
5085
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
3711
5086
|
amount: this.todaySpending,
|
|
@@ -3714,7 +5089,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3714
5089
|
(0, import_fs4.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
|
|
3715
5090
|
}
|
|
3716
5091
|
loadWallet() {
|
|
3717
|
-
const walletPath = (0,
|
|
5092
|
+
const walletPath = (0, import_path3.join)(this.configDir, "wallet.json");
|
|
3718
5093
|
if ((0, import_fs4.existsSync)(walletPath)) {
|
|
3719
5094
|
if (process.platform !== "win32") {
|
|
3720
5095
|
try {
|
|
@@ -3744,7 +5119,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3744
5119
|
privateKey: wallet.privateKey,
|
|
3745
5120
|
createdAt: Date.now()
|
|
3746
5121
|
};
|
|
3747
|
-
const walletPath = (0,
|
|
5122
|
+
const walletPath = (0, import_path3.join)(configDir, "wallet.json");
|
|
3748
5123
|
(0, import_fs4.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
3749
5124
|
const config = {
|
|
3750
5125
|
chain: options.chain,
|
|
@@ -3753,7 +5128,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3753
5128
|
maxPerDay: options.maxPerDay
|
|
3754
5129
|
}
|
|
3755
5130
|
};
|
|
3756
|
-
const configPath = (0,
|
|
5131
|
+
const configPath = (0, import_path3.join)(configDir, "config.json");
|
|
3757
5132
|
(0, import_fs4.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
3758
5133
|
return { address: wallet.address, configDir };
|
|
3759
5134
|
}
|
|
@@ -3967,9 +5342,9 @@ var import_ethers4 = require("ethers");
|
|
|
3967
5342
|
// src/wallet/createWallet.ts
|
|
3968
5343
|
var import_ethers5 = require("ethers");
|
|
3969
5344
|
var import_fs5 = require("fs");
|
|
3970
|
-
var
|
|
5345
|
+
var import_path4 = require("path");
|
|
3971
5346
|
var import_crypto = require("crypto");
|
|
3972
|
-
var DEFAULT_STORAGE_DIR = (0,
|
|
5347
|
+
var DEFAULT_STORAGE_DIR = (0, import_path4.join)(process.env.HOME || "~", ".moltspay");
|
|
3973
5348
|
var DEFAULT_STORAGE_FILE = "wallet.json";
|
|
3974
5349
|
function encryptPrivateKey(privateKey, password) {
|
|
3975
5350
|
const salt = (0, import_crypto.randomBytes)(16);
|
|
@@ -3992,7 +5367,7 @@ function decryptPrivateKey(encrypted, password, iv, salt) {
|
|
|
3992
5367
|
return decrypted;
|
|
3993
5368
|
}
|
|
3994
5369
|
function createWallet(options = {}) {
|
|
3995
|
-
const storagePath = options.storagePath || (0,
|
|
5370
|
+
const storagePath = options.storagePath || (0, import_path4.join)(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3996
5371
|
if ((0, import_fs5.existsSync)(storagePath) && !options.overwrite) {
|
|
3997
5372
|
try {
|
|
3998
5373
|
const existing = JSON.parse((0, import_fs5.readFileSync)(storagePath, "utf8"));
|
|
@@ -4026,7 +5401,7 @@ function createWallet(options = {}) {
|
|
|
4026
5401
|
} else {
|
|
4027
5402
|
walletData.privateKey = wallet.privateKey;
|
|
4028
5403
|
}
|
|
4029
|
-
const dir = (0,
|
|
5404
|
+
const dir = (0, import_path4.dirname)(storagePath);
|
|
4030
5405
|
if (!(0, import_fs5.existsSync)(dir)) {
|
|
4031
5406
|
(0, import_fs5.mkdirSync)(dir, { recursive: true });
|
|
4032
5407
|
}
|
|
@@ -4045,7 +5420,7 @@ function createWallet(options = {}) {
|
|
|
4045
5420
|
}
|
|
4046
5421
|
}
|
|
4047
5422
|
function loadWallet(options = {}) {
|
|
4048
|
-
const storagePath = options.storagePath || (0,
|
|
5423
|
+
const storagePath = options.storagePath || (0, import_path4.join)(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
4049
5424
|
if (!(0, import_fs5.existsSync)(storagePath)) {
|
|
4050
5425
|
return { success: false, error: "Wallet not found. Run createWallet() first." };
|
|
4051
5426
|
}
|
|
@@ -4065,7 +5440,7 @@ function loadWallet(options = {}) {
|
|
|
4065
5440
|
}
|
|
4066
5441
|
}
|
|
4067
5442
|
function getWalletAddress(storagePath) {
|
|
4068
|
-
const path4 = storagePath || (0,
|
|
5443
|
+
const path4 = storagePath || (0, import_path4.join)(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
4069
5444
|
if (!(0, import_fs5.existsSync)(path4)) {
|
|
4070
5445
|
return null;
|
|
4071
5446
|
}
|
|
@@ -4077,7 +5452,7 @@ function getWalletAddress(storagePath) {
|
|
|
4077
5452
|
}
|
|
4078
5453
|
}
|
|
4079
5454
|
function walletExists(storagePath) {
|
|
4080
|
-
const path4 = storagePath || (0,
|
|
5455
|
+
const path4 = storagePath || (0, import_path4.join)(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
4081
5456
|
return (0, import_fs5.existsSync)(path4);
|
|
4082
5457
|
}
|
|
4083
5458
|
|
|
@@ -4443,8 +5818,10 @@ var CDPWallet = class {
|
|
|
4443
5818
|
FacilitatorRegistry,
|
|
4444
5819
|
MoltsPayClient,
|
|
4445
5820
|
MoltsPayServer,
|
|
5821
|
+
alipayLog,
|
|
4446
5822
|
createRegistry,
|
|
4447
5823
|
createWallet,
|
|
5824
|
+
getAlipayLogLevel,
|
|
4448
5825
|
getCDPWalletAddress,
|
|
4449
5826
|
getChain,
|
|
4450
5827
|
getChainById,
|
|
@@ -4456,6 +5833,10 @@ var CDPWallet = class {
|
|
|
4456
5833
|
listChains,
|
|
4457
5834
|
loadCDPWallet,
|
|
4458
5835
|
loadWallet,
|
|
5836
|
+
resetIntentCache,
|
|
5837
|
+
resetWalletCache,
|
|
5838
|
+
setAlipayLogLevel,
|
|
5839
|
+
setAlipayLogSink,
|
|
4459
5840
|
verifyPayment,
|
|
4460
5841
|
waitForTransaction,
|
|
4461
5842
|
walletExists
|