z-zero-mcp-server 1.1.0 → 1.1.1
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 +3 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +123 -265
- package/dist/lib/extract-total-price.js +3 -1
- package/dist/lib/key-store.js +2 -2
- package/dist/lib/web3-detector.d.ts +2 -3
- package/dist/lib/web3-detector.js +28 -53
- package/dist/playwright_bridge.d.ts +1 -1
- package/dist/playwright_bridge.js +174 -91
- package/dist/version.d.ts +1 -0
- package/dist/version.js +6 -0
- package/dist/wdk_backend.js +32 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,6 +52,9 @@ Get your Passport Key at: **[clawcard.store/dashboard/agents](https://www.clawca
|
|
|
52
52
|
| `execute_payment` | Auto-fill checkout form and execute payment |
|
|
53
53
|
| `cancel_payment_token` | Cancel unused token, refund to wallet |
|
|
54
54
|
| `request_human_approval` | Pause and ask human for approval |
|
|
55
|
+
| `set_api_key` | **(Hot-Swap)** Update the Passport Key *without restarting Claude* |
|
|
56
|
+
| `show_api_key_status` | Check if a Passport Key is currently loaded |
|
|
57
|
+
| `check_for_updates` | **(Maintenance)** Check if a new MCP version is available |
|
|
55
58
|
|
|
56
59
|
---
|
|
57
60
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export {};
|
|
2
|
+
export { CURRENT_MCP_VERSION } from "./version.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// OpenClaw MCP Server (z-zero-mcp-server) v1.1.0
|
|
4
4
|
// Exposes secure JIT payment tools to AI Agents via Model Context Protocol
|
|
5
5
|
// Status: Connected to Z-ZERO Gateway — produces secure JIT virtual cards
|
|
6
|
-
// WDK Mode: Set Z_ZERO_WALLET_MODE=wdk for non-custodial WDK payments
|
|
7
6
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
7
|
if (k2 === undefined) k2 = k;
|
|
9
8
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -38,21 +37,17 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
37
|
};
|
|
39
38
|
})();
|
|
40
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
exports.CURRENT_MCP_VERSION = void 0;
|
|
41
|
+
var version_js_1 = require("./version.js");
|
|
42
|
+
Object.defineProperty(exports, "CURRENT_MCP_VERSION", { enumerable: true, get: function () { return version_js_1.CURRENT_MCP_VERSION; } });
|
|
43
|
+
const version_js_2 = require("./version.js");
|
|
44
|
+
// Note: version warnings are now delivered automatically via X-MCP-Version header
|
|
45
|
+
// in each API call — no need for a separate check_for_updates tool.
|
|
43
46
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
44
47
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
45
48
|
const zod_1 = require("zod");
|
|
46
|
-
// ── Backend
|
|
47
|
-
|
|
48
|
-
// Both backends export identical function signatures (drop-in swap).
|
|
49
|
-
const WALLET_MODE = process.env.Z_ZERO_WALLET_MODE || "custodial";
|
|
50
|
-
const isWDKMode = WALLET_MODE === "wdk";
|
|
51
|
-
// Import both backends; pick which one to use based on env var.
|
|
52
|
-
// In production build, tree-shaking will handle this. In dev (tsx), both load.
|
|
53
|
-
const custodialBackend = __importStar(require("./api_backend.js"));
|
|
54
|
-
const wdkBackend = __importStar(require("./wdk_backend.js"));
|
|
55
|
-
const activeBackend = isWDKMode ? wdkBackend : custodialBackend;
|
|
49
|
+
// ── WDK Non-Custodial Backend (single backend, no custodial fallback) ─────────
|
|
50
|
+
const activeBackend = __importStar(require("./wdk_backend.js"));
|
|
56
51
|
const issueTokenRemote = activeBackend.issueTokenRemote;
|
|
57
52
|
const resolveTokenRemote = activeBackend.resolveTokenRemote;
|
|
58
53
|
const burnTokenRemote = activeBackend.burnTokenRemote;
|
|
@@ -61,7 +56,7 @@ const refundUnderspendRemote = activeBackend.refundUnderspendRemote;
|
|
|
61
56
|
const getBalanceRemote = activeBackend.getBalanceRemote;
|
|
62
57
|
const listCardsRemote = activeBackend.listCardsRemote;
|
|
63
58
|
const getDepositAddressesRemote = activeBackend.getDepositAddressesRemote;
|
|
64
|
-
console.error(`[Z-ZERO MCP] 🚀
|
|
59
|
+
console.error(`[Z-ZERO MCP] 🚀 Pure WDK Non-Custodial Mode`);
|
|
65
60
|
// ────────────────────────────────────────────────────────────────────────────
|
|
66
61
|
const playwright_bridge_js_1 = require("./playwright_bridge.js");
|
|
67
62
|
const web3_detector_js_1 = require("./lib/web3-detector.js");
|
|
@@ -73,7 +68,7 @@ const key_store_js_1 = require("./lib/key-store.js"); // ✅ Hot-Swap support
|
|
|
73
68
|
// ============================================================
|
|
74
69
|
const server = new mcp_js_1.McpServer({
|
|
75
70
|
name: "z-zero-mcp-server",
|
|
76
|
-
version:
|
|
71
|
+
version: version_js_2.CURRENT_MCP_VERSION,
|
|
77
72
|
});
|
|
78
73
|
// ============================================================
|
|
79
74
|
// TOOL 1: List available cards (safe - no sensitive data)
|
|
@@ -177,57 +172,36 @@ server.tool("get_deposit_addresses", "Get your unique deposit addresses for EVM
|
|
|
177
172
|
if (data?.wdk_wallet?.address) {
|
|
178
173
|
const wdkAddr = data.wdk_wallet.address;
|
|
179
174
|
const balance = data.wdk_wallet.balance_usdt ?? 0;
|
|
175
|
+
const tronAddr = data.wdk_wallet.tron_address;
|
|
180
176
|
return {
|
|
181
177
|
content: [{
|
|
182
178
|
type: "text",
|
|
183
179
|
text: JSON.stringify({
|
|
184
180
|
wallet_type: "non-custodial (WDK)",
|
|
185
|
-
address: wdkAddr,
|
|
186
181
|
balance_usdt: balance,
|
|
187
182
|
supported_chains: [
|
|
188
|
-
{ chain: "
|
|
183
|
+
{ chain: "Ethereum", token: "USDT", address: wdkAddr },
|
|
184
|
+
...(tronAddr ? [{ chain: "Tron", token: "USDT", address: tronAddr }] : []),
|
|
189
185
|
],
|
|
190
|
-
instructions: `Send USDT (
|
|
191
|
-
note: "Gasless payments via ERC-4337 Paymaster
|
|
186
|
+
instructions: `Send USDT (Ethereum ERC-20) to: ${wdkAddr}${tronAddr ? `\nSend USDT (Tron TRC-20) to: ${tronAddr}` : ''}`,
|
|
187
|
+
note: "Gasless payments via ERC-4337 Paymaster (Ethereum) / GasFree (Tron)."
|
|
192
188
|
}, null, 2),
|
|
193
189
|
}],
|
|
194
190
|
};
|
|
195
191
|
}
|
|
196
|
-
//
|
|
197
|
-
const addresses = data?.deposit_addresses;
|
|
198
|
-
if (!addresses) {
|
|
199
|
-
return {
|
|
200
|
-
content: [{
|
|
201
|
-
type: "text",
|
|
202
|
-
text: "Failed to retrieve deposit addresses. Please ensure your Z_ZERO_API_KEY (Passport Key) is valid. You can find it at https://www.clawcard.store/dashboard/agents",
|
|
203
|
-
}],
|
|
204
|
-
isError: true,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
192
|
+
// No WDK wallet connected
|
|
207
193
|
return {
|
|
208
194
|
content: [{
|
|
209
195
|
type: "text",
|
|
210
|
-
text:
|
|
211
|
-
wallet_type: "custodial",
|
|
212
|
-
evm: {
|
|
213
|
-
address: addresses.evm,
|
|
214
|
-
supported_chains: ["Base", "BNB Smart Chain (BSC)", "Ethereum"],
|
|
215
|
-
tokens: ["USDC", "USDT"]
|
|
216
|
-
},
|
|
217
|
-
tron: {
|
|
218
|
-
address: addresses.tron,
|
|
219
|
-
supported_chains: ["Tron (TRC-20)"],
|
|
220
|
-
tokens: ["USDT"]
|
|
221
|
-
},
|
|
222
|
-
note: "Funds sent to these addresses will be automatically credited to your Z-ZERO balance within minutes."
|
|
223
|
-
}, null, 2),
|
|
196
|
+
text: "No WDK wallet found. Please create one at https://www.clawcard.store/dashboard/agent-wallet",
|
|
224
197
|
}],
|
|
198
|
+
isError: true,
|
|
225
199
|
};
|
|
226
200
|
});
|
|
227
201
|
// ============================================================
|
|
228
202
|
// TOOL 3: Request a temporary payment token (issues secure JIT card)
|
|
229
203
|
// ============================================================
|
|
230
|
-
server.tool("request_payment_token", "Request a temporary payment token for a specific amount. A secure single-use virtual card is issued via the Z-ZERO network. The token is valid for
|
|
204
|
+
server.tool("request_payment_token", "Request a temporary payment token for a specific amount. A secure single-use virtual card is issued via the Z-ZERO network. The token is valid for 1 hour. Min: $1, Max: $100. Use this token with execute_payment to complete a purchase.", {
|
|
231
205
|
card_alias: zod_1.z
|
|
232
206
|
.string()
|
|
233
207
|
.describe("Which card to charge, e.g. 'Card_01'"),
|
|
@@ -288,7 +262,8 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
|
|
|
288
262
|
merchant: token.merchant,
|
|
289
263
|
expires_at: expiresAt,
|
|
290
264
|
card_issued: true,
|
|
291
|
-
instructions: "Use this token with execute_payment within
|
|
265
|
+
instructions: "Use this token with execute_payment within 1 hour. IMPORTANT: If the actual checkout price is HIGHER than the token amount, do NOT proceed — call cancel_payment_token first and request a new token with the correct amount.",
|
|
266
|
+
...(token.mcp_warning ? { _mcp_warning: token.mcp_warning } : {}),
|
|
292
267
|
}, null, 2),
|
|
293
268
|
},
|
|
294
269
|
],
|
|
@@ -495,49 +470,6 @@ server.tool("request_human_approval", "Request human approval before proceeding
|
|
|
495
470
|
};
|
|
496
471
|
});
|
|
497
472
|
// ============================================================
|
|
498
|
-
// TOOL 6.4: Check for Updates (polls clawcard.store, not npm)
|
|
499
|
-
// ============================================================
|
|
500
|
-
server.tool("check_for_updates", "Check if a newer version of this MCP server is available. Polls clawcard.store/api/version — independent of npm so works even if distribution changes. Call this when: (1) user asks if MCP is up to date, (2) a payment fails and you want to rule out a stale MCP version.", {}, async () => {
|
|
501
|
-
try {
|
|
502
|
-
const res = await fetch(ZZERO_VERSION_API, {
|
|
503
|
-
headers: { "User-Agent": `z-zero-mcp/${CURRENT_MCP_VERSION}` },
|
|
504
|
-
signal: AbortSignal.timeout(8_000),
|
|
505
|
-
});
|
|
506
|
-
if (!res.ok)
|
|
507
|
-
throw new Error(`HTTP ${res.status}`);
|
|
508
|
-
const data = await res.json();
|
|
509
|
-
const latest = data.latest_version;
|
|
510
|
-
const isUpToDate = CURRENT_MCP_VERSION === latest;
|
|
511
|
-
return {
|
|
512
|
-
content: [{
|
|
513
|
-
type: "text",
|
|
514
|
-
text: JSON.stringify({
|
|
515
|
-
current_version: CURRENT_MCP_VERSION,
|
|
516
|
-
latest_version: latest,
|
|
517
|
-
up_to_date: isUpToDate,
|
|
518
|
-
status: isUpToDate ? "✅ You are on the latest version." : `⚠️ Update available: ${CURRENT_MCP_VERSION} → ${latest}`,
|
|
519
|
-
release_notes: isUpToDate ? null : data.release_notes,
|
|
520
|
-
how_to_update: isUpToDate ? null : data.update_instructions,
|
|
521
|
-
}, null, 2),
|
|
522
|
-
}],
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
catch (err) {
|
|
526
|
-
return {
|
|
527
|
-
content: [{
|
|
528
|
-
type: "text",
|
|
529
|
-
text: JSON.stringify({
|
|
530
|
-
current_version: CURRENT_MCP_VERSION,
|
|
531
|
-
status: "⚠️ Could not reach version server. Check your internet connection.",
|
|
532
|
-
error: err.message,
|
|
533
|
-
fallback_check: `Visit https://www.clawcard.store to see latest version.`,
|
|
534
|
-
}, null, 2),
|
|
535
|
-
}],
|
|
536
|
-
isError: true,
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
});
|
|
540
|
-
// ============================================================
|
|
541
473
|
// TOOL 6.5: Set API Key (Hot-Swap Passport Key — NO restart needed)
|
|
542
474
|
// ============================================================
|
|
543
475
|
server.tool("set_api_key", "Update the Z-ZERO Passport Key immediately WITHOUT restarting the AI tool. Call this when the human provides a new key (e.g. 'zk_live_xxxxx'). The key is validated and activated instantly for all subsequent API calls. IMPORTANT: Never ask for the key proactively — only call this when the human explicitly provides it.", {
|
|
@@ -584,9 +516,6 @@ server.tool("show_api_key_status", "Show whether a Passport Key is currently con
|
|
|
584
516
|
}],
|
|
585
517
|
};
|
|
586
518
|
});
|
|
587
|
-
// ============================================================
|
|
588
|
-
// TOOL 7: Auto Pay Checkout (Phase 2 — Smart Routing)
|
|
589
|
-
// ============================================================
|
|
590
519
|
server.tool("auto_pay_checkout", "[Phase 2] Autonomous Smart Routing checkout tool. Provide a checkout URL and this tool will:\n" +
|
|
591
520
|
"1. Scan the page to detect if it supports Web3 (Crypto) payments via window.ethereum or EIP-681 links.\n" +
|
|
592
521
|
"2. SCENARIO A (Web3): If detected, automatically send USDT on-chain via WDK (gas ~$0.001). No Visa card needed.\n" +
|
|
@@ -597,22 +526,20 @@ server.tool("auto_pay_checkout", "[Phase 2] Autonomous Smart Routing checkout to
|
|
|
597
526
|
.describe("Full URL of the checkout/payment page to analyze and pay."),
|
|
598
527
|
card_alias: zod_1.z
|
|
599
528
|
.string()
|
|
600
|
-
.describe("Card alias to charge for JIT Fiat fallback, e.g. 'Card_01'.
|
|
529
|
+
.describe("Card alias to charge for JIT Fiat fallback, e.g. 'Card_01'."),
|
|
601
530
|
}, async ({ checkout_url, card_alias }) => {
|
|
602
531
|
const ZZERO_API = process.env.Z_ZERO_API_BASE || "https://www.clawcard.store";
|
|
603
|
-
const API_KEY = process.env
|
|
604
|
-
// ── C2 FIX: API key guard ────────────────────────────────────────────
|
|
532
|
+
const API_KEY = (0, key_store_js_1.getPassportKey)(); // ✅ FIX: use hot-swap key store, not process.env
|
|
605
533
|
if (!API_KEY) {
|
|
606
534
|
return {
|
|
607
535
|
content: [{ type: "text", text: JSON.stringify({
|
|
608
536
|
status: "CONFIG_ERROR",
|
|
609
|
-
message: "Z_ZERO_API_KEY is not configured
|
|
537
|
+
message: "Z_ZERO_API_KEY is not configured.",
|
|
610
538
|
}, null, 2) }],
|
|
611
539
|
isError: true,
|
|
612
540
|
};
|
|
613
541
|
}
|
|
614
|
-
// ──
|
|
615
|
-
// Block non-https and internal/private IP ranges
|
|
542
|
+
// ── SSRF guard ───────────────────────────────────────────────
|
|
616
543
|
(() => {
|
|
617
544
|
let url;
|
|
618
545
|
try {
|
|
@@ -623,13 +550,9 @@ server.tool("auto_pay_checkout", "[Phase 2] Autonomous Smart Routing checkout to
|
|
|
623
550
|
}
|
|
624
551
|
const isDev = process.env.NODE_ENV !== "production";
|
|
625
552
|
const hostname = url.hostname;
|
|
626
|
-
|
|
627
|
-
if (isDev && (hostname === "localhost" || hostname === "127.0.0.1")) {
|
|
628
|
-
console.error(`[SMART ROUTE] Dev mode: allowing localhost checkout URL`);
|
|
629
|
-
}
|
|
630
|
-
else {
|
|
553
|
+
if (!(isDev && (hostname === "localhost" || hostname === "127.0.0.1"))) {
|
|
631
554
|
if (url.protocol !== "https:") {
|
|
632
|
-
throw new Error(`checkout_url must use HTTPS
|
|
555
|
+
throw new Error(`checkout_url must use HTTPS.`);
|
|
633
556
|
}
|
|
634
557
|
const privatePatterns = [
|
|
635
558
|
/^localhost$/i, /^127\./, /^10\./,
|
|
@@ -637,187 +560,115 @@ server.tool("auto_pay_checkout", "[Phase 2] Autonomous Smart Routing checkout to
|
|
|
637
560
|
/^169\.254\./, /^\[::1\]$/, /^0\.0\.0\.0$/,
|
|
638
561
|
];
|
|
639
562
|
if (privatePatterns.some(p => p.test(hostname))) {
|
|
640
|
-
throw new Error(`SSRF blocked
|
|
563
|
+
throw new Error(`SSRF blocked host: ${hostname}`);
|
|
641
564
|
}
|
|
642
565
|
}
|
|
643
566
|
})();
|
|
644
|
-
// ──
|
|
645
|
-
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
567
|
+
// ── Single Browser Instance for efficiency ──────────────────
|
|
568
|
+
const browser = await playwright_1.chromium.launch({ headless: true });
|
|
569
|
+
const context = await browser.newContext();
|
|
570
|
+
const page = await context.newPage();
|
|
571
|
+
try {
|
|
572
|
+
// STEP 1: Web3 Detection
|
|
573
|
+
console.error(`[SMART ROUTE] Scanning ${checkout_url} for Web3...`);
|
|
574
|
+
const web3Result = await (0, web3_detector_js_1.detectWeb3Payment)(page, checkout_url);
|
|
575
|
+
if (web3Result.detected && web3Result.params) {
|
|
576
|
+
const { to, eip681_amount, data } = web3Result.params;
|
|
577
|
+
let amount = eip681_amount ?? 0;
|
|
578
|
+
if (!amount && data && data.length >= 138) {
|
|
579
|
+
const amountHex = data.slice(-64);
|
|
580
|
+
amount = Number(BigInt(`0x${amountHex}`)) / 1_000_000;
|
|
581
|
+
}
|
|
582
|
+
if (!amount || amount <= 0) {
|
|
583
|
+
return {
|
|
584
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
585
|
+
route: "WEB3", status: "AMOUNT_REQUIRED", recipient: to,
|
|
586
|
+
message: "Web3 detected but amount is unknown.",
|
|
587
|
+
}, null, 2) }],
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
const resp = await fetch(`${ZZERO_API}/api/wdk/transfer`, {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: { "Content-Type": "application/json", "x-passport-key": API_KEY },
|
|
593
|
+
body: JSON.stringify({ to, amount, card_alias }),
|
|
594
|
+
});
|
|
595
|
+
const txResult = await resp.json();
|
|
596
|
+
if (!resp.ok || !txResult.success) {
|
|
597
|
+
return {
|
|
598
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
599
|
+
route: "WEB3", status: "FAILED", reason: txResult.message || "Transfer failed",
|
|
600
|
+
}, null, 2) }],
|
|
601
|
+
isError: true,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
652
604
|
return {
|
|
653
|
-
content: [{
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
recipient: to,
|
|
660
|
-
message: `Web3 payment gateway detected (${web3Result.method}), recipient: ${to}. Could not determine exact USDT amount automatically. Please confirm the amount with the user, then call this tool again with the amount in the checkout_url or use pay_crypto(to=${to}, amount=<confirmed_amount>).`,
|
|
661
|
-
}, null, 2),
|
|
662
|
-
}],
|
|
605
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
606
|
+
route: "WEB3", method: web3Result.method, status: "SUCCESS",
|
|
607
|
+
recipient: to, amount_usdt: amount, tx_hash: txResult.txHash,
|
|
608
|
+
message: `✅ Web3 payment sent on-chain! ${amount} USDT → ${to}.`,
|
|
609
|
+
gas_savings: "~$0.001 (ERC-4337 Paymaster, gasless for user)",
|
|
610
|
+
}, null, 2) }],
|
|
663
611
|
};
|
|
664
612
|
}
|
|
665
|
-
//
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if (!
|
|
669
|
-
const amountHex = data.slice(-64);
|
|
670
|
-
const amountRaw = BigInt(`0x${amountHex}`);
|
|
671
|
-
amount = Number(amountRaw) / 1_000_000;
|
|
672
|
-
}
|
|
673
|
-
// Guard: if we still can't determine amount, ask user
|
|
674
|
-
if (!amount || amount <= 0) {
|
|
613
|
+
// STEP 2: Fiat Fallback (Price Extraction)
|
|
614
|
+
console.error(`[SMART ROUTE] No Web3. Extracting price...`);
|
|
615
|
+
const totalPrice = await (0, extract_total_price_js_1.extractTotalPrice)(page);
|
|
616
|
+
if (!totalPrice) {
|
|
675
617
|
return {
|
|
676
|
-
content: [{
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
detected: true,
|
|
681
|
-
status: "AMOUNT_REQUIRED",
|
|
682
|
-
recipient: to,
|
|
683
|
-
message: `Web3 tx detected but could not decode USDT amount from calldata. Please confirm the exact amount with the user and use pay_crypto(to=${to}, amount=<amount>).`,
|
|
684
|
-
}, null, 2),
|
|
685
|
-
}],
|
|
618
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
619
|
+
route: "FIAT", status: "PRICE_NOT_FOUND",
|
|
620
|
+
}, null, 2) }],
|
|
621
|
+
isError: true,
|
|
686
622
|
};
|
|
687
623
|
}
|
|
688
|
-
|
|
689
|
-
method: "POST",
|
|
690
|
-
headers: {
|
|
691
|
-
"Content-Type": "application/json",
|
|
692
|
-
"x-passport-key": API_KEY,
|
|
693
|
-
},
|
|
694
|
-
body: JSON.stringify({ to, amount, card_alias }),
|
|
695
|
-
});
|
|
696
|
-
const txResult = await resp.json();
|
|
697
|
-
if (!resp.ok || !txResult.success) {
|
|
624
|
+
if (totalPrice < 1 || totalPrice > 100) {
|
|
698
625
|
return {
|
|
699
|
-
content: [{
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
route: "WEB3",
|
|
703
|
-
status: "FAILED",
|
|
704
|
-
reason: txResult.message || txResult.error || "Unknown error from WDK transfer API",
|
|
705
|
-
}, null, 2),
|
|
706
|
-
}],
|
|
626
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
627
|
+
route: "FIAT", status: "AMOUNT_OUT_OF_RANGE", detected_price: totalPrice,
|
|
628
|
+
}, null, 2) }],
|
|
707
629
|
isError: true,
|
|
708
630
|
};
|
|
709
631
|
}
|
|
632
|
+
// Issue Token
|
|
633
|
+
const token = await issueTokenRemote(card_alias, totalPrice, checkout_url);
|
|
634
|
+
if (!token || token.error)
|
|
635
|
+
throw new Error(token?.message || "Token issue failed");
|
|
636
|
+
// Resolve Card Data
|
|
637
|
+
const cardData = await resolveTokenRemote(token.token);
|
|
638
|
+
if (!cardData || cardData.error)
|
|
639
|
+
throw new Error("Card resolve failed");
|
|
640
|
+
// Fill Form (Reusing the same page!)
|
|
641
|
+
const fillResult = await (0, playwright_bridge_js_1.fillCheckoutForm)(checkout_url, cardData, page);
|
|
642
|
+
if (fillResult.success) {
|
|
643
|
+
const burnOk = await burnTokenRemote(token.token);
|
|
644
|
+
if (!burnOk)
|
|
645
|
+
console.error(`[WARN] Token burn failed for ${token.token} — manual check needed`);
|
|
646
|
+
}
|
|
710
647
|
return {
|
|
711
|
-
content: [{
|
|
712
|
-
|
|
713
|
-
text: JSON.stringify({
|
|
714
|
-
route: "WEB3",
|
|
715
|
-
method: web3Result.method,
|
|
716
|
-
status: "SUCCESS",
|
|
717
|
-
recipient: to,
|
|
718
|
-
amount_usdt: amount,
|
|
719
|
-
tx_hash: txResult.txHash,
|
|
720
|
-
message: `✅ Web3 payment sent on-chain! ${amount} USDT → ${to}. Tx: ${txResult.txHash}`,
|
|
721
|
-
gas_savings: "~$0.001 (ERC-4337 Paymaster, gasless for user)",
|
|
722
|
-
}, null, 2),
|
|
723
|
-
}],
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
// ── SCENARIO B: No Web3 — Fiat JIT Card fallback ────────────────────
|
|
727
|
-
console.error(`[SMART ROUTE] No Web3 detected. Scanning DOM for total price...`);
|
|
728
|
-
// ✅ BUG 1+2 FIX: Use try/finally so browser ALWAYS closes (even on success).
|
|
729
|
-
// fillCheckoutForm opens its own browser — this avoids triple browser + leak.
|
|
730
|
-
let totalPrice = null;
|
|
731
|
-
const priceBrowser = await playwright_1.chromium.launch({ headless: true });
|
|
732
|
-
try {
|
|
733
|
-
const pricePage = await priceBrowser.newPage();
|
|
734
|
-
await pricePage.goto(checkout_url, { waitUntil: "domcontentloaded", timeout: 20_000 });
|
|
735
|
-
totalPrice = await (0, extract_total_price_js_1.extractTotalPrice)(pricePage);
|
|
736
|
-
}
|
|
737
|
-
catch {
|
|
738
|
-
// ignore — totalPrice stays null, handled below
|
|
739
|
-
}
|
|
740
|
-
finally {
|
|
741
|
-
await priceBrowser.close().catch(() => { }); // ✅ Always close
|
|
742
|
-
}
|
|
743
|
-
if (!totalPrice) {
|
|
744
|
-
return {
|
|
745
|
-
content: [{
|
|
746
|
-
type: "text",
|
|
747
|
-
text: JSON.stringify({
|
|
748
|
-
route: "FIAT",
|
|
749
|
-
status: "PRICE_NOT_FOUND",
|
|
750
|
-
message: "Could not automatically detect the total price on this page. Please manually confirm the exact checkout price and use request_payment_token + execute_payment instead.",
|
|
751
|
-
}, null, 2),
|
|
752
|
-
}],
|
|
753
|
-
isError: true,
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
if (totalPrice < 1 || totalPrice > 100) {
|
|
757
|
-
return {
|
|
758
|
-
content: [{
|
|
759
|
-
type: "text",
|
|
760
|
-
text: JSON.stringify({
|
|
761
|
-
route: "FIAT",
|
|
762
|
-
status: "AMOUNT_OUT_OF_RANGE",
|
|
648
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
649
|
+
route: "FIAT", status: fillResult.success ? "SUCCESS" : "FILL_FAILED",
|
|
763
650
|
detected_price: totalPrice,
|
|
764
|
-
message:
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
// Issue JIT card for exactly the detected total
|
|
771
|
-
console.error(`[SMART ROUTE] Fiat route: issuing JIT card for $${totalPrice}`);
|
|
772
|
-
const token = await issueTokenRemote(card_alias, totalPrice, checkout_url);
|
|
773
|
-
if (!token || token.error) {
|
|
774
|
-
return {
|
|
775
|
-
content: [{
|
|
776
|
-
type: "text",
|
|
777
|
-
text: JSON.stringify({
|
|
778
|
-
route: "FIAT",
|
|
779
|
-
status: "TOKEN_ISSUE_FAILED",
|
|
780
|
-
message: token?.message || "Could not issue JIT card. Check balance or card alias.",
|
|
781
|
-
}, null, 2),
|
|
782
|
-
}],
|
|
783
|
-
isError: true,
|
|
651
|
+
message: fillResult.success
|
|
652
|
+
? `✅ JIT card issued for $${totalPrice} and checkout filled.`
|
|
653
|
+
: `❌ Fill failed: ${fillResult.message}`,
|
|
654
|
+
receipt_id: fillResult.receipt_id || null,
|
|
655
|
+
}, null, 2) }],
|
|
656
|
+
isError: !fillResult.success,
|
|
784
657
|
};
|
|
785
658
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
659
|
+
catch (err) {
|
|
660
|
+
// ✅ FIX 8: Sanitize error message before returning to agent — avoid leaking internal paths
|
|
661
|
+
const safeMsg = (err?.message || String(err)).replace(/\/.*(src|dist)\/.*\.ts/g, '[internal]').slice(0, 200);
|
|
789
662
|
return {
|
|
790
|
-
content: [{
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
route: "FIAT",
|
|
794
|
-
status: "CARD_RESOLVE_FAILED",
|
|
795
|
-
message: "Failed to resolve JIT card data. Token may have expired.",
|
|
796
|
-
}, null, 2),
|
|
797
|
-
}],
|
|
663
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
664
|
+
status: "ERROR", message: safeMsg,
|
|
665
|
+
}, null, 2) }],
|
|
798
666
|
isError: true,
|
|
799
667
|
};
|
|
800
668
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
await burnTokenRemote(token.token);
|
|
669
|
+
finally {
|
|
670
|
+
await browser.close().catch(() => { });
|
|
804
671
|
}
|
|
805
|
-
return {
|
|
806
|
-
content: [{
|
|
807
|
-
type: "text",
|
|
808
|
-
text: JSON.stringify({
|
|
809
|
-
route: "FIAT",
|
|
810
|
-
status: fillResult.success ? "SUCCESS" : "FILL_FAILED",
|
|
811
|
-
detected_price: totalPrice,
|
|
812
|
-
jit_card_issued: true,
|
|
813
|
-
message: fillResult.success
|
|
814
|
-
? `✅ JIT Visa card issued for $${totalPrice} and checkout filled successfully.`
|
|
815
|
-
: `❌ JIT card issued but checkout form fill failed: ${fillResult.message}`,
|
|
816
|
-
receipt_id: fillResult.receipt_id || null,
|
|
817
|
-
}, null, 2),
|
|
818
|
-
}],
|
|
819
|
-
isError: !fillResult.success,
|
|
820
|
-
};
|
|
821
672
|
});
|
|
822
673
|
// ============================================================
|
|
823
674
|
// RESOURCE: Z-ZERO Autonomous Payment SOP
|
|
@@ -840,7 +691,7 @@ When asked to make a purchase, execute the following steps precisely in order:
|
|
|
840
691
|
|
|
841
692
|
## Step 2: Requesting the JIT Token
|
|
842
693
|
1. Request Token: Call the \`request_payment_token\` tool with the exact \`amount\` required and the \`merchant\` name.
|
|
843
|
-
2. Receive Token: You will receive a temporary \`token\` (e.g., \`temp_auth_1a2b...\`). This token is locked to the requested amount and is valid for
|
|
694
|
+
2. Receive Token: You will receive a temporary \`token\` (e.g., \`temp_auth_1a2b...\`). This token is locked to the requested amount and is valid for 1 hour.
|
|
844
695
|
|
|
845
696
|
## Step 3: Locating the Checkout
|
|
846
697
|
1. Identify Checkout URL: Find the merchant's checkout/payment page where credit card details are normally entered.
|
|
@@ -857,6 +708,13 @@ When asked to make a purchase, execute the following steps precisely in order:
|
|
|
857
708
|
- NEVER print full tokens in the human chat logs.
|
|
858
709
|
- NO MANUAL ENTRY: If a merchant asks you to type a credit card number into a text box, REFUSE.
|
|
859
710
|
- FAIL GRACEFULLY: If \`execute_payment\` returns \`success: false\`, report the error message to the human. Do not try again.
|
|
711
|
+
|
|
712
|
+
## ⚡ Smart Route Alternative (Recommended)
|
|
713
|
+
For most purchases, use \`auto_pay_checkout\` instead of the 4-step flow above.
|
|
714
|
+
It auto-detects Web3 checkout (pay on-chain with USDT) vs Fiat checkout (JIT Visa card), all in a single call.
|
|
715
|
+
- Web3 route: sends USDT directly on-chain to merchant wallet
|
|
716
|
+
- Fiat route: issues JIT card + fills checkout form automatically
|
|
717
|
+
Call \`auto_pay_checkout\` with just \`checkout_url\` and \`card_alias\`. The tool handles the rest.
|
|
860
718
|
`;
|
|
861
719
|
return {
|
|
862
720
|
contents: [
|
|
@@ -874,7 +732,7 @@ When asked to make a purchase, execute the following steps precisely in order:
|
|
|
874
732
|
async function main() {
|
|
875
733
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
876
734
|
await server.connect(transport);
|
|
877
|
-
console.error(
|
|
735
|
+
console.error(`🔐 OpenClaw MCP Server v${version_js_2.CURRENT_MCP_VERSION} running (Phase 2: Smart Routing enabled)...`);
|
|
878
736
|
console.error("Status: Secure & Connected to Z-ZERO Gateway");
|
|
879
737
|
console.error("Tools: list_cards, check_balance, request_payment_token, execute_payment, cancel_payment_token, request_human_approval, auto_pay_checkout");
|
|
880
738
|
}
|
|
@@ -101,7 +101,9 @@ async function extractTotalPrice(page) {
|
|
|
101
101
|
}
|
|
102
102
|
if (values.length === 0)
|
|
103
103
|
return null;
|
|
104
|
-
|
|
104
|
+
// ✅ FIX 10: Use median instead of max — prevents merchant injecting hidden large price
|
|
105
|
+
values.sort((a, b) => a - b);
|
|
106
|
+
return values[Math.floor(values.length / 2)];
|
|
105
107
|
});
|
|
106
108
|
if (amount !== null && amount > 0) {
|
|
107
109
|
console.error(`[PRICE] ⚠️ Strategy 3 (largest $ on page): $${amount}. Low confidence.`);
|
package/dist/lib/key-store.js
CHANGED
|
@@ -27,9 +27,9 @@ function setPassportKey(newKey) {
|
|
|
27
27
|
if (!trimmed.startsWith("zk_live_") && !trimmed.startsWith("zk_test_")) {
|
|
28
28
|
return { ok: false, message: `Invalid key format — must start with "zk_live_" or "zk_test_". Got: "${trimmed.slice(0, 12)}..."` };
|
|
29
29
|
}
|
|
30
|
-
const previous = _passportKey;
|
|
31
30
|
_passportKey = trimmed;
|
|
32
|
-
|
|
31
|
+
// ✅ FIX 11: Don't log partial key — even 10 chars helps brute-force
|
|
32
|
+
console.error(`[KEY-STORE] ✅ Passport Key updated successfully.`);
|
|
33
33
|
return { ok: true, message: `Passport Key updated successfully. Active key prefix: ${trimmed.slice(0, 10)}...` };
|
|
34
34
|
}
|
|
35
35
|
/** Check if a key is currently configured. */
|
|
@@ -12,9 +12,8 @@ export interface Web3DetectionResult {
|
|
|
12
12
|
message: string;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Checks a page for Web3 payment indicators (EIP-681 or interrupted eth_sendTransaction).
|
|
16
16
|
* Injects a mock window.ethereum (MetaMask emulation).
|
|
17
|
-
* Scans for EIP-681 links.
|
|
18
17
|
* Returns the tx params if Web3 payment is detected, else null.
|
|
19
18
|
*/
|
|
20
|
-
export declare function detectWeb3Payment(checkoutUrl: string): Promise<Web3DetectionResult>;
|
|
19
|
+
export declare function detectWeb3Payment(page: import("playwright").Page, checkoutUrl: string): Promise<Web3DetectionResult>;
|
|
@@ -6,21 +6,15 @@
|
|
|
6
6
|
// If page is not Web3-aware → return null (caller falls back to Fiat JIT card).
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.detectWeb3Payment = detectWeb3Payment;
|
|
9
|
-
const playwright_1 = require("playwright");
|
|
10
9
|
const WEB3_DETECT_TIMEOUT_MS = 12_000; // 12s max to detect Web3 button click
|
|
11
10
|
/**
|
|
12
|
-
*
|
|
11
|
+
* Checks a page for Web3 payment indicators (EIP-681 or interrupted eth_sendTransaction).
|
|
13
12
|
* Injects a mock window.ethereum (MetaMask emulation).
|
|
14
|
-
* Scans for EIP-681 links.
|
|
15
13
|
* Returns the tx params if Web3 payment is detected, else null.
|
|
16
14
|
*/
|
|
17
|
-
async function detectWeb3Payment(checkoutUrl) {
|
|
18
|
-
const browser = await playwright_1.chromium.launch({ headless: true });
|
|
19
|
-
const context = await browser.newContext();
|
|
20
|
-
const page = await context.newPage();
|
|
15
|
+
async function detectWeb3Payment(page, checkoutUrl) {
|
|
21
16
|
try {
|
|
22
17
|
// ─── Inject mock window.ethereum BEFORE page loads ───────────────────
|
|
23
|
-
// This ensures the page's `if (typeof window.ethereum !== 'undefined')` check passes.
|
|
24
18
|
await page.addInitScript(() => {
|
|
25
19
|
const mockEthereum = {
|
|
26
20
|
isMetaMask: true,
|
|
@@ -44,7 +38,6 @@ async function detectWeb3Payment(checkoutUrl) {
|
|
|
44
38
|
return null;
|
|
45
39
|
if (method === "eth_getBalance")
|
|
46
40
|
return "0x0";
|
|
47
|
-
// C3 FIX: Log unrecognized methods for debugging DApp compatibility
|
|
48
41
|
window["__wdkUnknownMethod_" + method] = true;
|
|
49
42
|
return null;
|
|
50
43
|
},
|
|
@@ -67,7 +60,6 @@ async function detectWeb3Payment(checkoutUrl) {
|
|
|
67
60
|
// ─── Navigate to page ─────────────────────────────────────────────────
|
|
68
61
|
await page.goto(checkoutUrl, { waitUntil: "domcontentloaded", timeout: 20_000 });
|
|
69
62
|
// ─── Strategy A: Scan for EIP-681 links ──────────────────────────────
|
|
70
|
-
// Look for links like: ethereum:0xabc...@137/transfer?address=0xabc&uint256=10000000
|
|
71
63
|
const eip681Result = await page.evaluate(() => {
|
|
72
64
|
const links = Array.from(document.querySelectorAll("a[href]"))
|
|
73
65
|
.map(a => a.href)
|
|
@@ -78,16 +70,13 @@ async function detectWeb3Payment(checkoutUrl) {
|
|
|
78
70
|
const href = links[0] ?? inlineMatch?.[0] ?? null;
|
|
79
71
|
if (!href)
|
|
80
72
|
return null;
|
|
81
|
-
// Parse: ethereum:0xCONTRACT@137/transfer?address=0xRECIPIENT&uint256=10000000
|
|
82
73
|
const contractMatch = href.match(/ethereum:(0x[0-9a-fA-F]{40})/);
|
|
83
74
|
const chainMatch = href.match(/@(\d+)/);
|
|
84
75
|
const uint256Match = href.match(/uint256=(\d+)/);
|
|
85
|
-
// ✅ BUG 6 FIX: Parse recipient from 'address=' query param (not the contract address)
|
|
86
76
|
const recipientMatch = href.match(/[?&]address=(0x[0-9a-fA-F]{40})/);
|
|
87
77
|
return {
|
|
88
|
-
// Prefer recipient address from query param; fall back to contract only as last resort
|
|
89
78
|
to: recipientMatch?.[1] ?? contractMatch?.[1] ?? null,
|
|
90
|
-
contract: contractMatch?.[1] ?? null,
|
|
79
|
+
contract: contractMatch?.[1] ?? null,
|
|
91
80
|
chainId: chainMatch ? parseInt(chainMatch[1]) : 137,
|
|
92
81
|
uint256: uint256Match?.[1] ?? null,
|
|
93
82
|
raw: href,
|
|
@@ -95,9 +84,8 @@ async function detectWeb3Payment(checkoutUrl) {
|
|
|
95
84
|
});
|
|
96
85
|
if (eip681Result?.to) {
|
|
97
86
|
const usdtAmount = eip681Result.uint256
|
|
98
|
-
? parseInt(eip681Result.uint256) / 1_000_000
|
|
87
|
+
? parseInt(eip681Result.uint256) / 1_000_000
|
|
99
88
|
: null;
|
|
100
|
-
console.error(`[WEB3] ✅ EIP-681 detected: recipient=${eip681Result.to}, contract=${eip681Result.contract}, amount=${usdtAmount} USDT`);
|
|
101
89
|
return {
|
|
102
90
|
detected: true,
|
|
103
91
|
method: "EIP-681",
|
|
@@ -110,55 +98,46 @@ async function detectWeb3Payment(checkoutUrl) {
|
|
|
110
98
|
};
|
|
111
99
|
}
|
|
112
100
|
// ─── Strategy B: Wait for eth_sendTransaction after clicking "Pay" ────
|
|
113
|
-
// ✅ BUG 5 FIX: Auto-click common Web3 pay buttons so DApp triggers eth_sendTransaction
|
|
114
101
|
const web3ButtonSelectors = [
|
|
115
102
|
'button:has-text("MetaMask")', 'button:has-text("Pay with Crypto")',
|
|
116
103
|
'button:has-text("Connect Wallet")', 'button:has-text("Pay with Web3")',
|
|
117
104
|
'button:has-text("Pay with Wallet")', '#pay-metamask-btn',
|
|
118
105
|
'[class*="web3"] button', '[class*="metamask"] button',
|
|
119
106
|
];
|
|
107
|
+
// Click buttons sequentially, small delay to let UI reaction
|
|
120
108
|
for (const sel of web3ButtonSelectors) {
|
|
121
109
|
try {
|
|
122
110
|
const btn = await page.$(sel);
|
|
123
111
|
if (btn && await btn.isVisible()) {
|
|
124
112
|
console.error(`[WEB3] Clicking Web3 pay button: ${sel}`);
|
|
125
113
|
await btn.click();
|
|
126
|
-
|
|
114
|
+
await page.waitForTimeout(500);
|
|
127
115
|
}
|
|
128
116
|
}
|
|
129
|
-
catch { /*
|
|
117
|
+
catch { /* skip */ }
|
|
130
118
|
}
|
|
131
|
-
// Poll for captured tx
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
params: {
|
|
153
|
-
to: capturedTx["to"],
|
|
154
|
-
value: capturedTx["value"],
|
|
155
|
-
data: capturedTx["data"],
|
|
156
|
-
chainId: 137,
|
|
157
|
-
},
|
|
158
|
-
message: `Web3 payment detected via eth_sendTransaction. Recipient: ${capturedTx["to"]}`,
|
|
159
|
-
};
|
|
119
|
+
// Poll for captured tx
|
|
120
|
+
const start = Date.now();
|
|
121
|
+
while (Date.now() - start < WEB3_DETECT_TIMEOUT_MS) {
|
|
122
|
+
const tx = await page.evaluate(() => {
|
|
123
|
+
return window["__wdkCapturedTx"] ?? null;
|
|
124
|
+
});
|
|
125
|
+
if (tx) {
|
|
126
|
+
const capturedTx = tx;
|
|
127
|
+
return {
|
|
128
|
+
detected: true,
|
|
129
|
+
method: "eth_sendTransaction",
|
|
130
|
+
params: {
|
|
131
|
+
to: capturedTx["to"],
|
|
132
|
+
value: capturedTx["value"],
|
|
133
|
+
data: capturedTx["data"],
|
|
134
|
+
chainId: 137,
|
|
135
|
+
},
|
|
136
|
+
message: `Web3 payment detected via eth_sendTransaction. Recipient: ${capturedTx["to"]}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
await page.waitForTimeout(500);
|
|
160
140
|
}
|
|
161
|
-
console.error("[WEB3] ❌ No Web3 payment detected on this page. Fallback to Fiat.");
|
|
162
141
|
return {
|
|
163
142
|
detected: false,
|
|
164
143
|
method: null,
|
|
@@ -168,7 +147,6 @@ async function detectWeb3Payment(checkoutUrl) {
|
|
|
168
147
|
}
|
|
169
148
|
catch (err) {
|
|
170
149
|
const msg = err instanceof Error ? err.message : String(err);
|
|
171
|
-
console.error(`[WEB3] Error during detection: ${msg}`);
|
|
172
150
|
return {
|
|
173
151
|
detected: false,
|
|
174
152
|
method: null,
|
|
@@ -176,7 +154,4 @@ async function detectWeb3Payment(checkoutUrl) {
|
|
|
176
154
|
message: `Detection failed: ${msg}. Defaulting to Fiat route.`,
|
|
177
155
|
};
|
|
178
156
|
}
|
|
179
|
-
finally {
|
|
180
|
-
await browser.close().catch(() => { });
|
|
181
|
-
}
|
|
182
157
|
}
|
|
@@ -4,4 +4,4 @@ import type { CardData, PaymentResult } from "./types.js";
|
|
|
4
4
|
* Card data exists ONLY in RAM and is wiped after injection.
|
|
5
5
|
* Hard timeout of 60s prevents merchant page from hanging indefinitely.
|
|
6
6
|
*/
|
|
7
|
-
export declare function fillCheckoutForm(checkoutUrl: string, cardData: CardData): Promise<PaymentResult>;
|
|
7
|
+
export declare function fillCheckoutForm(checkoutUrl: string, cardData: CardData, existingPage?: import("playwright").Page): Promise<PaymentResult>;
|
|
@@ -11,17 +11,22 @@ const CHECKOUT_HARD_TIMEOUT_MS = 60_000; // 60s absolute cap — prevents slow-l
|
|
|
11
11
|
* Card data exists ONLY in RAM and is wiped after injection.
|
|
12
12
|
* Hard timeout of 60s prevents merchant page from hanging indefinitely.
|
|
13
13
|
*/
|
|
14
|
-
async function fillCheckoutForm(checkoutUrl, cardData) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
async function fillCheckoutForm(checkoutUrl, cardData, existingPage) {
|
|
15
|
+
let browser = null;
|
|
16
|
+
let page;
|
|
17
|
+
if (existingPage) {
|
|
18
|
+
page = existingPage;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
browser = await playwright_1.chromium.launch({ headless: true });
|
|
22
|
+
const context = await browser.newContext();
|
|
23
|
+
page = await context.newPage();
|
|
24
|
+
}
|
|
20
25
|
try {
|
|
21
|
-
return await (0, with_timeout_js_1.withTimeout)(_fillCheckoutFormInner(page, checkoutUrl, cardData),
|
|
22
|
-
CHECKOUT_HARD_TIMEOUT_MS, 'fillCheckoutForm', async () => {
|
|
26
|
+
return await (0, with_timeout_js_1.withTimeout)(_fillCheckoutFormInner(page, checkoutUrl, cardData), CHECKOUT_HARD_TIMEOUT_MS, 'fillCheckoutForm', async () => {
|
|
23
27
|
console.error('[PLAYWRIGHT] ⚠️ Hard timeout hit — force-closing browser');
|
|
24
|
-
|
|
28
|
+
if (browser)
|
|
29
|
+
await browser.close().catch(() => { });
|
|
25
30
|
});
|
|
26
31
|
}
|
|
27
32
|
catch (err) {
|
|
@@ -35,145 +40,223 @@ async function fillCheckoutForm(checkoutUrl, cardData) {
|
|
|
35
40
|
return { success: false, message: `Payment failed: ${errMsg}` };
|
|
36
41
|
}
|
|
37
42
|
finally {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
await browser.close().catch(() => { });
|
|
43
|
+
if (browser)
|
|
44
|
+
await browser.close().catch(() => { });
|
|
41
45
|
}
|
|
42
46
|
}
|
|
47
|
+
// ============================================================
|
|
48
|
+
// HELPER: Smart fill — handles both <input> and <select>
|
|
49
|
+
// Prevents crash when dropdown uses <select> instead of <input>
|
|
50
|
+
// ============================================================
|
|
51
|
+
async function smartFill(el, value) {
|
|
52
|
+
try {
|
|
53
|
+
const tag = await el.evaluate(e => e.tagName.toLowerCase());
|
|
54
|
+
if (tag === 'select') {
|
|
55
|
+
// Try by value first (most common: "01", "12", "2030")
|
|
56
|
+
try {
|
|
57
|
+
await el.selectOption({ value });
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch { /* next */ }
|
|
61
|
+
// Try matching option label containing the value
|
|
62
|
+
try {
|
|
63
|
+
await el.selectOption({ label: value });
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch { /* next */ }
|
|
67
|
+
// Last resort: numeric index (month "01" → index 1 if 0="Select...")
|
|
68
|
+
const idx = parseInt(value, 10);
|
|
69
|
+
if (!isNaN(idx)) {
|
|
70
|
+
try {
|
|
71
|
+
await el.selectOption({ index: idx });
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
catch { /* give up */ }
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
await el.fill(value);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Try each selector in order, smartFill the first match */
|
|
88
|
+
async function tryFillField(page, selectors, value) {
|
|
89
|
+
for (const selector of selectors) {
|
|
90
|
+
const el = await page.$(selector);
|
|
91
|
+
if (el) {
|
|
92
|
+
const ok = await smartFill(el, value);
|
|
93
|
+
if (ok)
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
43
99
|
/** Internal implementation — called by fillCheckoutForm inside a timeout wrapper */
|
|
44
|
-
async function _fillCheckoutFormInner(page, checkoutUrl,
|
|
45
|
-
cardData) {
|
|
100
|
+
async function _fillCheckoutFormInner(page, checkoutUrl, cardData) {
|
|
46
101
|
try {
|
|
47
|
-
// ✅
|
|
48
|
-
|
|
102
|
+
// ✅ FIX: Skip navigation if page already loaded (Single Browser reuse from auto_pay_checkout)
|
|
103
|
+
// Prevents double-navigate which would reload page and lose cart/session state.
|
|
104
|
+
const currentUrl = page.url();
|
|
105
|
+
const baseCheckoutUrl = checkoutUrl.split("?")[0];
|
|
106
|
+
if (!currentUrl || currentUrl === "about:blank" || !currentUrl.startsWith(baseCheckoutUrl)) {
|
|
107
|
+
await page.goto(checkoutUrl, { waitUntil: "domcontentloaded", timeout: 20_000 });
|
|
108
|
+
}
|
|
49
109
|
// ============================================================
|
|
50
110
|
// STRATEGY 1: Standard HTML form fields
|
|
111
|
+
// Covers: plain forms, Shopify, WooCommerce, custom checkouts
|
|
112
|
+
// Priority: autocomplete (W3C) → name → platform-specific → placeholder → aria-label
|
|
51
113
|
// ============================================================
|
|
52
|
-
const
|
|
114
|
+
const S = {
|
|
53
115
|
number: [
|
|
116
|
+
'[autocomplete="cc-number"]',
|
|
54
117
|
'input[name="cardnumber"]',
|
|
55
118
|
'input[name="card-number"]',
|
|
56
119
|
'input[name="cc-number"]',
|
|
57
|
-
'input[
|
|
120
|
+
'input[name="card_number"]',
|
|
121
|
+
'input[name="checkout[payment][card_number]"]', // Shopify
|
|
122
|
+
'input[id="wc-stripe-cc-number"]', // WooCommerce
|
|
58
123
|
'input[data-elements-stable-field-name="cardNumber"]',
|
|
59
124
|
'input[placeholder*="card number" i]',
|
|
60
|
-
'input[placeholder*="Card number" i]',
|
|
61
125
|
'input[aria-label*="card number" i]',
|
|
62
126
|
],
|
|
63
127
|
expiry: [
|
|
128
|
+
'[autocomplete="cc-exp"]',
|
|
64
129
|
'input[name="exp-date"]',
|
|
65
130
|
'input[name="cc-exp"]',
|
|
66
|
-
'input[
|
|
131
|
+
'input[name="expiry"]',
|
|
132
|
+
'input[name="checkout[payment][card_expiry]"]', // Shopify
|
|
67
133
|
'input[placeholder*="MM / YY" i]',
|
|
68
134
|
'input[placeholder*="MM/YY" i]',
|
|
69
135
|
'input[aria-label*="expir" i]',
|
|
70
136
|
],
|
|
71
137
|
exp_month: [
|
|
72
|
-
'
|
|
138
|
+
'[autocomplete="cc-exp-month"]',
|
|
73
139
|
'select[name="exp-month"]',
|
|
74
|
-
'
|
|
140
|
+
'select[name="exp_month"]',
|
|
141
|
+
'select[name="card_exp_month"]',
|
|
142
|
+
'select[id*="exp-month" i]',
|
|
143
|
+
'select[id*="exp_month" i]',
|
|
144
|
+
'input[name="exp-month"]',
|
|
145
|
+
'input[name="exp_month"]',
|
|
75
146
|
],
|
|
76
147
|
exp_year: [
|
|
77
|
-
'
|
|
148
|
+
'[autocomplete="cc-exp-year"]',
|
|
78
149
|
'select[name="exp-year"]',
|
|
79
|
-
'
|
|
150
|
+
'select[name="exp_year"]',
|
|
151
|
+
'select[name="card_exp_year"]',
|
|
152
|
+
'select[id*="exp-year" i]',
|
|
153
|
+
'select[id*="exp_year" i]',
|
|
154
|
+
'input[name="exp-year"]',
|
|
155
|
+
'input[name="exp_year"]',
|
|
80
156
|
],
|
|
81
157
|
cvv: [
|
|
158
|
+
'[autocomplete="cc-csc"]',
|
|
82
159
|
'input[name="cvc"]',
|
|
83
160
|
'input[name="cvv"]',
|
|
84
161
|
'input[name="cc-csc"]',
|
|
85
|
-
'input[
|
|
162
|
+
'input[name="security_code"]',
|
|
163
|
+
'input[name="checkout[payment][card_cvc]"]', // Shopify
|
|
164
|
+
'input[id="wc-stripe-cc-cvc"]', // WooCommerce
|
|
86
165
|
'input[placeholder*="CVC" i]',
|
|
87
166
|
'input[placeholder*="CVV" i]',
|
|
167
|
+
'input[placeholder*="security" i]',
|
|
88
168
|
'input[aria-label*="security code" i]',
|
|
169
|
+
'input[aria-label*="CVC" i]',
|
|
89
170
|
],
|
|
90
171
|
name: [
|
|
172
|
+
'[autocomplete="cc-name"]',
|
|
91
173
|
'input[name="ccname"]',
|
|
92
174
|
'input[name="cc-name"]',
|
|
93
|
-
'input[
|
|
175
|
+
'input[name="card-name"]',
|
|
176
|
+
'input[name="card_name"]',
|
|
94
177
|
'input[placeholder*="name on card" i]',
|
|
178
|
+
'input[placeholder*="cardholder" i]',
|
|
95
179
|
'input[aria-label*="name on card" i]',
|
|
96
180
|
],
|
|
97
181
|
};
|
|
98
|
-
// Try to fill each field
|
|
99
182
|
let filledFields = 0;
|
|
100
183
|
// Card Number
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
184
|
+
if (await tryFillField(page, S.number, cardData.number))
|
|
185
|
+
filledFields++;
|
|
186
|
+
// Expiry: try combined MM/YY first
|
|
187
|
+
const expiryValue = `${cardData.exp_month}/${cardData.exp_year.slice(-2)}`;
|
|
188
|
+
let expiryFilled = await tryFillField(page, S.expiry, expiryValue);
|
|
189
|
+
if (expiryFilled)
|
|
190
|
+
filledFields++;
|
|
191
|
+
// Expiry: fallback to separate month + year (works with both <input> AND <select>)
|
|
192
|
+
if (!expiryFilled) {
|
|
193
|
+
if (await tryFillField(page, S.exp_month, cardData.exp_month))
|
|
105
194
|
filledFields++;
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// Expiry (combined MM/YY format)
|
|
110
|
-
let expiryFilled = false;
|
|
111
|
-
for (const selector of standardSelectors.expiry) {
|
|
112
|
-
const el = await page.$(selector);
|
|
113
|
-
if (el) {
|
|
114
|
-
await el.fill(`${cardData.exp_month}/${cardData.exp_year.slice(-2)}`);
|
|
195
|
+
if (await tryFillField(page, S.exp_year, cardData.exp_year))
|
|
115
196
|
filledFields++;
|
|
116
|
-
expiryFilled = true;
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// Expiry (separate month/year fields)
|
|
121
|
-
if (!expiryFilled) {
|
|
122
|
-
for (const selector of standardSelectors.exp_month) {
|
|
123
|
-
const el = await page.$(selector);
|
|
124
|
-
if (el) {
|
|
125
|
-
await el.fill(cardData.exp_month);
|
|
126
|
-
filledFields++;
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
for (const selector of standardSelectors.exp_year) {
|
|
131
|
-
const el = await page.$(selector);
|
|
132
|
-
if (el) {
|
|
133
|
-
await el.fill(cardData.exp_year);
|
|
134
|
-
filledFields++;
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
197
|
}
|
|
139
198
|
// CVV
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (el) {
|
|
143
|
-
await el.fill(cardData.cvv);
|
|
144
|
-
filledFields++;
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
199
|
+
if (await tryFillField(page, S.cvv, cardData.cvv))
|
|
200
|
+
filledFields++;
|
|
148
201
|
// Name on Card
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (el) {
|
|
152
|
-
await el.fill(cardData.name);
|
|
153
|
-
filledFields++;
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
202
|
+
if (await tryFillField(page, S.name, cardData.name))
|
|
203
|
+
filledFields++;
|
|
157
204
|
// ============================================================
|
|
158
205
|
// STRATEGY 2: Stripe Elements (iframe-based)
|
|
206
|
+
// Stripe renders payment inputs inside iframes from js.stripe.com.
|
|
207
|
+
// Some merchants use __privateStripeFrame* named frames.
|
|
208
|
+
// Stripe has 2 modes:
|
|
209
|
+
// - Unified: 1 iframe with all fields (card, exp, cvc in one)
|
|
210
|
+
// - Split: separate iframes per field (cardNumber, cardExpiry, cardCvc)
|
|
211
|
+
// We handle both by scanning ALL matching Stripe frames.
|
|
159
212
|
// ============================================================
|
|
160
213
|
if (filledFields === 0) {
|
|
161
|
-
const stripeFrames = page.frames().filter((f) =>
|
|
214
|
+
const stripeFrames = page.frames().filter((f) => {
|
|
215
|
+
const url = f.url();
|
|
216
|
+
const name = f.name();
|
|
217
|
+
return url.includes('js.stripe.com')
|
|
218
|
+
|| url.includes('stripe.com/elements')
|
|
219
|
+
|| name.startsWith('__privateStripeFrame');
|
|
220
|
+
});
|
|
162
221
|
for (const frame of stripeFrames) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
222
|
+
// Card Number
|
|
223
|
+
for (const sel of [
|
|
224
|
+
'input[name="cardnumber"]',
|
|
225
|
+
'input[autocomplete="cc-number"]',
|
|
226
|
+
'input[data-elements-stable-field-name="cardNumber"]',
|
|
227
|
+
]) {
|
|
228
|
+
const el = await frame.$(sel);
|
|
229
|
+
if (el) {
|
|
230
|
+
await el.fill(cardData.number);
|
|
231
|
+
filledFields++;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
167
234
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
235
|
+
// Expiry (Stripe uses MM/YY without slash)
|
|
236
|
+
for (const sel of [
|
|
237
|
+
'input[name="exp-date"]',
|
|
238
|
+
'input[autocomplete="cc-exp"]',
|
|
239
|
+
'input[data-elements-stable-field-name="cardExpiry"]',
|
|
240
|
+
]) {
|
|
241
|
+
const el = await frame.$(sel);
|
|
242
|
+
if (el) {
|
|
243
|
+
await el.fill(`${cardData.exp_month}${cardData.exp_year.slice(-2)}`);
|
|
244
|
+
filledFields++;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
172
247
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
248
|
+
// CVC
|
|
249
|
+
for (const sel of [
|
|
250
|
+
'input[name="cvc"]',
|
|
251
|
+
'input[autocomplete="cc-csc"]',
|
|
252
|
+
'input[data-elements-stable-field-name="cardCvc"]',
|
|
253
|
+
]) {
|
|
254
|
+
const el = await frame.$(sel);
|
|
255
|
+
if (el) {
|
|
256
|
+
await el.fill(cardData.cvv);
|
|
257
|
+
filledFields++;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
177
260
|
}
|
|
178
261
|
}
|
|
179
262
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CURRENT_MCP_VERSION = "1.1.1";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CURRENT_MCP_VERSION = void 0;
|
|
4
|
+
// Single source of truth for MCP version
|
|
5
|
+
// Imported by both index.ts and wdk_backend.ts to avoid circular dependency
|
|
6
|
+
exports.CURRENT_MCP_VERSION = "1.1.1";
|
package/dist/wdk_backend.js
CHANGED
|
@@ -20,6 +20,8 @@ exports.refundUnderspendRemote = refundUnderspendRemote;
|
|
|
20
20
|
const key_store_js_1 = require("./lib/key-store.js");
|
|
21
21
|
const API_BASE_URL = process.env.Z_ZERO_API_BASE_URL || "https://www.clawcard.store";
|
|
22
22
|
const INTERNAL_SECRET = process.env.Z_ZERO_INTERNAL_SECRET || "";
|
|
23
|
+
// Injected at build time — always reflects the actual running version
|
|
24
|
+
const version_js_1 = require("./version.js");
|
|
23
25
|
if (!(0, key_store_js_1.hasPassportKey)()) {
|
|
24
26
|
console.error("❌ ERROR: Z_ZERO_API_KEY (Passport Key) is missing!");
|
|
25
27
|
console.error("🔐 Get your key: https://www.clawcard.store/dashboard/agents");
|
|
@@ -40,6 +42,7 @@ async function apiRequest(endpoint, method = 'GET', body = null) {
|
|
|
40
42
|
headers: {
|
|
41
43
|
"Authorization": `Bearer ${PASSPORT_KEY}`,
|
|
42
44
|
"Content-Type": "application/json",
|
|
45
|
+
"X-MCP-Version": version_js_1.CURRENT_MCP_VERSION,
|
|
43
46
|
},
|
|
44
47
|
body: body ? JSON.stringify(body) : null,
|
|
45
48
|
});
|
|
@@ -54,8 +57,9 @@ async function apiRequest(endpoint, method = 'GET', body = null) {
|
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
async function internalApiRequest(endpoint, method, body) {
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
// ✅ FIX 9: Validate INTERNAL_SECRET presence and minimum length
|
|
61
|
+
if (!INTERNAL_SECRET || INTERNAL_SECRET.length < 16) {
|
|
62
|
+
return { error: "CONFIG_ERROR", message: "INTERNAL_SECRET is missing or too short (min 16 chars)" };
|
|
59
63
|
}
|
|
60
64
|
const url = `${API_BASE_URL.replace(/\/$/, '')}${endpoint}`;
|
|
61
65
|
try {
|
|
@@ -64,6 +68,7 @@ async function internalApiRequest(endpoint, method, body) {
|
|
|
64
68
|
headers: {
|
|
65
69
|
"x-internal-secret": INTERNAL_SECRET,
|
|
66
70
|
"Content-Type": "application/json",
|
|
71
|
+
"X-MCP-Version": version_js_1.CURRENT_MCP_VERSION,
|
|
67
72
|
},
|
|
68
73
|
body: body ? JSON.stringify(body) : null,
|
|
69
74
|
});
|
|
@@ -94,25 +99,19 @@ async function getBalanceRemote(cardAlias) {
|
|
|
94
99
|
// finds connected WDK wallet, queries on-chain balance)
|
|
95
100
|
const data = await apiRequest('/api/wdk/balance', 'GET');
|
|
96
101
|
if (data?.error) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
wallet_balance: custodialData.cards[0].balance,
|
|
102
|
-
currency: 'USD',
|
|
103
|
-
mode: 'custodial_fallback',
|
|
104
|
-
note: 'WDK wallet not connected yet. Showing custodial balance.'
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
return data;
|
|
102
|
+
return {
|
|
103
|
+
error: true,
|
|
104
|
+
message: 'WDK wallet not connected. Create one at https://www.clawcard.store/dashboard/agent-wallet'
|
|
105
|
+
};
|
|
108
106
|
}
|
|
109
107
|
return {
|
|
110
108
|
wallet_balance: data.balance_usdt,
|
|
111
109
|
currency: 'USDT',
|
|
112
|
-
chain: data.chain || '
|
|
110
|
+
chain: data.chain || 'ethereum',
|
|
113
111
|
address: data.address,
|
|
112
|
+
tron_address: data.tron_address,
|
|
114
113
|
mode: 'wdk_onchain',
|
|
115
|
-
note: `Non-custodial WDK wallet
|
|
114
|
+
note: `Non-custodial WDK wallet. On-chain USDT balance. Address: ${data.address}`
|
|
116
115
|
};
|
|
117
116
|
}
|
|
118
117
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -121,22 +120,22 @@ async function getBalanceRemote(cardAlias) {
|
|
|
121
120
|
async function getDepositAddressesRemote() {
|
|
122
121
|
const data = await apiRequest('/api/wdk/balance', 'GET');
|
|
123
122
|
if (data?.error) {
|
|
124
|
-
// Fallback to custodial addresses
|
|
125
|
-
const custodial = await apiRequest('/api/tokens/cards', 'GET');
|
|
126
123
|
return {
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
error: true,
|
|
125
|
+
message: 'WDK wallet not connected. Create one at https://www.clawcard.store/dashboard/agent-wallet'
|
|
129
126
|
};
|
|
130
127
|
}
|
|
131
128
|
return {
|
|
132
129
|
cards: [{ alias: 'wdk-wallet', balance: data.balance_usdt, currency: 'USDT' }],
|
|
133
130
|
deposit_addresses: {
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
ethereum: data.address,
|
|
132
|
+
tron: data.tron_address || null,
|
|
133
|
+
note: 'Send USDT to your WDK wallet. Gasless via ERC-4337 Paymaster (Ethereum) or GasFree (Tron).'
|
|
136
134
|
},
|
|
137
135
|
wdk_wallet: {
|
|
138
136
|
address: data.address,
|
|
139
|
-
|
|
137
|
+
tron_address: data.tron_address || null,
|
|
138
|
+
chain: data.chain || 'ethereum',
|
|
140
139
|
balance_usdt: data.balance_usdt
|
|
141
140
|
}
|
|
142
141
|
};
|
|
@@ -169,10 +168,11 @@ async function issueTokenRemote(cardAlias, amount, merchant) {
|
|
|
169
168
|
amount,
|
|
170
169
|
merchant,
|
|
171
170
|
created_at: Date.now(),
|
|
172
|
-
ttl_seconds:
|
|
171
|
+
ttl_seconds: 3600,
|
|
173
172
|
used: false,
|
|
174
|
-
tx_hash: data.tx_hash,
|
|
175
|
-
mode: 'wdk_noncustodial'
|
|
173
|
+
tx_hash: data.tx_hash,
|
|
174
|
+
mode: 'wdk_noncustodial',
|
|
175
|
+
mcp_warning: data._mcp_warning || null, // Relay backend version warning to agent
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
178
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -182,11 +182,15 @@ async function resolveTokenRemote(token) {
|
|
|
182
182
|
const data = await internalApiRequest('/api/tokens/resolve', 'POST', { token });
|
|
183
183
|
if (!data || data.error)
|
|
184
184
|
return data;
|
|
185
|
+
// ✅ FIX 7: Reject incomplete card data — don't silently fallback to fake values
|
|
186
|
+
if (!data.number || !data.cvv || !data.exp) {
|
|
187
|
+
return { error: "INCOMPLETE_CARD", message: "Card data missing required fields (number/cvv/exp). Token may be invalid or expired." };
|
|
188
|
+
}
|
|
185
189
|
return {
|
|
186
190
|
number: data.number,
|
|
187
|
-
exp_month: data.exp
|
|
188
|
-
exp_year: "20" +
|
|
189
|
-
cvv: data.cvv
|
|
191
|
+
exp_month: data.exp.split('/')[0],
|
|
192
|
+
exp_year: "20" + data.exp.split('/')[1],
|
|
193
|
+
cvv: data.cvv,
|
|
190
194
|
name: data.name || "Z-ZERO AI AGENT",
|
|
191
195
|
authorized_amount: data.authorized_amount ? Number(data.authorized_amount) : undefined,
|
|
192
196
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "z-zero-mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Z-ZERO MCP Server — Secure JIT Virtual Card Payment tools for AI Agents (Claude, Cursor, AutoGPT) — Real Single-Use Visa Cards",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": "dist/index.js",
|