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.mjs
CHANGED
|
@@ -229,6 +229,9 @@ var CDPFacilitator = class extends BaseFacilitator {
|
|
|
229
229
|
}
|
|
230
230
|
};
|
|
231
231
|
|
|
232
|
+
// src/facilitators/tempo.ts
|
|
233
|
+
import { ethers } from "ethers";
|
|
234
|
+
|
|
232
235
|
// src/chains/index.ts
|
|
233
236
|
var CHAINS = {
|
|
234
237
|
// ============ Mainnet ============
|
|
@@ -395,18 +398,45 @@ var CHAINS = {
|
|
|
395
398
|
requiresApproval: true
|
|
396
399
|
}
|
|
397
400
|
};
|
|
401
|
+
var ALIPAY_CHAIN_ID = "alipay";
|
|
402
|
+
function isAlipayChainId(id) {
|
|
403
|
+
return id === ALIPAY_CHAIN_ID;
|
|
404
|
+
}
|
|
398
405
|
|
|
399
406
|
// src/facilitators/tempo.ts
|
|
400
407
|
var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
408
|
+
var TIP20_PERMIT_ABI = [
|
|
409
|
+
"function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
|
|
410
|
+
"function transferFrom(address from, address to, uint256 value) returns (bool)"
|
|
411
|
+
];
|
|
401
412
|
var TempoFacilitator = class extends BaseFacilitator {
|
|
402
413
|
name = "tempo";
|
|
403
414
|
displayName = "Tempo Testnet";
|
|
404
415
|
supportedNetworks = ["eip155:42431"];
|
|
405
416
|
// Tempo Moderato
|
|
406
417
|
rpcUrl;
|
|
418
|
+
settlerWallet = null;
|
|
407
419
|
constructor() {
|
|
408
420
|
super();
|
|
409
421
|
this.rpcUrl = CHAINS.tempo_moderato.rpc;
|
|
422
|
+
const settlerKey = process.env.TEMPO_SETTLER_KEY;
|
|
423
|
+
if (settlerKey) {
|
|
424
|
+
try {
|
|
425
|
+
const provider = new ethers.JsonRpcProvider(this.rpcUrl);
|
|
426
|
+
this.settlerWallet = new ethers.Wallet(settlerKey, provider);
|
|
427
|
+
} catch (err) {
|
|
428
|
+
console.warn("[TempoFacilitator] Invalid TEMPO_SETTLER_KEY, permit settlement disabled:", err);
|
|
429
|
+
this.settlerWallet = null;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Settler EOA address advertised to clients via `X-Payment-Required.extra.tempoSpender`.
|
|
435
|
+
* Web Client uses this as the `spender` field in the signed EIP-2612 Permit.
|
|
436
|
+
* Returns null if no TEMPO_SETTLER_KEY is configured — permit settlement unavailable.
|
|
437
|
+
*/
|
|
438
|
+
getSpenderAddress() {
|
|
439
|
+
return this.settlerWallet?.address ?? null;
|
|
410
440
|
}
|
|
411
441
|
async healthCheck() {
|
|
412
442
|
const start = Date.now();
|
|
@@ -432,6 +462,44 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
432
462
|
}
|
|
433
463
|
}
|
|
434
464
|
async verify(paymentPayload, requirements) {
|
|
465
|
+
const inner = paymentPayload.payload;
|
|
466
|
+
if (inner && "permit" in inner && inner.permit) {
|
|
467
|
+
return this.verifyPermit(inner, requirements);
|
|
468
|
+
}
|
|
469
|
+
return this.verifyTxHash(paymentPayload, requirements);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Structural validation of an EIP-2612 permit payload. Does NOT submit
|
|
473
|
+
* anything on-chain — actual submission happens in settlePermit().
|
|
474
|
+
*/
|
|
475
|
+
async verifyPermit(payload, requirements) {
|
|
476
|
+
if (!this.settlerWallet) {
|
|
477
|
+
return { valid: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
|
|
478
|
+
}
|
|
479
|
+
const p = payload.permit;
|
|
480
|
+
if (!p || !p.owner || !p.spender || !p.value || !p.deadline) {
|
|
481
|
+
return { valid: false, error: "Invalid permit payload: missing fields" };
|
|
482
|
+
}
|
|
483
|
+
if (p.spender.toLowerCase() !== this.settlerWallet.address.toLowerCase()) {
|
|
484
|
+
return {
|
|
485
|
+
valid: false,
|
|
486
|
+
error: `Permit spender ${p.spender} does not match configured settler ${this.settlerWallet.address}`
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
const deadline = BigInt(p.deadline);
|
|
490
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
491
|
+
if (deadline <= now) {
|
|
492
|
+
return { valid: false, error: "Permit deadline has expired" };
|
|
493
|
+
}
|
|
494
|
+
if (BigInt(p.value) < BigInt(requirements.amount || "0")) {
|
|
495
|
+
return {
|
|
496
|
+
valid: false,
|
|
497
|
+
error: `Permit value ${p.value} is less than required ${requirements.amount}`
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
return { valid: true, details: { scheme: "permit", owner: p.owner } };
|
|
501
|
+
}
|
|
502
|
+
async verifyTxHash(paymentPayload, requirements) {
|
|
435
503
|
try {
|
|
436
504
|
const tempoPayload = paymentPayload.payload;
|
|
437
505
|
if (!tempoPayload?.txHash) {
|
|
@@ -489,7 +557,11 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
489
557
|
}
|
|
490
558
|
}
|
|
491
559
|
async settle(paymentPayload, requirements) {
|
|
492
|
-
const
|
|
560
|
+
const inner = paymentPayload.payload;
|
|
561
|
+
if (inner && "permit" in inner && inner.permit) {
|
|
562
|
+
return this.settlePermit(inner, requirements);
|
|
563
|
+
}
|
|
564
|
+
const verifyResult = await this.verifyTxHash(paymentPayload, requirements);
|
|
493
565
|
if (!verifyResult.valid) {
|
|
494
566
|
return { success: false, error: verifyResult.error };
|
|
495
567
|
}
|
|
@@ -500,6 +572,52 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
500
572
|
status: "settled"
|
|
501
573
|
};
|
|
502
574
|
}
|
|
575
|
+
/**
|
|
576
|
+
* EIP-2612 permit settlement path. Submits two transactions on Tempo:
|
|
577
|
+
* 1. pathUSD.permit(owner, spender=settler, value, deadline, v, r, s)
|
|
578
|
+
* 2. pathUSD.transferFrom(owner, payTo, value)
|
|
579
|
+
*
|
|
580
|
+
* The settler EOA pays Tempo gas (via the TIP-20 `feeToken` mechanism — no
|
|
581
|
+
* native tTEMPO required; any held TIP-20 token balance covers fees).
|
|
582
|
+
*/
|
|
583
|
+
async settlePermit(payload, requirements) {
|
|
584
|
+
if (!this.settlerWallet) {
|
|
585
|
+
return { success: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
|
|
586
|
+
}
|
|
587
|
+
if (!requirements.asset || !requirements.payTo) {
|
|
588
|
+
return { success: false, error: "Missing asset or payTo in requirements" };
|
|
589
|
+
}
|
|
590
|
+
const verifyResult = await this.verifyPermit(payload, requirements);
|
|
591
|
+
if (!verifyResult.valid) {
|
|
592
|
+
return { success: false, error: verifyResult.error };
|
|
593
|
+
}
|
|
594
|
+
const token = new ethers.Contract(requirements.asset, TIP20_PERMIT_ABI, this.settlerWallet);
|
|
595
|
+
const p = payload.permit;
|
|
596
|
+
try {
|
|
597
|
+
const permitTx = await token.permit(
|
|
598
|
+
p.owner,
|
|
599
|
+
p.spender,
|
|
600
|
+
p.value,
|
|
601
|
+
p.deadline,
|
|
602
|
+
p.v,
|
|
603
|
+
p.r,
|
|
604
|
+
p.s
|
|
605
|
+
);
|
|
606
|
+
await permitTx.wait();
|
|
607
|
+
const transferTx = await token.transferFrom(p.owner, requirements.payTo, p.value);
|
|
608
|
+
await transferTx.wait();
|
|
609
|
+
return {
|
|
610
|
+
success: true,
|
|
611
|
+
transaction: transferTx.hash,
|
|
612
|
+
status: "settled"
|
|
613
|
+
};
|
|
614
|
+
} catch (err) {
|
|
615
|
+
return {
|
|
616
|
+
success: false,
|
|
617
|
+
error: `Tempo permit settlement failed: ${err.message}`
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
503
621
|
async getTransactionReceipt(txHash) {
|
|
504
622
|
const response = await fetch(this.rpcUrl, {
|
|
505
623
|
method: "POST",
|
|
@@ -742,12 +860,12 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
742
860
|
return this.spenderAddress;
|
|
743
861
|
}
|
|
744
862
|
async getServerAddress() {
|
|
745
|
-
const { ethers } = await import("ethers");
|
|
746
|
-
const wallet = new
|
|
863
|
+
const { ethers: ethers2 } = await import("ethers");
|
|
864
|
+
const wallet = new ethers2.Wallet(this.serverPrivateKey);
|
|
747
865
|
return wallet.address;
|
|
748
866
|
}
|
|
749
867
|
async recoverIntentSigner(intent, chainId) {
|
|
750
|
-
const { ethers } = await import("ethers");
|
|
868
|
+
const { ethers: ethers2 } = await import("ethers");
|
|
751
869
|
const domain = {
|
|
752
870
|
...EIP712_DOMAIN,
|
|
753
871
|
chainId
|
|
@@ -761,7 +879,7 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
761
879
|
nonce: intent.nonce,
|
|
762
880
|
deadline: intent.deadline
|
|
763
881
|
};
|
|
764
|
-
const recoveredAddress =
|
|
882
|
+
const recoveredAddress = ethers2.verifyTypedData(
|
|
765
883
|
domain,
|
|
766
884
|
INTENT_TYPES,
|
|
767
885
|
message,
|
|
@@ -805,10 +923,10 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
805
923
|
return result.result || "0x0";
|
|
806
924
|
}
|
|
807
925
|
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
808
|
-
const { ethers } = await import("ethers");
|
|
809
|
-
const provider = new
|
|
810
|
-
const wallet = new
|
|
811
|
-
const tokenContract = new
|
|
926
|
+
const { ethers: ethers2 } = await import("ethers");
|
|
927
|
+
const provider = new ethers2.JsonRpcProvider(rpcUrl);
|
|
928
|
+
const wallet = new ethers2.Wallet(this.serverPrivateKey, provider);
|
|
929
|
+
const tokenContract = new ethers2.Contract(token, [
|
|
812
930
|
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
813
931
|
], wallet);
|
|
814
932
|
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
@@ -1045,6 +1163,374 @@ var SolanaFacilitator = class extends BaseFacilitator {
|
|
|
1045
1163
|
}
|
|
1046
1164
|
};
|
|
1047
1165
|
|
|
1166
|
+
// src/facilitators/alipay.ts
|
|
1167
|
+
import crypto2 from "crypto";
|
|
1168
|
+
|
|
1169
|
+
// src/facilitators/alipay/rsa2.ts
|
|
1170
|
+
import crypto from "crypto";
|
|
1171
|
+
function rsa2Sign(data, privateKeyPem) {
|
|
1172
|
+
const signer = crypto.createSign("RSA-SHA256");
|
|
1173
|
+
signer.update(data, "utf-8");
|
|
1174
|
+
signer.end();
|
|
1175
|
+
return signer.sign(privateKeyPem, "base64");
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// src/facilitators/alipay/encoding.ts
|
|
1179
|
+
function base64url(input) {
|
|
1180
|
+
return Buffer.from(input, "utf-8").toString("base64url");
|
|
1181
|
+
}
|
|
1182
|
+
function decodeBase64UrlWithPadFix(input) {
|
|
1183
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
1184
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
1185
|
+
return Buffer.from(padded, "base64").toString("utf-8");
|
|
1186
|
+
}
|
|
1187
|
+
function toPem(key, kind) {
|
|
1188
|
+
const trimmed = key.trim();
|
|
1189
|
+
if (trimmed.includes("-----BEGIN")) return trimmed;
|
|
1190
|
+
const label = kind === "PRIVATE" ? "PRIVATE KEY" : "PUBLIC KEY";
|
|
1191
|
+
const body = trimmed.replace(/\s+/g, "").match(/.{1,64}/g)?.join("\n") ?? "";
|
|
1192
|
+
return `-----BEGIN ${label}-----
|
|
1193
|
+
${body}
|
|
1194
|
+
-----END ${label}-----
|
|
1195
|
+
`;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/facilitators/alipay/openapi.ts
|
|
1199
|
+
function formatAlipayTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
1200
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1201
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
1202
|
+
}
|
|
1203
|
+
function buildSigningString(params) {
|
|
1204
|
+
return Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join("&");
|
|
1205
|
+
}
|
|
1206
|
+
function responseWrapperKey(method) {
|
|
1207
|
+
return `${method.replace(/\./g, "_")}_response`;
|
|
1208
|
+
}
|
|
1209
|
+
async function alipayOpenApiCall(method, bizContent, config) {
|
|
1210
|
+
const publicParams = {
|
|
1211
|
+
app_id: config.app_id,
|
|
1212
|
+
method,
|
|
1213
|
+
format: "JSON",
|
|
1214
|
+
charset: "utf-8",
|
|
1215
|
+
sign_type: config.sign_type ?? "RSA2",
|
|
1216
|
+
timestamp: formatAlipayTimestamp(),
|
|
1217
|
+
version: "1.0",
|
|
1218
|
+
biz_content: JSON.stringify(bizContent)
|
|
1219
|
+
};
|
|
1220
|
+
const signingString = buildSigningString(publicParams);
|
|
1221
|
+
const sign = rsa2Sign(signingString, config.private_key_pem);
|
|
1222
|
+
const body = new URLSearchParams({ ...publicParams, sign }).toString();
|
|
1223
|
+
const response = await fetch(config.gateway_url, {
|
|
1224
|
+
method: "POST",
|
|
1225
|
+
headers: {
|
|
1226
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
|
|
1227
|
+
},
|
|
1228
|
+
body
|
|
1229
|
+
});
|
|
1230
|
+
if (!response.ok) {
|
|
1231
|
+
const text = await response.text().catch(() => "<unreadable>");
|
|
1232
|
+
throw new Error(
|
|
1233
|
+
`Alipay Open API HTTP ${response.status} for ${method}: ${text.slice(0, 500)}`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
const json = await response.json();
|
|
1237
|
+
const wrapperKey = responseWrapperKey(method);
|
|
1238
|
+
const business = json[wrapperKey];
|
|
1239
|
+
if (business === void 0 || business === null || typeof business !== "object") {
|
|
1240
|
+
throw new Error(
|
|
1241
|
+
`Alipay Open API response missing "${wrapperKey}" wrapper for ${method}: ${JSON.stringify(json).slice(0, 500)}`
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
return business;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// src/facilitators/alipay.ts
|
|
1248
|
+
var ALIPAY_NETWORK = "alipay";
|
|
1249
|
+
var ALIPAY_SCHEME = "alipay-aipay";
|
|
1250
|
+
var ALIPAY_GATEWAY_PROD = "https://openapi.alipay.com/gateway.do";
|
|
1251
|
+
var ALIPAY_AMOUNT_REGEX = /^\d+(\.\d{1,2})?$/;
|
|
1252
|
+
var ALIPAY_PAY_BEFORE_MS = 30 * 60 * 1e3;
|
|
1253
|
+
var ALIPAY_SIGNING_FIELDS = [
|
|
1254
|
+
"amount",
|
|
1255
|
+
"currency",
|
|
1256
|
+
"goods_name",
|
|
1257
|
+
"out_trade_no",
|
|
1258
|
+
"pay_before",
|
|
1259
|
+
"resource_id",
|
|
1260
|
+
"seller_id",
|
|
1261
|
+
"service_id"
|
|
1262
|
+
];
|
|
1263
|
+
var AlipayFacilitator = class extends BaseFacilitator {
|
|
1264
|
+
name = "alipay";
|
|
1265
|
+
displayName = "Alipay AI \u6536";
|
|
1266
|
+
supportedNetworks = [ALIPAY_NETWORK];
|
|
1267
|
+
config;
|
|
1268
|
+
constructor(config) {
|
|
1269
|
+
super();
|
|
1270
|
+
this.config = {
|
|
1271
|
+
gateway_url: ALIPAY_GATEWAY_PROD,
|
|
1272
|
+
sign_type: "RSA2",
|
|
1273
|
+
...config
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Build the 402 challenge for a service: signs the 8-field payload with
|
|
1278
|
+
* RSA2, packages the nested `{protocol, method}` JSON as Base64URL for
|
|
1279
|
+
* `Payment-Needed`, and emits the parallel x402 `accepts[]` entry.
|
|
1280
|
+
*/
|
|
1281
|
+
async createPaymentRequirements(opts) {
|
|
1282
|
+
if (!ALIPAY_AMOUNT_REGEX.test(opts.priceCny)) {
|
|
1283
|
+
throw new Error(
|
|
1284
|
+
`AlipayFacilitator.createPaymentRequirements: priceCny "${opts.priceCny}" does not match /^\\d+(\\.\\d{1,2})?$/ (unit is \u5143, not \u5206; e.g. "1.00" not "100")`
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
const now = /* @__PURE__ */ new Date();
|
|
1288
|
+
const outTradeNo = opts.outTradeNo ?? generateOutTradeNo();
|
|
1289
|
+
const payBefore = formatPayBefore(now);
|
|
1290
|
+
const signedFields = {
|
|
1291
|
+
amount: opts.priceCny,
|
|
1292
|
+
currency: "CNY",
|
|
1293
|
+
goods_name: opts.goodsName,
|
|
1294
|
+
out_trade_no: outTradeNo,
|
|
1295
|
+
pay_before: payBefore,
|
|
1296
|
+
resource_id: opts.resourceId,
|
|
1297
|
+
seller_id: this.config.seller_id,
|
|
1298
|
+
service_id: opts.serviceId
|
|
1299
|
+
};
|
|
1300
|
+
const signingString = ALIPAY_SIGNING_FIELDS.map((k) => `${k}=${signedFields[k]}`).join("&");
|
|
1301
|
+
const seller_signature = rsa2Sign(signingString, this.config.private_key_pem);
|
|
1302
|
+
const challenge = {
|
|
1303
|
+
protocol: {
|
|
1304
|
+
out_trade_no: outTradeNo,
|
|
1305
|
+
amount: signedFields.amount,
|
|
1306
|
+
currency: signedFields.currency,
|
|
1307
|
+
resource_id: signedFields.resource_id,
|
|
1308
|
+
pay_before: payBefore,
|
|
1309
|
+
seller_signature,
|
|
1310
|
+
seller_sign_type: this.config.sign_type ?? "RSA2",
|
|
1311
|
+
seller_unique_id: this.config.seller_id
|
|
1312
|
+
},
|
|
1313
|
+
method: {
|
|
1314
|
+
seller_name: this.config.seller_name,
|
|
1315
|
+
seller_id: this.config.seller_id,
|
|
1316
|
+
seller_app_id: this.config.app_id,
|
|
1317
|
+
goods_name: signedFields.goods_name,
|
|
1318
|
+
seller_unique_id_key: "seller_id",
|
|
1319
|
+
service_id: signedFields.service_id
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
const paymentNeededHeader = base64url(JSON.stringify(challenge));
|
|
1323
|
+
const x402Accepts = {
|
|
1324
|
+
scheme: ALIPAY_SCHEME,
|
|
1325
|
+
network: ALIPAY_NETWORK,
|
|
1326
|
+
asset: "CNY",
|
|
1327
|
+
amount: opts.priceCny,
|
|
1328
|
+
payTo: this.config.seller_id,
|
|
1329
|
+
maxTimeoutSeconds: ALIPAY_PAY_BEFORE_MS / 1e3,
|
|
1330
|
+
extra: {
|
|
1331
|
+
payment_needed_header: paymentNeededHeader,
|
|
1332
|
+
out_trade_no: outTradeNo,
|
|
1333
|
+
pay_before: payBefore,
|
|
1334
|
+
service_id: signedFields.service_id
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
return { x402Accepts, paymentNeededHeader };
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Verify a `Payment-Proof` by calling
|
|
1341
|
+
* `alipay.aipay.agent.payment.verify` against the Alipay Open API.
|
|
1342
|
+
*
|
|
1343
|
+
* The proof is conveyed via `paymentPayload.payload`, which may be:
|
|
1344
|
+
* - a raw Base64URL string (the `Payment-Proof` header value), or
|
|
1345
|
+
* - an object `{ paymentProof: string }` / `{ proofHeader: string }`.
|
|
1346
|
+
*
|
|
1347
|
+
* All failure modes (malformed payload, network errors, Alipay
|
|
1348
|
+
* `code != 10000`) return `{ valid: false, error }`. No exception
|
|
1349
|
+
* escapes, regardless of client-supplied input.
|
|
1350
|
+
*/
|
|
1351
|
+
async verify(paymentPayload, _requirements) {
|
|
1352
|
+
try {
|
|
1353
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
1354
|
+
const decoded = decodeProof(proofHeader);
|
|
1355
|
+
const response = await alipayOpenApiCall(
|
|
1356
|
+
"alipay.aipay.agent.payment.verify",
|
|
1357
|
+
{
|
|
1358
|
+
payment_proof: decoded.protocol.payment_proof,
|
|
1359
|
+
trade_no: decoded.protocol.trade_no,
|
|
1360
|
+
client_session: decoded.method.client_session
|
|
1361
|
+
},
|
|
1362
|
+
this.getOpenApiConfig()
|
|
1363
|
+
);
|
|
1364
|
+
if (response.code !== "10000") {
|
|
1365
|
+
return {
|
|
1366
|
+
valid: false,
|
|
1367
|
+
error: `alipay verify ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
1368
|
+
details: {
|
|
1369
|
+
code: response.code,
|
|
1370
|
+
sub_code: response.sub_code,
|
|
1371
|
+
sub_msg: response.sub_msg
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
return {
|
|
1376
|
+
valid: true,
|
|
1377
|
+
details: {
|
|
1378
|
+
trade_no: response.trade_no ?? decoded.protocol.trade_no,
|
|
1379
|
+
amount: response.amount,
|
|
1380
|
+
out_trade_no: response.out_trade_no,
|
|
1381
|
+
resource_id: response.resource_id,
|
|
1382
|
+
active: response.active
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
} catch (e) {
|
|
1386
|
+
return {
|
|
1387
|
+
valid: false,
|
|
1388
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Settle by calling `alipay.aipay.agent.fulfillment.confirm` after the
|
|
1394
|
+
* service resource has been returned to the buyer.
|
|
1395
|
+
*
|
|
1396
|
+
* Per the design (see ALIPAY-INTEGRATION-DESIGN.md §5.1, risk row
|
|
1397
|
+
* "履约确认失败"), this is **fire-and-forget**: the caller (registry /
|
|
1398
|
+
* server) is expected to log fulfillment failures but NOT roll back
|
|
1399
|
+
* the already-delivered resource.
|
|
1400
|
+
*
|
|
1401
|
+
* Re-decodes `paymentPayload.payload` to extract `trade_no` (verify
|
|
1402
|
+
* does the same; the redundant Base64URL decode is negligible).
|
|
1403
|
+
*/
|
|
1404
|
+
async settle(paymentPayload, _requirements) {
|
|
1405
|
+
try {
|
|
1406
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
1407
|
+
const decoded = decodeProof(proofHeader);
|
|
1408
|
+
const tradeNo = decoded.protocol.trade_no;
|
|
1409
|
+
const response = await alipayOpenApiCall(
|
|
1410
|
+
"alipay.aipay.agent.fulfillment.confirm",
|
|
1411
|
+
{ trade_no: tradeNo },
|
|
1412
|
+
this.getOpenApiConfig()
|
|
1413
|
+
);
|
|
1414
|
+
if (response.code !== "10000") {
|
|
1415
|
+
return {
|
|
1416
|
+
success: false,
|
|
1417
|
+
transaction: tradeNo,
|
|
1418
|
+
error: `alipay fulfillment ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
1419
|
+
status: "fulfillment_failed"
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
return {
|
|
1423
|
+
success: true,
|
|
1424
|
+
transaction: tradeNo,
|
|
1425
|
+
status: "fulfilled"
|
|
1426
|
+
};
|
|
1427
|
+
} catch (e) {
|
|
1428
|
+
return {
|
|
1429
|
+
success: false,
|
|
1430
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Validate that the configured RSA keys parse and that the Open API
|
|
1436
|
+
* gateway is reachable. Does NOT make a real business API call (would
|
|
1437
|
+
* burn quota and could appear as a legitimate verify attempt in logs).
|
|
1438
|
+
*/
|
|
1439
|
+
async healthCheck() {
|
|
1440
|
+
const start = Date.now();
|
|
1441
|
+
try {
|
|
1442
|
+
crypto2.createPrivateKey(this.config.private_key_pem);
|
|
1443
|
+
} catch (e) {
|
|
1444
|
+
return {
|
|
1445
|
+
healthy: false,
|
|
1446
|
+
error: `merchant private_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
try {
|
|
1450
|
+
crypto2.createPublicKey(this.config.alipay_public_key_pem);
|
|
1451
|
+
} catch (e) {
|
|
1452
|
+
return {
|
|
1453
|
+
healthy: false,
|
|
1454
|
+
error: `alipay_public_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
const gatewayUrl = this.config.gateway_url ?? ALIPAY_GATEWAY_PROD;
|
|
1458
|
+
const controller = new AbortController();
|
|
1459
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
1460
|
+
const response = await fetch(gatewayUrl, {
|
|
1461
|
+
method: "HEAD",
|
|
1462
|
+
signal: controller.signal
|
|
1463
|
+
}).catch(() => null);
|
|
1464
|
+
clearTimeout(timeout);
|
|
1465
|
+
const latencyMs = Date.now() - start;
|
|
1466
|
+
if (!response) {
|
|
1467
|
+
return { healthy: false, error: `gateway unreachable: ${gatewayUrl}`, latencyMs };
|
|
1468
|
+
}
|
|
1469
|
+
return { healthy: true, latencyMs };
|
|
1470
|
+
}
|
|
1471
|
+
/** Bundle the facilitator config into the shape openapi.ts wants. */
|
|
1472
|
+
getOpenApiConfig() {
|
|
1473
|
+
return {
|
|
1474
|
+
gateway_url: this.config.gateway_url ?? ALIPAY_GATEWAY_PROD,
|
|
1475
|
+
app_id: this.config.app_id,
|
|
1476
|
+
private_key_pem: this.config.private_key_pem,
|
|
1477
|
+
alipay_public_key_pem: this.config.alipay_public_key_pem,
|
|
1478
|
+
sign_type: this.config.sign_type
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
function generateOutTradeNo() {
|
|
1483
|
+
return "VID" + crypto2.randomBytes(22).toString("base64url").slice(0, 29);
|
|
1484
|
+
}
|
|
1485
|
+
function formatPayBefore(now) {
|
|
1486
|
+
const expiry = new Date(now.getTime() + ALIPAY_PAY_BEFORE_MS);
|
|
1487
|
+
return expiry.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1488
|
+
}
|
|
1489
|
+
function extractProofHeader(payload) {
|
|
1490
|
+
if (typeof payload === "string") {
|
|
1491
|
+
if (payload.length === 0) {
|
|
1492
|
+
throw new Error("alipay Payment-Proof is empty");
|
|
1493
|
+
}
|
|
1494
|
+
return payload;
|
|
1495
|
+
}
|
|
1496
|
+
if (payload !== null && typeof payload === "object") {
|
|
1497
|
+
const obj = payload;
|
|
1498
|
+
const candidate = obj.paymentProof ?? obj.proofHeader;
|
|
1499
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
1500
|
+
return candidate;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
throw new Error(
|
|
1504
|
+
"alipay payment payload must be a Base64URL string or {paymentProof: string} / {proofHeader: string}"
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
function decodeProof(proofHeader) {
|
|
1508
|
+
let parsed;
|
|
1509
|
+
try {
|
|
1510
|
+
parsed = JSON.parse(decodeBase64UrlWithPadFix(proofHeader));
|
|
1511
|
+
} catch (e) {
|
|
1512
|
+
throw new Error(
|
|
1513
|
+
`failed to decode Payment-Proof: ${e instanceof Error ? e.message : String(e)}`
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1517
|
+
throw new Error("decoded Payment-Proof is not an object");
|
|
1518
|
+
}
|
|
1519
|
+
const obj = parsed;
|
|
1520
|
+
const protocol = obj.protocol;
|
|
1521
|
+
const method = obj.method;
|
|
1522
|
+
if (!protocol || typeof protocol.payment_proof !== "string") {
|
|
1523
|
+
throw new Error("decoded Payment-Proof missing protocol.payment_proof");
|
|
1524
|
+
}
|
|
1525
|
+
if (typeof protocol.trade_no !== "string") {
|
|
1526
|
+
throw new Error("decoded Payment-Proof missing protocol.trade_no");
|
|
1527
|
+
}
|
|
1528
|
+
if (!method || typeof method.client_session !== "string") {
|
|
1529
|
+
throw new Error("decoded Payment-Proof missing method.client_session");
|
|
1530
|
+
}
|
|
1531
|
+
return parsed;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1048
1534
|
// src/facilitators/registry.ts
|
|
1049
1535
|
import { Keypair as Keypair2 } from "@solana/web3.js";
|
|
1050
1536
|
import bs58 from "bs58";
|
|
@@ -1069,6 +1555,7 @@ var FacilitatorRegistry = class {
|
|
|
1069
1555
|
}
|
|
1070
1556
|
return new SolanaFacilitator({ feePayerKeypair });
|
|
1071
1557
|
});
|
|
1558
|
+
this.registerFactory("alipay", (config) => new AlipayFacilitator(config));
|
|
1072
1559
|
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
1073
1560
|
}
|
|
1074
1561
|
/**
|
|
@@ -1293,6 +1780,11 @@ var PAYMENT_RESPONSE_HEADER = "x-payment-response";
|
|
|
1293
1780
|
var MPP_AUTH_HEADER = "authorization";
|
|
1294
1781
|
var MPP_WWW_AUTH_HEADER = "www-authenticate";
|
|
1295
1782
|
var MPP_RECEIPT_HEADER = "payment-receipt";
|
|
1783
|
+
var ALIPAY_PAYMENT_NEEDED_HEADER = "payment-needed";
|
|
1784
|
+
var ALIPAY_PAYMENT_PROOF_HEADER = "payment-proof";
|
|
1785
|
+
function headerSafe(value) {
|
|
1786
|
+
return String(value ?? "").replace(/[^\x20-\x7E]/g, (ch) => encodeURIComponent(ch)).replace(/"/g, "%22");
|
|
1787
|
+
}
|
|
1296
1788
|
var TOKEN_ADDRESSES = {
|
|
1297
1789
|
"eip155:8453": {
|
|
1298
1790
|
USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
@@ -1365,9 +1857,13 @@ var TOKEN_DOMAINS = {
|
|
|
1365
1857
|
USDT: { name: "(PoS) Tether USD", version: "2" }
|
|
1366
1858
|
},
|
|
1367
1859
|
// Tempo Moderato testnet - TIP-20 stablecoins
|
|
1860
|
+
// Domain names verified against on-chain DOMAIN_SEPARATOR values on 2026-04-21.
|
|
1861
|
+
// See docs/TEMPO-WEB-SUPPORT.md Section 2 and test/server/tempo-domain.test.ts.
|
|
1862
|
+
// All 4 Tempo TIP-20 tokens (pathUSD / AlphaUSD / BetaUSD / ThetaUSD) use
|
|
1863
|
+
// the token symbol with first letter capitalized + version "1".
|
|
1368
1864
|
"eip155:42431": {
|
|
1369
|
-
USDC: { name: "
|
|
1370
|
-
USDT: { name: "
|
|
1865
|
+
USDC: { name: "PathUSD", version: "1" },
|
|
1866
|
+
USDT: { name: "AlphaUSD", version: "1" }
|
|
1371
1867
|
},
|
|
1372
1868
|
// BNB Smart Chain mainnet
|
|
1373
1869
|
"eip155:56": {
|
|
@@ -1424,6 +1920,8 @@ var MoltsPayServer = class {
|
|
|
1424
1920
|
registry;
|
|
1425
1921
|
networkId;
|
|
1426
1922
|
useMainnet;
|
|
1923
|
+
/** Alipay AI 收 facilitator instance, set when `provider.alipay` is configured (2.0.0). */
|
|
1924
|
+
alipayFacilitator = null;
|
|
1427
1925
|
constructor(servicesPath, options = {}) {
|
|
1428
1926
|
loadEnvFile2();
|
|
1429
1927
|
const content = readFileSync2(servicesPath, "utf-8");
|
|
@@ -1445,7 +1943,38 @@ var MoltsPayServer = class {
|
|
|
1445
1943
|
cdp: { useMainnet: this.useMainnet }
|
|
1446
1944
|
}
|
|
1447
1945
|
};
|
|
1946
|
+
const providerAlipay = this.manifest.provider.alipay;
|
|
1947
|
+
if (providerAlipay) {
|
|
1948
|
+
try {
|
|
1949
|
+
const baseDir = path2.dirname(servicesPath);
|
|
1950
|
+
const resolvePem = (p, kind) => toPem(readFileSync2(path2.isAbsolute(p) ? p : path2.resolve(baseDir, p), "utf-8"), kind);
|
|
1951
|
+
const alipayFacilitatorConfig = {
|
|
1952
|
+
seller_id: providerAlipay.seller_id,
|
|
1953
|
+
app_id: providerAlipay.app_id,
|
|
1954
|
+
seller_name: providerAlipay.seller_name,
|
|
1955
|
+
service_id_default: providerAlipay.service_id_default,
|
|
1956
|
+
private_key_pem: resolvePem(providerAlipay.private_key_path, "PRIVATE"),
|
|
1957
|
+
alipay_public_key_pem: resolvePem(providerAlipay.alipay_public_key_path, "PUBLIC"),
|
|
1958
|
+
gateway_url: providerAlipay.gateway_url,
|
|
1959
|
+
sign_type: providerAlipay.sign_type
|
|
1960
|
+
};
|
|
1961
|
+
facilitatorConfig.config = {
|
|
1962
|
+
...facilitatorConfig.config,
|
|
1963
|
+
alipay: alipayFacilitatorConfig
|
|
1964
|
+
};
|
|
1965
|
+
facilitatorConfig.fallback = facilitatorConfig.fallback || [];
|
|
1966
|
+
if (facilitatorConfig.primary !== "alipay" && !facilitatorConfig.fallback.includes("alipay")) {
|
|
1967
|
+
facilitatorConfig.fallback.push("alipay");
|
|
1968
|
+
}
|
|
1969
|
+
} catch (err) {
|
|
1970
|
+
throw new Error(`[MoltsPay] Alipay rail configured but key load failed: ${err.message}`);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1448
1973
|
this.registry = new FacilitatorRegistry(facilitatorConfig);
|
|
1974
|
+
if (providerAlipay) {
|
|
1975
|
+
this.alipayFacilitator = this.registry.get("alipay");
|
|
1976
|
+
console.log(`[MoltsPay] Alipay AI \u6536 rail enabled (seller ${providerAlipay.seller_id})`);
|
|
1977
|
+
}
|
|
1449
1978
|
const primaryFacilitator = this.registry.get(facilitatorConfig.primary);
|
|
1450
1979
|
console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
|
|
1451
1980
|
console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
|
|
@@ -1535,14 +2064,63 @@ var MoltsPayServer = class {
|
|
|
1535
2064
|
console.log(` GET /health - Health check (incl. facilitators)`);
|
|
1536
2065
|
});
|
|
1537
2066
|
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Apply CORS response headers according to the `cors` option.
|
|
2069
|
+
*
|
|
2070
|
+
* Default (`cors` unset or `true`): `Access-Control-Allow-Origin: *`. Matches 1.5.x behavior
|
|
2071
|
+
* and works for every browser client whose origin does not need to send cookies.
|
|
2072
|
+
*
|
|
2073
|
+
* `cors: false`: emit no CORS headers. Same-origin only.
|
|
2074
|
+
* `cors: string[]`: origin allowlist — echo the origin back iff it matches.
|
|
2075
|
+
* `cors: CorsOptions`: full control (allowlist + credentials + maxAge).
|
|
2076
|
+
*
|
|
2077
|
+
* The required-for-Web response headers are always exposed when CORS is active:
|
|
2078
|
+
* `X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt`.
|
|
2079
|
+
*/
|
|
2080
|
+
applyCorsHeaders(req, res) {
|
|
2081
|
+
const cors = this.options.cors;
|
|
2082
|
+
if (cors === false) {
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
const requestOrigin = req.headers.origin ?? "*";
|
|
2086
|
+
if (cors === void 0 || cors === true) {
|
|
2087
|
+
this.writeCorsHeaders(res, "*");
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
if (Array.isArray(cors)) {
|
|
2091
|
+
if (cors.includes(requestOrigin)) {
|
|
2092
|
+
this.writeCorsHeaders(res, requestOrigin);
|
|
2093
|
+
res.setHeader("Vary", "Origin");
|
|
2094
|
+
}
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
const opt = cors;
|
|
2098
|
+
const isAllowed = typeof opt.origins === "function" ? opt.origins(requestOrigin) : opt.origins.includes(requestOrigin);
|
|
2099
|
+
if (!isAllowed) {
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
this.writeCorsHeaders(res, requestOrigin);
|
|
2103
|
+
res.setHeader("Vary", "Origin");
|
|
2104
|
+
if (opt.credentials) {
|
|
2105
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
2106
|
+
}
|
|
2107
|
+
const maxAge = opt.maxAge ?? 600;
|
|
2108
|
+
res.setHeader("Access-Control-Max-Age", String(maxAge));
|
|
2109
|
+
}
|
|
2110
|
+
writeCorsHeaders(res, origin) {
|
|
2111
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
2112
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
2113
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization, Payment-Proof");
|
|
2114
|
+
res.setHeader(
|
|
2115
|
+
"Access-Control-Expose-Headers",
|
|
2116
|
+
"X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt, Payment-Needed"
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
1538
2119
|
/**
|
|
1539
2120
|
* Handle incoming request
|
|
1540
2121
|
*/
|
|
1541
2122
|
async handleRequest(req, res) {
|
|
1542
|
-
|
|
1543
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1544
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
|
|
1545
|
-
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt");
|
|
2123
|
+
this.applyCorsHeaders(req, res);
|
|
1546
2124
|
if (req.method === "OPTIONS") {
|
|
1547
2125
|
res.writeHead(204);
|
|
1548
2126
|
res.end();
|
|
@@ -1562,7 +2140,8 @@ var MoltsPayServer = class {
|
|
|
1562
2140
|
if (url.pathname === "/execute" && req.method === "POST") {
|
|
1563
2141
|
const body = await this.readBody(req);
|
|
1564
2142
|
const paymentHeader = req.headers[PAYMENT_HEADER];
|
|
1565
|
-
|
|
2143
|
+
const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
|
|
2144
|
+
return await this.handleExecute(body, paymentHeader, res, proofHeader);
|
|
1566
2145
|
}
|
|
1567
2146
|
if (url.pathname === "/proxy" && req.method === "POST") {
|
|
1568
2147
|
const clientIP = req.headers["x-real-ip"] || req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "";
|
|
@@ -1580,7 +2159,8 @@ var MoltsPayServer = class {
|
|
|
1580
2159
|
const body = req.method === "POST" ? await this.readBody(req) : {};
|
|
1581
2160
|
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
1582
2161
|
const x402Header = req.headers[PAYMENT_HEADER];
|
|
1583
|
-
|
|
2162
|
+
const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
|
|
2163
|
+
return await this.handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader);
|
|
1584
2164
|
}
|
|
1585
2165
|
this.sendJson(res, 404, { error: "Not found" });
|
|
1586
2166
|
} catch (err) {
|
|
@@ -1677,7 +2257,7 @@ var MoltsPayServer = class {
|
|
|
1677
2257
|
/**
|
|
1678
2258
|
* POST /execute - Execute service with x402 payment
|
|
1679
2259
|
*/
|
|
1680
|
-
async handleExecute(body, paymentHeader, res) {
|
|
2260
|
+
async handleExecute(body, paymentHeader, res, proofHeader) {
|
|
1681
2261
|
const { service, params } = body;
|
|
1682
2262
|
if (!service) {
|
|
1683
2263
|
return this.sendJson(res, 400, { error: "Missing service" });
|
|
@@ -1691,6 +2271,15 @@ var MoltsPayServer = class {
|
|
|
1691
2271
|
return this.sendJson(res, 400, { error: `Missing required param: ${key}` });
|
|
1692
2272
|
}
|
|
1693
2273
|
}
|
|
2274
|
+
if (proofHeader) {
|
|
2275
|
+
const alipayPayment = {
|
|
2276
|
+
x402Version: X402_VERSION2,
|
|
2277
|
+
scheme: ALIPAY_SCHEME,
|
|
2278
|
+
network: ALIPAY_NETWORK,
|
|
2279
|
+
payload: proofHeader
|
|
2280
|
+
};
|
|
2281
|
+
return this.handleAlipayExecute(skill, params || {}, alipayPayment, res);
|
|
2282
|
+
}
|
|
1694
2283
|
if (!paymentHeader) {
|
|
1695
2284
|
return this.sendPaymentRequired(skill.config, res);
|
|
1696
2285
|
}
|
|
@@ -1701,6 +2290,11 @@ var MoltsPayServer = class {
|
|
|
1701
2290
|
} catch {
|
|
1702
2291
|
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
1703
2292
|
}
|
|
2293
|
+
const payScheme = payment.accepted?.scheme || payment.scheme;
|
|
2294
|
+
const payNetwork = payment.accepted?.network || payment.network;
|
|
2295
|
+
if (payScheme === ALIPAY_SCHEME || (payNetwork ? isAlipayChainId(payNetwork) : false)) {
|
|
2296
|
+
return this.handleAlipayExecute(skill, params || {}, payment, res);
|
|
2297
|
+
}
|
|
1704
2298
|
const validation = this.validatePayment(payment, skill.config);
|
|
1705
2299
|
if (!validation.valid) {
|
|
1706
2300
|
return this.sendJson(res, 402, { error: validation.error });
|
|
@@ -1765,6 +2359,14 @@ var MoltsPayServer = class {
|
|
|
1765
2359
|
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1766
2360
|
} catch (err) {
|
|
1767
2361
|
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
2362
|
+
settlement = { success: false, error: err.message, facilitator: "none" };
|
|
2363
|
+
}
|
|
2364
|
+
if (!settlement?.success) {
|
|
2365
|
+
return this.sendJson(res, 402, {
|
|
2366
|
+
error: "Payment settlement failed",
|
|
2367
|
+
message: settlement?.error || "Settlement returned no success state",
|
|
2368
|
+
facilitator: settlement?.facilitator
|
|
2369
|
+
});
|
|
1768
2370
|
}
|
|
1769
2371
|
}
|
|
1770
2372
|
const responseHeaders = {};
|
|
@@ -1785,13 +2387,111 @@ var MoltsPayServer = class {
|
|
|
1785
2387
|
payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
|
|
1786
2388
|
}, responseHeaders);
|
|
1787
2389
|
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Execute a service paid via the Alipay AI 收 fiat rail (2.0.0).
|
|
2392
|
+
*
|
|
2393
|
+
* Differs from the EVM/SVM path: no token detection, no EIP-3009/permit
|
|
2394
|
+
* validation. Verify hits the Alipay Open API (`payment.verify`). Settlement
|
|
2395
|
+
* (`fulfillment.confirm`) is FIRE-AND-FORGET per ALIPAY-INTEGRATION-DESIGN
|
|
2396
|
+
* §5.1: a confirm failure is logged but does NOT fail the already-delivered
|
|
2397
|
+
* response (the buyer's payment proof was already verified).
|
|
2398
|
+
*/
|
|
2399
|
+
async handleAlipayExecute(skill, params, payment, res) {
|
|
2400
|
+
if (!this.alipayFacilitator) {
|
|
2401
|
+
return this.sendJson(res, 402, { error: "Alipay rail not configured on this server" });
|
|
2402
|
+
}
|
|
2403
|
+
const requirements = {
|
|
2404
|
+
scheme: ALIPAY_SCHEME,
|
|
2405
|
+
network: ALIPAY_NETWORK,
|
|
2406
|
+
asset: "CNY",
|
|
2407
|
+
amount: skill.config.alipay?.price_cny || "0",
|
|
2408
|
+
payTo: this.manifest.provider.alipay?.seller_id || "",
|
|
2409
|
+
maxTimeoutSeconds: 1800
|
|
2410
|
+
};
|
|
2411
|
+
console.log(`[MoltsPay] Verifying Alipay payment...`);
|
|
2412
|
+
const verifyResult = await this.registry.verify(payment, requirements);
|
|
2413
|
+
if (!verifyResult.valid) {
|
|
2414
|
+
return this.sendJson(res, 402, {
|
|
2415
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
2416
|
+
facilitator: verifyResult.facilitator
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
console.log(`[MoltsPay] Alipay payment verified by ${verifyResult.facilitator}`);
|
|
2420
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
2421
|
+
console.log(`[MoltsPay] Executing skill: ${skill.id} (timeout: ${timeoutSeconds}s)`);
|
|
2422
|
+
let result;
|
|
2423
|
+
try {
|
|
2424
|
+
result = await Promise.race([
|
|
2425
|
+
skill.handler(params),
|
|
2426
|
+
new Promise(
|
|
2427
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
2428
|
+
)
|
|
2429
|
+
]);
|
|
2430
|
+
} catch (err) {
|
|
2431
|
+
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
2432
|
+
return this.sendJson(res, 500, {
|
|
2433
|
+
error: "Service execution failed",
|
|
2434
|
+
message: err.message
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
let settlement;
|
|
2438
|
+
try {
|
|
2439
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2440
|
+
if (settlement.success) {
|
|
2441
|
+
console.log(`[MoltsPay] Alipay fulfillment confirmed: ${settlement.transaction}`);
|
|
2442
|
+
} else {
|
|
2443
|
+
console.error(`[MoltsPay] Alipay fulfillment confirm failed (non-fatal): ${settlement.error}`);
|
|
2444
|
+
}
|
|
2445
|
+
} catch (err) {
|
|
2446
|
+
console.error(`[MoltsPay] Alipay fulfillment confirm threw (non-fatal): ${err.message}`);
|
|
2447
|
+
settlement = { success: false, error: err.message, facilitator: "alipay" };
|
|
2448
|
+
}
|
|
2449
|
+
const responseHeaders = {};
|
|
2450
|
+
if (settlement.success) {
|
|
2451
|
+
responseHeaders[PAYMENT_RESPONSE_HEADER] = Buffer.from(JSON.stringify({
|
|
2452
|
+
success: true,
|
|
2453
|
+
transaction: settlement.transaction,
|
|
2454
|
+
network: ALIPAY_NETWORK,
|
|
2455
|
+
facilitator: settlement.facilitator
|
|
2456
|
+
})).toString("base64");
|
|
2457
|
+
}
|
|
2458
|
+
this.sendJson(res, 200, {
|
|
2459
|
+
success: true,
|
|
2460
|
+
result,
|
|
2461
|
+
payment: settlement.success ? { transaction: settlement.transaction, status: "fulfilled", facilitator: settlement.facilitator } : { status: "delivered_unconfirmed", error: settlement.error }
|
|
2462
|
+
}, responseHeaders);
|
|
2463
|
+
}
|
|
2464
|
+
/**
|
|
2465
|
+
* Build the Alipay 402 challenge for a service, or null when the alipay rail
|
|
2466
|
+
* isn't configured for this server or this service. Returns the x402
|
|
2467
|
+
* `accepts[]` entry plus the Base64URL `Payment-Needed` header value so the
|
|
2468
|
+
* 402 responders can dual-emit both the x402 and legacy alipay-bot formats.
|
|
2469
|
+
*/
|
|
2470
|
+
async buildAlipayChallenge(config) {
|
|
2471
|
+
if (!this.alipayFacilitator || !config.alipay) return null;
|
|
2472
|
+
try {
|
|
2473
|
+
const req = await this.alipayFacilitator.createPaymentRequirements({
|
|
2474
|
+
serviceId: config.alipay.service_id || this.manifest.provider.alipay.service_id_default,
|
|
2475
|
+
priceCny: config.alipay.price_cny,
|
|
2476
|
+
goodsName: config.alipay.goods_name,
|
|
2477
|
+
resourceId: `/execute?service=${config.id}`
|
|
2478
|
+
});
|
|
2479
|
+
return { accepts: req.x402Accepts, paymentNeededHeader: req.paymentNeededHeader };
|
|
2480
|
+
} catch (err) {
|
|
2481
|
+
console.error(`[MoltsPay] Alipay challenge build failed for ${config.id}: ${err.message}`);
|
|
2482
|
+
return null;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
1788
2485
|
/**
|
|
1789
2486
|
* Handle MPP (Machine Payments Protocol) request
|
|
1790
2487
|
* Supports both x402 and MPP protocols on service endpoints
|
|
1791
2488
|
*/
|
|
1792
|
-
async handleMPPRequest(skill, body, authHeader, x402Header, res) {
|
|
2489
|
+
async handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader) {
|
|
1793
2490
|
const config = skill.config;
|
|
1794
2491
|
const params = body || {};
|
|
2492
|
+
if (proofHeader) {
|
|
2493
|
+
return await this.handleExecute({ service: config.id, params }, void 0, res, proofHeader);
|
|
2494
|
+
}
|
|
1795
2495
|
if (x402Header) {
|
|
1796
2496
|
return await this.handleExecute({ service: config.id, params }, x402Header, res);
|
|
1797
2497
|
}
|
|
@@ -1897,7 +2597,7 @@ var MoltsPayServer = class {
|
|
|
1897
2597
|
/**
|
|
1898
2598
|
* Return 402 with both x402 and MPP payment requirements
|
|
1899
2599
|
*/
|
|
1900
|
-
sendMPPPaymentRequired(config, res) {
|
|
2600
|
+
async sendMPPPaymentRequired(config, res) {
|
|
1901
2601
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
1902
2602
|
const providerChains = this.getProviderChains();
|
|
1903
2603
|
const accepts = [];
|
|
@@ -1908,6 +2608,10 @@ var MoltsPayServer = class {
|
|
|
1908
2608
|
}
|
|
1909
2609
|
}
|
|
1910
2610
|
}
|
|
2611
|
+
const alipayChallenge = await this.buildAlipayChallenge(config);
|
|
2612
|
+
if (alipayChallenge) {
|
|
2613
|
+
accepts.push(alipayChallenge.accepts);
|
|
2614
|
+
}
|
|
1911
2615
|
const x402PaymentRequired = {
|
|
1912
2616
|
x402Version: X402_VERSION2,
|
|
1913
2617
|
accepts,
|
|
@@ -1935,7 +2639,7 @@ var MoltsPayServer = class {
|
|
|
1935
2639
|
};
|
|
1936
2640
|
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
1937
2641
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
1938
|
-
mppWwwAuth = `Payment id="${challengeId}", realm="${this.manifest.provider.name}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
2642
|
+
mppWwwAuth = `Payment id="${challengeId}", realm="${headerSafe(this.manifest.provider.name)}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
|
|
1939
2643
|
}
|
|
1940
2644
|
const headers = {
|
|
1941
2645
|
"Content-Type": "application/problem+json",
|
|
@@ -1944,6 +2648,9 @@ var MoltsPayServer = class {
|
|
|
1944
2648
|
if (mppWwwAuth) {
|
|
1945
2649
|
headers[MPP_WWW_AUTH_HEADER] = mppWwwAuth;
|
|
1946
2650
|
}
|
|
2651
|
+
if (alipayChallenge) {
|
|
2652
|
+
headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
|
|
2653
|
+
}
|
|
1947
2654
|
res.writeHead(402, headers);
|
|
1948
2655
|
res.end(JSON.stringify({
|
|
1949
2656
|
type: "https://paymentauth.org/problems/payment-required",
|
|
@@ -1970,7 +2677,7 @@ var MoltsPayServer = class {
|
|
|
1970
2677
|
* Return 402 with x402 payment requirements (v2 format)
|
|
1971
2678
|
* Includes requirements for all chains and all accepted currencies
|
|
1972
2679
|
*/
|
|
1973
|
-
sendPaymentRequired(config, res) {
|
|
2680
|
+
async sendPaymentRequired(config, res) {
|
|
1974
2681
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
1975
2682
|
const providerChains = this.getProviderChains();
|
|
1976
2683
|
const accepts = [];
|
|
@@ -1981,6 +2688,10 @@ var MoltsPayServer = class {
|
|
|
1981
2688
|
}
|
|
1982
2689
|
}
|
|
1983
2690
|
}
|
|
2691
|
+
const alipayChallenge = await this.buildAlipayChallenge(config);
|
|
2692
|
+
if (alipayChallenge) {
|
|
2693
|
+
accepts.push(alipayChallenge.accepts);
|
|
2694
|
+
}
|
|
1984
2695
|
const acceptedChains = providerChains.map((c) => {
|
|
1985
2696
|
if (c.network === "eip155:8453") return "base";
|
|
1986
2697
|
if (c.network === "eip155:137") return "polygon";
|
|
@@ -1998,10 +2709,14 @@ var MoltsPayServer = class {
|
|
|
1998
2709
|
}
|
|
1999
2710
|
};
|
|
2000
2711
|
const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
|
|
2001
|
-
|
|
2712
|
+
const headers = {
|
|
2002
2713
|
"Content-Type": "application/json",
|
|
2003
2714
|
[PAYMENT_REQUIRED_HEADER]: encoded
|
|
2004
|
-
}
|
|
2715
|
+
};
|
|
2716
|
+
if (alipayChallenge) {
|
|
2717
|
+
headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
|
|
2718
|
+
}
|
|
2719
|
+
res.writeHead(402, headers);
|
|
2005
2720
|
res.end(JSON.stringify({
|
|
2006
2721
|
error: "Payment required",
|
|
2007
2722
|
message: `Service requires $${config.price} ${config.currency}`,
|
|
@@ -2019,7 +2734,7 @@ var MoltsPayServer = class {
|
|
|
2019
2734
|
}
|
|
2020
2735
|
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
2021
2736
|
const network = payment.accepted?.network || payment.network || this.networkId;
|
|
2022
|
-
if (scheme !== "exact") {
|
|
2737
|
+
if (scheme !== "exact" && scheme !== "permit") {
|
|
2023
2738
|
return { valid: false, error: `Unsupported scheme: ${scheme}` };
|
|
2024
2739
|
}
|
|
2025
2740
|
if (!this.isNetworkAccepted(network)) {
|
|
@@ -2041,8 +2756,10 @@ var MoltsPayServer = class {
|
|
|
2041
2756
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
2042
2757
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
2043
2758
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
2759
|
+
const isTempo = selectedNetwork === "eip155:42431";
|
|
2760
|
+
const scheme = isTempo ? "permit" : "exact";
|
|
2044
2761
|
const requirements = {
|
|
2045
|
-
scheme
|
|
2762
|
+
scheme,
|
|
2046
2763
|
network: selectedNetwork,
|
|
2047
2764
|
asset: tokenAddress,
|
|
2048
2765
|
amount: amountInUnits,
|
|
@@ -2070,6 +2787,16 @@ var MoltsPayServer = class {
|
|
|
2070
2787
|
};
|
|
2071
2788
|
}
|
|
2072
2789
|
}
|
|
2790
|
+
if (isTempo) {
|
|
2791
|
+
const tempoFacilitator = this.registry.get("tempo");
|
|
2792
|
+
const tempoSpender = tempoFacilitator?.getSpenderAddress?.();
|
|
2793
|
+
if (tempoSpender) {
|
|
2794
|
+
requirements.extra = {
|
|
2795
|
+
...requirements.extra || {},
|
|
2796
|
+
tempoSpender
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2073
2800
|
return requirements;
|
|
2074
2801
|
}
|
|
2075
2802
|
/**
|
|
@@ -2096,12 +2823,12 @@ var MoltsPayServer = class {
|
|
|
2096
2823
|
return accepted.includes(token);
|
|
2097
2824
|
}
|
|
2098
2825
|
async readBody(req) {
|
|
2099
|
-
return new Promise((
|
|
2826
|
+
return new Promise((resolve2, reject) => {
|
|
2100
2827
|
let body = "";
|
|
2101
2828
|
req.on("data", (chunk) => body += chunk);
|
|
2102
2829
|
req.on("end", () => {
|
|
2103
2830
|
try {
|
|
2104
|
-
|
|
2831
|
+
resolve2(body ? JSON.parse(body) : {});
|
|
2105
2832
|
} catch {
|
|
2106
2833
|
reject(new Error("Invalid JSON"));
|
|
2107
2834
|
}
|
|
@@ -2204,7 +2931,7 @@ var MoltsPayServer = class {
|
|
|
2204
2931
|
}
|
|
2205
2932
|
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
2206
2933
|
const network = payment.accepted?.network || payment.network;
|
|
2207
|
-
if (scheme !== "exact") {
|
|
2934
|
+
if (scheme !== "exact" && scheme !== "permit") {
|
|
2208
2935
|
return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
|
|
2209
2936
|
}
|
|
2210
2937
|
const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;
|
|
@@ -2359,7 +3086,7 @@ var MoltsPayServer = class {
|
|
|
2359
3086
|
};
|
|
2360
3087
|
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
2361
3088
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
2362
|
-
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
3089
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
|
|
2363
3090
|
res.writeHead(402, {
|
|
2364
3091
|
"Content-Type": "application/problem+json",
|
|
2365
3092
|
[MPP_WWW_AUTH_HEADER]: wwwAuth
|