moltspay 1.5.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 +143 -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 +1941 -204
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1938 -201
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +46 -0
- package/dist/client/index.d.ts +46 -0
- package/dist/client/index.js +1059 -118
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +1055 -116
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/web/index.d.mts +434 -0
- package/dist/client/web/index.mjs +1300 -0
- package/dist/client/web/index.mjs.map +1 -0
- package/dist/facilitators/index.d.mts +185 -6
- package/dist/facilitators/index.d.ts +185 -6
- package/dist/facilitators/index.js +497 -13
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +492 -13
- 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 +1865 -172
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1854 -167
- package/dist/index.mjs.map +1 -1
- package/dist/mcp/index.js +1062 -123
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/index.mjs +1059 -120
- 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 +137 -2
- package/dist/server/index.d.ts +137 -2
- package/dist/server/index.js +757 -30
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +757 -30
- 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 +15 -2
- package/schemas/moltspay.services.schema.json +100 -6
- package/scripts/postinstall.js +91 -0
package/dist/server/index.js
CHANGED
|
@@ -263,6 +263,9 @@ var CDPFacilitator = class extends BaseFacilitator {
|
|
|
263
263
|
}
|
|
264
264
|
};
|
|
265
265
|
|
|
266
|
+
// src/facilitators/tempo.ts
|
|
267
|
+
var import_ethers = require("ethers");
|
|
268
|
+
|
|
266
269
|
// src/chains/index.ts
|
|
267
270
|
var CHAINS = {
|
|
268
271
|
// ============ Mainnet ============
|
|
@@ -429,18 +432,45 @@ var CHAINS = {
|
|
|
429
432
|
requiresApproval: true
|
|
430
433
|
}
|
|
431
434
|
};
|
|
435
|
+
var ALIPAY_CHAIN_ID = "alipay";
|
|
436
|
+
function isAlipayChainId(id) {
|
|
437
|
+
return id === ALIPAY_CHAIN_ID;
|
|
438
|
+
}
|
|
432
439
|
|
|
433
440
|
// src/facilitators/tempo.ts
|
|
434
441
|
var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
442
|
+
var TIP20_PERMIT_ABI = [
|
|
443
|
+
"function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
|
|
444
|
+
"function transferFrom(address from, address to, uint256 value) returns (bool)"
|
|
445
|
+
];
|
|
435
446
|
var TempoFacilitator = class extends BaseFacilitator {
|
|
436
447
|
name = "tempo";
|
|
437
448
|
displayName = "Tempo Testnet";
|
|
438
449
|
supportedNetworks = ["eip155:42431"];
|
|
439
450
|
// Tempo Moderato
|
|
440
451
|
rpcUrl;
|
|
452
|
+
settlerWallet = null;
|
|
441
453
|
constructor() {
|
|
442
454
|
super();
|
|
443
455
|
this.rpcUrl = CHAINS.tempo_moderato.rpc;
|
|
456
|
+
const settlerKey = process.env.TEMPO_SETTLER_KEY;
|
|
457
|
+
if (settlerKey) {
|
|
458
|
+
try {
|
|
459
|
+
const provider = new import_ethers.ethers.JsonRpcProvider(this.rpcUrl);
|
|
460
|
+
this.settlerWallet = new import_ethers.ethers.Wallet(settlerKey, provider);
|
|
461
|
+
} catch (err) {
|
|
462
|
+
console.warn("[TempoFacilitator] Invalid TEMPO_SETTLER_KEY, permit settlement disabled:", err);
|
|
463
|
+
this.settlerWallet = null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Settler EOA address advertised to clients via `X-Payment-Required.extra.tempoSpender`.
|
|
469
|
+
* Web Client uses this as the `spender` field in the signed EIP-2612 Permit.
|
|
470
|
+
* Returns null if no TEMPO_SETTLER_KEY is configured — permit settlement unavailable.
|
|
471
|
+
*/
|
|
472
|
+
getSpenderAddress() {
|
|
473
|
+
return this.settlerWallet?.address ?? null;
|
|
444
474
|
}
|
|
445
475
|
async healthCheck() {
|
|
446
476
|
const start = Date.now();
|
|
@@ -466,6 +496,44 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
466
496
|
}
|
|
467
497
|
}
|
|
468
498
|
async verify(paymentPayload, requirements) {
|
|
499
|
+
const inner = paymentPayload.payload;
|
|
500
|
+
if (inner && "permit" in inner && inner.permit) {
|
|
501
|
+
return this.verifyPermit(inner, requirements);
|
|
502
|
+
}
|
|
503
|
+
return this.verifyTxHash(paymentPayload, requirements);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Structural validation of an EIP-2612 permit payload. Does NOT submit
|
|
507
|
+
* anything on-chain — actual submission happens in settlePermit().
|
|
508
|
+
*/
|
|
509
|
+
async verifyPermit(payload, requirements) {
|
|
510
|
+
if (!this.settlerWallet) {
|
|
511
|
+
return { valid: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
|
|
512
|
+
}
|
|
513
|
+
const p = payload.permit;
|
|
514
|
+
if (!p || !p.owner || !p.spender || !p.value || !p.deadline) {
|
|
515
|
+
return { valid: false, error: "Invalid permit payload: missing fields" };
|
|
516
|
+
}
|
|
517
|
+
if (p.spender.toLowerCase() !== this.settlerWallet.address.toLowerCase()) {
|
|
518
|
+
return {
|
|
519
|
+
valid: false,
|
|
520
|
+
error: `Permit spender ${p.spender} does not match configured settler ${this.settlerWallet.address}`
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
const deadline = BigInt(p.deadline);
|
|
524
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
525
|
+
if (deadline <= now) {
|
|
526
|
+
return { valid: false, error: "Permit deadline has expired" };
|
|
527
|
+
}
|
|
528
|
+
if (BigInt(p.value) < BigInt(requirements.amount || "0")) {
|
|
529
|
+
return {
|
|
530
|
+
valid: false,
|
|
531
|
+
error: `Permit value ${p.value} is less than required ${requirements.amount}`
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
return { valid: true, details: { scheme: "permit", owner: p.owner } };
|
|
535
|
+
}
|
|
536
|
+
async verifyTxHash(paymentPayload, requirements) {
|
|
469
537
|
try {
|
|
470
538
|
const tempoPayload = paymentPayload.payload;
|
|
471
539
|
if (!tempoPayload?.txHash) {
|
|
@@ -523,7 +591,11 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
523
591
|
}
|
|
524
592
|
}
|
|
525
593
|
async settle(paymentPayload, requirements) {
|
|
526
|
-
const
|
|
594
|
+
const inner = paymentPayload.payload;
|
|
595
|
+
if (inner && "permit" in inner && inner.permit) {
|
|
596
|
+
return this.settlePermit(inner, requirements);
|
|
597
|
+
}
|
|
598
|
+
const verifyResult = await this.verifyTxHash(paymentPayload, requirements);
|
|
527
599
|
if (!verifyResult.valid) {
|
|
528
600
|
return { success: false, error: verifyResult.error };
|
|
529
601
|
}
|
|
@@ -534,6 +606,52 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
534
606
|
status: "settled"
|
|
535
607
|
};
|
|
536
608
|
}
|
|
609
|
+
/**
|
|
610
|
+
* EIP-2612 permit settlement path. Submits two transactions on Tempo:
|
|
611
|
+
* 1. pathUSD.permit(owner, spender=settler, value, deadline, v, r, s)
|
|
612
|
+
* 2. pathUSD.transferFrom(owner, payTo, value)
|
|
613
|
+
*
|
|
614
|
+
* The settler EOA pays Tempo gas (via the TIP-20 `feeToken` mechanism — no
|
|
615
|
+
* native tTEMPO required; any held TIP-20 token balance covers fees).
|
|
616
|
+
*/
|
|
617
|
+
async settlePermit(payload, requirements) {
|
|
618
|
+
if (!this.settlerWallet) {
|
|
619
|
+
return { success: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
|
|
620
|
+
}
|
|
621
|
+
if (!requirements.asset || !requirements.payTo) {
|
|
622
|
+
return { success: false, error: "Missing asset or payTo in requirements" };
|
|
623
|
+
}
|
|
624
|
+
const verifyResult = await this.verifyPermit(payload, requirements);
|
|
625
|
+
if (!verifyResult.valid) {
|
|
626
|
+
return { success: false, error: verifyResult.error };
|
|
627
|
+
}
|
|
628
|
+
const token = new import_ethers.ethers.Contract(requirements.asset, TIP20_PERMIT_ABI, this.settlerWallet);
|
|
629
|
+
const p = payload.permit;
|
|
630
|
+
try {
|
|
631
|
+
const permitTx = await token.permit(
|
|
632
|
+
p.owner,
|
|
633
|
+
p.spender,
|
|
634
|
+
p.value,
|
|
635
|
+
p.deadline,
|
|
636
|
+
p.v,
|
|
637
|
+
p.r,
|
|
638
|
+
p.s
|
|
639
|
+
);
|
|
640
|
+
await permitTx.wait();
|
|
641
|
+
const transferTx = await token.transferFrom(p.owner, requirements.payTo, p.value);
|
|
642
|
+
await transferTx.wait();
|
|
643
|
+
return {
|
|
644
|
+
success: true,
|
|
645
|
+
transaction: transferTx.hash,
|
|
646
|
+
status: "settled"
|
|
647
|
+
};
|
|
648
|
+
} catch (err) {
|
|
649
|
+
return {
|
|
650
|
+
success: false,
|
|
651
|
+
error: `Tempo permit settlement failed: ${err.message}`
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
}
|
|
537
655
|
async getTransactionReceipt(txHash) {
|
|
538
656
|
const response = await fetch(this.rpcUrl, {
|
|
539
657
|
method: "POST",
|
|
@@ -776,12 +894,12 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
776
894
|
return this.spenderAddress;
|
|
777
895
|
}
|
|
778
896
|
async getServerAddress() {
|
|
779
|
-
const { ethers } = await import("ethers");
|
|
780
|
-
const wallet = new
|
|
897
|
+
const { ethers: ethers2 } = await import("ethers");
|
|
898
|
+
const wallet = new ethers2.Wallet(this.serverPrivateKey);
|
|
781
899
|
return wallet.address;
|
|
782
900
|
}
|
|
783
901
|
async recoverIntentSigner(intent, chainId) {
|
|
784
|
-
const { ethers } = await import("ethers");
|
|
902
|
+
const { ethers: ethers2 } = await import("ethers");
|
|
785
903
|
const domain = {
|
|
786
904
|
...EIP712_DOMAIN,
|
|
787
905
|
chainId
|
|
@@ -795,7 +913,7 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
795
913
|
nonce: intent.nonce,
|
|
796
914
|
deadline: intent.deadline
|
|
797
915
|
};
|
|
798
|
-
const recoveredAddress =
|
|
916
|
+
const recoveredAddress = ethers2.verifyTypedData(
|
|
799
917
|
domain,
|
|
800
918
|
INTENT_TYPES,
|
|
801
919
|
message,
|
|
@@ -839,10 +957,10 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
839
957
|
return result.result || "0x0";
|
|
840
958
|
}
|
|
841
959
|
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
842
|
-
const { ethers } = await import("ethers");
|
|
843
|
-
const provider = new
|
|
844
|
-
const wallet = new
|
|
845
|
-
const tokenContract = new
|
|
960
|
+
const { ethers: ethers2 } = await import("ethers");
|
|
961
|
+
const provider = new ethers2.JsonRpcProvider(rpcUrl);
|
|
962
|
+
const wallet = new ethers2.Wallet(this.serverPrivateKey, provider);
|
|
963
|
+
const tokenContract = new ethers2.Contract(token, [
|
|
846
964
|
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
847
965
|
], wallet);
|
|
848
966
|
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
@@ -1069,6 +1187,374 @@ var SolanaFacilitator = class extends BaseFacilitator {
|
|
|
1069
1187
|
}
|
|
1070
1188
|
};
|
|
1071
1189
|
|
|
1190
|
+
// src/facilitators/alipay.ts
|
|
1191
|
+
var import_node_crypto2 = __toESM(require("crypto"));
|
|
1192
|
+
|
|
1193
|
+
// src/facilitators/alipay/rsa2.ts
|
|
1194
|
+
var import_node_crypto = __toESM(require("crypto"));
|
|
1195
|
+
function rsa2Sign(data, privateKeyPem) {
|
|
1196
|
+
const signer = import_node_crypto.default.createSign("RSA-SHA256");
|
|
1197
|
+
signer.update(data, "utf-8");
|
|
1198
|
+
signer.end();
|
|
1199
|
+
return signer.sign(privateKeyPem, "base64");
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// src/facilitators/alipay/encoding.ts
|
|
1203
|
+
function base64url(input) {
|
|
1204
|
+
return Buffer.from(input, "utf-8").toString("base64url");
|
|
1205
|
+
}
|
|
1206
|
+
function decodeBase64UrlWithPadFix(input) {
|
|
1207
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
1208
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
1209
|
+
return Buffer.from(padded, "base64").toString("utf-8");
|
|
1210
|
+
}
|
|
1211
|
+
function toPem(key, kind) {
|
|
1212
|
+
const trimmed = key.trim();
|
|
1213
|
+
if (trimmed.includes("-----BEGIN")) return trimmed;
|
|
1214
|
+
const label = kind === "PRIVATE" ? "PRIVATE KEY" : "PUBLIC KEY";
|
|
1215
|
+
const body = trimmed.replace(/\s+/g, "").match(/.{1,64}/g)?.join("\n") ?? "";
|
|
1216
|
+
return `-----BEGIN ${label}-----
|
|
1217
|
+
${body}
|
|
1218
|
+
-----END ${label}-----
|
|
1219
|
+
`;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// src/facilitators/alipay/openapi.ts
|
|
1223
|
+
function formatAlipayTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
1224
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1225
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
1226
|
+
}
|
|
1227
|
+
function buildSigningString(params) {
|
|
1228
|
+
return Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join("&");
|
|
1229
|
+
}
|
|
1230
|
+
function responseWrapperKey(method) {
|
|
1231
|
+
return `${method.replace(/\./g, "_")}_response`;
|
|
1232
|
+
}
|
|
1233
|
+
async function alipayOpenApiCall(method, bizContent, config) {
|
|
1234
|
+
const publicParams = {
|
|
1235
|
+
app_id: config.app_id,
|
|
1236
|
+
method,
|
|
1237
|
+
format: "JSON",
|
|
1238
|
+
charset: "utf-8",
|
|
1239
|
+
sign_type: config.sign_type ?? "RSA2",
|
|
1240
|
+
timestamp: formatAlipayTimestamp(),
|
|
1241
|
+
version: "1.0",
|
|
1242
|
+
biz_content: JSON.stringify(bizContent)
|
|
1243
|
+
};
|
|
1244
|
+
const signingString = buildSigningString(publicParams);
|
|
1245
|
+
const sign = rsa2Sign(signingString, config.private_key_pem);
|
|
1246
|
+
const body = new URLSearchParams({ ...publicParams, sign }).toString();
|
|
1247
|
+
const response = await fetch(config.gateway_url, {
|
|
1248
|
+
method: "POST",
|
|
1249
|
+
headers: {
|
|
1250
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
|
|
1251
|
+
},
|
|
1252
|
+
body
|
|
1253
|
+
});
|
|
1254
|
+
if (!response.ok) {
|
|
1255
|
+
const text = await response.text().catch(() => "<unreadable>");
|
|
1256
|
+
throw new Error(
|
|
1257
|
+
`Alipay Open API HTTP ${response.status} for ${method}: ${text.slice(0, 500)}`
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
const json = await response.json();
|
|
1261
|
+
const wrapperKey = responseWrapperKey(method);
|
|
1262
|
+
const business = json[wrapperKey];
|
|
1263
|
+
if (business === void 0 || business === null || typeof business !== "object") {
|
|
1264
|
+
throw new Error(
|
|
1265
|
+
`Alipay Open API response missing "${wrapperKey}" wrapper for ${method}: ${JSON.stringify(json).slice(0, 500)}`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
return business;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/facilitators/alipay.ts
|
|
1272
|
+
var ALIPAY_NETWORK = "alipay";
|
|
1273
|
+
var ALIPAY_SCHEME = "alipay-aipay";
|
|
1274
|
+
var ALIPAY_GATEWAY_PROD = "https://openapi.alipay.com/gateway.do";
|
|
1275
|
+
var ALIPAY_AMOUNT_REGEX = /^\d+(\.\d{1,2})?$/;
|
|
1276
|
+
var ALIPAY_PAY_BEFORE_MS = 30 * 60 * 1e3;
|
|
1277
|
+
var ALIPAY_SIGNING_FIELDS = [
|
|
1278
|
+
"amount",
|
|
1279
|
+
"currency",
|
|
1280
|
+
"goods_name",
|
|
1281
|
+
"out_trade_no",
|
|
1282
|
+
"pay_before",
|
|
1283
|
+
"resource_id",
|
|
1284
|
+
"seller_id",
|
|
1285
|
+
"service_id"
|
|
1286
|
+
];
|
|
1287
|
+
var AlipayFacilitator = class extends BaseFacilitator {
|
|
1288
|
+
name = "alipay";
|
|
1289
|
+
displayName = "Alipay AI \u6536";
|
|
1290
|
+
supportedNetworks = [ALIPAY_NETWORK];
|
|
1291
|
+
config;
|
|
1292
|
+
constructor(config) {
|
|
1293
|
+
super();
|
|
1294
|
+
this.config = {
|
|
1295
|
+
gateway_url: ALIPAY_GATEWAY_PROD,
|
|
1296
|
+
sign_type: "RSA2",
|
|
1297
|
+
...config
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Build the 402 challenge for a service: signs the 8-field payload with
|
|
1302
|
+
* RSA2, packages the nested `{protocol, method}` JSON as Base64URL for
|
|
1303
|
+
* `Payment-Needed`, and emits the parallel x402 `accepts[]` entry.
|
|
1304
|
+
*/
|
|
1305
|
+
async createPaymentRequirements(opts) {
|
|
1306
|
+
if (!ALIPAY_AMOUNT_REGEX.test(opts.priceCny)) {
|
|
1307
|
+
throw new Error(
|
|
1308
|
+
`AlipayFacilitator.createPaymentRequirements: priceCny "${opts.priceCny}" does not match /^\\d+(\\.\\d{1,2})?$/ (unit is \u5143, not \u5206; e.g. "1.00" not "100")`
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
const now = /* @__PURE__ */ new Date();
|
|
1312
|
+
const outTradeNo = opts.outTradeNo ?? generateOutTradeNo();
|
|
1313
|
+
const payBefore = formatPayBefore(now);
|
|
1314
|
+
const signedFields = {
|
|
1315
|
+
amount: opts.priceCny,
|
|
1316
|
+
currency: "CNY",
|
|
1317
|
+
goods_name: opts.goodsName,
|
|
1318
|
+
out_trade_no: outTradeNo,
|
|
1319
|
+
pay_before: payBefore,
|
|
1320
|
+
resource_id: opts.resourceId,
|
|
1321
|
+
seller_id: this.config.seller_id,
|
|
1322
|
+
service_id: opts.serviceId
|
|
1323
|
+
};
|
|
1324
|
+
const signingString = ALIPAY_SIGNING_FIELDS.map((k) => `${k}=${signedFields[k]}`).join("&");
|
|
1325
|
+
const seller_signature = rsa2Sign(signingString, this.config.private_key_pem);
|
|
1326
|
+
const challenge = {
|
|
1327
|
+
protocol: {
|
|
1328
|
+
out_trade_no: outTradeNo,
|
|
1329
|
+
amount: signedFields.amount,
|
|
1330
|
+
currency: signedFields.currency,
|
|
1331
|
+
resource_id: signedFields.resource_id,
|
|
1332
|
+
pay_before: payBefore,
|
|
1333
|
+
seller_signature,
|
|
1334
|
+
seller_sign_type: this.config.sign_type ?? "RSA2",
|
|
1335
|
+
seller_unique_id: this.config.seller_id
|
|
1336
|
+
},
|
|
1337
|
+
method: {
|
|
1338
|
+
seller_name: this.config.seller_name,
|
|
1339
|
+
seller_id: this.config.seller_id,
|
|
1340
|
+
seller_app_id: this.config.app_id,
|
|
1341
|
+
goods_name: signedFields.goods_name,
|
|
1342
|
+
seller_unique_id_key: "seller_id",
|
|
1343
|
+
service_id: signedFields.service_id
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
const paymentNeededHeader = base64url(JSON.stringify(challenge));
|
|
1347
|
+
const x402Accepts = {
|
|
1348
|
+
scheme: ALIPAY_SCHEME,
|
|
1349
|
+
network: ALIPAY_NETWORK,
|
|
1350
|
+
asset: "CNY",
|
|
1351
|
+
amount: opts.priceCny,
|
|
1352
|
+
payTo: this.config.seller_id,
|
|
1353
|
+
maxTimeoutSeconds: ALIPAY_PAY_BEFORE_MS / 1e3,
|
|
1354
|
+
extra: {
|
|
1355
|
+
payment_needed_header: paymentNeededHeader,
|
|
1356
|
+
out_trade_no: outTradeNo,
|
|
1357
|
+
pay_before: payBefore,
|
|
1358
|
+
service_id: signedFields.service_id
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
return { x402Accepts, paymentNeededHeader };
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Verify a `Payment-Proof` by calling
|
|
1365
|
+
* `alipay.aipay.agent.payment.verify` against the Alipay Open API.
|
|
1366
|
+
*
|
|
1367
|
+
* The proof is conveyed via `paymentPayload.payload`, which may be:
|
|
1368
|
+
* - a raw Base64URL string (the `Payment-Proof` header value), or
|
|
1369
|
+
* - an object `{ paymentProof: string }` / `{ proofHeader: string }`.
|
|
1370
|
+
*
|
|
1371
|
+
* All failure modes (malformed payload, network errors, Alipay
|
|
1372
|
+
* `code != 10000`) return `{ valid: false, error }`. No exception
|
|
1373
|
+
* escapes, regardless of client-supplied input.
|
|
1374
|
+
*/
|
|
1375
|
+
async verify(paymentPayload, _requirements) {
|
|
1376
|
+
try {
|
|
1377
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
1378
|
+
const decoded = decodeProof(proofHeader);
|
|
1379
|
+
const response = await alipayOpenApiCall(
|
|
1380
|
+
"alipay.aipay.agent.payment.verify",
|
|
1381
|
+
{
|
|
1382
|
+
payment_proof: decoded.protocol.payment_proof,
|
|
1383
|
+
trade_no: decoded.protocol.trade_no,
|
|
1384
|
+
client_session: decoded.method.client_session
|
|
1385
|
+
},
|
|
1386
|
+
this.getOpenApiConfig()
|
|
1387
|
+
);
|
|
1388
|
+
if (response.code !== "10000") {
|
|
1389
|
+
return {
|
|
1390
|
+
valid: false,
|
|
1391
|
+
error: `alipay verify ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
1392
|
+
details: {
|
|
1393
|
+
code: response.code,
|
|
1394
|
+
sub_code: response.sub_code,
|
|
1395
|
+
sub_msg: response.sub_msg
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
return {
|
|
1400
|
+
valid: true,
|
|
1401
|
+
details: {
|
|
1402
|
+
trade_no: response.trade_no ?? decoded.protocol.trade_no,
|
|
1403
|
+
amount: response.amount,
|
|
1404
|
+
out_trade_no: response.out_trade_no,
|
|
1405
|
+
resource_id: response.resource_id,
|
|
1406
|
+
active: response.active
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
} catch (e) {
|
|
1410
|
+
return {
|
|
1411
|
+
valid: false,
|
|
1412
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Settle by calling `alipay.aipay.agent.fulfillment.confirm` after the
|
|
1418
|
+
* service resource has been returned to the buyer.
|
|
1419
|
+
*
|
|
1420
|
+
* Per the design (see ALIPAY-INTEGRATION-DESIGN.md §5.1, risk row
|
|
1421
|
+
* "履约确认失败"), this is **fire-and-forget**: the caller (registry /
|
|
1422
|
+
* server) is expected to log fulfillment failures but NOT roll back
|
|
1423
|
+
* the already-delivered resource.
|
|
1424
|
+
*
|
|
1425
|
+
* Re-decodes `paymentPayload.payload` to extract `trade_no` (verify
|
|
1426
|
+
* does the same; the redundant Base64URL decode is negligible).
|
|
1427
|
+
*/
|
|
1428
|
+
async settle(paymentPayload, _requirements) {
|
|
1429
|
+
try {
|
|
1430
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
1431
|
+
const decoded = decodeProof(proofHeader);
|
|
1432
|
+
const tradeNo = decoded.protocol.trade_no;
|
|
1433
|
+
const response = await alipayOpenApiCall(
|
|
1434
|
+
"alipay.aipay.agent.fulfillment.confirm",
|
|
1435
|
+
{ trade_no: tradeNo },
|
|
1436
|
+
this.getOpenApiConfig()
|
|
1437
|
+
);
|
|
1438
|
+
if (response.code !== "10000") {
|
|
1439
|
+
return {
|
|
1440
|
+
success: false,
|
|
1441
|
+
transaction: tradeNo,
|
|
1442
|
+
error: `alipay fulfillment ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
1443
|
+
status: "fulfillment_failed"
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
return {
|
|
1447
|
+
success: true,
|
|
1448
|
+
transaction: tradeNo,
|
|
1449
|
+
status: "fulfilled"
|
|
1450
|
+
};
|
|
1451
|
+
} catch (e) {
|
|
1452
|
+
return {
|
|
1453
|
+
success: false,
|
|
1454
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Validate that the configured RSA keys parse and that the Open API
|
|
1460
|
+
* gateway is reachable. Does NOT make a real business API call (would
|
|
1461
|
+
* burn quota and could appear as a legitimate verify attempt in logs).
|
|
1462
|
+
*/
|
|
1463
|
+
async healthCheck() {
|
|
1464
|
+
const start = Date.now();
|
|
1465
|
+
try {
|
|
1466
|
+
import_node_crypto2.default.createPrivateKey(this.config.private_key_pem);
|
|
1467
|
+
} catch (e) {
|
|
1468
|
+
return {
|
|
1469
|
+
healthy: false,
|
|
1470
|
+
error: `merchant private_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
try {
|
|
1474
|
+
import_node_crypto2.default.createPublicKey(this.config.alipay_public_key_pem);
|
|
1475
|
+
} catch (e) {
|
|
1476
|
+
return {
|
|
1477
|
+
healthy: false,
|
|
1478
|
+
error: `alipay_public_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
const gatewayUrl = this.config.gateway_url ?? ALIPAY_GATEWAY_PROD;
|
|
1482
|
+
const controller = new AbortController();
|
|
1483
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
1484
|
+
const response = await fetch(gatewayUrl, {
|
|
1485
|
+
method: "HEAD",
|
|
1486
|
+
signal: controller.signal
|
|
1487
|
+
}).catch(() => null);
|
|
1488
|
+
clearTimeout(timeout);
|
|
1489
|
+
const latencyMs = Date.now() - start;
|
|
1490
|
+
if (!response) {
|
|
1491
|
+
return { healthy: false, error: `gateway unreachable: ${gatewayUrl}`, latencyMs };
|
|
1492
|
+
}
|
|
1493
|
+
return { healthy: true, latencyMs };
|
|
1494
|
+
}
|
|
1495
|
+
/** Bundle the facilitator config into the shape openapi.ts wants. */
|
|
1496
|
+
getOpenApiConfig() {
|
|
1497
|
+
return {
|
|
1498
|
+
gateway_url: this.config.gateway_url ?? ALIPAY_GATEWAY_PROD,
|
|
1499
|
+
app_id: this.config.app_id,
|
|
1500
|
+
private_key_pem: this.config.private_key_pem,
|
|
1501
|
+
alipay_public_key_pem: this.config.alipay_public_key_pem,
|
|
1502
|
+
sign_type: this.config.sign_type
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
function generateOutTradeNo() {
|
|
1507
|
+
return "VID" + import_node_crypto2.default.randomBytes(22).toString("base64url").slice(0, 29);
|
|
1508
|
+
}
|
|
1509
|
+
function formatPayBefore(now) {
|
|
1510
|
+
const expiry = new Date(now.getTime() + ALIPAY_PAY_BEFORE_MS);
|
|
1511
|
+
return expiry.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1512
|
+
}
|
|
1513
|
+
function extractProofHeader(payload) {
|
|
1514
|
+
if (typeof payload === "string") {
|
|
1515
|
+
if (payload.length === 0) {
|
|
1516
|
+
throw new Error("alipay Payment-Proof is empty");
|
|
1517
|
+
}
|
|
1518
|
+
return payload;
|
|
1519
|
+
}
|
|
1520
|
+
if (payload !== null && typeof payload === "object") {
|
|
1521
|
+
const obj = payload;
|
|
1522
|
+
const candidate = obj.paymentProof ?? obj.proofHeader;
|
|
1523
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
1524
|
+
return candidate;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
throw new Error(
|
|
1528
|
+
"alipay payment payload must be a Base64URL string or {paymentProof: string} / {proofHeader: string}"
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
function decodeProof(proofHeader) {
|
|
1532
|
+
let parsed;
|
|
1533
|
+
try {
|
|
1534
|
+
parsed = JSON.parse(decodeBase64UrlWithPadFix(proofHeader));
|
|
1535
|
+
} catch (e) {
|
|
1536
|
+
throw new Error(
|
|
1537
|
+
`failed to decode Payment-Proof: ${e instanceof Error ? e.message : String(e)}`
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1541
|
+
throw new Error("decoded Payment-Proof is not an object");
|
|
1542
|
+
}
|
|
1543
|
+
const obj = parsed;
|
|
1544
|
+
const protocol = obj.protocol;
|
|
1545
|
+
const method = obj.method;
|
|
1546
|
+
if (!protocol || typeof protocol.payment_proof !== "string") {
|
|
1547
|
+
throw new Error("decoded Payment-Proof missing protocol.payment_proof");
|
|
1548
|
+
}
|
|
1549
|
+
if (typeof protocol.trade_no !== "string") {
|
|
1550
|
+
throw new Error("decoded Payment-Proof missing protocol.trade_no");
|
|
1551
|
+
}
|
|
1552
|
+
if (!method || typeof method.client_session !== "string") {
|
|
1553
|
+
throw new Error("decoded Payment-Proof missing method.client_session");
|
|
1554
|
+
}
|
|
1555
|
+
return parsed;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1072
1558
|
// src/facilitators/registry.ts
|
|
1073
1559
|
var import_web33 = require("@solana/web3.js");
|
|
1074
1560
|
var import_bs58 = __toESM(require("bs58"));
|
|
@@ -1093,6 +1579,7 @@ var FacilitatorRegistry = class {
|
|
|
1093
1579
|
}
|
|
1094
1580
|
return new SolanaFacilitator({ feePayerKeypair });
|
|
1095
1581
|
});
|
|
1582
|
+
this.registerFactory("alipay", (config) => new AlipayFacilitator(config));
|
|
1096
1583
|
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
1097
1584
|
}
|
|
1098
1585
|
/**
|
|
@@ -1317,6 +1804,11 @@ var PAYMENT_RESPONSE_HEADER = "x-payment-response";
|
|
|
1317
1804
|
var MPP_AUTH_HEADER = "authorization";
|
|
1318
1805
|
var MPP_WWW_AUTH_HEADER = "www-authenticate";
|
|
1319
1806
|
var MPP_RECEIPT_HEADER = "payment-receipt";
|
|
1807
|
+
var ALIPAY_PAYMENT_NEEDED_HEADER = "payment-needed";
|
|
1808
|
+
var ALIPAY_PAYMENT_PROOF_HEADER = "payment-proof";
|
|
1809
|
+
function headerSafe(value) {
|
|
1810
|
+
return String(value ?? "").replace(/[^\x20-\x7E]/g, (ch) => encodeURIComponent(ch)).replace(/"/g, "%22");
|
|
1811
|
+
}
|
|
1320
1812
|
var TOKEN_ADDRESSES = {
|
|
1321
1813
|
"eip155:8453": {
|
|
1322
1814
|
USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
@@ -1389,9 +1881,13 @@ var TOKEN_DOMAINS = {
|
|
|
1389
1881
|
USDT: { name: "(PoS) Tether USD", version: "2" }
|
|
1390
1882
|
},
|
|
1391
1883
|
// Tempo Moderato testnet - TIP-20 stablecoins
|
|
1884
|
+
// Domain names verified against on-chain DOMAIN_SEPARATOR values on 2026-04-21.
|
|
1885
|
+
// See docs/TEMPO-WEB-SUPPORT.md Section 2 and test/server/tempo-domain.test.ts.
|
|
1886
|
+
// All 4 Tempo TIP-20 tokens (pathUSD / AlphaUSD / BetaUSD / ThetaUSD) use
|
|
1887
|
+
// the token symbol with first letter capitalized + version "1".
|
|
1392
1888
|
"eip155:42431": {
|
|
1393
|
-
USDC: { name: "
|
|
1394
|
-
USDT: { name: "
|
|
1889
|
+
USDC: { name: "PathUSD", version: "1" },
|
|
1890
|
+
USDT: { name: "AlphaUSD", version: "1" }
|
|
1395
1891
|
},
|
|
1396
1892
|
// BNB Smart Chain mainnet
|
|
1397
1893
|
"eip155:56": {
|
|
@@ -1448,6 +1944,8 @@ var MoltsPayServer = class {
|
|
|
1448
1944
|
registry;
|
|
1449
1945
|
networkId;
|
|
1450
1946
|
useMainnet;
|
|
1947
|
+
/** Alipay AI 收 facilitator instance, set when `provider.alipay` is configured (2.0.0). */
|
|
1948
|
+
alipayFacilitator = null;
|
|
1451
1949
|
constructor(servicesPath, options = {}) {
|
|
1452
1950
|
loadEnvFile2();
|
|
1453
1951
|
const content = (0, import_fs2.readFileSync)(servicesPath, "utf-8");
|
|
@@ -1469,7 +1967,38 @@ var MoltsPayServer = class {
|
|
|
1469
1967
|
cdp: { useMainnet: this.useMainnet }
|
|
1470
1968
|
}
|
|
1471
1969
|
};
|
|
1970
|
+
const providerAlipay = this.manifest.provider.alipay;
|
|
1971
|
+
if (providerAlipay) {
|
|
1972
|
+
try {
|
|
1973
|
+
const baseDir = path2.dirname(servicesPath);
|
|
1974
|
+
const resolvePem = (p, kind) => toPem((0, import_fs2.readFileSync)(path2.isAbsolute(p) ? p : path2.resolve(baseDir, p), "utf-8"), kind);
|
|
1975
|
+
const alipayFacilitatorConfig = {
|
|
1976
|
+
seller_id: providerAlipay.seller_id,
|
|
1977
|
+
app_id: providerAlipay.app_id,
|
|
1978
|
+
seller_name: providerAlipay.seller_name,
|
|
1979
|
+
service_id_default: providerAlipay.service_id_default,
|
|
1980
|
+
private_key_pem: resolvePem(providerAlipay.private_key_path, "PRIVATE"),
|
|
1981
|
+
alipay_public_key_pem: resolvePem(providerAlipay.alipay_public_key_path, "PUBLIC"),
|
|
1982
|
+
gateway_url: providerAlipay.gateway_url,
|
|
1983
|
+
sign_type: providerAlipay.sign_type
|
|
1984
|
+
};
|
|
1985
|
+
facilitatorConfig.config = {
|
|
1986
|
+
...facilitatorConfig.config,
|
|
1987
|
+
alipay: alipayFacilitatorConfig
|
|
1988
|
+
};
|
|
1989
|
+
facilitatorConfig.fallback = facilitatorConfig.fallback || [];
|
|
1990
|
+
if (facilitatorConfig.primary !== "alipay" && !facilitatorConfig.fallback.includes("alipay")) {
|
|
1991
|
+
facilitatorConfig.fallback.push("alipay");
|
|
1992
|
+
}
|
|
1993
|
+
} catch (err) {
|
|
1994
|
+
throw new Error(`[MoltsPay] Alipay rail configured but key load failed: ${err.message}`);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1472
1997
|
this.registry = new FacilitatorRegistry(facilitatorConfig);
|
|
1998
|
+
if (providerAlipay) {
|
|
1999
|
+
this.alipayFacilitator = this.registry.get("alipay");
|
|
2000
|
+
console.log(`[MoltsPay] Alipay AI \u6536 rail enabled (seller ${providerAlipay.seller_id})`);
|
|
2001
|
+
}
|
|
1473
2002
|
const primaryFacilitator = this.registry.get(facilitatorConfig.primary);
|
|
1474
2003
|
console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
|
|
1475
2004
|
console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
|
|
@@ -1559,14 +2088,63 @@ var MoltsPayServer = class {
|
|
|
1559
2088
|
console.log(` GET /health - Health check (incl. facilitators)`);
|
|
1560
2089
|
});
|
|
1561
2090
|
}
|
|
2091
|
+
/**
|
|
2092
|
+
* Apply CORS response headers according to the `cors` option.
|
|
2093
|
+
*
|
|
2094
|
+
* Default (`cors` unset or `true`): `Access-Control-Allow-Origin: *`. Matches 1.5.x behavior
|
|
2095
|
+
* and works for every browser client whose origin does not need to send cookies.
|
|
2096
|
+
*
|
|
2097
|
+
* `cors: false`: emit no CORS headers. Same-origin only.
|
|
2098
|
+
* `cors: string[]`: origin allowlist — echo the origin back iff it matches.
|
|
2099
|
+
* `cors: CorsOptions`: full control (allowlist + credentials + maxAge).
|
|
2100
|
+
*
|
|
2101
|
+
* The required-for-Web response headers are always exposed when CORS is active:
|
|
2102
|
+
* `X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt`.
|
|
2103
|
+
*/
|
|
2104
|
+
applyCorsHeaders(req, res) {
|
|
2105
|
+
const cors = this.options.cors;
|
|
2106
|
+
if (cors === false) {
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
const requestOrigin = req.headers.origin ?? "*";
|
|
2110
|
+
if (cors === void 0 || cors === true) {
|
|
2111
|
+
this.writeCorsHeaders(res, "*");
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
if (Array.isArray(cors)) {
|
|
2115
|
+
if (cors.includes(requestOrigin)) {
|
|
2116
|
+
this.writeCorsHeaders(res, requestOrigin);
|
|
2117
|
+
res.setHeader("Vary", "Origin");
|
|
2118
|
+
}
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
const opt = cors;
|
|
2122
|
+
const isAllowed = typeof opt.origins === "function" ? opt.origins(requestOrigin) : opt.origins.includes(requestOrigin);
|
|
2123
|
+
if (!isAllowed) {
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
this.writeCorsHeaders(res, requestOrigin);
|
|
2127
|
+
res.setHeader("Vary", "Origin");
|
|
2128
|
+
if (opt.credentials) {
|
|
2129
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
2130
|
+
}
|
|
2131
|
+
const maxAge = opt.maxAge ?? 600;
|
|
2132
|
+
res.setHeader("Access-Control-Max-Age", String(maxAge));
|
|
2133
|
+
}
|
|
2134
|
+
writeCorsHeaders(res, origin) {
|
|
2135
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
2136
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
2137
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization, Payment-Proof");
|
|
2138
|
+
res.setHeader(
|
|
2139
|
+
"Access-Control-Expose-Headers",
|
|
2140
|
+
"X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt, Payment-Needed"
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
1562
2143
|
/**
|
|
1563
2144
|
* Handle incoming request
|
|
1564
2145
|
*/
|
|
1565
2146
|
async handleRequest(req, res) {
|
|
1566
|
-
|
|
1567
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1568
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
|
|
1569
|
-
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt");
|
|
2147
|
+
this.applyCorsHeaders(req, res);
|
|
1570
2148
|
if (req.method === "OPTIONS") {
|
|
1571
2149
|
res.writeHead(204);
|
|
1572
2150
|
res.end();
|
|
@@ -1586,7 +2164,8 @@ var MoltsPayServer = class {
|
|
|
1586
2164
|
if (url.pathname === "/execute" && req.method === "POST") {
|
|
1587
2165
|
const body = await this.readBody(req);
|
|
1588
2166
|
const paymentHeader = req.headers[PAYMENT_HEADER];
|
|
1589
|
-
|
|
2167
|
+
const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
|
|
2168
|
+
return await this.handleExecute(body, paymentHeader, res, proofHeader);
|
|
1590
2169
|
}
|
|
1591
2170
|
if (url.pathname === "/proxy" && req.method === "POST") {
|
|
1592
2171
|
const clientIP = req.headers["x-real-ip"] || req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "";
|
|
@@ -1604,7 +2183,8 @@ var MoltsPayServer = class {
|
|
|
1604
2183
|
const body = req.method === "POST" ? await this.readBody(req) : {};
|
|
1605
2184
|
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
1606
2185
|
const x402Header = req.headers[PAYMENT_HEADER];
|
|
1607
|
-
|
|
2186
|
+
const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
|
|
2187
|
+
return await this.handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader);
|
|
1608
2188
|
}
|
|
1609
2189
|
this.sendJson(res, 404, { error: "Not found" });
|
|
1610
2190
|
} catch (err) {
|
|
@@ -1701,7 +2281,7 @@ var MoltsPayServer = class {
|
|
|
1701
2281
|
/**
|
|
1702
2282
|
* POST /execute - Execute service with x402 payment
|
|
1703
2283
|
*/
|
|
1704
|
-
async handleExecute(body, paymentHeader, res) {
|
|
2284
|
+
async handleExecute(body, paymentHeader, res, proofHeader) {
|
|
1705
2285
|
const { service, params } = body;
|
|
1706
2286
|
if (!service) {
|
|
1707
2287
|
return this.sendJson(res, 400, { error: "Missing service" });
|
|
@@ -1715,6 +2295,15 @@ var MoltsPayServer = class {
|
|
|
1715
2295
|
return this.sendJson(res, 400, { error: `Missing required param: ${key}` });
|
|
1716
2296
|
}
|
|
1717
2297
|
}
|
|
2298
|
+
if (proofHeader) {
|
|
2299
|
+
const alipayPayment = {
|
|
2300
|
+
x402Version: X402_VERSION2,
|
|
2301
|
+
scheme: ALIPAY_SCHEME,
|
|
2302
|
+
network: ALIPAY_NETWORK,
|
|
2303
|
+
payload: proofHeader
|
|
2304
|
+
};
|
|
2305
|
+
return this.handleAlipayExecute(skill, params || {}, alipayPayment, res);
|
|
2306
|
+
}
|
|
1718
2307
|
if (!paymentHeader) {
|
|
1719
2308
|
return this.sendPaymentRequired(skill.config, res);
|
|
1720
2309
|
}
|
|
@@ -1725,6 +2314,11 @@ var MoltsPayServer = class {
|
|
|
1725
2314
|
} catch {
|
|
1726
2315
|
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
1727
2316
|
}
|
|
2317
|
+
const payScheme = payment.accepted?.scheme || payment.scheme;
|
|
2318
|
+
const payNetwork = payment.accepted?.network || payment.network;
|
|
2319
|
+
if (payScheme === ALIPAY_SCHEME || (payNetwork ? isAlipayChainId(payNetwork) : false)) {
|
|
2320
|
+
return this.handleAlipayExecute(skill, params || {}, payment, res);
|
|
2321
|
+
}
|
|
1728
2322
|
const validation = this.validatePayment(payment, skill.config);
|
|
1729
2323
|
if (!validation.valid) {
|
|
1730
2324
|
return this.sendJson(res, 402, { error: validation.error });
|
|
@@ -1789,6 +2383,14 @@ var MoltsPayServer = class {
|
|
|
1789
2383
|
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1790
2384
|
} catch (err) {
|
|
1791
2385
|
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
2386
|
+
settlement = { success: false, error: err.message, facilitator: "none" };
|
|
2387
|
+
}
|
|
2388
|
+
if (!settlement?.success) {
|
|
2389
|
+
return this.sendJson(res, 402, {
|
|
2390
|
+
error: "Payment settlement failed",
|
|
2391
|
+
message: settlement?.error || "Settlement returned no success state",
|
|
2392
|
+
facilitator: settlement?.facilitator
|
|
2393
|
+
});
|
|
1792
2394
|
}
|
|
1793
2395
|
}
|
|
1794
2396
|
const responseHeaders = {};
|
|
@@ -1809,13 +2411,111 @@ var MoltsPayServer = class {
|
|
|
1809
2411
|
payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
|
|
1810
2412
|
}, responseHeaders);
|
|
1811
2413
|
}
|
|
2414
|
+
/**
|
|
2415
|
+
* Execute a service paid via the Alipay AI 收 fiat rail (2.0.0).
|
|
2416
|
+
*
|
|
2417
|
+
* Differs from the EVM/SVM path: no token detection, no EIP-3009/permit
|
|
2418
|
+
* validation. Verify hits the Alipay Open API (`payment.verify`). Settlement
|
|
2419
|
+
* (`fulfillment.confirm`) is FIRE-AND-FORGET per ALIPAY-INTEGRATION-DESIGN
|
|
2420
|
+
* §5.1: a confirm failure is logged but does NOT fail the already-delivered
|
|
2421
|
+
* response (the buyer's payment proof was already verified).
|
|
2422
|
+
*/
|
|
2423
|
+
async handleAlipayExecute(skill, params, payment, res) {
|
|
2424
|
+
if (!this.alipayFacilitator) {
|
|
2425
|
+
return this.sendJson(res, 402, { error: "Alipay rail not configured on this server" });
|
|
2426
|
+
}
|
|
2427
|
+
const requirements = {
|
|
2428
|
+
scheme: ALIPAY_SCHEME,
|
|
2429
|
+
network: ALIPAY_NETWORK,
|
|
2430
|
+
asset: "CNY",
|
|
2431
|
+
amount: skill.config.alipay?.price_cny || "0",
|
|
2432
|
+
payTo: this.manifest.provider.alipay?.seller_id || "",
|
|
2433
|
+
maxTimeoutSeconds: 1800
|
|
2434
|
+
};
|
|
2435
|
+
console.log(`[MoltsPay] Verifying Alipay payment...`);
|
|
2436
|
+
const verifyResult = await this.registry.verify(payment, requirements);
|
|
2437
|
+
if (!verifyResult.valid) {
|
|
2438
|
+
return this.sendJson(res, 402, {
|
|
2439
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
2440
|
+
facilitator: verifyResult.facilitator
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
console.log(`[MoltsPay] Alipay payment verified by ${verifyResult.facilitator}`);
|
|
2444
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
2445
|
+
console.log(`[MoltsPay] Executing skill: ${skill.id} (timeout: ${timeoutSeconds}s)`);
|
|
2446
|
+
let result;
|
|
2447
|
+
try {
|
|
2448
|
+
result = await Promise.race([
|
|
2449
|
+
skill.handler(params),
|
|
2450
|
+
new Promise(
|
|
2451
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
2452
|
+
)
|
|
2453
|
+
]);
|
|
2454
|
+
} catch (err) {
|
|
2455
|
+
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
2456
|
+
return this.sendJson(res, 500, {
|
|
2457
|
+
error: "Service execution failed",
|
|
2458
|
+
message: err.message
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
let settlement;
|
|
2462
|
+
try {
|
|
2463
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2464
|
+
if (settlement.success) {
|
|
2465
|
+
console.log(`[MoltsPay] Alipay fulfillment confirmed: ${settlement.transaction}`);
|
|
2466
|
+
} else {
|
|
2467
|
+
console.error(`[MoltsPay] Alipay fulfillment confirm failed (non-fatal): ${settlement.error}`);
|
|
2468
|
+
}
|
|
2469
|
+
} catch (err) {
|
|
2470
|
+
console.error(`[MoltsPay] Alipay fulfillment confirm threw (non-fatal): ${err.message}`);
|
|
2471
|
+
settlement = { success: false, error: err.message, facilitator: "alipay" };
|
|
2472
|
+
}
|
|
2473
|
+
const responseHeaders = {};
|
|
2474
|
+
if (settlement.success) {
|
|
2475
|
+
responseHeaders[PAYMENT_RESPONSE_HEADER] = Buffer.from(JSON.stringify({
|
|
2476
|
+
success: true,
|
|
2477
|
+
transaction: settlement.transaction,
|
|
2478
|
+
network: ALIPAY_NETWORK,
|
|
2479
|
+
facilitator: settlement.facilitator
|
|
2480
|
+
})).toString("base64");
|
|
2481
|
+
}
|
|
2482
|
+
this.sendJson(res, 200, {
|
|
2483
|
+
success: true,
|
|
2484
|
+
result,
|
|
2485
|
+
payment: settlement.success ? { transaction: settlement.transaction, status: "fulfilled", facilitator: settlement.facilitator } : { status: "delivered_unconfirmed", error: settlement.error }
|
|
2486
|
+
}, responseHeaders);
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Build the Alipay 402 challenge for a service, or null when the alipay rail
|
|
2490
|
+
* isn't configured for this server or this service. Returns the x402
|
|
2491
|
+
* `accepts[]` entry plus the Base64URL `Payment-Needed` header value so the
|
|
2492
|
+
* 402 responders can dual-emit both the x402 and legacy alipay-bot formats.
|
|
2493
|
+
*/
|
|
2494
|
+
async buildAlipayChallenge(config) {
|
|
2495
|
+
if (!this.alipayFacilitator || !config.alipay) return null;
|
|
2496
|
+
try {
|
|
2497
|
+
const req = await this.alipayFacilitator.createPaymentRequirements({
|
|
2498
|
+
serviceId: config.alipay.service_id || this.manifest.provider.alipay.service_id_default,
|
|
2499
|
+
priceCny: config.alipay.price_cny,
|
|
2500
|
+
goodsName: config.alipay.goods_name,
|
|
2501
|
+
resourceId: `/execute?service=${config.id}`
|
|
2502
|
+
});
|
|
2503
|
+
return { accepts: req.x402Accepts, paymentNeededHeader: req.paymentNeededHeader };
|
|
2504
|
+
} catch (err) {
|
|
2505
|
+
console.error(`[MoltsPay] Alipay challenge build failed for ${config.id}: ${err.message}`);
|
|
2506
|
+
return null;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
1812
2509
|
/**
|
|
1813
2510
|
* Handle MPP (Machine Payments Protocol) request
|
|
1814
2511
|
* Supports both x402 and MPP protocols on service endpoints
|
|
1815
2512
|
*/
|
|
1816
|
-
async handleMPPRequest(skill, body, authHeader, x402Header, res) {
|
|
2513
|
+
async handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader) {
|
|
1817
2514
|
const config = skill.config;
|
|
1818
2515
|
const params = body || {};
|
|
2516
|
+
if (proofHeader) {
|
|
2517
|
+
return await this.handleExecute({ service: config.id, params }, void 0, res, proofHeader);
|
|
2518
|
+
}
|
|
1819
2519
|
if (x402Header) {
|
|
1820
2520
|
return await this.handleExecute({ service: config.id, params }, x402Header, res);
|
|
1821
2521
|
}
|
|
@@ -1921,7 +2621,7 @@ var MoltsPayServer = class {
|
|
|
1921
2621
|
/**
|
|
1922
2622
|
* Return 402 with both x402 and MPP payment requirements
|
|
1923
2623
|
*/
|
|
1924
|
-
sendMPPPaymentRequired(config, res) {
|
|
2624
|
+
async sendMPPPaymentRequired(config, res) {
|
|
1925
2625
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
1926
2626
|
const providerChains = this.getProviderChains();
|
|
1927
2627
|
const accepts = [];
|
|
@@ -1932,6 +2632,10 @@ var MoltsPayServer = class {
|
|
|
1932
2632
|
}
|
|
1933
2633
|
}
|
|
1934
2634
|
}
|
|
2635
|
+
const alipayChallenge = await this.buildAlipayChallenge(config);
|
|
2636
|
+
if (alipayChallenge) {
|
|
2637
|
+
accepts.push(alipayChallenge.accepts);
|
|
2638
|
+
}
|
|
1935
2639
|
const x402PaymentRequired = {
|
|
1936
2640
|
x402Version: X402_VERSION2,
|
|
1937
2641
|
accepts,
|
|
@@ -1959,7 +2663,7 @@ var MoltsPayServer = class {
|
|
|
1959
2663
|
};
|
|
1960
2664
|
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
1961
2665
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
1962
|
-
mppWwwAuth = `Payment id="${challengeId}", realm="${this.manifest.provider.name}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
2666
|
+
mppWwwAuth = `Payment id="${challengeId}", realm="${headerSafe(this.manifest.provider.name)}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
|
|
1963
2667
|
}
|
|
1964
2668
|
const headers = {
|
|
1965
2669
|
"Content-Type": "application/problem+json",
|
|
@@ -1968,6 +2672,9 @@ var MoltsPayServer = class {
|
|
|
1968
2672
|
if (mppWwwAuth) {
|
|
1969
2673
|
headers[MPP_WWW_AUTH_HEADER] = mppWwwAuth;
|
|
1970
2674
|
}
|
|
2675
|
+
if (alipayChallenge) {
|
|
2676
|
+
headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
|
|
2677
|
+
}
|
|
1971
2678
|
res.writeHead(402, headers);
|
|
1972
2679
|
res.end(JSON.stringify({
|
|
1973
2680
|
type: "https://paymentauth.org/problems/payment-required",
|
|
@@ -1994,7 +2701,7 @@ var MoltsPayServer = class {
|
|
|
1994
2701
|
* Return 402 with x402 payment requirements (v2 format)
|
|
1995
2702
|
* Includes requirements for all chains and all accepted currencies
|
|
1996
2703
|
*/
|
|
1997
|
-
sendPaymentRequired(config, res) {
|
|
2704
|
+
async sendPaymentRequired(config, res) {
|
|
1998
2705
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
1999
2706
|
const providerChains = this.getProviderChains();
|
|
2000
2707
|
const accepts = [];
|
|
@@ -2005,6 +2712,10 @@ var MoltsPayServer = class {
|
|
|
2005
2712
|
}
|
|
2006
2713
|
}
|
|
2007
2714
|
}
|
|
2715
|
+
const alipayChallenge = await this.buildAlipayChallenge(config);
|
|
2716
|
+
if (alipayChallenge) {
|
|
2717
|
+
accepts.push(alipayChallenge.accepts);
|
|
2718
|
+
}
|
|
2008
2719
|
const acceptedChains = providerChains.map((c) => {
|
|
2009
2720
|
if (c.network === "eip155:8453") return "base";
|
|
2010
2721
|
if (c.network === "eip155:137") return "polygon";
|
|
@@ -2022,10 +2733,14 @@ var MoltsPayServer = class {
|
|
|
2022
2733
|
}
|
|
2023
2734
|
};
|
|
2024
2735
|
const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
|
|
2025
|
-
|
|
2736
|
+
const headers = {
|
|
2026
2737
|
"Content-Type": "application/json",
|
|
2027
2738
|
[PAYMENT_REQUIRED_HEADER]: encoded
|
|
2028
|
-
}
|
|
2739
|
+
};
|
|
2740
|
+
if (alipayChallenge) {
|
|
2741
|
+
headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
|
|
2742
|
+
}
|
|
2743
|
+
res.writeHead(402, headers);
|
|
2029
2744
|
res.end(JSON.stringify({
|
|
2030
2745
|
error: "Payment required",
|
|
2031
2746
|
message: `Service requires $${config.price} ${config.currency}`,
|
|
@@ -2043,7 +2758,7 @@ var MoltsPayServer = class {
|
|
|
2043
2758
|
}
|
|
2044
2759
|
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
2045
2760
|
const network = payment.accepted?.network || payment.network || this.networkId;
|
|
2046
|
-
if (scheme !== "exact") {
|
|
2761
|
+
if (scheme !== "exact" && scheme !== "permit") {
|
|
2047
2762
|
return { valid: false, error: `Unsupported scheme: ${scheme}` };
|
|
2048
2763
|
}
|
|
2049
2764
|
if (!this.isNetworkAccepted(network)) {
|
|
@@ -2065,8 +2780,10 @@ var MoltsPayServer = class {
|
|
|
2065
2780
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
2066
2781
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
2067
2782
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
2783
|
+
const isTempo = selectedNetwork === "eip155:42431";
|
|
2784
|
+
const scheme = isTempo ? "permit" : "exact";
|
|
2068
2785
|
const requirements = {
|
|
2069
|
-
scheme
|
|
2786
|
+
scheme,
|
|
2070
2787
|
network: selectedNetwork,
|
|
2071
2788
|
asset: tokenAddress,
|
|
2072
2789
|
amount: amountInUnits,
|
|
@@ -2094,6 +2811,16 @@ var MoltsPayServer = class {
|
|
|
2094
2811
|
};
|
|
2095
2812
|
}
|
|
2096
2813
|
}
|
|
2814
|
+
if (isTempo) {
|
|
2815
|
+
const tempoFacilitator = this.registry.get("tempo");
|
|
2816
|
+
const tempoSpender = tempoFacilitator?.getSpenderAddress?.();
|
|
2817
|
+
if (tempoSpender) {
|
|
2818
|
+
requirements.extra = {
|
|
2819
|
+
...requirements.extra || {},
|
|
2820
|
+
tempoSpender
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2097
2824
|
return requirements;
|
|
2098
2825
|
}
|
|
2099
2826
|
/**
|
|
@@ -2120,12 +2847,12 @@ var MoltsPayServer = class {
|
|
|
2120
2847
|
return accepted.includes(token);
|
|
2121
2848
|
}
|
|
2122
2849
|
async readBody(req) {
|
|
2123
|
-
return new Promise((
|
|
2850
|
+
return new Promise((resolve2, reject) => {
|
|
2124
2851
|
let body = "";
|
|
2125
2852
|
req.on("data", (chunk) => body += chunk);
|
|
2126
2853
|
req.on("end", () => {
|
|
2127
2854
|
try {
|
|
2128
|
-
|
|
2855
|
+
resolve2(body ? JSON.parse(body) : {});
|
|
2129
2856
|
} catch {
|
|
2130
2857
|
reject(new Error("Invalid JSON"));
|
|
2131
2858
|
}
|
|
@@ -2228,7 +2955,7 @@ var MoltsPayServer = class {
|
|
|
2228
2955
|
}
|
|
2229
2956
|
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
2230
2957
|
const network = payment.accepted?.network || payment.network;
|
|
2231
|
-
if (scheme !== "exact") {
|
|
2958
|
+
if (scheme !== "exact" && scheme !== "permit") {
|
|
2232
2959
|
return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
|
|
2233
2960
|
}
|
|
2234
2961
|
const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;
|
|
@@ -2383,7 +3110,7 @@ var MoltsPayServer = class {
|
|
|
2383
3110
|
};
|
|
2384
3111
|
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
2385
3112
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
2386
|
-
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
3113
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
|
|
2387
3114
|
res.writeHead(402, {
|
|
2388
3115
|
"Content-Type": "application/problem+json",
|
|
2389
3116
|
[MPP_WWW_AUTH_HEADER]: wwwAuth
|