globodai-mcp-payment-manager 1.0.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/.env.example +23 -0
- package/.github/workflows/ci.yml +26 -0
- package/.github/workflows/release.yml +82 -0
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +122 -0
- package/dist/lib/blockchain.d.ts +50 -0
- package/dist/lib/blockchain.js +287 -0
- package/dist/lib/cards.d.ts +83 -0
- package/dist/lib/cards.js +276 -0
- package/dist/lib/cli-runner.d.ts +31 -0
- package/dist/lib/cli-runner.js +77 -0
- package/dist/lib/crypto.d.ts +39 -0
- package/dist/lib/crypto.js +228 -0
- package/dist/lib/cvv-crypto.d.ts +23 -0
- package/dist/lib/cvv-crypto.js +67 -0
- package/dist/lib/mcp-core.d.ts +46 -0
- package/dist/lib/mcp-core.js +86 -0
- package/dist/lib/pin-manager.d.ts +69 -0
- package/dist/lib/pin-manager.js +199 -0
- package/dist/lib/wallets.d.ts +91 -0
- package/dist/lib/wallets.js +227 -0
- package/dist/tools/add-card.d.ts +65 -0
- package/dist/tools/add-card.js +97 -0
- package/dist/tools/add-wallet.d.ts +65 -0
- package/dist/tools/add-wallet.js +104 -0
- package/dist/tools/card-status.d.ts +20 -0
- package/dist/tools/card-status.js +26 -0
- package/dist/tools/confirm-payment.d.ts +44 -0
- package/dist/tools/confirm-payment.js +88 -0
- package/dist/tools/get-total-balance.d.ts +41 -0
- package/dist/tools/get-total-balance.js +98 -0
- package/dist/tools/get-transactions.d.ts +39 -0
- package/dist/tools/get-transactions.js +40 -0
- package/dist/tools/get-wallet-balance.d.ts +43 -0
- package/dist/tools/get-wallet-balance.js +69 -0
- package/dist/tools/list-cards.d.ts +36 -0
- package/dist/tools/list-cards.js +39 -0
- package/dist/tools/list-wallet-transactions.d.ts +63 -0
- package/dist/tools/list-wallet-transactions.js +76 -0
- package/dist/tools/list-wallets.d.ts +41 -0
- package/dist/tools/list-wallets.js +50 -0
- package/dist/tools/lock-cards.d.ts +16 -0
- package/dist/tools/lock-cards.js +23 -0
- package/dist/tools/prepare-crypto-tx.d.ts +69 -0
- package/dist/tools/prepare-crypto-tx.js +93 -0
- package/dist/tools/prepare-payment.d.ts +57 -0
- package/dist/tools/prepare-payment.js +93 -0
- package/dist/tools/remove-card.d.ts +25 -0
- package/dist/tools/remove-card.js +39 -0
- package/dist/tools/remove-wallet.d.ts +27 -0
- package/dist/tools/remove-wallet.js +40 -0
- package/dist/tools/setup-pin.d.ts +26 -0
- package/dist/tools/setup-pin.js +33 -0
- package/dist/tools/sign-crypto-tx.d.ts +42 -0
- package/dist/tools/sign-crypto-tx.js +75 -0
- package/dist/tools/unlock-cards.d.ts +35 -0
- package/dist/tools/unlock-cards.js +41 -0
- package/package.json +50 -0
- package/src/index.ts +139 -0
- package/src/lib/blockchain.ts +375 -0
- package/src/lib/cards.ts +372 -0
- package/src/lib/cli-runner.ts +113 -0
- package/src/lib/crypto.ts +284 -0
- package/src/lib/cvv-crypto.ts +81 -0
- package/src/lib/mcp-core.ts +127 -0
- package/src/lib/pin-manager.ts +252 -0
- package/src/lib/wallets.ts +331 -0
- package/src/tools/add-card.ts +108 -0
- package/src/tools/add-wallet.ts +114 -0
- package/src/tools/card-status.ts +32 -0
- package/src/tools/confirm-payment.ts +103 -0
- package/src/tools/get-total-balance.ts +123 -0
- package/src/tools/get-transactions.ts +49 -0
- package/src/tools/get-wallet-balance.ts +75 -0
- package/src/tools/list-cards.ts +52 -0
- package/src/tools/list-wallet-transactions.ts +83 -0
- package/src/tools/list-wallets.ts +63 -0
- package/src/tools/lock-cards.ts +31 -0
- package/src/tools/prepare-crypto-tx.ts +108 -0
- package/src/tools/prepare-payment.ts +108 -0
- package/src/tools/remove-card.ts +46 -0
- package/src/tools/remove-wallet.ts +47 -0
- package/src/tools/setup-pin.ts +39 -0
- package/src/tools/sign-crypto-tx.ts +90 -0
- package/src/tools/unlock-cards.ts +48 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blockchain API Client
|
|
3
|
+
*
|
|
4
|
+
* Fetches balances and transactions from various blockchains.
|
|
5
|
+
* Uses public RPC endpoints and explorer APIs.
|
|
6
|
+
*/
|
|
7
|
+
import { type Chain } from "./wallets";
|
|
8
|
+
export interface WalletBalance {
|
|
9
|
+
address: string;
|
|
10
|
+
chain: Chain;
|
|
11
|
+
nativeBalance: string;
|
|
12
|
+
nativeBalanceFormatted: string;
|
|
13
|
+
nativeCurrency: string;
|
|
14
|
+
usdValue?: number;
|
|
15
|
+
tokens?: TokenBalance[];
|
|
16
|
+
}
|
|
17
|
+
export interface TokenBalance {
|
|
18
|
+
symbol: string;
|
|
19
|
+
name: string;
|
|
20
|
+
balance: string;
|
|
21
|
+
balanceFormatted: string;
|
|
22
|
+
contractAddress: string;
|
|
23
|
+
decimals: number;
|
|
24
|
+
usdValue?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface WalletTransaction {
|
|
27
|
+
hash: string;
|
|
28
|
+
from: string;
|
|
29
|
+
to: string;
|
|
30
|
+
value: string;
|
|
31
|
+
valueFormatted: string;
|
|
32
|
+
timestamp: number;
|
|
33
|
+
blockNumber: number;
|
|
34
|
+
isIncoming: boolean;
|
|
35
|
+
status: "success" | "failed" | "pending";
|
|
36
|
+
fee?: string;
|
|
37
|
+
tokenSymbol?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get wallet balance
|
|
41
|
+
*/
|
|
42
|
+
export declare function getWalletBalance(address: string, chain: Chain): Promise<WalletBalance>;
|
|
43
|
+
/**
|
|
44
|
+
* Get wallet transactions
|
|
45
|
+
*/
|
|
46
|
+
export declare function getWalletTransactions(address: string, chain: Chain, limit?: number): Promise<WalletTransaction[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Get USD price for a currency (simple coingecko fetch)
|
|
49
|
+
*/
|
|
50
|
+
export declare function getUsdPrice(currency: string): Promise<number | null>;
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blockchain API Client
|
|
3
|
+
*
|
|
4
|
+
* Fetches balances and transactions from various blockchains.
|
|
5
|
+
* Uses public RPC endpoints and explorer APIs.
|
|
6
|
+
*/
|
|
7
|
+
import { CHAIN_CONFIG } from "./wallets";
|
|
8
|
+
// Default public RPC endpoints (can be overridden via env vars)
|
|
9
|
+
const DEFAULT_RPC = {
|
|
10
|
+
ethereum: "https://eth.llamarpc.com",
|
|
11
|
+
polygon: "https://polygon-rpc.com",
|
|
12
|
+
arbitrum: "https://arb1.arbitrum.io/rpc",
|
|
13
|
+
optimism: "https://mainnet.optimism.io",
|
|
14
|
+
base: "https://mainnet.base.org",
|
|
15
|
+
avalanche: "https://api.avax.network/ext/bc/C/rpc",
|
|
16
|
+
bsc: "https://bsc-dataseed.binance.org",
|
|
17
|
+
bitcoin: "", // Needs special handling
|
|
18
|
+
solana: "https://api.mainnet-beta.solana.com",
|
|
19
|
+
starknet: "https://starknet-mainnet.public.blastapi.io",
|
|
20
|
+
};
|
|
21
|
+
// Explorer APIs for transactions
|
|
22
|
+
const EXPLORER_API = {
|
|
23
|
+
ethereum: "https://api.etherscan.io/api",
|
|
24
|
+
polygon: "https://api.polygonscan.com/api",
|
|
25
|
+
arbitrum: "https://api.arbiscan.io/api",
|
|
26
|
+
optimism: "https://api-optimistic.etherscan.io/api",
|
|
27
|
+
base: "https://api.basescan.org/api",
|
|
28
|
+
bsc: "https://api.bscscan.com/api",
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Get RPC URL for a chain
|
|
32
|
+
*/
|
|
33
|
+
function getRpcUrl(chain) {
|
|
34
|
+
const envVar = CHAIN_CONFIG[chain]?.rpcEnvVar;
|
|
35
|
+
if (envVar && process.env[envVar]) {
|
|
36
|
+
return process.env[envVar];
|
|
37
|
+
}
|
|
38
|
+
return DEFAULT_RPC[chain] || "";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get explorer API key (optional, for higher rate limits)
|
|
42
|
+
*/
|
|
43
|
+
function getExplorerApiKey(chain) {
|
|
44
|
+
const keyMap = {
|
|
45
|
+
ethereum: "ETHERSCAN_API_KEY",
|
|
46
|
+
polygon: "POLYGONSCAN_API_KEY",
|
|
47
|
+
arbitrum: "ARBISCAN_API_KEY",
|
|
48
|
+
bsc: "BSCSCAN_API_KEY",
|
|
49
|
+
};
|
|
50
|
+
const envVar = keyMap[chain];
|
|
51
|
+
return envVar ? process.env[envVar] : undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Make JSON-RPC call
|
|
55
|
+
*/
|
|
56
|
+
async function rpcCall(chain, method, params) {
|
|
57
|
+
const url = getRpcUrl(chain);
|
|
58
|
+
if (!url) {
|
|
59
|
+
throw new Error(`No RPC URL configured for ${chain}`);
|
|
60
|
+
}
|
|
61
|
+
const response = await fetch(url, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { "Content-Type": "application/json" },
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
jsonrpc: "2.0",
|
|
66
|
+
id: 1,
|
|
67
|
+
method,
|
|
68
|
+
params,
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
const data = await response.json();
|
|
72
|
+
if (data.error) {
|
|
73
|
+
throw new Error(data.error.message || "RPC error");
|
|
74
|
+
}
|
|
75
|
+
return data.result;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Format wei to ETH (or equivalent)
|
|
79
|
+
*/
|
|
80
|
+
function formatWei(wei, decimals = 18) {
|
|
81
|
+
const weiNum = BigInt(wei);
|
|
82
|
+
const divisor = BigInt(10 ** decimals);
|
|
83
|
+
const whole = weiNum / divisor;
|
|
84
|
+
const fraction = weiNum % divisor;
|
|
85
|
+
const fractionStr = fraction.toString().padStart(decimals, "0").slice(0, 6);
|
|
86
|
+
return `${whole}.${fractionStr}`.replace(/\.?0+$/, "") || "0";
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get native balance for EVM chains
|
|
90
|
+
*/
|
|
91
|
+
async function getEvmBalance(address, chain) {
|
|
92
|
+
const result = await rpcCall(chain, "eth_getBalance", [address, "latest"]);
|
|
93
|
+
const config = CHAIN_CONFIG[chain];
|
|
94
|
+
return {
|
|
95
|
+
address,
|
|
96
|
+
chain,
|
|
97
|
+
nativeBalance: result,
|
|
98
|
+
nativeBalanceFormatted: formatWei(result),
|
|
99
|
+
nativeCurrency: config?.nativeCurrency || "ETH",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get Solana balance
|
|
104
|
+
*/
|
|
105
|
+
async function getSolanaBalance(address) {
|
|
106
|
+
const url = getRpcUrl("solana");
|
|
107
|
+
const response = await fetch(url, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { "Content-Type": "application/json" },
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
jsonrpc: "2.0",
|
|
112
|
+
id: 1,
|
|
113
|
+
method: "getBalance",
|
|
114
|
+
params: [address],
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
const data = await response.json();
|
|
118
|
+
const lamports = data.result?.value || 0;
|
|
119
|
+
return {
|
|
120
|
+
address,
|
|
121
|
+
chain: "solana",
|
|
122
|
+
nativeBalance: lamports.toString(),
|
|
123
|
+
nativeBalanceFormatted: (lamports / 1e9).toFixed(4),
|
|
124
|
+
nativeCurrency: "SOL",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get Bitcoin balance via public API
|
|
129
|
+
*/
|
|
130
|
+
async function getBitcoinBalance(address) {
|
|
131
|
+
// Using blockchain.info API
|
|
132
|
+
const response = await fetch(`https://blockchain.info/balance?active=${address}`);
|
|
133
|
+
const data = await response.json();
|
|
134
|
+
const satoshis = data[address]?.final_balance || 0;
|
|
135
|
+
return {
|
|
136
|
+
address,
|
|
137
|
+
chain: "bitcoin",
|
|
138
|
+
nativeBalance: satoshis.toString(),
|
|
139
|
+
nativeBalanceFormatted: (satoshis / 1e8).toFixed(8),
|
|
140
|
+
nativeCurrency: "BTC",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Get wallet balance
|
|
145
|
+
*/
|
|
146
|
+
export async function getWalletBalance(address, chain) {
|
|
147
|
+
switch (chain) {
|
|
148
|
+
case "bitcoin":
|
|
149
|
+
return getBitcoinBalance(address);
|
|
150
|
+
case "solana":
|
|
151
|
+
return getSolanaBalance(address);
|
|
152
|
+
default:
|
|
153
|
+
// All EVM chains
|
|
154
|
+
return getEvmBalance(address, chain);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get EVM transactions via explorer API
|
|
159
|
+
*/
|
|
160
|
+
async function getEvmTransactions(address, chain, limit = 20) {
|
|
161
|
+
const apiUrl = EXPLORER_API[chain];
|
|
162
|
+
if (!apiUrl) {
|
|
163
|
+
return []; // No explorer API for this chain
|
|
164
|
+
}
|
|
165
|
+
const apiKey = getExplorerApiKey(chain);
|
|
166
|
+
const url = new URL(apiUrl);
|
|
167
|
+
url.searchParams.set("module", "account");
|
|
168
|
+
url.searchParams.set("action", "txlist");
|
|
169
|
+
url.searchParams.set("address", address);
|
|
170
|
+
url.searchParams.set("sort", "desc");
|
|
171
|
+
url.searchParams.set("page", "1");
|
|
172
|
+
url.searchParams.set("offset", limit.toString());
|
|
173
|
+
if (apiKey) {
|
|
174
|
+
url.searchParams.set("apikey", apiKey);
|
|
175
|
+
}
|
|
176
|
+
const response = await fetch(url.toString());
|
|
177
|
+
const data = await response.json();
|
|
178
|
+
if (data.status !== "1" || !Array.isArray(data.result)) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
const config = CHAIN_CONFIG[chain];
|
|
182
|
+
return data.result.map((tx) => ({
|
|
183
|
+
hash: tx.hash,
|
|
184
|
+
from: tx.from,
|
|
185
|
+
to: tx.to,
|
|
186
|
+
value: tx.value,
|
|
187
|
+
valueFormatted: `${formatWei(tx.value)} ${config?.nativeCurrency || "ETH"}`,
|
|
188
|
+
timestamp: parseInt(tx.timeStamp) * 1000,
|
|
189
|
+
blockNumber: parseInt(tx.blockNumber),
|
|
190
|
+
isIncoming: tx.to.toLowerCase() === address.toLowerCase(),
|
|
191
|
+
status: tx.isError === "0" ? "success" : "failed",
|
|
192
|
+
fee: formatWei((BigInt(tx.gasUsed) * BigInt(tx.gasPrice)).toString()),
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Get Solana transactions
|
|
197
|
+
*/
|
|
198
|
+
async function getSolanaTransactions(address, limit = 20) {
|
|
199
|
+
const url = getRpcUrl("solana");
|
|
200
|
+
// Get signatures
|
|
201
|
+
const sigResponse = await fetch(url, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: { "Content-Type": "application/json" },
|
|
204
|
+
body: JSON.stringify({
|
|
205
|
+
jsonrpc: "2.0",
|
|
206
|
+
id: 1,
|
|
207
|
+
method: "getSignaturesForAddress",
|
|
208
|
+
params: [address, { limit }],
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
const sigData = await sigResponse.json();
|
|
212
|
+
const signatures = sigData.result || [];
|
|
213
|
+
return signatures.map((sig) => ({
|
|
214
|
+
hash: sig.signature,
|
|
215
|
+
from: address,
|
|
216
|
+
to: "",
|
|
217
|
+
value: "0",
|
|
218
|
+
valueFormatted: "N/A",
|
|
219
|
+
timestamp: sig.blockTime ? sig.blockTime * 1000 : Date.now(),
|
|
220
|
+
blockNumber: sig.slot,
|
|
221
|
+
isIncoming: false,
|
|
222
|
+
status: sig.err ? "failed" : "success",
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Get Bitcoin transactions
|
|
227
|
+
*/
|
|
228
|
+
async function getBitcoinTransactions(address, limit = 20) {
|
|
229
|
+
const response = await fetch(`https://blockchain.info/rawaddr/${address}?limit=${limit}`);
|
|
230
|
+
const data = await response.json();
|
|
231
|
+
if (!data.txs)
|
|
232
|
+
return [];
|
|
233
|
+
return data.txs.map((tx) => {
|
|
234
|
+
const isIncoming = tx.out.some((o) => o.addr === address);
|
|
235
|
+
const value = tx.out
|
|
236
|
+
.filter((o) => (isIncoming ? o.addr === address : o.addr !== address))
|
|
237
|
+
.reduce((sum, o) => sum + o.value, 0);
|
|
238
|
+
return {
|
|
239
|
+
hash: tx.hash,
|
|
240
|
+
from: tx.inputs[0]?.prev_out?.addr || "coinbase",
|
|
241
|
+
to: tx.out[0]?.addr || "",
|
|
242
|
+
value: value.toString(),
|
|
243
|
+
valueFormatted: `${(value / 1e8).toFixed(8)} BTC`,
|
|
244
|
+
timestamp: tx.time * 1000,
|
|
245
|
+
blockNumber: tx.block_height || 0,
|
|
246
|
+
isIncoming,
|
|
247
|
+
status: "success",
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get wallet transactions
|
|
253
|
+
*/
|
|
254
|
+
export async function getWalletTransactions(address, chain, limit = 20) {
|
|
255
|
+
switch (chain) {
|
|
256
|
+
case "bitcoin":
|
|
257
|
+
return getBitcoinTransactions(address, limit);
|
|
258
|
+
case "solana":
|
|
259
|
+
return getSolanaTransactions(address, limit);
|
|
260
|
+
default:
|
|
261
|
+
return getEvmTransactions(address, chain, limit);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Get USD price for a currency (simple coingecko fetch)
|
|
266
|
+
*/
|
|
267
|
+
export async function getUsdPrice(currency) {
|
|
268
|
+
const coinIds = {
|
|
269
|
+
ETH: "ethereum",
|
|
270
|
+
BTC: "bitcoin",
|
|
271
|
+
SOL: "solana",
|
|
272
|
+
MATIC: "matic-network",
|
|
273
|
+
AVAX: "avalanche-2",
|
|
274
|
+
BNB: "binancecoin",
|
|
275
|
+
};
|
|
276
|
+
const coinId = coinIds[currency.toUpperCase()];
|
|
277
|
+
if (!coinId)
|
|
278
|
+
return null;
|
|
279
|
+
try {
|
|
280
|
+
const response = await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=usd`);
|
|
281
|
+
const data = await response.json();
|
|
282
|
+
return data[coinId]?.usd || null;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment Card Management
|
|
3
|
+
*
|
|
4
|
+
* Secure storage for payment cards with encryption at rest.
|
|
5
|
+
* Uses the same crypto system as mail-manager for consistency.
|
|
6
|
+
*
|
|
7
|
+
* Security model:
|
|
8
|
+
* - Card numbers stored encrypted (only last 4 digits visible in plaintext)
|
|
9
|
+
* - CVV encrypted with PIN-derived key (requires unlock to use)
|
|
10
|
+
* - Expiration dates encrypted
|
|
11
|
+
* - All sensitive operations require explicit confirmation
|
|
12
|
+
*/
|
|
13
|
+
export type CardType = "visa" | "mastercard" | "amex" | "discover" | "other";
|
|
14
|
+
export type CardUsage = "flight" | "train" | "hotel" | "general" | "all";
|
|
15
|
+
export interface PaymentCard {
|
|
16
|
+
id: string;
|
|
17
|
+
nickname: string;
|
|
18
|
+
cardType: CardType;
|
|
19
|
+
lastFourDigits: string;
|
|
20
|
+
cardNumber: string;
|
|
21
|
+
expirationDate: string;
|
|
22
|
+
cvv?: string;
|
|
23
|
+
cardholderName: string;
|
|
24
|
+
billingAddress?: {
|
|
25
|
+
street: string;
|
|
26
|
+
city: string;
|
|
27
|
+
postalCode: string;
|
|
28
|
+
country: string;
|
|
29
|
+
};
|
|
30
|
+
allowedUsage: CardUsage[];
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
limits?: {
|
|
33
|
+
perTransaction?: number;
|
|
34
|
+
daily?: number;
|
|
35
|
+
monthly?: number;
|
|
36
|
+
currency: string;
|
|
37
|
+
};
|
|
38
|
+
addedAt: string;
|
|
39
|
+
lastUsedAt?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface Transaction {
|
|
42
|
+
id: string;
|
|
43
|
+
cardId: string;
|
|
44
|
+
type: "flight" | "train" | "hotel" | "general";
|
|
45
|
+
amount: number;
|
|
46
|
+
currency: string;
|
|
47
|
+
description: string;
|
|
48
|
+
provider: string;
|
|
49
|
+
status: "pending" | "confirmed" | "completed" | "failed" | "refunded";
|
|
50
|
+
createdAt: string;
|
|
51
|
+
confirmedAt?: string;
|
|
52
|
+
completedAt?: string;
|
|
53
|
+
reference?: string;
|
|
54
|
+
details?: Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Detect card type from number
|
|
58
|
+
*/
|
|
59
|
+
export declare function detectCardType(cardNumber: string): CardType;
|
|
60
|
+
/**
|
|
61
|
+
* Validate card number using Luhn algorithm
|
|
62
|
+
*/
|
|
63
|
+
export declare function validateCardNumber(cardNumber: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Validate expiration date
|
|
66
|
+
*/
|
|
67
|
+
export declare function validateExpiration(expiration: string): boolean;
|
|
68
|
+
export declare function getCards(): Promise<PaymentCard[]>;
|
|
69
|
+
export declare function getCard(id: string): Promise<PaymentCard | null>;
|
|
70
|
+
/**
|
|
71
|
+
* Get cards without sensitive data (for listing)
|
|
72
|
+
*/
|
|
73
|
+
export declare function getCardsSafe(): Promise<Omit<PaymentCard, "cardNumber" | "expirationDate" | "cvv">[]>;
|
|
74
|
+
/**
|
|
75
|
+
* Get decrypted CVV for a card (requires PIN unlock)
|
|
76
|
+
*/
|
|
77
|
+
export declare function getCardCvv(cardId: string): Promise<string | null>;
|
|
78
|
+
export declare function addCard(card: PaymentCard): Promise<void>;
|
|
79
|
+
export declare function removeCard(id: string): Promise<boolean>;
|
|
80
|
+
export declare function updateCardUsage(id: string): Promise<void>;
|
|
81
|
+
export declare function getTransactions(limit?: number): Promise<Transaction[]>;
|
|
82
|
+
export declare function logTransaction(tx: Transaction): Promise<void>;
|
|
83
|
+
export declare function updateTransaction(id: string, updates: Partial<Transaction>): Promise<void>;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment Card Management
|
|
3
|
+
*
|
|
4
|
+
* Secure storage for payment cards with encryption at rest.
|
|
5
|
+
* Uses the same crypto system as mail-manager for consistency.
|
|
6
|
+
*
|
|
7
|
+
* Security model:
|
|
8
|
+
* - Card numbers stored encrypted (only last 4 digits visible in plaintext)
|
|
9
|
+
* - CVV encrypted with PIN-derived key (requires unlock to use)
|
|
10
|
+
* - Expiration dates encrypted
|
|
11
|
+
* - All sensitive operations require explicit confirmation
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
import { encrypt, decrypt, isEncrypted } from "./crypto";
|
|
17
|
+
import { decryptCvv } from "./cvv-crypto";
|
|
18
|
+
import { isUnlocked } from "./pin-manager";
|
|
19
|
+
// Fields that must be encrypted with system key
|
|
20
|
+
const SENSITIVE_FIELDS = ["cardNumber", "expirationDate"];
|
|
21
|
+
const CONFIG_DIR = join(homedir(), ".mcp-ecosystem");
|
|
22
|
+
const CARDS_FILE = join(CONFIG_DIR, "payment-cards.json");
|
|
23
|
+
const TRANSACTIONS_FILE = join(CONFIG_DIR, "payment-transactions.json");
|
|
24
|
+
/**
|
|
25
|
+
* Detect card type from number
|
|
26
|
+
*/
|
|
27
|
+
export function detectCardType(cardNumber) {
|
|
28
|
+
const cleaned = cardNumber.replace(/\D/g, "");
|
|
29
|
+
if (/^4/.test(cleaned))
|
|
30
|
+
return "visa";
|
|
31
|
+
if (/^5[1-5]/.test(cleaned) || /^2[2-7]/.test(cleaned))
|
|
32
|
+
return "mastercard";
|
|
33
|
+
if (/^3[47]/.test(cleaned))
|
|
34
|
+
return "amex";
|
|
35
|
+
if (/^6(?:011|5)/.test(cleaned))
|
|
36
|
+
return "discover";
|
|
37
|
+
return "other";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Validate card number using Luhn algorithm
|
|
41
|
+
*/
|
|
42
|
+
export function validateCardNumber(cardNumber) {
|
|
43
|
+
const cleaned = cardNumber.replace(/\D/g, "");
|
|
44
|
+
if (cleaned.length < 13 || cleaned.length > 19)
|
|
45
|
+
return false;
|
|
46
|
+
let sum = 0;
|
|
47
|
+
let isEven = false;
|
|
48
|
+
for (let i = cleaned.length - 1; i >= 0; i--) {
|
|
49
|
+
let digit = parseInt(cleaned[i], 10);
|
|
50
|
+
if (isEven) {
|
|
51
|
+
digit *= 2;
|
|
52
|
+
if (digit > 9)
|
|
53
|
+
digit -= 9;
|
|
54
|
+
}
|
|
55
|
+
sum += digit;
|
|
56
|
+
isEven = !isEven;
|
|
57
|
+
}
|
|
58
|
+
return sum % 10 === 0;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Validate expiration date
|
|
62
|
+
*/
|
|
63
|
+
export function validateExpiration(expiration) {
|
|
64
|
+
const match = expiration.match(/^(\d{2})\/(\d{2})$/);
|
|
65
|
+
if (!match)
|
|
66
|
+
return false;
|
|
67
|
+
const month = parseInt(match[1], 10);
|
|
68
|
+
const year = parseInt(match[2], 10) + 2000;
|
|
69
|
+
if (month < 1 || month > 12)
|
|
70
|
+
return false;
|
|
71
|
+
const now = new Date();
|
|
72
|
+
const expDate = new Date(year, month, 0); // Last day of expiration month
|
|
73
|
+
return expDate > now;
|
|
74
|
+
}
|
|
75
|
+
function ensureConfigDir() {
|
|
76
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
77
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Decrypt sensitive fields in a card
|
|
82
|
+
*/
|
|
83
|
+
async function decryptCard(card) {
|
|
84
|
+
const decrypted = { ...card };
|
|
85
|
+
for (const field of SENSITIVE_FIELDS) {
|
|
86
|
+
const value = decrypted[field];
|
|
87
|
+
if (value && typeof value === "string" && isEncrypted(value)) {
|
|
88
|
+
try {
|
|
89
|
+
decrypted[field] = await decrypt(value);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
console.error(`[cards] Failed to decrypt ${field} for card ${card.nickname}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return decrypted;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Encrypt sensitive fields in a card
|
|
100
|
+
*/
|
|
101
|
+
async function encryptCard(card) {
|
|
102
|
+
const encrypted = { ...card };
|
|
103
|
+
for (const field of SENSITIVE_FIELDS) {
|
|
104
|
+
const value = encrypted[field];
|
|
105
|
+
if (value && typeof value === "string" && !isEncrypted(value)) {
|
|
106
|
+
encrypted[field] = await encrypt(value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return encrypted;
|
|
110
|
+
}
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// Card CRUD
|
|
113
|
+
// ============================================================================
|
|
114
|
+
export async function getCards() {
|
|
115
|
+
ensureConfigDir();
|
|
116
|
+
if (!existsSync(CARDS_FILE)) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const data = readFileSync(CARDS_FILE, "utf-8");
|
|
121
|
+
const cards = JSON.parse(data);
|
|
122
|
+
return Promise.all(cards.map(decryptCard));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export async function getCard(id) {
|
|
129
|
+
const cards = await getCards();
|
|
130
|
+
return cards.find((c) => c.id === id) ?? null;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get cards without sensitive data (for listing)
|
|
134
|
+
*/
|
|
135
|
+
export async function getCardsSafe() {
|
|
136
|
+
const cards = await getCards();
|
|
137
|
+
return cards.map(({ cardNumber, expirationDate, cvv, ...safe }) => safe);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get decrypted CVV for a card (requires PIN unlock)
|
|
141
|
+
*/
|
|
142
|
+
export async function getCardCvv(cardId) {
|
|
143
|
+
if (!isUnlocked()) {
|
|
144
|
+
return null; // Must be unlocked
|
|
145
|
+
}
|
|
146
|
+
// Read raw card to get encrypted CVV
|
|
147
|
+
if (!existsSync(CARDS_FILE)) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const rawCards = JSON.parse(readFileSync(CARDS_FILE, "utf-8"));
|
|
152
|
+
const card = rawCards.find((c) => c.id === cardId);
|
|
153
|
+
if (!card?.cvv) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return decryptCvv(card.cvv);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export async function addCard(card) {
|
|
163
|
+
ensureConfigDir();
|
|
164
|
+
// Validate card
|
|
165
|
+
if (!validateCardNumber(card.cardNumber)) {
|
|
166
|
+
throw new Error("Invalid card number");
|
|
167
|
+
}
|
|
168
|
+
if (!validateExpiration(card.expirationDate)) {
|
|
169
|
+
throw new Error("Card is expired or invalid expiration date");
|
|
170
|
+
}
|
|
171
|
+
// Set derived fields
|
|
172
|
+
card.cardType = detectCardType(card.cardNumber);
|
|
173
|
+
card.lastFourDigits = card.cardNumber.replace(/\D/g, "").slice(-4);
|
|
174
|
+
card.addedAt = new Date().toISOString();
|
|
175
|
+
// Load existing (raw/encrypted)
|
|
176
|
+
let rawCards = [];
|
|
177
|
+
if (existsSync(CARDS_FILE)) {
|
|
178
|
+
try {
|
|
179
|
+
rawCards = JSON.parse(readFileSync(CARDS_FILE, "utf-8"));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
rawCards = [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Encrypt and save
|
|
186
|
+
const encryptedCard = await encryptCard(card);
|
|
187
|
+
const existingIndex = rawCards.findIndex((c) => c.id === card.id);
|
|
188
|
+
if (existingIndex >= 0) {
|
|
189
|
+
rawCards[existingIndex] = encryptedCard;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
rawCards.push(encryptedCard);
|
|
193
|
+
}
|
|
194
|
+
writeFileSync(CARDS_FILE, JSON.stringify(rawCards, null, 2), { mode: 0o600 });
|
|
195
|
+
console.error(`[cards] Saved card ${card.nickname} (****${card.lastFourDigits}) - encrypted`);
|
|
196
|
+
}
|
|
197
|
+
export async function removeCard(id) {
|
|
198
|
+
if (!existsSync(CARDS_FILE)) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
let rawCards = [];
|
|
202
|
+
try {
|
|
203
|
+
rawCards = JSON.parse(readFileSync(CARDS_FILE, "utf-8"));
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
const filtered = rawCards.filter((c) => c.id !== id);
|
|
209
|
+
if (filtered.length === rawCards.length) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
writeFileSync(CARDS_FILE, JSON.stringify(filtered, null, 2), { mode: 0o600 });
|
|
213
|
+
console.error(`[cards] Removed card ${id}`);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
export async function updateCardUsage(id) {
|
|
217
|
+
const cards = await getCards();
|
|
218
|
+
const card = cards.find((c) => c.id === id);
|
|
219
|
+
if (card) {
|
|
220
|
+
card.lastUsedAt = new Date().toISOString();
|
|
221
|
+
// Re-encrypt and save all
|
|
222
|
+
const encrypted = await Promise.all(cards.map(encryptCard));
|
|
223
|
+
writeFileSync(CARDS_FILE, JSON.stringify(encrypted, null, 2), { mode: 0o600 });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// ============================================================================
|
|
227
|
+
// Transaction Log
|
|
228
|
+
// ============================================================================
|
|
229
|
+
export async function getTransactions(limit = 50) {
|
|
230
|
+
ensureConfigDir();
|
|
231
|
+
if (!existsSync(TRANSACTIONS_FILE)) {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const data = readFileSync(TRANSACTIONS_FILE, "utf-8");
|
|
236
|
+
const transactions = JSON.parse(data);
|
|
237
|
+
return transactions.slice(-limit);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
export async function logTransaction(tx) {
|
|
244
|
+
ensureConfigDir();
|
|
245
|
+
let transactions = [];
|
|
246
|
+
if (existsSync(TRANSACTIONS_FILE)) {
|
|
247
|
+
try {
|
|
248
|
+
transactions = JSON.parse(readFileSync(TRANSACTIONS_FILE, "utf-8"));
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
transactions = [];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
transactions.push(tx);
|
|
255
|
+
// Keep last 1000 transactions
|
|
256
|
+
if (transactions.length > 1000) {
|
|
257
|
+
transactions = transactions.slice(-1000);
|
|
258
|
+
}
|
|
259
|
+
writeFileSync(TRANSACTIONS_FILE, JSON.stringify(transactions, null, 2), { mode: 0o600 });
|
|
260
|
+
}
|
|
261
|
+
export async function updateTransaction(id, updates) {
|
|
262
|
+
if (!existsSync(TRANSACTIONS_FILE))
|
|
263
|
+
return;
|
|
264
|
+
let transactions = [];
|
|
265
|
+
try {
|
|
266
|
+
transactions = JSON.parse(readFileSync(TRANSACTIONS_FILE, "utf-8"));
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const idx = transactions.findIndex((t) => t.id === id);
|
|
272
|
+
if (idx >= 0) {
|
|
273
|
+
transactions[idx] = { ...transactions[idx], ...updates };
|
|
274
|
+
writeFileSync(TRANSACTIONS_FILE, JSON.stringify(transactions, null, 2), { mode: 0o600 });
|
|
275
|
+
}
|
|
276
|
+
}
|