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.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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
|
2805
|
-
import { join as
|
|
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 ||
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
5297
|
+
import { join as join6, dirname as dirname2 } from "path";
|
|
3929
5298
|
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
|
|
3930
|
-
var DEFAULT_STORAGE_DIR =
|
|
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 ||
|
|
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 =
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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
|