moltspay 1.6.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/{cdp-DeohBe1o.d.ts → cdp-B5PhDUTC.d.ts} +1 -1
- package/dist/{cdp-p_eHuQpb.d.mts → cdp-D-5Hg3y_.d.mts} +1 -1
- package/dist/chains/index.d.mts +37 -1
- package/dist/chains/index.d.ts +37 -1
- package/dist/chains/index.js +22 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +19 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +1466 -61
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1466 -61
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +41 -0
- package/dist/client/index.d.ts +41 -0
- package/dist/client/index.js +824 -10
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +824 -10
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/web/index.d.mts +17 -1
- package/dist/client/web/index.mjs +11 -0
- package/dist/client/web/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +161 -4
- package/dist/facilitators/index.d.ts +161 -4
- package/dist/facilitators/index.js +370 -0
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +365 -0
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/index.d.mts +77 -2
- package/dist/index.d.ts +77 -2
- package/dist/index.js +1412 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1406 -31
- package/dist/index.mjs.map +1 -1
- package/dist/mcp/index.js +828 -14
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/index.mjs +828 -14
- package/dist/mcp/index.mjs.map +1 -1
- package/dist/{registry-OsEO2dOu.d.mts → registry-DIo0WNoO.d.mts} +8 -1
- package/dist/{registry-OsEO2dOu.d.ts → registry-DIo0WNoO.d.ts} +8 -1
- package/dist/server/index.d.mts +95 -2
- package/dist/server/index.d.ts +95 -2
- package/dist/server/index.js +554 -14
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +554 -14
- package/dist/server/index.mjs.map +1 -1
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +7 -1
- package/schemas/moltspay.services.schema.json +100 -6
- package/scripts/postinstall.js +91 -0
package/dist/cli/index.mjs
CHANGED
|
@@ -21,10 +21,10 @@ var init_esm_shims = __esm({
|
|
|
21
21
|
init_esm_shims();
|
|
22
22
|
import { webcrypto } from "crypto";
|
|
23
23
|
import { Command } from "commander";
|
|
24
|
-
import { homedir as
|
|
25
|
-
import { join as
|
|
24
|
+
import { homedir as homedir4 } from "os";
|
|
25
|
+
import { join as join6, dirname as dirname2, resolve as resolve2 } from "path";
|
|
26
26
|
import { existsSync as existsSync5, writeFileSync as writeFileSync3, readFileSync as readFileSync5, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
|
|
27
|
-
import { spawn } from "child_process";
|
|
27
|
+
import { spawn as spawn2 } from "child_process";
|
|
28
28
|
import { ethers as ethers4 } from "ethers";
|
|
29
29
|
|
|
30
30
|
// src/client/index.ts
|
|
@@ -33,8 +33,8 @@ init_esm_shims();
|
|
|
33
33
|
// src/client/node/index.ts
|
|
34
34
|
init_esm_shims();
|
|
35
35
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, statSync, chmodSync } from "fs";
|
|
36
|
-
import { homedir as
|
|
37
|
-
import { join as
|
|
36
|
+
import { homedir as homedir3 } from "os";
|
|
37
|
+
import { join as join3 } from "path";
|
|
38
38
|
import { Wallet as Wallet2, ethers as ethers2 } from "ethers";
|
|
39
39
|
|
|
40
40
|
// src/chains/index.ts
|
|
@@ -211,6 +211,10 @@ function getChain(name) {
|
|
|
211
211
|
}
|
|
212
212
|
return config;
|
|
213
213
|
}
|
|
214
|
+
var ALIPAY_CHAIN_ID = "alipay";
|
|
215
|
+
function isAlipayChainId(id) {
|
|
216
|
+
return id === ALIPAY_CHAIN_ID;
|
|
217
|
+
}
|
|
214
218
|
|
|
215
219
|
// src/wallet/solana.ts
|
|
216
220
|
init_esm_shims();
|
|
@@ -612,6 +616,14 @@ var BufferCtor = globalThis.Buffer;
|
|
|
612
616
|
|
|
613
617
|
// src/client/core/errors.ts
|
|
614
618
|
init_esm_shims();
|
|
619
|
+
var MoltsPayError = class extends Error {
|
|
620
|
+
code;
|
|
621
|
+
constructor(code, message) {
|
|
622
|
+
super(message);
|
|
623
|
+
this.name = "MoltsPayError";
|
|
624
|
+
this.code = code;
|
|
625
|
+
}
|
|
626
|
+
};
|
|
615
627
|
|
|
616
628
|
// src/client/core/eip3009.ts
|
|
617
629
|
init_esm_shims();
|
|
@@ -761,6 +773,740 @@ function findChainByChainId(chainId) {
|
|
|
761
773
|
return void 0;
|
|
762
774
|
}
|
|
763
775
|
|
|
776
|
+
// src/client/alipay/index.ts
|
|
777
|
+
init_esm_shims();
|
|
778
|
+
import { randomUUID } from "crypto";
|
|
779
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
780
|
+
import { homedir as homedir2 } from "os";
|
|
781
|
+
import { join as join2 } from "path";
|
|
782
|
+
|
|
783
|
+
// src/client/alipay/cli.ts
|
|
784
|
+
init_esm_shims();
|
|
785
|
+
import { spawn } from "child_process";
|
|
786
|
+
|
|
787
|
+
// src/client/alipay/log.ts
|
|
788
|
+
init_esm_shims();
|
|
789
|
+
var RANK = { off: 0, info: 1, debug: 2 };
|
|
790
|
+
function normalizeLevel(v) {
|
|
791
|
+
const s = (v ?? "").toLowerCase();
|
|
792
|
+
return s === "info" || s === "debug" ? s : "off";
|
|
793
|
+
}
|
|
794
|
+
var level = normalizeLevel(process.env.MOLTSPAY_ALIPAY_LOG);
|
|
795
|
+
var sink = (line) => {
|
|
796
|
+
process.stderr.write(line + "\n");
|
|
797
|
+
};
|
|
798
|
+
function enabledFor(want) {
|
|
799
|
+
return RANK[level] >= RANK[want];
|
|
800
|
+
}
|
|
801
|
+
function render(event, fields) {
|
|
802
|
+
const parts = ["[moltspay:alipay]", String(fields.ts), event];
|
|
803
|
+
if (fields.flow) parts.push(`flow=${fields.flow}`);
|
|
804
|
+
if (fields.step) parts.push(`step=${fields.step}`);
|
|
805
|
+
if (typeof fields.ms === "number") parts.push(`${fields.ms}ms`);
|
|
806
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
807
|
+
if (["ts", "level", "event", "flow", "step", "ms"].includes(k)) continue;
|
|
808
|
+
parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
809
|
+
}
|
|
810
|
+
return parts.join(" ");
|
|
811
|
+
}
|
|
812
|
+
function emit(lvl, event, fields) {
|
|
813
|
+
if (!enabledFor(lvl)) return;
|
|
814
|
+
const full = { ts: (/* @__PURE__ */ new Date()).toISOString(), level: lvl, event, ...fields };
|
|
815
|
+
try {
|
|
816
|
+
sink(render(event, full), full);
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
var alipayLog = {
|
|
821
|
+
info: (event, fields = {}) => emit("info", event, fields),
|
|
822
|
+
debug: (event, fields = {}) => emit("debug", event, fields),
|
|
823
|
+
/** True if anything at all would be logged (cheap guard for hot paths). */
|
|
824
|
+
enabled: () => level !== "off"
|
|
825
|
+
};
|
|
826
|
+
async function timeStep(step, flow, fn) {
|
|
827
|
+
if (!alipayLog.enabled()) return fn();
|
|
828
|
+
const t0 = Date.now();
|
|
829
|
+
alipayLog.debug("step.start", { flow, step });
|
|
830
|
+
try {
|
|
831
|
+
const out = await fn();
|
|
832
|
+
alipayLog.info("step.end", { flow, step, ms: Date.now() - t0, ok: true });
|
|
833
|
+
return out;
|
|
834
|
+
} catch (err) {
|
|
835
|
+
alipayLog.info("step.end", {
|
|
836
|
+
flow,
|
|
837
|
+
step,
|
|
838
|
+
ms: Date.now() - t0,
|
|
839
|
+
ok: false,
|
|
840
|
+
err: err instanceof Error ? err.message : String(err)
|
|
841
|
+
});
|
|
842
|
+
throw err;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/client/alipay/cli.ts
|
|
847
|
+
var ALLOWED_ENV = /* @__PURE__ */ new Set([
|
|
848
|
+
"AIPAY_OUTPUT_CHANNEL",
|
|
849
|
+
"AIPAY_SESSION_ID",
|
|
850
|
+
"AIPAY_FRAMEWORK",
|
|
851
|
+
"AIPAY_MODEL",
|
|
852
|
+
"AIPAY_OS",
|
|
853
|
+
// Minimal survival set for spawn to find the binary and a home dir.
|
|
854
|
+
"PATH",
|
|
855
|
+
"HOME"
|
|
856
|
+
]);
|
|
857
|
+
function filterEnv(env) {
|
|
858
|
+
return Object.fromEntries(
|
|
859
|
+
Object.entries(env).filter(([k]) => ALLOWED_ENV.has(k))
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
function makeLineSplitter(onLine) {
|
|
863
|
+
let buf = "";
|
|
864
|
+
return (chunk) => {
|
|
865
|
+
buf += chunk.toString("utf-8");
|
|
866
|
+
let nl;
|
|
867
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
868
|
+
onLine(buf.slice(0, nl));
|
|
869
|
+
buf = buf.slice(nl + 1);
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function profileEnv(step, flow) {
|
|
874
|
+
const want = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE;
|
|
875
|
+
const hook = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_HOOK;
|
|
876
|
+
if (!want || !hook) return {};
|
|
877
|
+
const steps = want.split(",").map((s) => s.trim());
|
|
878
|
+
if (!steps.includes("all") && !steps.includes(step)) return {};
|
|
879
|
+
const dir = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_DIR || "/tmp";
|
|
880
|
+
const safe = `${flow ?? "noflow"}-${step}-${Date.now()}`.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
881
|
+
const out = `${dir}/cli-profile-${safe}.json`;
|
|
882
|
+
alipayLog.info("cli.profile", { flow, step, out });
|
|
883
|
+
return {
|
|
884
|
+
NODE_OPTIONS: `--require=${hook}`,
|
|
885
|
+
MOLTSPAY_CLI_PROFILE_OUT: out
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
var runCli = (args, opts = {}) => {
|
|
889
|
+
const bin = opts.bin ?? "alipay-bot";
|
|
890
|
+
const step = opts.step ?? args[0] ?? "(no-arg)";
|
|
891
|
+
const startedAt = Date.now();
|
|
892
|
+
const lines = [];
|
|
893
|
+
const collect = (line) => {
|
|
894
|
+
lines.push(line);
|
|
895
|
+
alipayLog.debug("cli.line", {
|
|
896
|
+
flow: opts.flow,
|
|
897
|
+
step,
|
|
898
|
+
ms: Date.now() - startedAt,
|
|
899
|
+
preview: line.slice(0, 120)
|
|
900
|
+
});
|
|
901
|
+
opts.onLine?.(line);
|
|
902
|
+
};
|
|
903
|
+
alipayLog.debug("step.start", { flow: opts.flow, step });
|
|
904
|
+
return new Promise((resolve3, reject) => {
|
|
905
|
+
const child = spawn(bin, args, {
|
|
906
|
+
env: { ...filterEnv(process.env), ...profileEnv(step, opts.flow), ...opts.env ?? {} }
|
|
907
|
+
});
|
|
908
|
+
if (opts.signal) {
|
|
909
|
+
if (opts.signal.aborted) child.kill("SIGTERM");
|
|
910
|
+
opts.signal.addEventListener("abort", () => child.kill("SIGTERM"), { once: true });
|
|
911
|
+
}
|
|
912
|
+
let sawByte = false;
|
|
913
|
+
const onChunk = (stream) => (chunk) => {
|
|
914
|
+
const ms = Date.now() - startedAt;
|
|
915
|
+
if (!sawByte) {
|
|
916
|
+
sawByte = true;
|
|
917
|
+
alipayLog.debug("cli.firstbyte", { flow: opts.flow, step, ms });
|
|
918
|
+
}
|
|
919
|
+
alipayLog.debug("cli.chunk", { flow: opts.flow, step, ms, stream, bytes: chunk.length });
|
|
920
|
+
};
|
|
921
|
+
const onStdout = makeLineSplitter(collect);
|
|
922
|
+
const onStderr = makeLineSplitter(collect);
|
|
923
|
+
child.stdout?.on("data", onChunk("out"));
|
|
924
|
+
child.stderr?.on("data", onChunk("err"));
|
|
925
|
+
child.stdout?.on("data", onStdout);
|
|
926
|
+
child.stderr?.on("data", onStderr);
|
|
927
|
+
child.on("error", (err) => {
|
|
928
|
+
alipayLog.info("cli.exit", {
|
|
929
|
+
flow: opts.flow,
|
|
930
|
+
step,
|
|
931
|
+
ms: Date.now() - startedAt,
|
|
932
|
+
ok: false,
|
|
933
|
+
err: err.message
|
|
934
|
+
});
|
|
935
|
+
reject(err);
|
|
936
|
+
});
|
|
937
|
+
child.on("close", (code) => {
|
|
938
|
+
alipayLog.info("cli.exit", {
|
|
939
|
+
flow: opts.flow,
|
|
940
|
+
step,
|
|
941
|
+
ms: Date.now() - startedAt,
|
|
942
|
+
ok: (code ?? 1) === 0,
|
|
943
|
+
code: code ?? 1
|
|
944
|
+
});
|
|
945
|
+
resolve3({ exitCode: code ?? 1, lines });
|
|
946
|
+
});
|
|
947
|
+
});
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
// src/client/alipay/install.ts
|
|
951
|
+
init_esm_shims();
|
|
952
|
+
import { execFile } from "child_process";
|
|
953
|
+
import { promisify } from "util";
|
|
954
|
+
|
|
955
|
+
// src/client/alipay/errors.ts
|
|
956
|
+
init_esm_shims();
|
|
957
|
+
var AlipayCliNotFoundError = class extends MoltsPayError {
|
|
958
|
+
constructor(message) {
|
|
959
|
+
super("ALIPAY_CLI_NOT_FOUND", message);
|
|
960
|
+
this.name = "AlipayCliNotFoundError";
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
var AlipayCliVersionError = class extends MoltsPayError {
|
|
964
|
+
constructor(message) {
|
|
965
|
+
super("ALIPAY_CLI_VERSION", message);
|
|
966
|
+
this.name = "AlipayCliVersionError";
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
var NeedsWalletSetupError = class extends MoltsPayError {
|
|
970
|
+
constructor(message = "Alipay wallet not set up. Run: moltspay alipay apply") {
|
|
971
|
+
super("ALIPAY_NEEDS_WALLET_SETUP", message);
|
|
972
|
+
this.name = "NeedsWalletSetupError";
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
var AlipayPaymentRejectedError = class extends MoltsPayError {
|
|
976
|
+
constructor(message) {
|
|
977
|
+
super("ALIPAY_PAYMENT_REJECTED", message);
|
|
978
|
+
this.name = "AlipayPaymentRejectedError";
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
var AlipayPaymentTimeoutError = class extends MoltsPayError {
|
|
982
|
+
constructor(message) {
|
|
983
|
+
super("ALIPAY_PAYMENT_TIMEOUT", message);
|
|
984
|
+
this.name = "AlipayPaymentTimeoutError";
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
var AlipayProtocolError = class extends MoltsPayError {
|
|
988
|
+
constructor(message) {
|
|
989
|
+
super("ALIPAY_PROTOCOL", message);
|
|
990
|
+
this.name = "AlipayProtocolError";
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
var UnsupportedRailError = class extends MoltsPayError {
|
|
994
|
+
constructor(rail, message) {
|
|
995
|
+
super("UNSUPPORTED_RAIL", message ?? `Rail not supported by server: ${rail}`);
|
|
996
|
+
this.rail = rail;
|
|
997
|
+
this.name = "UnsupportedRailError";
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
// src/client/alipay/install.ts
|
|
1002
|
+
var execFileAsync = promisify(execFile);
|
|
1003
|
+
var MIN_CLI_VERSION = "0.3.15";
|
|
1004
|
+
function semverLt(a, b) {
|
|
1005
|
+
const pa = a.split(".").map((n) => parseInt(n, 10));
|
|
1006
|
+
const pb = b.split(".").map((n) => parseInt(n, 10));
|
|
1007
|
+
for (let i = 0; i < 3; i++) {
|
|
1008
|
+
const x = pa[i] ?? 0;
|
|
1009
|
+
const y = pb[i] ?? 0;
|
|
1010
|
+
if (x !== y) return x < y;
|
|
1011
|
+
}
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
function parseVersion(stdout) {
|
|
1015
|
+
return stdout.match(/v?(\d+\.\d+\.\d+)/)?.[1] ?? null;
|
|
1016
|
+
}
|
|
1017
|
+
var defaultGetVersion = async () => {
|
|
1018
|
+
const { stdout } = await execFileAsync("alipay-bot", ["--version"]);
|
|
1019
|
+
return stdout;
|
|
1020
|
+
};
|
|
1021
|
+
async function ensureCliUncached(getVersion2) {
|
|
1022
|
+
let stdout;
|
|
1023
|
+
try {
|
|
1024
|
+
stdout = await getVersion2();
|
|
1025
|
+
} catch (e) {
|
|
1026
|
+
if (e?.code === "ENOENT") {
|
|
1027
|
+
throw new AlipayCliNotFoundError(
|
|
1028
|
+
"alipay-bot not installed. Run: npx -y @alipay/agent-payment install-cli"
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
throw e;
|
|
1032
|
+
}
|
|
1033
|
+
const version = parseVersion(stdout);
|
|
1034
|
+
if (!version || semverLt(version, MIN_CLI_VERSION)) {
|
|
1035
|
+
throw new AlipayCliVersionError(
|
|
1036
|
+
`alipay-bot ${version ?? "?"} found, need \u2265 ${MIN_CLI_VERSION}. Run: npx -y @alipay/agent-payment@latest update`
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
return version;
|
|
1040
|
+
}
|
|
1041
|
+
var cachedVersion = null;
|
|
1042
|
+
var inflight = null;
|
|
1043
|
+
async function ensureCli(getVersion2 = defaultGetVersion) {
|
|
1044
|
+
if (getVersion2 !== defaultGetVersion) {
|
|
1045
|
+
return ensureCliUncached(getVersion2);
|
|
1046
|
+
}
|
|
1047
|
+
if (cachedVersion) return cachedVersion;
|
|
1048
|
+
if (!inflight) {
|
|
1049
|
+
inflight = ensureCliUncached(getVersion2).then((v) => {
|
|
1050
|
+
cachedVersion = v;
|
|
1051
|
+
return v;
|
|
1052
|
+
}).finally(() => {
|
|
1053
|
+
inflight = null;
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
return inflight;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/client/alipay/poll.ts
|
|
1060
|
+
init_esm_shims();
|
|
1061
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
1062
|
+
var POLL_MAX_INFLIGHT = 2;
|
|
1063
|
+
function resolveMaxInflight(override) {
|
|
1064
|
+
if (override !== void 0) return Math.max(1, Math.floor(override));
|
|
1065
|
+
const raw = process.env.MOLTSPAY_ALIPAY_POLL_MAX_INFLIGHT;
|
|
1066
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
1067
|
+
const n = Number(raw);
|
|
1068
|
+
if (Number.isFinite(n) && n >= 1) return Math.floor(n);
|
|
1069
|
+
}
|
|
1070
|
+
return POLL_MAX_INFLIGHT;
|
|
1071
|
+
}
|
|
1072
|
+
function resolveLaunchGap(override) {
|
|
1073
|
+
if (override !== void 0) return Math.max(0, override);
|
|
1074
|
+
const raw = process.env.MOLTSPAY_ALIPAY_POLL_LAUNCH_MS;
|
|
1075
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
1076
|
+
const n = Number(raw);
|
|
1077
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
1078
|
+
}
|
|
1079
|
+
return POLL_INTERVAL_MS;
|
|
1080
|
+
}
|
|
1081
|
+
function parseStatus(lines) {
|
|
1082
|
+
const raw = lines.join("\n").trim();
|
|
1083
|
+
try {
|
|
1084
|
+
const json = JSON.parse(raw);
|
|
1085
|
+
if (json && typeof json === "object") {
|
|
1086
|
+
if (typeof json.success === "boolean") {
|
|
1087
|
+
if (json.success) return "paid";
|
|
1088
|
+
const err = String(json.errorCode ?? "").toUpperCase();
|
|
1089
|
+
if (/UNPAID|WAIT|PENDING|PROCESS|NOTPAY/.test(err)) return "pending";
|
|
1090
|
+
if (/CLOSED|CANCEL|FAIL|REJECT|REFUSE|TIMEOUT|EXPIRE/.test(err)) return "rejected";
|
|
1091
|
+
return "pending";
|
|
1092
|
+
}
|
|
1093
|
+
if (typeof json.code !== "undefined") {
|
|
1094
|
+
if (Number(json.code) === 200) return "paid";
|
|
1095
|
+
const msg = `${json.message ?? ""}${json.reason ?? ""}`;
|
|
1096
|
+
if (/关闭|失败|拒绝|取消|超时|已撤销/.test(msg)) return "rejected";
|
|
1097
|
+
return "pending";
|
|
1098
|
+
}
|
|
1099
|
+
if (typeof json.body === "string") {
|
|
1100
|
+
return classifyReportText(json.body);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
} catch {
|
|
1104
|
+
}
|
|
1105
|
+
return classifyReportText(raw);
|
|
1106
|
+
}
|
|
1107
|
+
function classifyReportText(s) {
|
|
1108
|
+
const text = s.toUpperCase();
|
|
1109
|
+
if (/查询支付状态成功并获取资源/.test(s) || /资源响应状态[^0-9\n]{0,8}200/.test(s) || /TRADE_SUCCESS|TRADE_FINISHED|"STATUS":\s*"FULFILLED"/.test(text)) return "paid";
|
|
1110
|
+
if (/TRADE_CLOSED|REJECTED|REFUSE|CANCEL/.test(text) || /交易关闭|支付失败|已拒绝|已取消|已撤销|交易超时/.test(s)) return "rejected";
|
|
1111
|
+
if (/WAIT_BUYER_PAY|UNPAID|PENDING|WAITING/.test(text) || /交易未支付|等待付款|待支付|未支付/.test(s)) return "pending";
|
|
1112
|
+
return "unknown";
|
|
1113
|
+
}
|
|
1114
|
+
function defaultSleep(ms, signal) {
|
|
1115
|
+
return new Promise((resolve3, reject) => {
|
|
1116
|
+
if (signal?.aborted) return reject(new Error("aborted"));
|
|
1117
|
+
const t = setTimeout(resolve3, ms);
|
|
1118
|
+
signal?.addEventListener(
|
|
1119
|
+
"abort",
|
|
1120
|
+
() => {
|
|
1121
|
+
clearTimeout(t);
|
|
1122
|
+
reject(new Error("aborted"));
|
|
1123
|
+
},
|
|
1124
|
+
{ once: true }
|
|
1125
|
+
);
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
var LAUNCH = /* @__PURE__ */ Symbol("launch");
|
|
1129
|
+
async function pollUntil(tradeNo, resourceUrl, opts) {
|
|
1130
|
+
const runner = opts.runner ?? runCli;
|
|
1131
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
1132
|
+
const now = opts.now ?? Date.now;
|
|
1133
|
+
const maxInflight = resolveMaxInflight(opts.maxInflight);
|
|
1134
|
+
const launchGap = resolveLaunchGap(opts.launchIntervalMs);
|
|
1135
|
+
const args = [
|
|
1136
|
+
"402-query-payment-status",
|
|
1137
|
+
"-t",
|
|
1138
|
+
tradeNo,
|
|
1139
|
+
"-r",
|
|
1140
|
+
resourceUrl,
|
|
1141
|
+
...opts.method ? ["-m", opts.method] : [],
|
|
1142
|
+
...opts.data ? ["-d", opts.data] : []
|
|
1143
|
+
];
|
|
1144
|
+
const internal = new AbortController();
|
|
1145
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, internal.signal]) : internal.signal;
|
|
1146
|
+
let tick = 0;
|
|
1147
|
+
let lastLaunch = -Infinity;
|
|
1148
|
+
const inflight2 = /* @__PURE__ */ new Set();
|
|
1149
|
+
const launch = () => {
|
|
1150
|
+
tick += 1;
|
|
1151
|
+
const myTick = tick;
|
|
1152
|
+
lastLaunch = now();
|
|
1153
|
+
const run = (async () => {
|
|
1154
|
+
const { lines } = await runner(args, {
|
|
1155
|
+
signal,
|
|
1156
|
+
step: "query-payment-status",
|
|
1157
|
+
flow: tradeNo
|
|
1158
|
+
});
|
|
1159
|
+
const status = parseStatus(lines);
|
|
1160
|
+
alipayLog.debug("poll.tick", { flow: tradeNo, tick: myTick, status });
|
|
1161
|
+
return { status, lines };
|
|
1162
|
+
})();
|
|
1163
|
+
const tracked = run.finally(() => {
|
|
1164
|
+
inflight2.delete(tracked);
|
|
1165
|
+
});
|
|
1166
|
+
inflight2.add(tracked);
|
|
1167
|
+
};
|
|
1168
|
+
try {
|
|
1169
|
+
for (; ; ) {
|
|
1170
|
+
if (opts.signal?.aborted) throw new Error("aborted");
|
|
1171
|
+
const expired = now() >= opts.deadline;
|
|
1172
|
+
if (expired && inflight2.size === 0) {
|
|
1173
|
+
throw new AlipayPaymentTimeoutError(
|
|
1174
|
+
`Payment ${tradeNo} not completed before pay_before deadline`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
while (!expired && inflight2.size < maxInflight && now() - lastLaunch >= launchGap) {
|
|
1178
|
+
launch();
|
|
1179
|
+
}
|
|
1180
|
+
const waiters = [...inflight2];
|
|
1181
|
+
if (!expired && inflight2.size < maxInflight) {
|
|
1182
|
+
const untilNext = Math.max(0, launchGap - (now() - lastLaunch));
|
|
1183
|
+
waiters.push(sleep(untilNext, signal).then(() => LAUNCH, () => LAUNCH));
|
|
1184
|
+
}
|
|
1185
|
+
const winner = await Promise.race(waiters);
|
|
1186
|
+
if (winner === LAUNCH) continue;
|
|
1187
|
+
if (winner.status === "paid") return { status: "paid", lines: winner.lines };
|
|
1188
|
+
if (winner.status === "rejected") {
|
|
1189
|
+
throw new AlipayPaymentRejectedError(`Payment ${tradeNo} was rejected`);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
} finally {
|
|
1193
|
+
internal.abort();
|
|
1194
|
+
for (const p of inflight2) p.catch(() => void 0);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/client/alipay/index.ts
|
|
1199
|
+
var TRADE_NO_RE = /^\d{32}$/;
|
|
1200
|
+
function resolveSessionId(explicit, envSession) {
|
|
1201
|
+
return explicit ?? envSession ?? randomUUID();
|
|
1202
|
+
}
|
|
1203
|
+
function assertTradeNo(t) {
|
|
1204
|
+
if (!TRADE_NO_RE.test(t)) {
|
|
1205
|
+
throw new AlipayProtocolError(`invalid tradeNo (expect 32 digits): ${t}`);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
function parseTradeNo(lines) {
|
|
1209
|
+
for (const line of lines) {
|
|
1210
|
+
const labeled = line.match(/trade[_-]?no["'\s:=]+(\d{32})/i);
|
|
1211
|
+
if (labeled) return labeled[1];
|
|
1212
|
+
const bare = line.match(/\b(\d{32})\b/);
|
|
1213
|
+
if (bare) return bare[1];
|
|
1214
|
+
}
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
function parsePaymentUrl(lines) {
|
|
1218
|
+
let paymentUrl;
|
|
1219
|
+
let shortenUrl;
|
|
1220
|
+
for (const line of lines) {
|
|
1221
|
+
const m = line.match(/(alipays?:\/\/\S+|https?:\/\/\S+)/i);
|
|
1222
|
+
if (!m) continue;
|
|
1223
|
+
const url = m[1].replace(/[)\]`>]+$/, "");
|
|
1224
|
+
if (/short|qr\.alipay|surl|\/s\//i.test(line) && !shortenUrl) shortenUrl = url;
|
|
1225
|
+
else if (!paymentUrl) paymentUrl = url;
|
|
1226
|
+
}
|
|
1227
|
+
if (!paymentUrl && shortenUrl) paymentUrl = shortenUrl;
|
|
1228
|
+
return { paymentUrl, shortenUrl };
|
|
1229
|
+
}
|
|
1230
|
+
function extractMedia(line) {
|
|
1231
|
+
const m = line.match(/^\s*MEDIA:\s*(.+?)\s*$/);
|
|
1232
|
+
return m ? m[1] : null;
|
|
1233
|
+
}
|
|
1234
|
+
function isWalletReady(lines) {
|
|
1235
|
+
const text = lines.join("\n").trim();
|
|
1236
|
+
try {
|
|
1237
|
+
const json = JSON.parse(text);
|
|
1238
|
+
if (json && typeof json.code !== "undefined") return Number(json.code) === 200;
|
|
1239
|
+
} catch {
|
|
1240
|
+
}
|
|
1241
|
+
return !/未开通|未开启|NOT[_\s-]*(OPEN|BOUND|SET)|NEEDS?[_\s-]*SETUP|NO[_\s-]*WALLET/i.test(text);
|
|
1242
|
+
}
|
|
1243
|
+
function extractResourceFromReport(report) {
|
|
1244
|
+
const label = report.search(/资源响应体/);
|
|
1245
|
+
const start = report.indexOf("{", label === -1 ? 0 : label);
|
|
1246
|
+
if (start === -1) return void 0;
|
|
1247
|
+
let depth = 0, inStr = false, esc = false;
|
|
1248
|
+
for (let i = start; i < report.length; i++) {
|
|
1249
|
+
const ch = report[i];
|
|
1250
|
+
if (inStr) {
|
|
1251
|
+
if (esc) esc = false;
|
|
1252
|
+
else if (ch === "\\") esc = true;
|
|
1253
|
+
else if (ch === '"') inStr = false;
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
if (ch === '"') inStr = true;
|
|
1257
|
+
else if (ch === "{") depth++;
|
|
1258
|
+
else if (ch === "}" && --depth === 0) {
|
|
1259
|
+
const slice = report.slice(start, i + 1);
|
|
1260
|
+
try {
|
|
1261
|
+
const obj = JSON.parse(slice);
|
|
1262
|
+
const r = obj?.resourceResponse ?? obj?.result ?? obj?.data ?? obj?.body ?? obj;
|
|
1263
|
+
return typeof r === "string" ? r : JSON.stringify(r);
|
|
1264
|
+
} catch {
|
|
1265
|
+
return slice;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return void 0;
|
|
1270
|
+
}
|
|
1271
|
+
function extractBody(lines) {
|
|
1272
|
+
const text = lines.join("\n").trim();
|
|
1273
|
+
try {
|
|
1274
|
+
const json = JSON.parse(text);
|
|
1275
|
+
if (json && typeof json === "object") {
|
|
1276
|
+
if (json.resourceResponse !== void 0 && json.resourceResponse !== null) {
|
|
1277
|
+
const rr = json.resourceResponse;
|
|
1278
|
+
const r = typeof rr === "object" ? rr.result ?? rr.data ?? rr.body ?? rr : rr;
|
|
1279
|
+
return typeof r === "string" ? r : JSON.stringify(r);
|
|
1280
|
+
}
|
|
1281
|
+
if (typeof json.body === "string") {
|
|
1282
|
+
const inner = extractResourceFromReport(json.body);
|
|
1283
|
+
return inner ?? json.body;
|
|
1284
|
+
}
|
|
1285
|
+
const body = json.data ?? json.result ?? json.body ?? json.resource;
|
|
1286
|
+
if (body !== void 0) return typeof body === "string" ? body : JSON.stringify(body);
|
|
1287
|
+
return text;
|
|
1288
|
+
}
|
|
1289
|
+
} catch {
|
|
1290
|
+
}
|
|
1291
|
+
const idx = lines.findIndex((l) => /^\s*BODY:/.test(l));
|
|
1292
|
+
if (idx !== -1) {
|
|
1293
|
+
const first = lines[idx].replace(/^\s*BODY:\s*/, "");
|
|
1294
|
+
return [first, ...lines.slice(idx + 1)].join("\n").trim();
|
|
1295
|
+
}
|
|
1296
|
+
return lines.filter((l) => !/^\s*(MEDIA|STATUS|INFO):/.test(l)).join("\n").trim();
|
|
1297
|
+
}
|
|
1298
|
+
var DEFAULT_WALLET_TTL_MS = 10 * 60 * 1e3;
|
|
1299
|
+
function resolveWalletTtlMs() {
|
|
1300
|
+
const raw = process.env.MOLTSPAY_ALIPAY_WALLET_TTL_MS;
|
|
1301
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_WALLET_TTL_MS;
|
|
1302
|
+
const n = Number(raw);
|
|
1303
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_WALLET_TTL_MS;
|
|
1304
|
+
}
|
|
1305
|
+
var walletReadyUntil = /* @__PURE__ */ new Map();
|
|
1306
|
+
var DEFAULT_INTENT_TTL_MS = 10 * 60 * 1e3;
|
|
1307
|
+
function resolveIntentTtlMs() {
|
|
1308
|
+
const raw = process.env.MOLTSPAY_ALIPAY_INTENT_TTL_MS;
|
|
1309
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_INTENT_TTL_MS;
|
|
1310
|
+
const n = Number(raw);
|
|
1311
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_INTENT_TTL_MS;
|
|
1312
|
+
}
|
|
1313
|
+
var intentDoneUntil = /* @__PURE__ */ new Map();
|
|
1314
|
+
var AlipayClient = class {
|
|
1315
|
+
sessionId;
|
|
1316
|
+
configDir;
|
|
1317
|
+
framework;
|
|
1318
|
+
runner;
|
|
1319
|
+
getVersion;
|
|
1320
|
+
now;
|
|
1321
|
+
/** Only the default runner may use the process-level wallet cache. */
|
|
1322
|
+
walletCacheable;
|
|
1323
|
+
/** Only the default runner may use the process-level payment-intent cache. */
|
|
1324
|
+
intentCacheable;
|
|
1325
|
+
constructor(opts = {}) {
|
|
1326
|
+
this.sessionId = resolveSessionId(opts.sessionId, process.env.AIPAY_SESSION_ID);
|
|
1327
|
+
this.configDir = opts.configDir ?? join2(homedir2(), ".moltspay");
|
|
1328
|
+
this.framework = opts.framework ?? process.env.AIPAY_FRAMEWORK ?? "openclaw";
|
|
1329
|
+
this.runner = opts.runner ?? runCli;
|
|
1330
|
+
this.getVersion = opts.getVersion;
|
|
1331
|
+
this.now = opts.now ?? Date.now;
|
|
1332
|
+
this.walletCacheable = opts.cacheWallet ?? !opts.runner;
|
|
1333
|
+
this.intentCacheable = opts.cacheIntent ?? !opts.runner;
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Throws NeedsWalletSetupError unless alipay-bot reports an opened wallet.
|
|
1337
|
+
*
|
|
1338
|
+
* Skips the ~22s `check-wallet` spawn entirely when a prior call cached a
|
|
1339
|
+
* "ready" verdict within the TTL (see {@link DEFAULT_WALLET_TTL_MS}).
|
|
1340
|
+
*/
|
|
1341
|
+
async checkWallet(signal, flow) {
|
|
1342
|
+
const ttlMs = resolveWalletTtlMs();
|
|
1343
|
+
const cacheKey = `${this.configDir}::${this.framework}`;
|
|
1344
|
+
const useCache = this.walletCacheable && ttlMs > 0;
|
|
1345
|
+
if (useCache) {
|
|
1346
|
+
const until = walletReadyUntil.get(cacheKey);
|
|
1347
|
+
if (until !== void 0 && this.now() < until) {
|
|
1348
|
+
alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: true, ttlMsLeft: until - this.now() });
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
const { lines } = await this.runner(["check-wallet"], { signal, step: "check-wallet", flow });
|
|
1353
|
+
if (!isWalletReady(lines)) {
|
|
1354
|
+
if (this.walletCacheable) walletReadyUntil.delete(cacheKey);
|
|
1355
|
+
throw new NeedsWalletSetupError(
|
|
1356
|
+
"Alipay wallet not opened. Run: moltspay alipay apply (then: moltspay alipay bind)"
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
if (useCache) {
|
|
1360
|
+
walletReadyUntil.set(cacheKey, this.now() + ttlMs);
|
|
1361
|
+
alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: false, cachedForMs: ttlMs });
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
/** Run the full 8-step flow and resolve with the resource body. */
|
|
1365
|
+
async pay402(opts) {
|
|
1366
|
+
const { resourceUrl, requirement, signal } = opts;
|
|
1367
|
+
const extra = requirement.extra ?? {};
|
|
1368
|
+
const paymentNeededHeader = String(extra.payment_needed_header ?? "");
|
|
1369
|
+
if (!paymentNeededHeader) {
|
|
1370
|
+
throw new AlipayProtocolError("alipay requirement missing extra.payment_needed_header");
|
|
1371
|
+
}
|
|
1372
|
+
const flow = this.sessionId;
|
|
1373
|
+
const flowStart = this.now();
|
|
1374
|
+
alipayLog.info("flow.start", { flow, resource: resourceUrl });
|
|
1375
|
+
const media = [];
|
|
1376
|
+
const onLine = (line) => {
|
|
1377
|
+
const m = extractMedia(line);
|
|
1378
|
+
if (m) media.push(m);
|
|
1379
|
+
else opts.onLine?.(line);
|
|
1380
|
+
};
|
|
1381
|
+
const intentSummary = opts.intentSummary?.trim() || `\u652F\u4ED8 ${requirement.amount ?? ""} ${requirement.asset ?? "CNY"}`.trim();
|
|
1382
|
+
await timeStep("ensure-cli", flow, () => ensureCli(this.getVersion));
|
|
1383
|
+
const intentTtlMs = resolveIntentTtlMs();
|
|
1384
|
+
const intentKey = `${this.configDir}::${this.framework}`;
|
|
1385
|
+
const useIntentCache = this.intentCacheable && intentTtlMs > 0;
|
|
1386
|
+
const intentUntil = useIntentCache ? intentDoneUntil.get(intentKey) : void 0;
|
|
1387
|
+
if (intentUntil !== void 0 && this.now() < intentUntil) {
|
|
1388
|
+
alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: true, ttlMsLeft: intentUntil - this.now() });
|
|
1389
|
+
} else {
|
|
1390
|
+
await this.runner(
|
|
1391
|
+
[
|
|
1392
|
+
"payment-intent",
|
|
1393
|
+
"--session-id",
|
|
1394
|
+
this.sessionId,
|
|
1395
|
+
"--intent-summary",
|
|
1396
|
+
intentSummary,
|
|
1397
|
+
"--framework",
|
|
1398
|
+
this.framework
|
|
1399
|
+
],
|
|
1400
|
+
{ onLine, signal, step: "payment-intent", flow }
|
|
1401
|
+
);
|
|
1402
|
+
if (useIntentCache) {
|
|
1403
|
+
intentDoneUntil.set(intentKey, this.now() + intentTtlMs);
|
|
1404
|
+
alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: false, cachedForMs: intentTtlMs });
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
await this.checkWallet(signal, flow);
|
|
1408
|
+
const reqId = String(extra.out_trade_no ?? randomUUID());
|
|
1409
|
+
const dir = join2(this.configDir, "alipay");
|
|
1410
|
+
await mkdir(dir, { recursive: true });
|
|
1411
|
+
const challengeFile = join2(dir, `402_${reqId}.txt`);
|
|
1412
|
+
await writeFile(challengeFile, paymentNeededHeader, "utf-8");
|
|
1413
|
+
const payArgs = [
|
|
1414
|
+
"402-buyer-pay",
|
|
1415
|
+
"-f",
|
|
1416
|
+
challengeFile,
|
|
1417
|
+
"-r",
|
|
1418
|
+
resourceUrl,
|
|
1419
|
+
"-s",
|
|
1420
|
+
this.sessionId,
|
|
1421
|
+
"-i",
|
|
1422
|
+
intentSummary,
|
|
1423
|
+
"-w",
|
|
1424
|
+
this.framework
|
|
1425
|
+
];
|
|
1426
|
+
if (opts.method) payArgs.push("-m", opts.method);
|
|
1427
|
+
if (opts.data) payArgs.push("-d", opts.data);
|
|
1428
|
+
const payRun = await this.runner(payArgs, { onLine, signal, step: "402-buyer-pay", flow });
|
|
1429
|
+
const tradeNo = parseTradeNo(payRun.lines);
|
|
1430
|
+
if (!tradeNo) {
|
|
1431
|
+
throw new AlipayProtocolError("402-buyer-pay did not return a tradeNo");
|
|
1432
|
+
}
|
|
1433
|
+
assertTradeNo(tradeNo);
|
|
1434
|
+
const { paymentUrl, shortenUrl } = parsePaymentUrl(payRun.lines);
|
|
1435
|
+
if (paymentUrl) {
|
|
1436
|
+
alipayLog.info("flow.pending", { flow, tradeNo, ms: this.now() - flowStart });
|
|
1437
|
+
opts.onPaymentPending?.({ paymentUrl, shortenUrl, tradeNo });
|
|
1438
|
+
}
|
|
1439
|
+
const pendingAt = this.now();
|
|
1440
|
+
const windowMs = (requirement.maxTimeoutSeconds ?? 30 * 60) * 1e3;
|
|
1441
|
+
const deadline = this.now() + (opts.timeoutMs ?? windowMs);
|
|
1442
|
+
const poll = await pollUntil(tradeNo, resourceUrl, {
|
|
1443
|
+
// No onLine: the status-poll output embeds the resource and must not reach
|
|
1444
|
+
// the log stream — the body is surfaced via the return value below (§9.3).
|
|
1445
|
+
deadline,
|
|
1446
|
+
signal,
|
|
1447
|
+
runner: this.runner,
|
|
1448
|
+
now: this.now,
|
|
1449
|
+
// Re-fetch the resource the same way it was paid (POST + body), else the
|
|
1450
|
+
// status poll defaults to GET and 404s on a POST-only `/execute`.
|
|
1451
|
+
method: opts.method,
|
|
1452
|
+
data: opts.data
|
|
1453
|
+
});
|
|
1454
|
+
alipayLog.info("flow.settled", { flow, tradeNo, ms: this.now() - pendingAt });
|
|
1455
|
+
const body = extractBody(poll.lines);
|
|
1456
|
+
void this.runner(["402-buyer-fulfillment-ack", "-t", tradeNo], {
|
|
1457
|
+
onLine,
|
|
1458
|
+
signal,
|
|
1459
|
+
step: "402-buyer-fulfillment-ack",
|
|
1460
|
+
flow
|
|
1461
|
+
}).catch(() => void 0);
|
|
1462
|
+
return { body, payment: { tradeNo, outTradeNo: reqId }, media };
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
// src/client/alipay/router.ts
|
|
1467
|
+
init_esm_shims();
|
|
1468
|
+
var ALIPAY_RAIL = "alipay";
|
|
1469
|
+
function railOf(req) {
|
|
1470
|
+
if (req.scheme === "alipay-aipay" || req.network === ALIPAY_RAIL) return ALIPAY_RAIL;
|
|
1471
|
+
return networkToChainName(req.network) ?? req.network;
|
|
1472
|
+
}
|
|
1473
|
+
function findRail(accepts, rail) {
|
|
1474
|
+
const requirement = accepts.find((r) => railOf(r) === rail);
|
|
1475
|
+
return requirement ? { rail, requirement } : null;
|
|
1476
|
+
}
|
|
1477
|
+
function selectRail(input) {
|
|
1478
|
+
const { serverAccepts, explicitRail, preference, availability } = input;
|
|
1479
|
+
if (!serverAccepts || serverAccepts.length === 0) {
|
|
1480
|
+
throw new UnsupportedRailError(explicitRail ?? "unknown", "Server offered no payment options");
|
|
1481
|
+
}
|
|
1482
|
+
if (explicitRail) {
|
|
1483
|
+
const hit = findRail(serverAccepts, explicitRail);
|
|
1484
|
+
if (!hit) {
|
|
1485
|
+
const offered = serverAccepts.map(railOf);
|
|
1486
|
+
throw new UnsupportedRailError(
|
|
1487
|
+
explicitRail,
|
|
1488
|
+
`Server doesn't accept rail '${explicitRail}'. Offered: ${offered.join(", ")}`
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
return hit;
|
|
1492
|
+
}
|
|
1493
|
+
if (preference) {
|
|
1494
|
+
for (const pref of preference) {
|
|
1495
|
+
const hit = findRail(serverAccepts, pref);
|
|
1496
|
+
if (hit) return hit;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (availability?.evmReady) {
|
|
1500
|
+
const evm = serverAccepts.find((r) => railOf(r) !== ALIPAY_RAIL);
|
|
1501
|
+
if (evm) return { rail: railOf(evm), requirement: evm };
|
|
1502
|
+
}
|
|
1503
|
+
if (availability?.alipayReady) {
|
|
1504
|
+
const hit = findRail(serverAccepts, ALIPAY_RAIL);
|
|
1505
|
+
if (hit) return hit;
|
|
1506
|
+
}
|
|
1507
|
+
return { rail: railOf(serverAccepts[0]), requirement: serverAccepts[0] };
|
|
1508
|
+
}
|
|
1509
|
+
|
|
764
1510
|
// src/client/types.ts
|
|
765
1511
|
init_esm_shims();
|
|
766
1512
|
|
|
@@ -780,9 +1526,13 @@ var MoltsPayClient = class {
|
|
|
780
1526
|
signer = null;
|
|
781
1527
|
todaySpending = 0;
|
|
782
1528
|
lastSpendingReset = 0;
|
|
1529
|
+
railPreference;
|
|
1530
|
+
alipaySessionId;
|
|
783
1531
|
constructor(options = {}) {
|
|
784
|
-
this.configDir = options.configDir ||
|
|
1532
|
+
this.configDir = options.configDir || join3(homedir3(), ".moltspay");
|
|
785
1533
|
this.config = this.loadConfig();
|
|
1534
|
+
this.railPreference = options.railPreference ?? this.config.railPreference;
|
|
1535
|
+
this.alipaySessionId = options.alipaySessionId;
|
|
786
1536
|
this.walletData = this.loadWallet();
|
|
787
1537
|
this.loadSpending();
|
|
788
1538
|
if (this.walletData) {
|
|
@@ -860,6 +1610,9 @@ var MoltsPayClient = class {
|
|
|
860
1610
|
* @param options - Payment options (token selection)
|
|
861
1611
|
*/
|
|
862
1612
|
async pay(serverUrl, service, params, options = {}) {
|
|
1613
|
+
if (options.rail === ALIPAY_RAIL) {
|
|
1614
|
+
return this.payViaAlipay(serverUrl, service, params, options);
|
|
1615
|
+
}
|
|
863
1616
|
if (!this.wallet || !this.walletData) {
|
|
864
1617
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
865
1618
|
}
|
|
@@ -1051,6 +1804,76 @@ Please specify: --chain <chain_name>`
|
|
|
1051
1804
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
1052
1805
|
return result.result || result;
|
|
1053
1806
|
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Pay for a service over the Alipay fiat rail (2.0.0).
|
|
1809
|
+
*
|
|
1810
|
+
* Unlike the crypto path this needs no EVM wallet — it shells out to
|
|
1811
|
+
* alipay-bot via {@link AlipayClient}. Flow: hit the resource with no
|
|
1812
|
+
* payment to get the 402 challenge, confirm the server actually offers the
|
|
1813
|
+
* alipay rail (selectRail), then run the 8-step state machine and return the
|
|
1814
|
+
* resource body.
|
|
1815
|
+
*/
|
|
1816
|
+
async payViaAlipay(serverUrl, service, params, options) {
|
|
1817
|
+
const flow = this.alipaySessionId;
|
|
1818
|
+
let executeUrl = `${serverUrl}/execute`;
|
|
1819
|
+
try {
|
|
1820
|
+
const services = await timeStep(
|
|
1821
|
+
"discover-services",
|
|
1822
|
+
flow,
|
|
1823
|
+
() => this.getServices(serverUrl)
|
|
1824
|
+
);
|
|
1825
|
+
const svc = services.services?.find((s) => s.id === service);
|
|
1826
|
+
if (svc?.endpoint) executeUrl = `${serverUrl}${svc.endpoint}`;
|
|
1827
|
+
} catch {
|
|
1828
|
+
}
|
|
1829
|
+
const requestBody = options.rawData ? { service, ...params } : { service, params };
|
|
1830
|
+
const bodyJson = JSON.stringify(requestBody);
|
|
1831
|
+
const res = await timeStep(
|
|
1832
|
+
"challenge-402",
|
|
1833
|
+
flow,
|
|
1834
|
+
() => fetch(executeUrl, {
|
|
1835
|
+
method: "POST",
|
|
1836
|
+
headers: { "Content-Type": "application/json" },
|
|
1837
|
+
body: bodyJson
|
|
1838
|
+
})
|
|
1839
|
+
);
|
|
1840
|
+
if (res.status !== 402) {
|
|
1841
|
+
const data = await res.json().catch(() => ({}));
|
|
1842
|
+
if (res.ok && data.result) return data.result;
|
|
1843
|
+
throw new Error(data.error || `Expected 402, got ${res.status}`);
|
|
1844
|
+
}
|
|
1845
|
+
const header = res.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
1846
|
+
if (!header) throw new Error("Missing x-payment-required header on 402");
|
|
1847
|
+
const parsed = JSON.parse(Buffer.from(header, "base64").toString("utf-8"));
|
|
1848
|
+
const accepts = Array.isArray(parsed) ? parsed : parsed.accepts ?? [parsed];
|
|
1849
|
+
const { requirement } = selectRail({
|
|
1850
|
+
serverAccepts: accepts,
|
|
1851
|
+
explicitRail: ALIPAY_RAIL,
|
|
1852
|
+
preference: this.railPreference,
|
|
1853
|
+
availability: { evmReady: this.isInitialized }
|
|
1854
|
+
});
|
|
1855
|
+
const onLine = options.onLine ?? ((line) => process.stdout.write(line + "\n"));
|
|
1856
|
+
const alipay = new AlipayClient({
|
|
1857
|
+
sessionId: this.alipaySessionId,
|
|
1858
|
+
configDir: this.configDir
|
|
1859
|
+
});
|
|
1860
|
+
const result = await alipay.pay402({
|
|
1861
|
+
resourceUrl: executeUrl,
|
|
1862
|
+
requirement,
|
|
1863
|
+
method: "POST",
|
|
1864
|
+
data: bodyJson,
|
|
1865
|
+
onLine,
|
|
1866
|
+
onPaymentPending: options.onPaymentPending,
|
|
1867
|
+
timeoutMs: options.timeoutMs,
|
|
1868
|
+
signal: options.signal
|
|
1869
|
+
});
|
|
1870
|
+
try {
|
|
1871
|
+
const json = JSON.parse(result.body);
|
|
1872
|
+
return json.result ?? json;
|
|
1873
|
+
} catch {
|
|
1874
|
+
return { body: result.body, payment: result.payment, media: result.media };
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1054
1877
|
/**
|
|
1055
1878
|
* Handle MPP (Machine Payments Protocol) payment flow
|
|
1056
1879
|
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
@@ -1391,7 +2214,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1391
2214
|
}
|
|
1392
2215
|
// --- Config & Wallet Management ---
|
|
1393
2216
|
loadConfig() {
|
|
1394
|
-
const configPath =
|
|
2217
|
+
const configPath = join3(this.configDir, "config.json");
|
|
1395
2218
|
if (existsSync2(configPath)) {
|
|
1396
2219
|
const content = readFileSync2(configPath, "utf-8");
|
|
1397
2220
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
@@ -1400,14 +2223,14 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1400
2223
|
}
|
|
1401
2224
|
saveConfig() {
|
|
1402
2225
|
mkdirSync2(this.configDir, { recursive: true });
|
|
1403
|
-
const configPath =
|
|
2226
|
+
const configPath = join3(this.configDir, "config.json");
|
|
1404
2227
|
writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
|
|
1405
2228
|
}
|
|
1406
2229
|
/**
|
|
1407
2230
|
* Load spending data from disk
|
|
1408
2231
|
*/
|
|
1409
2232
|
loadSpending() {
|
|
1410
|
-
const spendingPath =
|
|
2233
|
+
const spendingPath = join3(this.configDir, "spending.json");
|
|
1411
2234
|
if (existsSync2(spendingPath)) {
|
|
1412
2235
|
try {
|
|
1413
2236
|
const data = JSON.parse(readFileSync2(spendingPath, "utf-8"));
|
|
@@ -1430,7 +2253,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1430
2253
|
*/
|
|
1431
2254
|
saveSpending() {
|
|
1432
2255
|
mkdirSync2(this.configDir, { recursive: true });
|
|
1433
|
-
const spendingPath =
|
|
2256
|
+
const spendingPath = join3(this.configDir, "spending.json");
|
|
1434
2257
|
const data = {
|
|
1435
2258
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
1436
2259
|
amount: this.todaySpending,
|
|
@@ -1439,7 +2262,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1439
2262
|
writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
|
|
1440
2263
|
}
|
|
1441
2264
|
loadWallet() {
|
|
1442
|
-
const walletPath =
|
|
2265
|
+
const walletPath = join3(this.configDir, "wallet.json");
|
|
1443
2266
|
if (existsSync2(walletPath)) {
|
|
1444
2267
|
if (process.platform !== "win32") {
|
|
1445
2268
|
try {
|
|
@@ -1469,7 +2292,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1469
2292
|
privateKey: wallet.privateKey,
|
|
1470
2293
|
createdAt: Date.now()
|
|
1471
2294
|
};
|
|
1472
|
-
const walletPath =
|
|
2295
|
+
const walletPath = join3(configDir, "wallet.json");
|
|
1473
2296
|
writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
1474
2297
|
const config = {
|
|
1475
2298
|
chain: options.chain,
|
|
@@ -1478,7 +2301,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1478
2301
|
maxPerDay: options.maxPerDay
|
|
1479
2302
|
}
|
|
1480
2303
|
};
|
|
1481
|
-
const configPath =
|
|
2304
|
+
const configPath = join3(configDir, "config.json");
|
|
1482
2305
|
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
1483
2306
|
return { address: wallet.address, configDir };
|
|
1484
2307
|
}
|
|
@@ -2464,6 +3287,378 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
2464
3287
|
}
|
|
2465
3288
|
};
|
|
2466
3289
|
|
|
3290
|
+
// src/facilitators/alipay.ts
|
|
3291
|
+
init_esm_shims();
|
|
3292
|
+
import crypto2 from "crypto";
|
|
3293
|
+
|
|
3294
|
+
// src/facilitators/alipay/rsa2.ts
|
|
3295
|
+
init_esm_shims();
|
|
3296
|
+
import crypto from "crypto";
|
|
3297
|
+
function rsa2Sign(data, privateKeyPem) {
|
|
3298
|
+
const signer = crypto.createSign("RSA-SHA256");
|
|
3299
|
+
signer.update(data, "utf-8");
|
|
3300
|
+
signer.end();
|
|
3301
|
+
return signer.sign(privateKeyPem, "base64");
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
// src/facilitators/alipay/encoding.ts
|
|
3305
|
+
init_esm_shims();
|
|
3306
|
+
function base64url(input) {
|
|
3307
|
+
return Buffer.from(input, "utf-8").toString("base64url");
|
|
3308
|
+
}
|
|
3309
|
+
function decodeBase64UrlWithPadFix(input) {
|
|
3310
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
3311
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
3312
|
+
return Buffer.from(padded, "base64").toString("utf-8");
|
|
3313
|
+
}
|
|
3314
|
+
function toPem(key, kind) {
|
|
3315
|
+
const trimmed = key.trim();
|
|
3316
|
+
if (trimmed.includes("-----BEGIN")) return trimmed;
|
|
3317
|
+
const label = kind === "PRIVATE" ? "PRIVATE KEY" : "PUBLIC KEY";
|
|
3318
|
+
const body = trimmed.replace(/\s+/g, "").match(/.{1,64}/g)?.join("\n") ?? "";
|
|
3319
|
+
return `-----BEGIN ${label}-----
|
|
3320
|
+
${body}
|
|
3321
|
+
-----END ${label}-----
|
|
3322
|
+
`;
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
// src/facilitators/alipay/openapi.ts
|
|
3326
|
+
init_esm_shims();
|
|
3327
|
+
function formatAlipayTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
3328
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
3329
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
3330
|
+
}
|
|
3331
|
+
function buildSigningString(params) {
|
|
3332
|
+
return Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join("&");
|
|
3333
|
+
}
|
|
3334
|
+
function responseWrapperKey(method) {
|
|
3335
|
+
return `${method.replace(/\./g, "_")}_response`;
|
|
3336
|
+
}
|
|
3337
|
+
async function alipayOpenApiCall(method, bizContent, config) {
|
|
3338
|
+
const publicParams = {
|
|
3339
|
+
app_id: config.app_id,
|
|
3340
|
+
method,
|
|
3341
|
+
format: "JSON",
|
|
3342
|
+
charset: "utf-8",
|
|
3343
|
+
sign_type: config.sign_type ?? "RSA2",
|
|
3344
|
+
timestamp: formatAlipayTimestamp(),
|
|
3345
|
+
version: "1.0",
|
|
3346
|
+
biz_content: JSON.stringify(bizContent)
|
|
3347
|
+
};
|
|
3348
|
+
const signingString = buildSigningString(publicParams);
|
|
3349
|
+
const sign = rsa2Sign(signingString, config.private_key_pem);
|
|
3350
|
+
const body = new URLSearchParams({ ...publicParams, sign }).toString();
|
|
3351
|
+
const response = await fetch(config.gateway_url, {
|
|
3352
|
+
method: "POST",
|
|
3353
|
+
headers: {
|
|
3354
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
|
|
3355
|
+
},
|
|
3356
|
+
body
|
|
3357
|
+
});
|
|
3358
|
+
if (!response.ok) {
|
|
3359
|
+
const text = await response.text().catch(() => "<unreadable>");
|
|
3360
|
+
throw new Error(
|
|
3361
|
+
`Alipay Open API HTTP ${response.status} for ${method}: ${text.slice(0, 500)}`
|
|
3362
|
+
);
|
|
3363
|
+
}
|
|
3364
|
+
const json = await response.json();
|
|
3365
|
+
const wrapperKey = responseWrapperKey(method);
|
|
3366
|
+
const business = json[wrapperKey];
|
|
3367
|
+
if (business === void 0 || business === null || typeof business !== "object") {
|
|
3368
|
+
throw new Error(
|
|
3369
|
+
`Alipay Open API response missing "${wrapperKey}" wrapper for ${method}: ${JSON.stringify(json).slice(0, 500)}`
|
|
3370
|
+
);
|
|
3371
|
+
}
|
|
3372
|
+
return business;
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
// src/facilitators/alipay.ts
|
|
3376
|
+
var ALIPAY_NETWORK = "alipay";
|
|
3377
|
+
var ALIPAY_SCHEME = "alipay-aipay";
|
|
3378
|
+
var ALIPAY_GATEWAY_PROD = "https://openapi.alipay.com/gateway.do";
|
|
3379
|
+
var ALIPAY_AMOUNT_REGEX = /^\d+(\.\d{1,2})?$/;
|
|
3380
|
+
var ALIPAY_PAY_BEFORE_MS = 30 * 60 * 1e3;
|
|
3381
|
+
var ALIPAY_SIGNING_FIELDS = [
|
|
3382
|
+
"amount",
|
|
3383
|
+
"currency",
|
|
3384
|
+
"goods_name",
|
|
3385
|
+
"out_trade_no",
|
|
3386
|
+
"pay_before",
|
|
3387
|
+
"resource_id",
|
|
3388
|
+
"seller_id",
|
|
3389
|
+
"service_id"
|
|
3390
|
+
];
|
|
3391
|
+
var AlipayFacilitator = class extends BaseFacilitator {
|
|
3392
|
+
name = "alipay";
|
|
3393
|
+
displayName = "Alipay AI \u6536";
|
|
3394
|
+
supportedNetworks = [ALIPAY_NETWORK];
|
|
3395
|
+
config;
|
|
3396
|
+
constructor(config) {
|
|
3397
|
+
super();
|
|
3398
|
+
this.config = {
|
|
3399
|
+
gateway_url: ALIPAY_GATEWAY_PROD,
|
|
3400
|
+
sign_type: "RSA2",
|
|
3401
|
+
...config
|
|
3402
|
+
};
|
|
3403
|
+
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Build the 402 challenge for a service: signs the 8-field payload with
|
|
3406
|
+
* RSA2, packages the nested `{protocol, method}` JSON as Base64URL for
|
|
3407
|
+
* `Payment-Needed`, and emits the parallel x402 `accepts[]` entry.
|
|
3408
|
+
*/
|
|
3409
|
+
async createPaymentRequirements(opts) {
|
|
3410
|
+
if (!ALIPAY_AMOUNT_REGEX.test(opts.priceCny)) {
|
|
3411
|
+
throw new Error(
|
|
3412
|
+
`AlipayFacilitator.createPaymentRequirements: priceCny "${opts.priceCny}" does not match /^\\d+(\\.\\d{1,2})?$/ (unit is \u5143, not \u5206; e.g. "1.00" not "100")`
|
|
3413
|
+
);
|
|
3414
|
+
}
|
|
3415
|
+
const now = /* @__PURE__ */ new Date();
|
|
3416
|
+
const outTradeNo = opts.outTradeNo ?? generateOutTradeNo();
|
|
3417
|
+
const payBefore = formatPayBefore(now);
|
|
3418
|
+
const signedFields = {
|
|
3419
|
+
amount: opts.priceCny,
|
|
3420
|
+
currency: "CNY",
|
|
3421
|
+
goods_name: opts.goodsName,
|
|
3422
|
+
out_trade_no: outTradeNo,
|
|
3423
|
+
pay_before: payBefore,
|
|
3424
|
+
resource_id: opts.resourceId,
|
|
3425
|
+
seller_id: this.config.seller_id,
|
|
3426
|
+
service_id: opts.serviceId
|
|
3427
|
+
};
|
|
3428
|
+
const signingString = ALIPAY_SIGNING_FIELDS.map((k) => `${k}=${signedFields[k]}`).join("&");
|
|
3429
|
+
const seller_signature = rsa2Sign(signingString, this.config.private_key_pem);
|
|
3430
|
+
const challenge = {
|
|
3431
|
+
protocol: {
|
|
3432
|
+
out_trade_no: outTradeNo,
|
|
3433
|
+
amount: signedFields.amount,
|
|
3434
|
+
currency: signedFields.currency,
|
|
3435
|
+
resource_id: signedFields.resource_id,
|
|
3436
|
+
pay_before: payBefore,
|
|
3437
|
+
seller_signature,
|
|
3438
|
+
seller_sign_type: this.config.sign_type ?? "RSA2",
|
|
3439
|
+
seller_unique_id: this.config.seller_id
|
|
3440
|
+
},
|
|
3441
|
+
method: {
|
|
3442
|
+
seller_name: this.config.seller_name,
|
|
3443
|
+
seller_id: this.config.seller_id,
|
|
3444
|
+
seller_app_id: this.config.app_id,
|
|
3445
|
+
goods_name: signedFields.goods_name,
|
|
3446
|
+
seller_unique_id_key: "seller_id",
|
|
3447
|
+
service_id: signedFields.service_id
|
|
3448
|
+
}
|
|
3449
|
+
};
|
|
3450
|
+
const paymentNeededHeader = base64url(JSON.stringify(challenge));
|
|
3451
|
+
const x402Accepts = {
|
|
3452
|
+
scheme: ALIPAY_SCHEME,
|
|
3453
|
+
network: ALIPAY_NETWORK,
|
|
3454
|
+
asset: "CNY",
|
|
3455
|
+
amount: opts.priceCny,
|
|
3456
|
+
payTo: this.config.seller_id,
|
|
3457
|
+
maxTimeoutSeconds: ALIPAY_PAY_BEFORE_MS / 1e3,
|
|
3458
|
+
extra: {
|
|
3459
|
+
payment_needed_header: paymentNeededHeader,
|
|
3460
|
+
out_trade_no: outTradeNo,
|
|
3461
|
+
pay_before: payBefore,
|
|
3462
|
+
service_id: signedFields.service_id
|
|
3463
|
+
}
|
|
3464
|
+
};
|
|
3465
|
+
return { x402Accepts, paymentNeededHeader };
|
|
3466
|
+
}
|
|
3467
|
+
/**
|
|
3468
|
+
* Verify a `Payment-Proof` by calling
|
|
3469
|
+
* `alipay.aipay.agent.payment.verify` against the Alipay Open API.
|
|
3470
|
+
*
|
|
3471
|
+
* The proof is conveyed via `paymentPayload.payload`, which may be:
|
|
3472
|
+
* - a raw Base64URL string (the `Payment-Proof` header value), or
|
|
3473
|
+
* - an object `{ paymentProof: string }` / `{ proofHeader: string }`.
|
|
3474
|
+
*
|
|
3475
|
+
* All failure modes (malformed payload, network errors, Alipay
|
|
3476
|
+
* `code != 10000`) return `{ valid: false, error }`. No exception
|
|
3477
|
+
* escapes, regardless of client-supplied input.
|
|
3478
|
+
*/
|
|
3479
|
+
async verify(paymentPayload, _requirements) {
|
|
3480
|
+
try {
|
|
3481
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
3482
|
+
const decoded = decodeProof(proofHeader);
|
|
3483
|
+
const response = await alipayOpenApiCall(
|
|
3484
|
+
"alipay.aipay.agent.payment.verify",
|
|
3485
|
+
{
|
|
3486
|
+
payment_proof: decoded.protocol.payment_proof,
|
|
3487
|
+
trade_no: decoded.protocol.trade_no,
|
|
3488
|
+
client_session: decoded.method.client_session
|
|
3489
|
+
},
|
|
3490
|
+
this.getOpenApiConfig()
|
|
3491
|
+
);
|
|
3492
|
+
if (response.code !== "10000") {
|
|
3493
|
+
return {
|
|
3494
|
+
valid: false,
|
|
3495
|
+
error: `alipay verify ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
3496
|
+
details: {
|
|
3497
|
+
code: response.code,
|
|
3498
|
+
sub_code: response.sub_code,
|
|
3499
|
+
sub_msg: response.sub_msg
|
|
3500
|
+
}
|
|
3501
|
+
};
|
|
3502
|
+
}
|
|
3503
|
+
return {
|
|
3504
|
+
valid: true,
|
|
3505
|
+
details: {
|
|
3506
|
+
trade_no: response.trade_no ?? decoded.protocol.trade_no,
|
|
3507
|
+
amount: response.amount,
|
|
3508
|
+
out_trade_no: response.out_trade_no,
|
|
3509
|
+
resource_id: response.resource_id,
|
|
3510
|
+
active: response.active
|
|
3511
|
+
}
|
|
3512
|
+
};
|
|
3513
|
+
} catch (e) {
|
|
3514
|
+
return {
|
|
3515
|
+
valid: false,
|
|
3516
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
/**
|
|
3521
|
+
* Settle by calling `alipay.aipay.agent.fulfillment.confirm` after the
|
|
3522
|
+
* service resource has been returned to the buyer.
|
|
3523
|
+
*
|
|
3524
|
+
* Per the design (see ALIPAY-INTEGRATION-DESIGN.md §5.1, risk row
|
|
3525
|
+
* "履约确认失败"), this is **fire-and-forget**: the caller (registry /
|
|
3526
|
+
* server) is expected to log fulfillment failures but NOT roll back
|
|
3527
|
+
* the already-delivered resource.
|
|
3528
|
+
*
|
|
3529
|
+
* Re-decodes `paymentPayload.payload` to extract `trade_no` (verify
|
|
3530
|
+
* does the same; the redundant Base64URL decode is negligible).
|
|
3531
|
+
*/
|
|
3532
|
+
async settle(paymentPayload, _requirements) {
|
|
3533
|
+
try {
|
|
3534
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
3535
|
+
const decoded = decodeProof(proofHeader);
|
|
3536
|
+
const tradeNo = decoded.protocol.trade_no;
|
|
3537
|
+
const response = await alipayOpenApiCall(
|
|
3538
|
+
"alipay.aipay.agent.fulfillment.confirm",
|
|
3539
|
+
{ trade_no: tradeNo },
|
|
3540
|
+
this.getOpenApiConfig()
|
|
3541
|
+
);
|
|
3542
|
+
if (response.code !== "10000") {
|
|
3543
|
+
return {
|
|
3544
|
+
success: false,
|
|
3545
|
+
transaction: tradeNo,
|
|
3546
|
+
error: `alipay fulfillment ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
3547
|
+
status: "fulfillment_failed"
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3550
|
+
return {
|
|
3551
|
+
success: true,
|
|
3552
|
+
transaction: tradeNo,
|
|
3553
|
+
status: "fulfilled"
|
|
3554
|
+
};
|
|
3555
|
+
} catch (e) {
|
|
3556
|
+
return {
|
|
3557
|
+
success: false,
|
|
3558
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3559
|
+
};
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
/**
|
|
3563
|
+
* Validate that the configured RSA keys parse and that the Open API
|
|
3564
|
+
* gateway is reachable. Does NOT make a real business API call (would
|
|
3565
|
+
* burn quota and could appear as a legitimate verify attempt in logs).
|
|
3566
|
+
*/
|
|
3567
|
+
async healthCheck() {
|
|
3568
|
+
const start = Date.now();
|
|
3569
|
+
try {
|
|
3570
|
+
crypto2.createPrivateKey(this.config.private_key_pem);
|
|
3571
|
+
} catch (e) {
|
|
3572
|
+
return {
|
|
3573
|
+
healthy: false,
|
|
3574
|
+
error: `merchant private_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3577
|
+
try {
|
|
3578
|
+
crypto2.createPublicKey(this.config.alipay_public_key_pem);
|
|
3579
|
+
} catch (e) {
|
|
3580
|
+
return {
|
|
3581
|
+
healthy: false,
|
|
3582
|
+
error: `alipay_public_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
const gatewayUrl = this.config.gateway_url ?? ALIPAY_GATEWAY_PROD;
|
|
3586
|
+
const controller = new AbortController();
|
|
3587
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
3588
|
+
const response = await fetch(gatewayUrl, {
|
|
3589
|
+
method: "HEAD",
|
|
3590
|
+
signal: controller.signal
|
|
3591
|
+
}).catch(() => null);
|
|
3592
|
+
clearTimeout(timeout);
|
|
3593
|
+
const latencyMs = Date.now() - start;
|
|
3594
|
+
if (!response) {
|
|
3595
|
+
return { healthy: false, error: `gateway unreachable: ${gatewayUrl}`, latencyMs };
|
|
3596
|
+
}
|
|
3597
|
+
return { healthy: true, latencyMs };
|
|
3598
|
+
}
|
|
3599
|
+
/** Bundle the facilitator config into the shape openapi.ts wants. */
|
|
3600
|
+
getOpenApiConfig() {
|
|
3601
|
+
return {
|
|
3602
|
+
gateway_url: this.config.gateway_url ?? ALIPAY_GATEWAY_PROD,
|
|
3603
|
+
app_id: this.config.app_id,
|
|
3604
|
+
private_key_pem: this.config.private_key_pem,
|
|
3605
|
+
alipay_public_key_pem: this.config.alipay_public_key_pem,
|
|
3606
|
+
sign_type: this.config.sign_type
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
};
|
|
3610
|
+
function generateOutTradeNo() {
|
|
3611
|
+
return "VID" + crypto2.randomBytes(22).toString("base64url").slice(0, 29);
|
|
3612
|
+
}
|
|
3613
|
+
function formatPayBefore(now) {
|
|
3614
|
+
const expiry = new Date(now.getTime() + ALIPAY_PAY_BEFORE_MS);
|
|
3615
|
+
return expiry.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
3616
|
+
}
|
|
3617
|
+
function extractProofHeader(payload) {
|
|
3618
|
+
if (typeof payload === "string") {
|
|
3619
|
+
if (payload.length === 0) {
|
|
3620
|
+
throw new Error("alipay Payment-Proof is empty");
|
|
3621
|
+
}
|
|
3622
|
+
return payload;
|
|
3623
|
+
}
|
|
3624
|
+
if (payload !== null && typeof payload === "object") {
|
|
3625
|
+
const obj = payload;
|
|
3626
|
+
const candidate = obj.paymentProof ?? obj.proofHeader;
|
|
3627
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
3628
|
+
return candidate;
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
throw new Error(
|
|
3632
|
+
"alipay payment payload must be a Base64URL string or {paymentProof: string} / {proofHeader: string}"
|
|
3633
|
+
);
|
|
3634
|
+
}
|
|
3635
|
+
function decodeProof(proofHeader) {
|
|
3636
|
+
let parsed;
|
|
3637
|
+
try {
|
|
3638
|
+
parsed = JSON.parse(decodeBase64UrlWithPadFix(proofHeader));
|
|
3639
|
+
} catch (e) {
|
|
3640
|
+
throw new Error(
|
|
3641
|
+
`failed to decode Payment-Proof: ${e instanceof Error ? e.message : String(e)}`
|
|
3642
|
+
);
|
|
3643
|
+
}
|
|
3644
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
3645
|
+
throw new Error("decoded Payment-Proof is not an object");
|
|
3646
|
+
}
|
|
3647
|
+
const obj = parsed;
|
|
3648
|
+
const protocol = obj.protocol;
|
|
3649
|
+
const method = obj.method;
|
|
3650
|
+
if (!protocol || typeof protocol.payment_proof !== "string") {
|
|
3651
|
+
throw new Error("decoded Payment-Proof missing protocol.payment_proof");
|
|
3652
|
+
}
|
|
3653
|
+
if (typeof protocol.trade_no !== "string") {
|
|
3654
|
+
throw new Error("decoded Payment-Proof missing protocol.trade_no");
|
|
3655
|
+
}
|
|
3656
|
+
if (!method || typeof method.client_session !== "string") {
|
|
3657
|
+
throw new Error("decoded Payment-Proof missing method.client_session");
|
|
3658
|
+
}
|
|
3659
|
+
return parsed;
|
|
3660
|
+
}
|
|
3661
|
+
|
|
2467
3662
|
// src/facilitators/registry.ts
|
|
2468
3663
|
init_esm_shims();
|
|
2469
3664
|
import { Keypair as Keypair5 } from "@solana/web3.js";
|
|
@@ -2489,6 +3684,7 @@ var FacilitatorRegistry = class {
|
|
|
2489
3684
|
}
|
|
2490
3685
|
return new SolanaFacilitator({ feePayerKeypair });
|
|
2491
3686
|
});
|
|
3687
|
+
this.registerFactory("alipay", (config) => new AlipayFacilitator(config));
|
|
2492
3688
|
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
2493
3689
|
}
|
|
2494
3690
|
/**
|
|
@@ -2716,6 +3912,11 @@ var PAYMENT_RESPONSE_HEADER = "x-payment-response";
|
|
|
2716
3912
|
var MPP_AUTH_HEADER = "authorization";
|
|
2717
3913
|
var MPP_WWW_AUTH_HEADER = "www-authenticate";
|
|
2718
3914
|
var MPP_RECEIPT_HEADER = "payment-receipt";
|
|
3915
|
+
var ALIPAY_PAYMENT_NEEDED_HEADER = "payment-needed";
|
|
3916
|
+
var ALIPAY_PAYMENT_PROOF_HEADER = "payment-proof";
|
|
3917
|
+
function headerSafe(value) {
|
|
3918
|
+
return String(value ?? "").replace(/[^\x20-\x7E]/g, (ch) => encodeURIComponent(ch)).replace(/"/g, "%22");
|
|
3919
|
+
}
|
|
2719
3920
|
var TOKEN_ADDRESSES = {
|
|
2720
3921
|
"eip155:8453": {
|
|
2721
3922
|
USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
@@ -2851,6 +4052,8 @@ var MoltsPayServer = class {
|
|
|
2851
4052
|
registry;
|
|
2852
4053
|
networkId;
|
|
2853
4054
|
useMainnet;
|
|
4055
|
+
/** Alipay AI 收 facilitator instance, set when `provider.alipay` is configured (2.0.0). */
|
|
4056
|
+
alipayFacilitator = null;
|
|
2854
4057
|
constructor(servicesPath, options = {}) {
|
|
2855
4058
|
loadEnvFile2();
|
|
2856
4059
|
const content = readFileSync4(servicesPath, "utf-8");
|
|
@@ -2872,7 +4075,38 @@ var MoltsPayServer = class {
|
|
|
2872
4075
|
cdp: { useMainnet: this.useMainnet }
|
|
2873
4076
|
}
|
|
2874
4077
|
};
|
|
4078
|
+
const providerAlipay = this.manifest.provider.alipay;
|
|
4079
|
+
if (providerAlipay) {
|
|
4080
|
+
try {
|
|
4081
|
+
const baseDir = path3.dirname(servicesPath);
|
|
4082
|
+
const resolvePem = (p, kind) => toPem(readFileSync4(path3.isAbsolute(p) ? p : path3.resolve(baseDir, p), "utf-8"), kind);
|
|
4083
|
+
const alipayFacilitatorConfig = {
|
|
4084
|
+
seller_id: providerAlipay.seller_id,
|
|
4085
|
+
app_id: providerAlipay.app_id,
|
|
4086
|
+
seller_name: providerAlipay.seller_name,
|
|
4087
|
+
service_id_default: providerAlipay.service_id_default,
|
|
4088
|
+
private_key_pem: resolvePem(providerAlipay.private_key_path, "PRIVATE"),
|
|
4089
|
+
alipay_public_key_pem: resolvePem(providerAlipay.alipay_public_key_path, "PUBLIC"),
|
|
4090
|
+
gateway_url: providerAlipay.gateway_url,
|
|
4091
|
+
sign_type: providerAlipay.sign_type
|
|
4092
|
+
};
|
|
4093
|
+
facilitatorConfig.config = {
|
|
4094
|
+
...facilitatorConfig.config,
|
|
4095
|
+
alipay: alipayFacilitatorConfig
|
|
4096
|
+
};
|
|
4097
|
+
facilitatorConfig.fallback = facilitatorConfig.fallback || [];
|
|
4098
|
+
if (facilitatorConfig.primary !== "alipay" && !facilitatorConfig.fallback.includes("alipay")) {
|
|
4099
|
+
facilitatorConfig.fallback.push("alipay");
|
|
4100
|
+
}
|
|
4101
|
+
} catch (err) {
|
|
4102
|
+
throw new Error(`[MoltsPay] Alipay rail configured but key load failed: ${err.message}`);
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
2875
4105
|
this.registry = new FacilitatorRegistry(facilitatorConfig);
|
|
4106
|
+
if (providerAlipay) {
|
|
4107
|
+
this.alipayFacilitator = this.registry.get("alipay");
|
|
4108
|
+
console.log(`[MoltsPay] Alipay AI \u6536 rail enabled (seller ${providerAlipay.seller_id})`);
|
|
4109
|
+
}
|
|
2876
4110
|
const primaryFacilitator = this.registry.get(facilitatorConfig.primary);
|
|
2877
4111
|
console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
|
|
2878
4112
|
console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
|
|
@@ -3008,10 +4242,10 @@ var MoltsPayServer = class {
|
|
|
3008
4242
|
writeCorsHeaders(res, origin) {
|
|
3009
4243
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
3010
4244
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
3011
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
|
|
4245
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization, Payment-Proof");
|
|
3012
4246
|
res.setHeader(
|
|
3013
4247
|
"Access-Control-Expose-Headers",
|
|
3014
|
-
"X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt"
|
|
4248
|
+
"X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt, Payment-Needed"
|
|
3015
4249
|
);
|
|
3016
4250
|
}
|
|
3017
4251
|
/**
|
|
@@ -3038,7 +4272,8 @@ var MoltsPayServer = class {
|
|
|
3038
4272
|
if (url.pathname === "/execute" && req.method === "POST") {
|
|
3039
4273
|
const body = await this.readBody(req);
|
|
3040
4274
|
const paymentHeader = req.headers[PAYMENT_HEADER2];
|
|
3041
|
-
|
|
4275
|
+
const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
|
|
4276
|
+
return await this.handleExecute(body, paymentHeader, res, proofHeader);
|
|
3042
4277
|
}
|
|
3043
4278
|
if (url.pathname === "/proxy" && req.method === "POST") {
|
|
3044
4279
|
const clientIP = req.headers["x-real-ip"] || req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "";
|
|
@@ -3056,7 +4291,8 @@ var MoltsPayServer = class {
|
|
|
3056
4291
|
const body = req.method === "POST" ? await this.readBody(req) : {};
|
|
3057
4292
|
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
3058
4293
|
const x402Header = req.headers[PAYMENT_HEADER2];
|
|
3059
|
-
|
|
4294
|
+
const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
|
|
4295
|
+
return await this.handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader);
|
|
3060
4296
|
}
|
|
3061
4297
|
this.sendJson(res, 404, { error: "Not found" });
|
|
3062
4298
|
} catch (err) {
|
|
@@ -3153,7 +4389,7 @@ var MoltsPayServer = class {
|
|
|
3153
4389
|
/**
|
|
3154
4390
|
* POST /execute - Execute service with x402 payment
|
|
3155
4391
|
*/
|
|
3156
|
-
async handleExecute(body, paymentHeader, res) {
|
|
4392
|
+
async handleExecute(body, paymentHeader, res, proofHeader) {
|
|
3157
4393
|
const { service, params } = body;
|
|
3158
4394
|
if (!service) {
|
|
3159
4395
|
return this.sendJson(res, 400, { error: "Missing service" });
|
|
@@ -3167,6 +4403,15 @@ var MoltsPayServer = class {
|
|
|
3167
4403
|
return this.sendJson(res, 400, { error: `Missing required param: ${key}` });
|
|
3168
4404
|
}
|
|
3169
4405
|
}
|
|
4406
|
+
if (proofHeader) {
|
|
4407
|
+
const alipayPayment = {
|
|
4408
|
+
x402Version: X402_VERSION3,
|
|
4409
|
+
scheme: ALIPAY_SCHEME,
|
|
4410
|
+
network: ALIPAY_NETWORK,
|
|
4411
|
+
payload: proofHeader
|
|
4412
|
+
};
|
|
4413
|
+
return this.handleAlipayExecute(skill, params || {}, alipayPayment, res);
|
|
4414
|
+
}
|
|
3170
4415
|
if (!paymentHeader) {
|
|
3171
4416
|
return this.sendPaymentRequired(skill.config, res);
|
|
3172
4417
|
}
|
|
@@ -3177,6 +4422,11 @@ var MoltsPayServer = class {
|
|
|
3177
4422
|
} catch {
|
|
3178
4423
|
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
3179
4424
|
}
|
|
4425
|
+
const payScheme = payment.accepted?.scheme || payment.scheme;
|
|
4426
|
+
const payNetwork = payment.accepted?.network || payment.network;
|
|
4427
|
+
if (payScheme === ALIPAY_SCHEME || (payNetwork ? isAlipayChainId(payNetwork) : false)) {
|
|
4428
|
+
return this.handleAlipayExecute(skill, params || {}, payment, res);
|
|
4429
|
+
}
|
|
3180
4430
|
const validation = this.validatePayment(payment, skill.config);
|
|
3181
4431
|
if (!validation.valid) {
|
|
3182
4432
|
return this.sendJson(res, 402, { error: validation.error });
|
|
@@ -3269,13 +4519,111 @@ var MoltsPayServer = class {
|
|
|
3269
4519
|
payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
|
|
3270
4520
|
}, responseHeaders);
|
|
3271
4521
|
}
|
|
4522
|
+
/**
|
|
4523
|
+
* Execute a service paid via the Alipay AI 收 fiat rail (2.0.0).
|
|
4524
|
+
*
|
|
4525
|
+
* Differs from the EVM/SVM path: no token detection, no EIP-3009/permit
|
|
4526
|
+
* validation. Verify hits the Alipay Open API (`payment.verify`). Settlement
|
|
4527
|
+
* (`fulfillment.confirm`) is FIRE-AND-FORGET per ALIPAY-INTEGRATION-DESIGN
|
|
4528
|
+
* §5.1: a confirm failure is logged but does NOT fail the already-delivered
|
|
4529
|
+
* response (the buyer's payment proof was already verified).
|
|
4530
|
+
*/
|
|
4531
|
+
async handleAlipayExecute(skill, params, payment, res) {
|
|
4532
|
+
if (!this.alipayFacilitator) {
|
|
4533
|
+
return this.sendJson(res, 402, { error: "Alipay rail not configured on this server" });
|
|
4534
|
+
}
|
|
4535
|
+
const requirements = {
|
|
4536
|
+
scheme: ALIPAY_SCHEME,
|
|
4537
|
+
network: ALIPAY_NETWORK,
|
|
4538
|
+
asset: "CNY",
|
|
4539
|
+
amount: skill.config.alipay?.price_cny || "0",
|
|
4540
|
+
payTo: this.manifest.provider.alipay?.seller_id || "",
|
|
4541
|
+
maxTimeoutSeconds: 1800
|
|
4542
|
+
};
|
|
4543
|
+
console.log(`[MoltsPay] Verifying Alipay payment...`);
|
|
4544
|
+
const verifyResult = await this.registry.verify(payment, requirements);
|
|
4545
|
+
if (!verifyResult.valid) {
|
|
4546
|
+
return this.sendJson(res, 402, {
|
|
4547
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
4548
|
+
facilitator: verifyResult.facilitator
|
|
4549
|
+
});
|
|
4550
|
+
}
|
|
4551
|
+
console.log(`[MoltsPay] Alipay payment verified by ${verifyResult.facilitator}`);
|
|
4552
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
4553
|
+
console.log(`[MoltsPay] Executing skill: ${skill.id} (timeout: ${timeoutSeconds}s)`);
|
|
4554
|
+
let result;
|
|
4555
|
+
try {
|
|
4556
|
+
result = await Promise.race([
|
|
4557
|
+
skill.handler(params),
|
|
4558
|
+
new Promise(
|
|
4559
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
4560
|
+
)
|
|
4561
|
+
]);
|
|
4562
|
+
} catch (err) {
|
|
4563
|
+
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
4564
|
+
return this.sendJson(res, 500, {
|
|
4565
|
+
error: "Service execution failed",
|
|
4566
|
+
message: err.message
|
|
4567
|
+
});
|
|
4568
|
+
}
|
|
4569
|
+
let settlement;
|
|
4570
|
+
try {
|
|
4571
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
4572
|
+
if (settlement.success) {
|
|
4573
|
+
console.log(`[MoltsPay] Alipay fulfillment confirmed: ${settlement.transaction}`);
|
|
4574
|
+
} else {
|
|
4575
|
+
console.error(`[MoltsPay] Alipay fulfillment confirm failed (non-fatal): ${settlement.error}`);
|
|
4576
|
+
}
|
|
4577
|
+
} catch (err) {
|
|
4578
|
+
console.error(`[MoltsPay] Alipay fulfillment confirm threw (non-fatal): ${err.message}`);
|
|
4579
|
+
settlement = { success: false, error: err.message, facilitator: "alipay" };
|
|
4580
|
+
}
|
|
4581
|
+
const responseHeaders = {};
|
|
4582
|
+
if (settlement.success) {
|
|
4583
|
+
responseHeaders[PAYMENT_RESPONSE_HEADER] = Buffer.from(JSON.stringify({
|
|
4584
|
+
success: true,
|
|
4585
|
+
transaction: settlement.transaction,
|
|
4586
|
+
network: ALIPAY_NETWORK,
|
|
4587
|
+
facilitator: settlement.facilitator
|
|
4588
|
+
})).toString("base64");
|
|
4589
|
+
}
|
|
4590
|
+
this.sendJson(res, 200, {
|
|
4591
|
+
success: true,
|
|
4592
|
+
result,
|
|
4593
|
+
payment: settlement.success ? { transaction: settlement.transaction, status: "fulfilled", facilitator: settlement.facilitator } : { status: "delivered_unconfirmed", error: settlement.error }
|
|
4594
|
+
}, responseHeaders);
|
|
4595
|
+
}
|
|
4596
|
+
/**
|
|
4597
|
+
* Build the Alipay 402 challenge for a service, or null when the alipay rail
|
|
4598
|
+
* isn't configured for this server or this service. Returns the x402
|
|
4599
|
+
* `accepts[]` entry plus the Base64URL `Payment-Needed` header value so the
|
|
4600
|
+
* 402 responders can dual-emit both the x402 and legacy alipay-bot formats.
|
|
4601
|
+
*/
|
|
4602
|
+
async buildAlipayChallenge(config) {
|
|
4603
|
+
if (!this.alipayFacilitator || !config.alipay) return null;
|
|
4604
|
+
try {
|
|
4605
|
+
const req = await this.alipayFacilitator.createPaymentRequirements({
|
|
4606
|
+
serviceId: config.alipay.service_id || this.manifest.provider.alipay.service_id_default,
|
|
4607
|
+
priceCny: config.alipay.price_cny,
|
|
4608
|
+
goodsName: config.alipay.goods_name,
|
|
4609
|
+
resourceId: `/execute?service=${config.id}`
|
|
4610
|
+
});
|
|
4611
|
+
return { accepts: req.x402Accepts, paymentNeededHeader: req.paymentNeededHeader };
|
|
4612
|
+
} catch (err) {
|
|
4613
|
+
console.error(`[MoltsPay] Alipay challenge build failed for ${config.id}: ${err.message}`);
|
|
4614
|
+
return null;
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
3272
4617
|
/**
|
|
3273
4618
|
* Handle MPP (Machine Payments Protocol) request
|
|
3274
4619
|
* Supports both x402 and MPP protocols on service endpoints
|
|
3275
4620
|
*/
|
|
3276
|
-
async handleMPPRequest(skill, body, authHeader, x402Header, res) {
|
|
4621
|
+
async handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader) {
|
|
3277
4622
|
const config = skill.config;
|
|
3278
4623
|
const params = body || {};
|
|
4624
|
+
if (proofHeader) {
|
|
4625
|
+
return await this.handleExecute({ service: config.id, params }, void 0, res, proofHeader);
|
|
4626
|
+
}
|
|
3279
4627
|
if (x402Header) {
|
|
3280
4628
|
return await this.handleExecute({ service: config.id, params }, x402Header, res);
|
|
3281
4629
|
}
|
|
@@ -3381,7 +4729,7 @@ var MoltsPayServer = class {
|
|
|
3381
4729
|
/**
|
|
3382
4730
|
* Return 402 with both x402 and MPP payment requirements
|
|
3383
4731
|
*/
|
|
3384
|
-
sendMPPPaymentRequired(config, res) {
|
|
4732
|
+
async sendMPPPaymentRequired(config, res) {
|
|
3385
4733
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
3386
4734
|
const providerChains = this.getProviderChains();
|
|
3387
4735
|
const accepts = [];
|
|
@@ -3392,6 +4740,10 @@ var MoltsPayServer = class {
|
|
|
3392
4740
|
}
|
|
3393
4741
|
}
|
|
3394
4742
|
}
|
|
4743
|
+
const alipayChallenge = await this.buildAlipayChallenge(config);
|
|
4744
|
+
if (alipayChallenge) {
|
|
4745
|
+
accepts.push(alipayChallenge.accepts);
|
|
4746
|
+
}
|
|
3395
4747
|
const x402PaymentRequired = {
|
|
3396
4748
|
x402Version: X402_VERSION3,
|
|
3397
4749
|
accepts,
|
|
@@ -3419,7 +4771,7 @@ var MoltsPayServer = class {
|
|
|
3419
4771
|
};
|
|
3420
4772
|
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
3421
4773
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
3422
|
-
mppWwwAuth = `Payment id="${challengeId}", realm="${this.manifest.provider.name}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
4774
|
+
mppWwwAuth = `Payment id="${challengeId}", realm="${headerSafe(this.manifest.provider.name)}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
|
|
3423
4775
|
}
|
|
3424
4776
|
const headers = {
|
|
3425
4777
|
"Content-Type": "application/problem+json",
|
|
@@ -3428,6 +4780,9 @@ var MoltsPayServer = class {
|
|
|
3428
4780
|
if (mppWwwAuth) {
|
|
3429
4781
|
headers[MPP_WWW_AUTH_HEADER] = mppWwwAuth;
|
|
3430
4782
|
}
|
|
4783
|
+
if (alipayChallenge) {
|
|
4784
|
+
headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
|
|
4785
|
+
}
|
|
3431
4786
|
res.writeHead(402, headers);
|
|
3432
4787
|
res.end(JSON.stringify({
|
|
3433
4788
|
type: "https://paymentauth.org/problems/payment-required",
|
|
@@ -3454,7 +4809,7 @@ var MoltsPayServer = class {
|
|
|
3454
4809
|
* Return 402 with x402 payment requirements (v2 format)
|
|
3455
4810
|
* Includes requirements for all chains and all accepted currencies
|
|
3456
4811
|
*/
|
|
3457
|
-
sendPaymentRequired(config, res) {
|
|
4812
|
+
async sendPaymentRequired(config, res) {
|
|
3458
4813
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
3459
4814
|
const providerChains = this.getProviderChains();
|
|
3460
4815
|
const accepts = [];
|
|
@@ -3465,6 +4820,10 @@ var MoltsPayServer = class {
|
|
|
3465
4820
|
}
|
|
3466
4821
|
}
|
|
3467
4822
|
}
|
|
4823
|
+
const alipayChallenge = await this.buildAlipayChallenge(config);
|
|
4824
|
+
if (alipayChallenge) {
|
|
4825
|
+
accepts.push(alipayChallenge.accepts);
|
|
4826
|
+
}
|
|
3468
4827
|
const acceptedChains = providerChains.map((c) => {
|
|
3469
4828
|
if (c.network === "eip155:8453") return "base";
|
|
3470
4829
|
if (c.network === "eip155:137") return "polygon";
|
|
@@ -3482,10 +4841,14 @@ var MoltsPayServer = class {
|
|
|
3482
4841
|
}
|
|
3483
4842
|
};
|
|
3484
4843
|
const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
|
|
3485
|
-
|
|
4844
|
+
const headers = {
|
|
3486
4845
|
"Content-Type": "application/json",
|
|
3487
4846
|
[PAYMENT_REQUIRED_HEADER2]: encoded
|
|
3488
|
-
}
|
|
4847
|
+
};
|
|
4848
|
+
if (alipayChallenge) {
|
|
4849
|
+
headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
|
|
4850
|
+
}
|
|
4851
|
+
res.writeHead(402, headers);
|
|
3489
4852
|
res.end(JSON.stringify({
|
|
3490
4853
|
error: "Payment required",
|
|
3491
4854
|
message: `Service requires $${config.price} ${config.currency}`,
|
|
@@ -3592,12 +4955,12 @@ var MoltsPayServer = class {
|
|
|
3592
4955
|
return accepted.includes(token);
|
|
3593
4956
|
}
|
|
3594
4957
|
async readBody(req) {
|
|
3595
|
-
return new Promise((
|
|
4958
|
+
return new Promise((resolve3, reject) => {
|
|
3596
4959
|
let body = "";
|
|
3597
4960
|
req.on("data", (chunk) => body += chunk);
|
|
3598
4961
|
req.on("end", () => {
|
|
3599
4962
|
try {
|
|
3600
|
-
|
|
4963
|
+
resolve3(body ? JSON.parse(body) : {});
|
|
3601
4964
|
} catch {
|
|
3602
4965
|
reject(new Error("Invalid JSON"));
|
|
3603
4966
|
}
|
|
@@ -3855,7 +5218,7 @@ var MoltsPayServer = class {
|
|
|
3855
5218
|
};
|
|
3856
5219
|
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
3857
5220
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
3858
|
-
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
5221
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
|
|
3859
5222
|
res.writeHead(402, {
|
|
3860
5223
|
"Content-Type": "application/problem+json",
|
|
3861
5224
|
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
@@ -4025,10 +5388,10 @@ init_esm_shims();
|
|
|
4025
5388
|
async function printQRCode(url) {
|
|
4026
5389
|
const qrcodeModule = await import("qrcode-terminal");
|
|
4027
5390
|
const qrcode = qrcodeModule.default || qrcodeModule;
|
|
4028
|
-
return new Promise((
|
|
5391
|
+
return new Promise((resolve3) => {
|
|
4029
5392
|
qrcode.generate(url, { small: true }, (qr) => {
|
|
4030
5393
|
console.log(qr);
|
|
4031
|
-
|
|
5394
|
+
resolve3();
|
|
4032
5395
|
});
|
|
4033
5396
|
});
|
|
4034
5397
|
}
|
|
@@ -4040,9 +5403,9 @@ if (!globalThis.crypto) {
|
|
|
4040
5403
|
}
|
|
4041
5404
|
function getVersion() {
|
|
4042
5405
|
const locations = [
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
5406
|
+
join6(__dirname, "../../package.json"),
|
|
5407
|
+
join6(__dirname, "../package.json"),
|
|
5408
|
+
join6(process.cwd(), "node_modules/moltspay/package.json")
|
|
4046
5409
|
];
|
|
4047
5410
|
for (const loc of locations) {
|
|
4048
5411
|
try {
|
|
@@ -4121,7 +5484,7 @@ async function checkBNBApprovals(address, chain, configDir = DEFAULT_CONFIG_DIR2
|
|
|
4121
5484
|
const provider = new ethers4.JsonRpcProvider(chainConfig.rpc);
|
|
4122
5485
|
let spenderAddress = null;
|
|
4123
5486
|
try {
|
|
4124
|
-
const walletPath =
|
|
5487
|
+
const walletPath = join6(configDir, "wallet.json");
|
|
4125
5488
|
const walletData = JSON.parse(readFileSync5(walletPath, "utf-8"));
|
|
4126
5489
|
spenderAddress = walletData.approvals?.[chain] || null;
|
|
4127
5490
|
} catch {
|
|
@@ -4139,8 +5502,8 @@ async function checkBNBApprovals(address, chain, configDir = DEFAULT_CONFIG_DIR2
|
|
|
4139
5502
|
return result;
|
|
4140
5503
|
}
|
|
4141
5504
|
var program = new Command();
|
|
4142
|
-
var DEFAULT_CONFIG_DIR2 =
|
|
4143
|
-
var PID_FILE =
|
|
5505
|
+
var DEFAULT_CONFIG_DIR2 = join6(homedir4(), ".moltspay");
|
|
5506
|
+
var PID_FILE = join6(DEFAULT_CONFIG_DIR2, "server.pid");
|
|
4144
5507
|
if (!existsSync5(DEFAULT_CONFIG_DIR2)) {
|
|
4145
5508
|
mkdirSync3(DEFAULT_CONFIG_DIR2, { recursive: true });
|
|
4146
5509
|
}
|
|
@@ -4149,10 +5512,10 @@ function prompt(question) {
|
|
|
4149
5512
|
input: process.stdin,
|
|
4150
5513
|
output: process.stdout
|
|
4151
5514
|
});
|
|
4152
|
-
return new Promise((
|
|
5515
|
+
return new Promise((resolve3) => {
|
|
4153
5516
|
rl.question(question, (answer) => {
|
|
4154
5517
|
rl.close();
|
|
4155
|
-
|
|
5518
|
+
resolve3(answer.trim());
|
|
4156
5519
|
});
|
|
4157
5520
|
});
|
|
4158
5521
|
}
|
|
@@ -4180,7 +5543,7 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
|
|
|
4180
5543
|
console.log(`
|
|
4181
5544
|
\u2705 Solana wallet created: ${address}`);
|
|
4182
5545
|
console.log(`
|
|
4183
|
-
\u{1F4C1} Config saved to: ${
|
|
5546
|
+
\u{1F4C1} Config saved to: ${join6(options.configDir, "wallet-solana.json")}`);
|
|
4184
5547
|
console.log(`
|
|
4185
5548
|
\u26A0\uFE0F IMPORTANT: Back up your wallet file!`);
|
|
4186
5549
|
console.log(` This file contains your private key!
|
|
@@ -4195,7 +5558,7 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
|
|
|
4195
5558
|
return;
|
|
4196
5559
|
}
|
|
4197
5560
|
console.log("\n\u{1F510} MoltsPay Client Setup\n");
|
|
4198
|
-
if (existsSync5(
|
|
5561
|
+
if (existsSync5(join6(options.configDir, "wallet.json"))) {
|
|
4199
5562
|
console.log('\u26A0\uFE0F EVM wallet already initialized. Use "moltspay config" to update settings.');
|
|
4200
5563
|
console.log(` Config dir: ${options.configDir}`);
|
|
4201
5564
|
return;
|
|
@@ -4221,7 +5584,7 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
|
|
|
4221
5584
|
console.log(`
|
|
4222
5585
|
\u{1F4C1} Config saved to: ${result.configDir}`);
|
|
4223
5586
|
console.log(`
|
|
4224
|
-
\u26A0\uFE0F IMPORTANT: Back up ${
|
|
5587
|
+
\u26A0\uFE0F IMPORTANT: Back up ${join6(result.configDir, "wallet.json")}`);
|
|
4225
5588
|
console.log(` This file contains your private key!
|
|
4226
5589
|
`);
|
|
4227
5590
|
if (chain === "bnb" || chain === "bnb_testnet") {
|
|
@@ -4386,7 +5749,7 @@ program.command("approve").description("Approve a spender address for BNB chain
|
|
|
4386
5749
|
\u{1F510} Approving spender for ${chain}...
|
|
4387
5750
|
`);
|
|
4388
5751
|
await setupBNBApprovals(client, chain, options.spender, false);
|
|
4389
|
-
const walletPath =
|
|
5752
|
+
const walletPath = join6(options.configDir || DEFAULT_CONFIG_DIR2, "wallet.json");
|
|
4390
5753
|
try {
|
|
4391
5754
|
const walletData = JSON.parse(readFileSync5(walletPath, "utf-8"));
|
|
4392
5755
|
walletData.approvals = walletData.approvals || {};
|
|
@@ -5023,17 +6386,17 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
5023
6386
|
const handlers = /* @__PURE__ */ new Map();
|
|
5024
6387
|
let provider = null;
|
|
5025
6388
|
for (const inputPath of allPaths) {
|
|
5026
|
-
const resolvedPath =
|
|
6389
|
+
const resolvedPath = resolve2(inputPath);
|
|
5027
6390
|
let manifestPath;
|
|
5028
6391
|
let skillDir;
|
|
5029
6392
|
let isSkillDir = false;
|
|
5030
|
-
if (existsSync5(
|
|
5031
|
-
manifestPath =
|
|
6393
|
+
if (existsSync5(join6(resolvedPath, "moltspay.services.json"))) {
|
|
6394
|
+
manifestPath = join6(resolvedPath, "moltspay.services.json");
|
|
5032
6395
|
skillDir = resolvedPath;
|
|
5033
6396
|
isSkillDir = true;
|
|
5034
6397
|
} else if (existsSync5(resolvedPath) && resolvedPath.endsWith(".json")) {
|
|
5035
6398
|
manifestPath = resolvedPath;
|
|
5036
|
-
skillDir =
|
|
6399
|
+
skillDir = dirname2(resolvedPath);
|
|
5037
6400
|
} else if (existsSync5(resolvedPath)) {
|
|
5038
6401
|
console.error(`\u274C No moltspay.services.json found in: ${resolvedPath}`);
|
|
5039
6402
|
continue;
|
|
@@ -5050,7 +6413,7 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
5050
6413
|
let skillModule = null;
|
|
5051
6414
|
if (isSkillDir) {
|
|
5052
6415
|
let entryPoint = "index.js";
|
|
5053
|
-
const pkgJsonPath =
|
|
6416
|
+
const pkgJsonPath = join6(skillDir, "package.json");
|
|
5054
6417
|
if (existsSync5(pkgJsonPath)) {
|
|
5055
6418
|
try {
|
|
5056
6419
|
const pkgJson = JSON.parse(readFileSync5(pkgJsonPath, "utf-8"));
|
|
@@ -5060,7 +6423,7 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
5060
6423
|
} catch {
|
|
5061
6424
|
}
|
|
5062
6425
|
}
|
|
5063
|
-
const modulePath =
|
|
6426
|
+
const modulePath = join6(skillDir, entryPoint);
|
|
5064
6427
|
if (existsSync5(modulePath)) {
|
|
5065
6428
|
try {
|
|
5066
6429
|
skillModule = await import(modulePath);
|
|
@@ -5086,7 +6449,7 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
5086
6449
|
const workdir = skillDir;
|
|
5087
6450
|
handlers.set(service.id, async (params) => {
|
|
5088
6451
|
return new Promise((resolvePromise, reject) => {
|
|
5089
|
-
const proc =
|
|
6452
|
+
const proc = spawn2("sh", ["-c", service.command], {
|
|
5090
6453
|
cwd: workdir,
|
|
5091
6454
|
stdio: ["pipe", "pipe", "pipe"]
|
|
5092
6455
|
});
|
|
@@ -5139,7 +6502,7 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
5139
6502
|
provider,
|
|
5140
6503
|
services: allServices
|
|
5141
6504
|
};
|
|
5142
|
-
const tempManifestPath =
|
|
6505
|
+
const tempManifestPath = join6(DEFAULT_CONFIG_DIR2, "combined-manifest.json");
|
|
5143
6506
|
writeFileSync3(tempManifestPath, JSON.stringify(combinedManifest, null, 2));
|
|
5144
6507
|
console.log(`
|
|
5145
6508
|
\u{1F4CB} Combined manifest: ${allServices.length} services`);
|
|
@@ -5202,7 +6565,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
5202
6565
|
}
|
|
5203
6566
|
process.kill(pid, "SIGTERM");
|
|
5204
6567
|
console.log("\u2705 Sent SIGTERM to server");
|
|
5205
|
-
await new Promise((
|
|
6568
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
5206
6569
|
try {
|
|
5207
6570
|
process.kill(pid, 0);
|
|
5208
6571
|
console.log("\u26A0\uFE0F Server still running, sending SIGKILL...");
|
|
@@ -5218,9 +6581,10 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
5218
6581
|
process.exit(1);
|
|
5219
6582
|
}
|
|
5220
6583
|
});
|
|
5221
|
-
program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <path>", "Image URL or local file path").option("--data <json>", "Raw JSON data to send (for custom input formats)").option("--token <token>", "Token to pay with (USDC or USDT)", "USDC").option("--chain <chain>", "Chain to pay on (base, polygon, base_sepolia, tempo_moderato, solana, or solana_devnet).").option("--config-dir <dir>", "Config directory with wallet.json", DEFAULT_CONFIG_DIR2).option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
|
|
6584
|
+
program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <path>", "Image URL or local file path").option("--data <json>", "Raw JSON data to send (for custom input formats)").option("--token <token>", "Token to pay with (USDC or USDT)", "USDC").option("--chain <chain>", "Chain to pay on (base, polygon, base_sepolia, tempo_moderato, solana, or solana_devnet).").option("--rail <rail>", 'Payment rail: a chain name, or "alipay" for the Alipay fiat rail (CNY via alipay-bot)').option("--config-dir <dir>", "Config directory with wallet.json", DEFAULT_CONFIG_DIR2).option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
|
|
5222
6585
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
5223
|
-
|
|
6586
|
+
const useAlipay = options.rail?.toLowerCase() === "alipay";
|
|
6587
|
+
if (!useAlipay && !client.isInitialized) {
|
|
5224
6588
|
console.error("\u274C Wallet not initialized. Run: npx moltspay init");
|
|
5225
6589
|
process.exit(1);
|
|
5226
6590
|
}
|
|
@@ -5248,7 +6612,7 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
5248
6612
|
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
|
5249
6613
|
params.image_url = imagePath;
|
|
5250
6614
|
} else {
|
|
5251
|
-
const filePath =
|
|
6615
|
+
const filePath = resolve2(imagePath);
|
|
5252
6616
|
if (!existsSync5(filePath)) {
|
|
5253
6617
|
console.error(`\u274C Image file not found: ${filePath}`);
|
|
5254
6618
|
process.exit(1);
|
|
@@ -5265,7 +6629,7 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
5265
6629
|
}
|
|
5266
6630
|
const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
|
|
5267
6631
|
const token = (options.token || "USDC").toUpperCase();
|
|
5268
|
-
if (token === "USDT") {
|
|
6632
|
+
if (!useAlipay && token === "USDT") {
|
|
5269
6633
|
const balance = await client.getBalance();
|
|
5270
6634
|
if (balance.native < 1e-4) {
|
|
5271
6635
|
console.log("\n\u26A0\uFE0F USDT requires a small amount of ETH for gas (~$0.01)");
|
|
@@ -5289,13 +6653,31 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
5289
6653
|
console.log(` Prompt: ${params.prompt}`);
|
|
5290
6654
|
}
|
|
5291
6655
|
if (imageDisplay) console.log(` Image: ${imageDisplay}`);
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
6656
|
+
if (useAlipay) {
|
|
6657
|
+
console.log(` Rail: alipay (CNY via alipay-bot)`);
|
|
6658
|
+
} else {
|
|
6659
|
+
console.log(` Chain: ${chain || "(auto)"}`);
|
|
6660
|
+
console.log(` Token: ${token}`);
|
|
6661
|
+
console.log(` Wallet: ${client.address}`);
|
|
6662
|
+
}
|
|
5295
6663
|
console.log("");
|
|
5296
6664
|
}
|
|
5297
6665
|
try {
|
|
5298
|
-
const result = await client.pay(server, service, params, {
|
|
6666
|
+
const result = await client.pay(server, service, params, useAlipay ? {
|
|
6667
|
+
rail: "alipay",
|
|
6668
|
+
rawData: useRawData,
|
|
6669
|
+
onPaymentPending: ({ paymentUrl, shortenUrl }) => {
|
|
6670
|
+
if (!options.json) {
|
|
6671
|
+
process.stdout.write(`
|
|
6672
|
+
\u{1F4F2} \u8BF7\u7528\u652F\u4ED8\u5B9D\u626B\u7801\u6216\u8BBF\u95EE\uFF1A${shortenUrl ?? paymentUrl}
|
|
6673
|
+
|
|
6674
|
+
`);
|
|
6675
|
+
}
|
|
6676
|
+
},
|
|
6677
|
+
onLine: (line) => {
|
|
6678
|
+
if (!options.json) process.stdout.write(line + "\n");
|
|
6679
|
+
}
|
|
6680
|
+
} : {
|
|
5299
6681
|
token,
|
|
5300
6682
|
chain,
|
|
5301
6683
|
rawData: useRawData
|
|
@@ -5316,11 +6698,34 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
5316
6698
|
process.exit(1);
|
|
5317
6699
|
}
|
|
5318
6700
|
});
|
|
6701
|
+
program.command("alipay <action> [args...]").description("Alipay wallet setup via alipay-bot: check | apply | bind").allowUnknownOption().action(async (action, args) => {
|
|
6702
|
+
const map = {
|
|
6703
|
+
check: "check-wallet",
|
|
6704
|
+
apply: "apply-wallet",
|
|
6705
|
+
bind: "bind-wallet"
|
|
6706
|
+
};
|
|
6707
|
+
const sub = map[action] ?? action;
|
|
6708
|
+
const child = spawn2("alipay-bot", [sub, ...args ?? []], {
|
|
6709
|
+
stdio: "inherit",
|
|
6710
|
+
env: filterEnv(process.env)
|
|
6711
|
+
});
|
|
6712
|
+
child.on("error", (e) => {
|
|
6713
|
+
if (e?.code === "ENOENT") {
|
|
6714
|
+
console.error(
|
|
6715
|
+
"\u274C alipay-bot not installed. Run: npx -y @alipay/agent-payment install-cli"
|
|
6716
|
+
);
|
|
6717
|
+
} else {
|
|
6718
|
+
console.error(`\u274C ${e.message}`);
|
|
6719
|
+
}
|
|
6720
|
+
process.exit(1);
|
|
6721
|
+
});
|
|
6722
|
+
child.on("exit", (code) => process.exit(code ?? 1));
|
|
6723
|
+
});
|
|
5319
6724
|
program.command("validate <path>").description("Validate a moltspay.services.json file against the schema").action(async (inputPath) => {
|
|
5320
|
-
const resolvedPath =
|
|
6725
|
+
const resolvedPath = resolve2(inputPath);
|
|
5321
6726
|
let manifestPath;
|
|
5322
|
-
if (existsSync5(
|
|
5323
|
-
manifestPath =
|
|
6727
|
+
if (existsSync5(join6(resolvedPath, "moltspay.services.json"))) {
|
|
6728
|
+
manifestPath = join6(resolvedPath, "moltspay.services.json");
|
|
5324
6729
|
} else if (resolvedPath.endsWith(".json") && existsSync5(resolvedPath)) {
|
|
5325
6730
|
manifestPath = resolvedPath;
|
|
5326
6731
|
} else {
|