z-zero-mcp-server 1.0.1 → 1.0.2

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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
- // Z-ZERO MCP Server (z-zero-mcp-server)
3
+ // Z-ZERO MCP Server (z-zero-mcp-server) v1.0.2
4
4
  // Exposes secure JIT payment tools to AI Agents via Model Context Protocol
5
- // Connected to Z-ZERO Supabase backend via Z_ZERO_API_KEY
5
+ // Now connected to REAL Airwallex Issuing API — produces real Mastercards
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
8
8
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
@@ -14,7 +14,7 @@ const playwright_bridge_js_1 = require("./playwright_bridge.js");
14
14
  // ============================================================
15
15
  const server = new mcp_js_1.McpServer({
16
16
  name: "z-zero-mcp-server",
17
- version: "1.0.0",
17
+ version: "1.0.2",
18
18
  });
19
19
  // ============================================================
20
20
  // TOOL 1: List available cards (safe - no sensitive data)
@@ -63,16 +63,17 @@ server.tool("check_balance", "Check the remaining balance of a virtual card by i
63
63
  };
64
64
  });
65
65
  // ============================================================
66
- // TOOL 3: Request a temporary payment token
66
+ // TOOL 3: Request a temporary payment token (issues real JIT card)
67
67
  // ============================================================
68
- server.tool("request_payment_token", "Request a temporary payment token for a specific amount. The token is valid for 15 minutes and locked to the specified amount. Use this token with execute_payment to complete a purchase.", {
68
+ server.tool("request_payment_token", "Request a temporary payment token for a specific amount. A real single-use virtual Mastercard is issued via Airwallex. The token is valid for 30 minutes. Min: $1, Max: $100. Use this token with execute_payment to complete a purchase.", {
69
69
  card_alias: zod_1.z
70
70
  .string()
71
71
  .describe("Which card to charge, e.g. 'Card_01'"),
72
72
  amount: zod_1.z
73
73
  .number()
74
- .positive()
75
- .describe("Amount in USD to authorize"),
74
+ .min(1, "Minimum amount is $1.00")
75
+ .max(100, "Maximum amount is $100.00")
76
+ .describe("Amount in USD to authorize (min: $1, max: $100)"),
76
77
  merchant: zod_1.z
77
78
  .string()
78
79
  .describe("Name or URL of the merchant/service being purchased"),
@@ -85,7 +86,7 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
85
86
  {
86
87
  type: "text",
87
88
  text: balance
88
- ? `Insufficient balance. Card "${card_alias}" has $${balance.balance} but you requested $${amount}.`
89
+ ? `Insufficient balance. Card "${card_alias}" has $${balance.balance} but you requested $${amount}. Or amount is outside the $1-$100 limit.`
89
90
  : `Card "${card_alias}" not found or API key is invalid.`,
90
91
  },
91
92
  ],
@@ -102,7 +103,8 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
102
103
  amount: token.amount,
103
104
  merchant: token.merchant,
104
105
  expires_at: expiresAt,
105
- instructions: "Use this token with execute_payment tool within 15 minutes.",
106
+ real_card_issued: !!token.airwallex_card_id,
107
+ instructions: "Use this token with execute_payment within 30 minutes. 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.",
106
108
  }, null, 2),
107
109
  },
108
110
  ],
@@ -119,15 +121,19 @@ server.tool("execute_payment", "Execute a payment using a temporary token. This
119
121
  .string()
120
122
  .url()
121
123
  .describe("The full URL of the checkout/payment page"),
122
- }, async ({ token, checkout_url }) => {
124
+ actual_amount: zod_1.z
125
+ .number()
126
+ .optional()
127
+ .describe("The actual final amount on the checkout page. If different from token amount, system will auto-refund the difference."),
128
+ }, async ({ token, checkout_url, actual_amount }) => {
123
129
  // Step 1: Resolve token → card data (RAM only)
124
- const cardData = (0, supabase_backend_js_1.resolveTokenRemote)(token);
130
+ const cardData = await (0, supabase_backend_js_1.resolveTokenRemote)(token);
125
131
  if (!cardData) {
126
132
  return {
127
133
  content: [
128
134
  {
129
135
  type: "text",
130
- text: "Payment failed: Token is invalid, expired, or already used. Request a new token.",
136
+ text: "Payment failed: Token is invalid, expired, cancelled, or already used. Request a new token.",
131
137
  },
132
138
  ],
133
139
  isError: true,
@@ -135,9 +141,13 @@ server.tool("execute_payment", "Execute a payment using a temporary token. This
135
141
  }
136
142
  // Step 2: Use Playwright to inject card into checkout form
137
143
  const result = await (0, playwright_bridge_js_1.fillCheckoutForm)(checkout_url, cardData);
138
- // Step 3: Burn the token + deduct from Supabase wallet
144
+ // Step 3: Burn the token (wallet was pre-debited at issue time)
139
145
  await (0, supabase_backend_js_1.burnTokenRemote)(token);
140
- // Step 4: Return result (NEVER includes card numbers)
146
+ // Step 4: Refund underspend if actual amount was less than token amount
147
+ if (actual_amount !== undefined && result.success) {
148
+ await (0, supabase_backend_js_1.refundUnderspendRemote)(token, actual_amount);
149
+ }
150
+ // Step 5: Return result (NEVER includes card numbers)
141
151
  return {
142
152
  content: [
143
153
  {
@@ -154,13 +164,93 @@ server.tool("execute_payment", "Execute a payment using a temporary token. This
154
164
  };
155
165
  });
156
166
  // ============================================================
167
+ // TOOL 5: Cancel payment token (returns funds to wallet)
168
+ // ============================================================
169
+ server.tool("cancel_payment_token", "Cancel a payment token that has not been used yet. This will cancel the Airwallex card and refund the full amount back to the wallet. Use this when: (1) checkout price is higher than token amount, (2) purchase is no longer needed, or (3) human requests cancellation. IMPORTANT: Do NOT auto-cancel without human awareness — always inform the human first.", {
170
+ token: zod_1.z
171
+ .string()
172
+ .describe("The payment token to cancel"),
173
+ reason: zod_1.z
174
+ .string()
175
+ .describe("Reason for cancellation, e.g. 'Price mismatch: checkout shows $20 but token is $15'"),
176
+ }, async ({ token, reason }) => {
177
+ const result = await (0, supabase_backend_js_1.cancelTokenRemote)(token);
178
+ if (!result.success) {
179
+ return {
180
+ content: [
181
+ {
182
+ type: "text",
183
+ text: "Cancellation failed: Token not found, already used, or already cancelled.",
184
+ },
185
+ ],
186
+ isError: true,
187
+ };
188
+ }
189
+ return {
190
+ content: [
191
+ {
192
+ type: "text",
193
+ text: JSON.stringify({
194
+ cancelled: true,
195
+ refunded_amount: result.refunded_amount,
196
+ reason,
197
+ message: `Token cancelled. $${result.refunded_amount} has been returned to the wallet. You may request a new token with the correct amount.`,
198
+ }, null, 2),
199
+ },
200
+ ],
201
+ };
202
+ });
203
+ // ============================================================
204
+ // TOOL 6: Request human approval (Human-in-the-loop)
205
+ // ============================================================
206
+ server.tool("request_human_approval", "Request human approval before proceeding with an action that requires human judgment. Use this when: (1) checkout price is higher than token amount and you need authorization for a new token, (2) any unusual or irreversible action is required. This PAUSES the bot and waits for human decision.", {
207
+ situation: zod_1.z
208
+ .string()
209
+ .describe("Clear description of what the bot found, e.g. 'Checkout shows $20 total (includes $3 tax) but current token is only $15'"),
210
+ current_token: zod_1.z
211
+ .string()
212
+ .optional()
213
+ .describe("Current active token ID if any"),
214
+ recommended_action: zod_1.z
215
+ .string()
216
+ .describe("What the bot recommends doing, e.g. 'Cancel current $15 token and issue a new $20 token'"),
217
+ alternative_action: zod_1.z
218
+ .string()
219
+ .optional()
220
+ .describe("Alternative option if available"),
221
+ }, async ({ situation, current_token, recommended_action, alternative_action }) => {
222
+ // This tool surfaces the situation to the human operator via the MCP interface.
223
+ // The LLM host (Claude/AutoGPT) will pause and show this to the user.
224
+ const message = [
225
+ "⚠️ HUMAN APPROVAL REQUIRED",
226
+ "",
227
+ `📋 Situation: ${situation}`,
228
+ current_token ? `🎫 Current Token: ${current_token}` : "",
229
+ `✅ Recommended: ${recommended_action}`,
230
+ alternative_action ? `🔄 Alternative: ${alternative_action}` : "",
231
+ "",
232
+ "Please respond with one of:",
233
+ '• "approve" — proceed with recommended action',
234
+ '• "deny" — cancel and do nothing',
235
+ '• Custom instruction — e.g. "issue new token for $22 instead"',
236
+ ].filter(Boolean).join("\n");
237
+ return {
238
+ content: [
239
+ {
240
+ type: "text",
241
+ text: message,
242
+ },
243
+ ],
244
+ };
245
+ });
246
+ // ============================================================
157
247
  // START SERVER
158
248
  // ============================================================
159
249
  async function main() {
160
250
  const transport = new stdio_js_1.StdioServerTransport();
161
251
  await server.connect(transport);
162
- console.error("🔐 Z-ZERO MCP Server running...");
163
- console.error("Connected to: Supabase backend");
164
- console.error("Tools: list_cards, check_balance, request_payment_token, execute_payment");
252
+ console.error("🔐 Z-ZERO MCP Server v1.0.2 running...");
253
+ console.error("Backend: Supabase + Airwallex Issuing API (Real JIT Cards)");
254
+ console.error("Tools: list_cards, check_balance, request_payment_token, execute_payment, cancel_payment_token, request_human_approval");
165
255
  }
166
256
  main().catch(console.error);
@@ -1,4 +1,8 @@
1
1
  import type { CardData, PaymentToken } from "./types.js";
2
+ interface ExtendedToken extends PaymentToken {
3
+ airwallex_card_id?: string;
4
+ cancelled?: boolean;
5
+ }
2
6
  export declare function listCardsRemote(): Promise<Array<{
3
7
  alias: string;
4
8
  balance: number;
@@ -8,6 +12,12 @@ export declare function getBalanceRemote(cardAlias: string): Promise<{
8
12
  balance: number;
9
13
  currency: string;
10
14
  } | null>;
11
- export declare function issueTokenRemote(cardAlias: string, amount: number, merchant: string, ttlSeconds?: number): Promise<PaymentToken | null>;
12
- export declare function resolveTokenRemote(tokenId: string): CardData | null;
15
+ export declare function issueTokenRemote(cardAlias: string, amount: number, merchant: string, ttlSeconds?: number): Promise<ExtendedToken | null>;
16
+ export declare function resolveTokenRemote(tokenId: string): Promise<CardData | null>;
17
+ export declare function cancelTokenRemote(tokenId: string): Promise<{
18
+ success: boolean;
19
+ refunded_amount: number;
20
+ }>;
13
21
  export declare function burnTokenRemote(tokenId: string): Promise<boolean>;
22
+ export declare function refundUnderspendRemote(tokenId: string, actualSpent: number): Promise<void>;
23
+ export {};
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
- // Z-ZERO Supabase Backend
3
- // Replaces mock_backend.ts - connects to real Supabase database
4
- // Card data is looked up from the 'cards' table via Z_ZERO_API_KEY
2
+ // Z-ZERO Supabase Backend v2
3
+ // Connects to real Supabase DB + Airwallex Issuing API for real JIT Mastercards
4
+ // Card data is looked up via Z_ZERO_API_KEY, cards are issued via Airwallex
5
5
  var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  return (mod && mod.__esModule) ? mod : { "default": mod };
7
7
  };
@@ -10,11 +10,13 @@ exports.listCardsRemote = listCardsRemote;
10
10
  exports.getBalanceRemote = getBalanceRemote;
11
11
  exports.issueTokenRemote = issueTokenRemote;
12
12
  exports.resolveTokenRemote = resolveTokenRemote;
13
+ exports.cancelTokenRemote = cancelTokenRemote;
13
14
  exports.burnTokenRemote = burnTokenRemote;
15
+ exports.refundUnderspendRemote = refundUnderspendRemote;
14
16
  const supabase_js_1 = require("@supabase/supabase-js");
15
17
  const crypto_1 = __importDefault(require("crypto"));
16
18
  // ============================================================
17
- // SUPABASE CLIENT (reads env vars at runtime)
19
+ // SUPABASE CLIENT
18
20
  // ============================================================
19
21
  const SUPABASE_URL = process.env.Z_ZERO_SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL || "";
20
22
  const SUPABASE_ANON_KEY = process.env.Z_ZERO_SUPABASE_ANON_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
@@ -22,21 +24,52 @@ const API_KEY = process.env.Z_ZERO_API_KEY || "";
22
24
  if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
23
25
  console.error("❌ Missing Z_ZERO_SUPABASE_URL or Z_ZERO_SUPABASE_ANON_KEY env vars");
24
26
  }
25
- if (!API_KEY) {
26
- console.error("⚠️ Missing Z_ZERO_API_KEY — some tools will be limited");
27
- }
28
27
  const supabase = (0, supabase_js_1.createClient)(SUPABASE_URL, SUPABASE_ANON_KEY);
29
28
  // ============================================================
30
- // IN-MEMORY TOKEN STORE (tokens never hit the database)
29
+ // AIRWALLEX CONFIG
31
30
  // ============================================================
31
+ const AIRWALLEX_API_KEY = process.env.Z_ZERO_AIRWALLEX_API_KEY || "";
32
+ const AIRWALLEX_CLIENT_ID = process.env.Z_ZERO_AIRWALLEX_CLIENT_ID || "";
33
+ const AIRWALLEX_ENV = process.env.Z_ZERO_AIRWALLEX_ENV || "demo";
34
+ const AIRWALLEX_BASE = AIRWALLEX_ENV === "prod"
35
+ ? "https://api.airwallex.com/api/v1"
36
+ : "https://api-demo.airwallex.com/api/v1";
37
+ // Cached auth token for Airwallex (avoids re-login per call)
38
+ let airwallexToken = null;
39
+ let airwallexTokenExpiry = 0;
40
+ async function getAirwallexToken() {
41
+ if (!AIRWALLEX_API_KEY || !AIRWALLEX_CLIENT_ID)
42
+ return null;
43
+ if (airwallexToken && Date.now() < airwallexTokenExpiry)
44
+ return airwallexToken;
45
+ try {
46
+ const res = await fetch(`${AIRWALLEX_BASE}/authentication/login`, {
47
+ method: "POST",
48
+ headers: {
49
+ "x-api-key": AIRWALLEX_API_KEY,
50
+ "x-client-id": AIRWALLEX_CLIENT_ID,
51
+ "Content-Type": "application/json",
52
+ },
53
+ });
54
+ if (!res.ok)
55
+ throw new Error(await res.text());
56
+ const data = await res.json();
57
+ airwallexToken = data.token;
58
+ airwallexTokenExpiry = Date.now() + 25 * 60 * 1000; // 25 min cache
59
+ return airwallexToken;
60
+ }
61
+ catch (err) {
62
+ console.error("[AIRWALLEX] Auth failed:", err);
63
+ return null;
64
+ }
65
+ }
32
66
  const tokenStore = new Map();
33
67
  // ============================================================
34
- // RESOLVE API KEY → USER + WALLET INFO
68
+ // RESOLVE API KEY → USER INFO
35
69
  // ============================================================
36
70
  async function resolveApiKey() {
37
71
  if (!API_KEY)
38
72
  return null;
39
- // Lookup card by its stored key (card_number_encrypted stores the api key for now)
40
73
  const { data: card, error } = await supabase
41
74
  .from("cards")
42
75
  .select("id, alias, user_id, allocated_limit_usd, is_active")
@@ -44,10 +77,9 @@ async function resolveApiKey() {
44
77
  .eq("is_active", true)
45
78
  .single();
46
79
  if (error || !card) {
47
- console.error("API Key not found or card is inactive:", error?.message);
80
+ console.error("API Key not found:", error?.message);
48
81
  return null;
49
82
  }
50
- // Get wallet balance for this user
51
83
  const { data: wallet } = await supabase
52
84
  .from("wallets")
53
85
  .select("balance")
@@ -61,17 +93,13 @@ async function resolveApiKey() {
61
93
  };
62
94
  }
63
95
  // ============================================================
64
- // PUBLIC API (mirrors mock_backend.ts interface)
96
+ // PUBLIC API
65
97
  // ============================================================
66
98
  async function listCardsRemote() {
67
99
  const info = await resolveApiKey();
68
100
  if (!info)
69
101
  return [];
70
- return [{
71
- alias: info.cardAlias,
72
- balance: info.walletBalance,
73
- currency: "USD",
74
- }];
102
+ return [{ alias: info.cardAlias, balance: info.walletBalance, currency: "USD" }];
75
103
  }
76
104
  async function getBalanceRemote(cardAlias) {
77
105
  const info = await resolveApiKey();
@@ -79,37 +107,132 @@ async function getBalanceRemote(cardAlias) {
79
107
  return null;
80
108
  return { balance: info.walletBalance, currency: "USD" };
81
109
  }
82
- async function issueTokenRemote(cardAlias, amount, merchant, ttlSeconds = 900) {
110
+ async function issueTokenRemote(cardAlias, amount, merchant, ttlSeconds = 1800 // 30 minutes default
111
+ ) {
112
+ // 1. Validate business rules
113
+ if (amount < 1) {
114
+ console.error("[ISSUE] Card amount must be at least $1.00");
115
+ return null;
116
+ }
117
+ if (amount > 100) {
118
+ console.error("[ISSUE] Card amount exceeds $100 maximum limit");
119
+ return null;
120
+ }
121
+ // 2. Resolve user from API key
83
122
  const info = await resolveApiKey();
84
123
  if (!info)
85
124
  return null;
86
125
  if (info.cardAlias !== cardAlias)
87
126
  return null;
88
- if (info.walletBalance < amount)
127
+ if (info.walletBalance < amount) {
128
+ console.error(`[ISSUE] Insufficient balance: $${info.walletBalance} < $${amount}`);
89
129
  return null;
130
+ }
131
+ const tokenId = `z_jit_${crypto_1.default.randomBytes(8).toString("hex")}`;
132
+ // 3. Try to create a REAL Airwallex card
133
+ let airwallexCardId;
134
+ const airwallexAuth = await getAirwallexToken();
135
+ if (airwallexAuth) {
136
+ try {
137
+ const expiryTime = new Date(Date.now() + 30 * 60 * 1000).toISOString();
138
+ const payload = {
139
+ request_id: tokenId,
140
+ issue_to: "ORGANISATION",
141
+ name_on_card: "Z-ZERO AI AGENT",
142
+ form_factor: "VIRTUAL",
143
+ primary_currency: "USD",
144
+ valid_to: expiryTime,
145
+ authorization_controls: {
146
+ allowed_transaction_count: "SINGLE",
147
+ transaction_limits: {
148
+ currency: "USD",
149
+ limits: [{ amount, interval: "ALL_TIME" }],
150
+ },
151
+ },
152
+ };
153
+ const res = await fetch(`${AIRWALLEX_BASE}/issuing/cards/create`, {
154
+ method: "POST",
155
+ headers: {
156
+ Authorization: `Bearer ${airwallexAuth}`,
157
+ "Content-Type": "application/json",
158
+ },
159
+ body: JSON.stringify(payload),
160
+ });
161
+ if (res.ok) {
162
+ const data = await res.json();
163
+ airwallexCardId = data.card_id;
164
+ console.error(`[AIRWALLEX] ✅ Real card issued: ${data.card_id} for $${amount}`);
165
+ }
166
+ else {
167
+ const errText = await res.text();
168
+ console.error("[AIRWALLEX] Card creation failed:", errText);
169
+ // Fall through — will still create token but without real card ID
170
+ }
171
+ }
172
+ catch (err) {
173
+ console.error("[AIRWALLEX] Error creating card:", err);
174
+ }
175
+ }
176
+ else {
177
+ console.error("[AIRWALLEX] No auth token — running in mock mode");
178
+ }
90
179
  const token = {
91
- token: `temp_auth_${crypto_1.default.randomBytes(6).toString("hex")}`,
180
+ token: tokenId,
92
181
  card_alias: cardAlias,
93
182
  amount,
94
183
  merchant,
95
184
  created_at: Date.now(),
96
185
  ttl_seconds: ttlSeconds,
97
186
  used: false,
187
+ cancelled: false,
188
+ airwallex_card_id: airwallexCardId,
98
189
  };
99
- tokenStore.set(token.token, token);
190
+ tokenStore.set(tokenId, token);
191
+ // 4. Pre-hold the amount in Supabase (deduct immediately, refund on cancel or underspend)
192
+ const newBalance = info.walletBalance - amount;
193
+ await supabase
194
+ .from("wallets")
195
+ .update({ balance: newBalance })
196
+ .eq("user_id", info.userId);
197
+ console.error(`[ISSUE] Token ${tokenId} created. Balance: $${info.walletBalance} → $${newBalance}`);
100
198
  return token;
101
199
  }
102
- function resolveTokenRemote(tokenId) {
200
+ async function resolveTokenRemote(tokenId) {
103
201
  const token = tokenStore.get(tokenId);
104
- if (!token || token.used)
202
+ if (!token || token.used || token.cancelled)
105
203
  return null;
106
204
  const age = (Date.now() - token.created_at) / 1000;
107
205
  if (age > token.ttl_seconds) {
108
206
  tokenStore.delete(tokenId);
109
207
  return null;
110
208
  }
111
- // In Phase 2 this will return a real Airwallex JIT card
112
- // For now, returns a Stripe test card for sandbox testing
209
+ // Try to fetch REAL card details from Airwallex
210
+ if (token.airwallex_card_id) {
211
+ const airwallexAuth = await getAirwallexToken();
212
+ if (airwallexAuth) {
213
+ try {
214
+ const res = await fetch(`${AIRWALLEX_BASE}/issuing/cards/${token.airwallex_card_id}`, {
215
+ headers: { Authorization: `Bearer ${airwallexAuth}` },
216
+ });
217
+ if (res.ok) {
218
+ const data = await res.json();
219
+ console.error("[AIRWALLEX] ✅ Real card details fetched.");
220
+ return {
221
+ number: data.card_number || "4242424242424242",
222
+ exp_month: data.expiry_month || "12",
223
+ exp_year: data.expiry_year || "2030",
224
+ cvv: data.cvv || "123",
225
+ name: data.name_on_card || "Z-ZERO AI AGENT",
226
+ };
227
+ }
228
+ }
229
+ catch (err) {
230
+ console.error("[AIRWALLEX] Failed to fetch card details:", err);
231
+ }
232
+ }
233
+ }
234
+ // Fallback to Stripe test card (demo/sandbox mode)
235
+ console.error("[AIRWALLEX] ⚠️ Using test card fallback (4242...)");
113
236
  return {
114
237
  number: "4242424242424242",
115
238
  exp_month: "12",
@@ -118,20 +241,56 @@ function resolveTokenRemote(tokenId) {
118
241
  name: "Z-ZERO Agent",
119
242
  };
120
243
  }
121
- async function burnTokenRemote(tokenId) {
244
+ async function cancelTokenRemote(tokenId) {
122
245
  const token = tokenStore.get(tokenId);
123
- if (!token)
124
- return false;
125
- token.used = true;
126
- // Deduct from Supabase wallet balance
246
+ if (!token || token.used || token.cancelled) {
247
+ return { success: false, refunded_amount: 0 };
248
+ }
249
+ // Mark token as cancelled in memory
250
+ token.cancelled = true;
251
+ // Cancel the Airwallex card (if one was issued)
252
+ if (token.airwallex_card_id) {
253
+ const airwallexAuth = await getAirwallexToken();
254
+ if (airwallexAuth) {
255
+ try {
256
+ await fetch(`${AIRWALLEX_BASE}/issuing/cards/${token.airwallex_card_id}/deactivate`, {
257
+ method: "POST",
258
+ headers: { Authorization: `Bearer ${airwallexAuth}`, "Content-Type": "application/json" },
259
+ body: JSON.stringify({ cancellation_reason: "CANCELLED_BY_USER" }),
260
+ });
261
+ console.error(`[AIRWALLEX] Card ${token.airwallex_card_id} cancelled.`);
262
+ }
263
+ catch (err) {
264
+ console.error("[AIRWALLEX] Failed to cancel card:", err);
265
+ }
266
+ }
267
+ }
268
+ // Refund the held amount back to wallet
127
269
  const info = await resolveApiKey();
128
270
  if (info) {
129
- const newBalance = info.walletBalance - token.amount;
271
+ const currentBalanceRes = await supabase
272
+ .from("wallets")
273
+ .select("balance")
274
+ .eq("user_id", info.userId)
275
+ .single();
276
+ const currentBalance = Number(currentBalanceRes.data?.balance || 0);
130
277
  await supabase
131
278
  .from("wallets")
132
- .update({ balance: newBalance })
279
+ .update({ balance: currentBalance + token.amount })
133
280
  .eq("user_id", info.userId);
134
- // Log the transaction
281
+ console.error(`[CANCEL] Refunded $${token.amount}. New balance: $${currentBalance + token.amount}`);
282
+ }
283
+ setTimeout(() => tokenStore.delete(tokenId), 5000);
284
+ return { success: true, refunded_amount: token.amount };
285
+ }
286
+ async function burnTokenRemote(tokenId) {
287
+ const token = tokenStore.get(tokenId);
288
+ if (!token || token.cancelled)
289
+ return false;
290
+ token.used = true;
291
+ // Log the transaction — NOTE: wallet was already debited at issue time
292
+ const info = await resolveApiKey();
293
+ if (info) {
135
294
  await supabase.from("transactions").insert({
136
295
  card_id: info.cardId,
137
296
  amount: token.amount,
@@ -142,3 +301,26 @@ async function burnTokenRemote(tokenId) {
142
301
  setTimeout(() => tokenStore.delete(tokenId), 5000);
143
302
  return true;
144
303
  }
304
+ // Handle underspend: called when transaction amount < token amount
305
+ async function refundUnderspendRemote(tokenId, actualSpent) {
306
+ const token = tokenStore.get(tokenId);
307
+ if (!token)
308
+ return;
309
+ const overheld = token.amount - actualSpent;
310
+ if (overheld <= 0)
311
+ return;
312
+ const info = await resolveApiKey();
313
+ if (!info)
314
+ return;
315
+ const currentBalanceRes = await supabase
316
+ .from("wallets")
317
+ .select("balance")
318
+ .eq("user_id", info.userId)
319
+ .single();
320
+ const currentBalance = Number(currentBalanceRes.data?.balance || 0);
321
+ await supabase
322
+ .from("wallets")
323
+ .update({ balance: currentBalance + overheld })
324
+ .eq("user_id", info.userId);
325
+ console.error(`[REFUND] Underspend refunded: $${overheld} (Spent $${actualSpent} of $${token.amount})`);
326
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "z-zero-mcp-server",
3
- "version": "1.0.1",
4
- "description": "Z-ZERO MCP Server — Secure JIT Virtual Card Payment tools for AI Agents (Claude, AutoGPT, CrewAI)",
3
+ "version": "1.0.2",
4
+ "description": "Z-ZERO MCP Server — Secure JIT Virtual Card Payment tools for AI Agents (Claude, AutoGPT, CrewAI) — Real Airwallex Mastercards",
5
5
  "main": "dist/index.js",
6
6
  "bin": "dist/index.js",
7
7
  "scripts": {