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.
Files changed (88) hide show
  1. package/.env.example +23 -0
  2. package/.github/workflows/ci.yml +26 -0
  3. package/.github/workflows/release.yml +82 -0
  4. package/LICENSE +21 -0
  5. package/README.md +362 -0
  6. package/dist/index.d.ts +31 -0
  7. package/dist/index.js +122 -0
  8. package/dist/lib/blockchain.d.ts +50 -0
  9. package/dist/lib/blockchain.js +287 -0
  10. package/dist/lib/cards.d.ts +83 -0
  11. package/dist/lib/cards.js +276 -0
  12. package/dist/lib/cli-runner.d.ts +31 -0
  13. package/dist/lib/cli-runner.js +77 -0
  14. package/dist/lib/crypto.d.ts +39 -0
  15. package/dist/lib/crypto.js +228 -0
  16. package/dist/lib/cvv-crypto.d.ts +23 -0
  17. package/dist/lib/cvv-crypto.js +67 -0
  18. package/dist/lib/mcp-core.d.ts +46 -0
  19. package/dist/lib/mcp-core.js +86 -0
  20. package/dist/lib/pin-manager.d.ts +69 -0
  21. package/dist/lib/pin-manager.js +199 -0
  22. package/dist/lib/wallets.d.ts +91 -0
  23. package/dist/lib/wallets.js +227 -0
  24. package/dist/tools/add-card.d.ts +65 -0
  25. package/dist/tools/add-card.js +97 -0
  26. package/dist/tools/add-wallet.d.ts +65 -0
  27. package/dist/tools/add-wallet.js +104 -0
  28. package/dist/tools/card-status.d.ts +20 -0
  29. package/dist/tools/card-status.js +26 -0
  30. package/dist/tools/confirm-payment.d.ts +44 -0
  31. package/dist/tools/confirm-payment.js +88 -0
  32. package/dist/tools/get-total-balance.d.ts +41 -0
  33. package/dist/tools/get-total-balance.js +98 -0
  34. package/dist/tools/get-transactions.d.ts +39 -0
  35. package/dist/tools/get-transactions.js +40 -0
  36. package/dist/tools/get-wallet-balance.d.ts +43 -0
  37. package/dist/tools/get-wallet-balance.js +69 -0
  38. package/dist/tools/list-cards.d.ts +36 -0
  39. package/dist/tools/list-cards.js +39 -0
  40. package/dist/tools/list-wallet-transactions.d.ts +63 -0
  41. package/dist/tools/list-wallet-transactions.js +76 -0
  42. package/dist/tools/list-wallets.d.ts +41 -0
  43. package/dist/tools/list-wallets.js +50 -0
  44. package/dist/tools/lock-cards.d.ts +16 -0
  45. package/dist/tools/lock-cards.js +23 -0
  46. package/dist/tools/prepare-crypto-tx.d.ts +69 -0
  47. package/dist/tools/prepare-crypto-tx.js +93 -0
  48. package/dist/tools/prepare-payment.d.ts +57 -0
  49. package/dist/tools/prepare-payment.js +93 -0
  50. package/dist/tools/remove-card.d.ts +25 -0
  51. package/dist/tools/remove-card.js +39 -0
  52. package/dist/tools/remove-wallet.d.ts +27 -0
  53. package/dist/tools/remove-wallet.js +40 -0
  54. package/dist/tools/setup-pin.d.ts +26 -0
  55. package/dist/tools/setup-pin.js +33 -0
  56. package/dist/tools/sign-crypto-tx.d.ts +42 -0
  57. package/dist/tools/sign-crypto-tx.js +75 -0
  58. package/dist/tools/unlock-cards.d.ts +35 -0
  59. package/dist/tools/unlock-cards.js +41 -0
  60. package/package.json +50 -0
  61. package/src/index.ts +139 -0
  62. package/src/lib/blockchain.ts +375 -0
  63. package/src/lib/cards.ts +372 -0
  64. package/src/lib/cli-runner.ts +113 -0
  65. package/src/lib/crypto.ts +284 -0
  66. package/src/lib/cvv-crypto.ts +81 -0
  67. package/src/lib/mcp-core.ts +127 -0
  68. package/src/lib/pin-manager.ts +252 -0
  69. package/src/lib/wallets.ts +331 -0
  70. package/src/tools/add-card.ts +108 -0
  71. package/src/tools/add-wallet.ts +114 -0
  72. package/src/tools/card-status.ts +32 -0
  73. package/src/tools/confirm-payment.ts +103 -0
  74. package/src/tools/get-total-balance.ts +123 -0
  75. package/src/tools/get-transactions.ts +49 -0
  76. package/src/tools/get-wallet-balance.ts +75 -0
  77. package/src/tools/list-cards.ts +52 -0
  78. package/src/tools/list-wallet-transactions.ts +83 -0
  79. package/src/tools/list-wallets.ts +63 -0
  80. package/src/tools/lock-cards.ts +31 -0
  81. package/src/tools/prepare-crypto-tx.ts +108 -0
  82. package/src/tools/prepare-payment.ts +108 -0
  83. package/src/tools/remove-card.ts +46 -0
  84. package/src/tools/remove-wallet.ts +47 -0
  85. package/src/tools/setup-pin.ts +39 -0
  86. package/src/tools/sign-crypto-tx.ts +90 -0
  87. package/src/tools/unlock-cards.ts +48 -0
  88. package/tsconfig.json +19 -0
@@ -0,0 +1,252 @@
1
+ /**
2
+ * PIN Manager - Master PIN for card access
3
+ *
4
+ * Security model:
5
+ * - CVV is encrypted with a key derived from the master PIN
6
+ * - PIN is stored in memory only (never persisted)
7
+ * - PIN auto-expires after inactivity timeout
8
+ * - Without PIN, cards cannot be used for payments
9
+ *
10
+ * Flow:
11
+ * 1. User sets PIN once (stored as hash for verification)
12
+ * 2. User unlocks with PIN → PIN held in memory
13
+ * 3. Payments work while unlocked
14
+ * 4. After timeout or explicit lock → PIN cleared from memory
15
+ */
16
+
17
+ import { createHash, scryptSync, randomBytes } from "crypto";
18
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
19
+ import { homedir } from "os";
20
+ import { join } from "path";
21
+
22
+ const CONFIG_DIR = join(homedir(), ".mcp-ecosystem");
23
+ const PIN_CONFIG_FILE = join(CONFIG_DIR, "pin-config.json");
24
+
25
+ // In-memory PIN storage (never persisted)
26
+ let currentPin: string | null = null;
27
+ let lastActivity: number = 0;
28
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
29
+
30
+ interface PinConfig {
31
+ // Hash of PIN for verification (not the PIN itself)
32
+ pinHash: string;
33
+ // Salt for PIN hashing
34
+ salt: string;
35
+ // Salt for CVV encryption key derivation
36
+ cvvKeySalt: string;
37
+ // When PIN was set
38
+ createdAt: string;
39
+ }
40
+
41
+ function ensureConfigDir(): void {
42
+ if (!existsSync(CONFIG_DIR)) {
43
+ mkdirSync(CONFIG_DIR, { recursive: true });
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Hash PIN for storage (verification only)
49
+ */
50
+ function hashPin(pin: string, salt: string): string {
51
+ return createHash("sha256")
52
+ .update(pin + salt)
53
+ .digest("hex");
54
+ }
55
+
56
+ /**
57
+ * Derive encryption key from PIN (for CVV encryption)
58
+ */
59
+ export function deriveKeyFromPin(pin: string, salt: string): Buffer {
60
+ return scryptSync(pin, salt, 32);
61
+ }
62
+
63
+ /**
64
+ * Check if PIN is configured
65
+ */
66
+ export function isPinConfigured(): boolean {
67
+ return existsSync(PIN_CONFIG_FILE);
68
+ }
69
+
70
+ /**
71
+ * Get PIN config (without sensitive data)
72
+ */
73
+ function getPinConfig(): PinConfig | null {
74
+ if (!existsSync(PIN_CONFIG_FILE)) {
75
+ return null;
76
+ }
77
+ try {
78
+ return JSON.parse(readFileSync(PIN_CONFIG_FILE, "utf-8"));
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Set up master PIN (first time)
86
+ */
87
+ export function setupPin(pin: string): { success: boolean; error?: string } {
88
+ if (pin.length < 4 || pin.length > 8) {
89
+ return { success: false, error: "PIN must be 4-8 characters" };
90
+ }
91
+
92
+ if (isPinConfigured()) {
93
+ return { success: false, error: "PIN already configured. Use change_pin to modify." };
94
+ }
95
+
96
+ ensureConfigDir();
97
+
98
+ const salt = randomBytes(16).toString("hex");
99
+ const cvvKeySalt = randomBytes(16).toString("hex");
100
+ const pinHash = hashPin(pin, salt);
101
+
102
+ const config: PinConfig = {
103
+ pinHash,
104
+ salt,
105
+ cvvKeySalt,
106
+ createdAt: new Date().toISOString(),
107
+ };
108
+
109
+ writeFileSync(PIN_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
110
+
111
+ // Auto-unlock after setup
112
+ currentPin = pin;
113
+ lastActivity = Date.now();
114
+
115
+ console.error("[pin-manager] Master PIN configured and unlocked");
116
+ return { success: true };
117
+ }
118
+
119
+ /**
120
+ * Change master PIN (requires current PIN)
121
+ */
122
+ export function changePin(currentPinInput: string, newPin: string): { success: boolean; error?: string } {
123
+ const config = getPinConfig();
124
+ if (!config) {
125
+ return { success: false, error: "No PIN configured" };
126
+ }
127
+
128
+ // Verify current PIN
129
+ if (hashPin(currentPinInput, config.salt) !== config.pinHash) {
130
+ return { success: false, error: "Current PIN is incorrect" };
131
+ }
132
+
133
+ if (newPin.length < 4 || newPin.length > 8) {
134
+ return { success: false, error: "New PIN must be 4-8 characters" };
135
+ }
136
+
137
+ // Generate new salts
138
+ const salt = randomBytes(16).toString("hex");
139
+ const cvvKeySalt = randomBytes(16).toString("hex");
140
+ const pinHash = hashPin(newPin, salt);
141
+
142
+ const newConfig: PinConfig = {
143
+ pinHash,
144
+ salt,
145
+ cvvKeySalt,
146
+ createdAt: new Date().toISOString(),
147
+ };
148
+
149
+ writeFileSync(PIN_CONFIG_FILE, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
150
+
151
+ // Update in-memory PIN
152
+ currentPin = newPin;
153
+ lastActivity = Date.now();
154
+
155
+ console.error("[pin-manager] Master PIN changed");
156
+ return { success: true };
157
+ }
158
+
159
+ /**
160
+ * Unlock cards with PIN
161
+ */
162
+ export function unlock(pin: string): { success: boolean; error?: string; expiresIn?: number } {
163
+ const config = getPinConfig();
164
+ if (!config) {
165
+ return { success: false, error: "No PIN configured. Use setup_pin first." };
166
+ }
167
+
168
+ if (hashPin(pin, config.salt) !== config.pinHash) {
169
+ return { success: false, error: "Invalid PIN" };
170
+ }
171
+
172
+ currentPin = pin;
173
+ lastActivity = Date.now();
174
+
175
+ console.error("[pin-manager] Cards unlocked");
176
+ return {
177
+ success: true,
178
+ expiresIn: SESSION_TIMEOUT_MS / 1000 / 60 // in minutes
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Lock cards (clear PIN from memory)
184
+ */
185
+ export function lock(): void {
186
+ currentPin = null;
187
+ lastActivity = 0;
188
+ console.error("[pin-manager] Cards locked");
189
+ }
190
+
191
+ /**
192
+ * Check if cards are currently unlocked
193
+ */
194
+ export function isUnlocked(): boolean {
195
+ if (!currentPin) {
196
+ return false;
197
+ }
198
+
199
+ // Check timeout
200
+ if (Date.now() - lastActivity > SESSION_TIMEOUT_MS) {
201
+ lock();
202
+ return false;
203
+ }
204
+
205
+ return true;
206
+ }
207
+
208
+ /**
209
+ * Refresh activity (extend session)
210
+ */
211
+ export function refreshActivity(): void {
212
+ if (currentPin) {
213
+ lastActivity = Date.now();
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Get CVV encryption key (only works when unlocked)
219
+ */
220
+ export function getCvvEncryptionKey(): Buffer | null {
221
+ if (!isUnlocked()) {
222
+ return null;
223
+ }
224
+
225
+ const config = getPinConfig();
226
+ if (!config || !currentPin) {
227
+ return null;
228
+ }
229
+
230
+ refreshActivity();
231
+ return deriveKeyFromPin(currentPin, config.cvvKeySalt);
232
+ }
233
+
234
+ /**
235
+ * Get status
236
+ */
237
+ export function getStatus(): {
238
+ configured: boolean;
239
+ unlocked: boolean;
240
+ remainingMinutes?: number;
241
+ } {
242
+ const configured = isPinConfigured();
243
+ const unlocked = isUnlocked();
244
+
245
+ let remainingMinutes: number | undefined;
246
+ if (unlocked && lastActivity > 0) {
247
+ const elapsed = Date.now() - lastActivity;
248
+ remainingMinutes = Math.ceil((SESSION_TIMEOUT_MS - elapsed) / 1000 / 60);
249
+ }
250
+
251
+ return { configured, unlocked, remainingMinutes };
252
+ }
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Crypto Wallet Management
3
+ *
4
+ * Secure storage for crypto wallets with encrypted private keys.
5
+ * Supports multiple chains and watch-only wallets.
6
+ *
7
+ * Security model:
8
+ * - Private keys encrypted at rest (same as card numbers)
9
+ * - Watch-only wallets for balance checking without spending
10
+ * - Transaction signing requires explicit confirmation
11
+ * - Support for hardware wallet addresses (no key storage)
12
+ */
13
+
14
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
15
+ import { homedir } from "os";
16
+ import { join } from "path";
17
+ import { encrypt, decrypt, isEncrypted } from "./crypto";
18
+
19
+ // Sensitive fields that must be encrypted
20
+ const SENSITIVE_FIELDS = ["privateKey", "mnemonic"];
21
+
22
+ export type Chain =
23
+ | "ethereum"
24
+ | "polygon"
25
+ | "arbitrum"
26
+ | "optimism"
27
+ | "base"
28
+ | "avalanche"
29
+ | "bsc"
30
+ | "bitcoin"
31
+ | "solana"
32
+ | "starknet";
33
+
34
+ export type WalletType = "hot" | "watch-only" | "hardware";
35
+
36
+ export interface CryptoWallet {
37
+ id: string;
38
+ nickname: string; // e.g., "ETH principal", "Trading wallet"
39
+ address: string; // Public address
40
+ chain: Chain;
41
+ type: WalletType;
42
+ // Only for hot wallets (encrypted)
43
+ privateKey?: string;
44
+ mnemonic?: string;
45
+ // Hardware wallet info
46
+ hardwareType?: "ledger" | "trezor" | "other";
47
+ derivationPath?: string;
48
+ // Settings
49
+ enabled: boolean;
50
+ allowedOperations: ("send" | "swap" | "approve" | "sign")[];
51
+ // Spending limits (optional)
52
+ limits?: {
53
+ perTransaction?: number;
54
+ daily?: number;
55
+ tokenSymbol: string; // e.g., "ETH", "USDC"
56
+ };
57
+ // Metadata
58
+ addedAt: string;
59
+ lastUsedAt?: string;
60
+ }
61
+
62
+ export interface CryptoTransaction {
63
+ id: string;
64
+ walletId: string;
65
+ chain: Chain;
66
+ type: "send" | "swap" | "approve" | "contract";
67
+ status: "pending" | "signed" | "broadcast" | "confirmed" | "failed";
68
+ // Transaction details
69
+ from: string;
70
+ to: string;
71
+ value?: string; // In wei/lamports/etc
72
+ tokenAddress?: string;
73
+ tokenSymbol?: string;
74
+ tokenAmount?: string;
75
+ // Gas/fees
76
+ gasLimit?: string;
77
+ gasPrice?: string;
78
+ maxFeePerGas?: string;
79
+ maxPriorityFeePerGas?: string;
80
+ // Result
81
+ txHash?: string;
82
+ blockNumber?: number;
83
+ // Timestamps
84
+ createdAt: string;
85
+ signedAt?: string;
86
+ broadcastAt?: string;
87
+ confirmedAt?: string;
88
+ // Description
89
+ description: string;
90
+ }
91
+
92
+ const CONFIG_DIR = join(homedir(), ".mcp-ecosystem");
93
+ const WALLETS_FILE = join(CONFIG_DIR, "crypto-wallets.json");
94
+ const CRYPTO_TX_FILE = join(CONFIG_DIR, "crypto-transactions.json");
95
+
96
+ // Chain configurations
97
+ export const CHAIN_CONFIG: Record<Chain, { name: string; nativeCurrency: string; explorerUrl: string; rpcEnvVar: string }> = {
98
+ ethereum: { name: "Ethereum", nativeCurrency: "ETH", explorerUrl: "https://etherscan.io", rpcEnvVar: "ETH_RPC_URL" },
99
+ polygon: { name: "Polygon", nativeCurrency: "MATIC", explorerUrl: "https://polygonscan.com", rpcEnvVar: "POLYGON_RPC_URL" },
100
+ arbitrum: { name: "Arbitrum", nativeCurrency: "ETH", explorerUrl: "https://arbiscan.io", rpcEnvVar: "ARBITRUM_RPC_URL" },
101
+ optimism: { name: "Optimism", nativeCurrency: "ETH", explorerUrl: "https://optimistic.etherscan.io", rpcEnvVar: "OPTIMISM_RPC_URL" },
102
+ base: { name: "Base", nativeCurrency: "ETH", explorerUrl: "https://basescan.org", rpcEnvVar: "BASE_RPC_URL" },
103
+ avalanche: { name: "Avalanche", nativeCurrency: "AVAX", explorerUrl: "https://snowtrace.io", rpcEnvVar: "AVAX_RPC_URL" },
104
+ bsc: { name: "BNB Chain", nativeCurrency: "BNB", explorerUrl: "https://bscscan.com", rpcEnvVar: "BSC_RPC_URL" },
105
+ bitcoin: { name: "Bitcoin", nativeCurrency: "BTC", explorerUrl: "https://mempool.space", rpcEnvVar: "BTC_RPC_URL" },
106
+ solana: { name: "Solana", nativeCurrency: "SOL", explorerUrl: "https://solscan.io", rpcEnvVar: "SOLANA_RPC_URL" },
107
+ starknet: { name: "Starknet", nativeCurrency: "ETH", explorerUrl: "https://starkscan.co", rpcEnvVar: "STARKNET_RPC_URL" },
108
+ };
109
+
110
+ /**
111
+ * Validate Ethereum-like address
112
+ */
113
+ export function isValidEvmAddress(address: string): boolean {
114
+ return /^0x[a-fA-F0-9]{40}$/.test(address);
115
+ }
116
+
117
+ /**
118
+ * Validate Bitcoin address (basic check)
119
+ */
120
+ export function isValidBtcAddress(address: string): boolean {
121
+ // Supports legacy (1...), SegWit (3...), and native SegWit (bc1...)
122
+ return /^(1|3)[a-km-zA-HJ-NP-Z1-9]{25,34}$/.test(address) || /^bc1[a-z0-9]{39,59}$/.test(address);
123
+ }
124
+
125
+ /**
126
+ * Validate Solana address
127
+ */
128
+ export function isValidSolanaAddress(address: string): boolean {
129
+ return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address);
130
+ }
131
+
132
+ /**
133
+ * Validate address based on chain
134
+ */
135
+ export function isValidAddress(address: string, chain: Chain): boolean {
136
+ if (chain === "bitcoin") return isValidBtcAddress(address);
137
+ if (chain === "solana") return isValidSolanaAddress(address);
138
+ // All EVM chains
139
+ return isValidEvmAddress(address);
140
+ }
141
+
142
+ function ensureConfigDir(): void {
143
+ if (!existsSync(CONFIG_DIR)) {
144
+ mkdirSync(CONFIG_DIR, { recursive: true });
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Decrypt sensitive fields in a wallet
150
+ */
151
+ async function decryptWallet(wallet: CryptoWallet): Promise<CryptoWallet> {
152
+ const decrypted = { ...wallet };
153
+
154
+ for (const field of SENSITIVE_FIELDS) {
155
+ const value = (decrypted as any)[field];
156
+ if (value && typeof value === "string" && isEncrypted(value)) {
157
+ try {
158
+ (decrypted as any)[field] = await decrypt(value);
159
+ } catch (err) {
160
+ console.error(`[wallets] Failed to decrypt ${field} for wallet ${wallet.nickname}`);
161
+ }
162
+ }
163
+ }
164
+
165
+ return decrypted;
166
+ }
167
+
168
+ /**
169
+ * Encrypt sensitive fields in a wallet
170
+ */
171
+ async function encryptWallet(wallet: CryptoWallet): Promise<CryptoWallet> {
172
+ const encrypted = { ...wallet };
173
+
174
+ for (const field of SENSITIVE_FIELDS) {
175
+ const value = (encrypted as any)[field];
176
+ if (value && typeof value === "string" && !isEncrypted(value)) {
177
+ (encrypted as any)[field] = await encrypt(value);
178
+ }
179
+ }
180
+
181
+ return encrypted;
182
+ }
183
+
184
+ // ============================================================================
185
+ // Wallet CRUD
186
+ // ============================================================================
187
+
188
+ export async function getWallets(): Promise<CryptoWallet[]> {
189
+ ensureConfigDir();
190
+
191
+ if (!existsSync(WALLETS_FILE)) {
192
+ return [];
193
+ }
194
+
195
+ try {
196
+ const data = readFileSync(WALLETS_FILE, "utf-8");
197
+ const wallets: CryptoWallet[] = JSON.parse(data);
198
+ return Promise.all(wallets.map(decryptWallet));
199
+ } catch {
200
+ return [];
201
+ }
202
+ }
203
+
204
+ export async function getWallet(id: string): Promise<CryptoWallet | null> {
205
+ const wallets = await getWallets();
206
+ return wallets.find((w) => w.id === id) ?? null;
207
+ }
208
+
209
+ /**
210
+ * Get wallets without sensitive data (for listing)
211
+ */
212
+ export async function getWalletsSafe(): Promise<Omit<CryptoWallet, "privateKey" | "mnemonic">[]> {
213
+ const wallets = await getWallets();
214
+ return wallets.map(({ privateKey, mnemonic, ...safe }) => safe);
215
+ }
216
+
217
+ export async function addWallet(wallet: CryptoWallet): Promise<void> {
218
+ ensureConfigDir();
219
+
220
+ // Validate address
221
+ if (!isValidAddress(wallet.address, wallet.chain)) {
222
+ throw new Error(`Invalid ${wallet.chain} address`);
223
+ }
224
+
225
+ wallet.addedAt = new Date().toISOString();
226
+
227
+ // Load existing
228
+ let rawWallets: CryptoWallet[] = [];
229
+ if (existsSync(WALLETS_FILE)) {
230
+ try {
231
+ rawWallets = JSON.parse(readFileSync(WALLETS_FILE, "utf-8"));
232
+ } catch {
233
+ rawWallets = [];
234
+ }
235
+ }
236
+
237
+ // Encrypt and save
238
+ const encryptedWallet = await encryptWallet(wallet);
239
+
240
+ const existingIndex = rawWallets.findIndex((w) => w.id === wallet.id);
241
+ if (existingIndex >= 0) {
242
+ rawWallets[existingIndex] = encryptedWallet;
243
+ } else {
244
+ rawWallets.push(encryptedWallet);
245
+ }
246
+
247
+ writeFileSync(WALLETS_FILE, JSON.stringify(rawWallets, null, 2), { mode: 0o600 });
248
+ console.error(`[wallets] Saved wallet ${wallet.nickname} (${wallet.address.slice(0, 8)}...) - ${wallet.type}`);
249
+ }
250
+
251
+ export async function removeWallet(id: string): Promise<boolean> {
252
+ if (!existsSync(WALLETS_FILE)) {
253
+ return false;
254
+ }
255
+
256
+ let rawWallets: CryptoWallet[] = [];
257
+ try {
258
+ rawWallets = JSON.parse(readFileSync(WALLETS_FILE, "utf-8"));
259
+ } catch {
260
+ return false;
261
+ }
262
+
263
+ const filtered = rawWallets.filter((w) => w.id !== id);
264
+
265
+ if (filtered.length === rawWallets.length) {
266
+ return false;
267
+ }
268
+
269
+ writeFileSync(WALLETS_FILE, JSON.stringify(filtered, null, 2), { mode: 0o600 });
270
+ console.error(`[wallets] Removed wallet ${id}`);
271
+ return true;
272
+ }
273
+
274
+ // ============================================================================
275
+ // Crypto Transaction Log
276
+ // ============================================================================
277
+
278
+ export async function getCryptoTransactions(limit = 50): Promise<CryptoTransaction[]> {
279
+ ensureConfigDir();
280
+
281
+ if (!existsSync(CRYPTO_TX_FILE)) {
282
+ return [];
283
+ }
284
+
285
+ try {
286
+ const data = readFileSync(CRYPTO_TX_FILE, "utf-8");
287
+ const transactions: CryptoTransaction[] = JSON.parse(data);
288
+ return transactions.slice(-limit);
289
+ } catch {
290
+ return [];
291
+ }
292
+ }
293
+
294
+ export async function logCryptoTransaction(tx: CryptoTransaction): Promise<void> {
295
+ ensureConfigDir();
296
+
297
+ let transactions: CryptoTransaction[] = [];
298
+ if (existsSync(CRYPTO_TX_FILE)) {
299
+ try {
300
+ transactions = JSON.parse(readFileSync(CRYPTO_TX_FILE, "utf-8"));
301
+ } catch {
302
+ transactions = [];
303
+ }
304
+ }
305
+
306
+ transactions.push(tx);
307
+
308
+ // Keep last 1000 transactions
309
+ if (transactions.length > 1000) {
310
+ transactions = transactions.slice(-1000);
311
+ }
312
+
313
+ writeFileSync(CRYPTO_TX_FILE, JSON.stringify(transactions, null, 2), { mode: 0o600 });
314
+ }
315
+
316
+ export async function updateCryptoTransaction(id: string, updates: Partial<CryptoTransaction>): Promise<void> {
317
+ if (!existsSync(CRYPTO_TX_FILE)) return;
318
+
319
+ let transactions: CryptoTransaction[] = [];
320
+ try {
321
+ transactions = JSON.parse(readFileSync(CRYPTO_TX_FILE, "utf-8"));
322
+ } catch {
323
+ return;
324
+ }
325
+
326
+ const idx = transactions.findIndex((t) => t.id === id);
327
+ if (idx >= 0) {
328
+ transactions[idx] = { ...transactions[idx]!, ...updates };
329
+ writeFileSync(CRYPTO_TX_FILE, JSON.stringify(transactions, null, 2), { mode: 0o600 });
330
+ }
331
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Add a new payment card (encrypted storage)
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import { randomUUID } from "crypto";
7
+ import { addCard } from "../lib/cards";
8
+ import type { CardUsage } from "../lib/cards";
9
+ import { isUnlocked, isPinConfigured } from "../lib/pin-manager";
10
+ import { encryptCvv } from "../lib/cvv-crypto";
11
+
12
+ export const name = "add_card";
13
+
14
+ export const description = "Add a new payment card. Card number and expiration are encrypted. CVV is encrypted with your master PIN (requires unlock).";
15
+
16
+ export const parameters = z.object({
17
+ nickname: z.string().describe("Friendly name for the card (e.g., 'Visa perso', 'Amex pro')"),
18
+ card_number: z.string().describe("Full card number (will be encrypted)"),
19
+ expiration: z.string().describe("Expiration date in MM/YY format"),
20
+ cvv: z.string().describe("CVV/CVC (3-4 digits) - encrypted with your PIN, never shown"),
21
+ cardholder_name: z.string().describe("Name as shown on the card"),
22
+ allowed_usage: z.array(z.enum(["flight", "train", "hotel", "general", "all"])).optional()
23
+ .describe("What this card can be used for (default: all)"),
24
+ billing_street: z.string().optional().describe("Billing address street"),
25
+ billing_city: z.string().optional().describe("Billing address city"),
26
+ billing_postal_code: z.string().optional().describe("Billing postal code"),
27
+ billing_country: z.string().optional().describe("Billing country (2-letter code)"),
28
+ per_transaction_limit: z.number().optional().describe("Max amount per transaction"),
29
+ daily_limit: z.number().optional().describe("Max daily spending"),
30
+ monthly_limit: z.number().optional().describe("Max monthly spending"),
31
+ limit_currency: z.string().optional().describe("Currency for limits (default: EUR)"),
32
+ });
33
+
34
+ export async function execute(args: z.infer<typeof parameters>) {
35
+ try {
36
+ // Check PIN is configured and unlocked
37
+ if (!isPinConfigured()) {
38
+ return {
39
+ success: false,
40
+ error: "Master PIN not configured. Use setup_pin first.",
41
+ };
42
+ }
43
+
44
+ if (!isUnlocked()) {
45
+ return {
46
+ success: false,
47
+ error: "Cards are locked. Use unlock_cards with your PIN first.",
48
+ };
49
+ }
50
+
51
+ // Validate CVV format
52
+ if (!/^\d{3,4}$/.test(args.cvv)) {
53
+ return {
54
+ success: false,
55
+ error: "Invalid CVV format (must be 3-4 digits)",
56
+ };
57
+ }
58
+
59
+ // Encrypt CVV with PIN-derived key
60
+ const encryptedCvv = encryptCvv(args.cvv);
61
+ if (!encryptedCvv) {
62
+ return {
63
+ success: false,
64
+ error: "Failed to encrypt CVV. Make sure cards are unlocked.",
65
+ };
66
+ }
67
+
68
+ const cardId = randomUUID();
69
+
70
+ await addCard({
71
+ id: cardId,
72
+ nickname: args.nickname,
73
+ cardNumber: args.card_number,
74
+ expirationDate: args.expiration,
75
+ cvv: encryptedCvv,
76
+ cardholderName: args.cardholder_name,
77
+ cardType: "other", // Will be auto-detected
78
+ lastFourDigits: "", // Will be set from card number
79
+ allowedUsage: (args.allowed_usage as CardUsage[]) ?? ["all"],
80
+ enabled: true,
81
+ billingAddress: args.billing_street ? {
82
+ street: args.billing_street,
83
+ city: args.billing_city ?? "",
84
+ postalCode: args.billing_postal_code ?? "",
85
+ country: args.billing_country ?? "FR",
86
+ } : undefined,
87
+ limits: args.per_transaction_limit || args.daily_limit || args.monthly_limit ? {
88
+ perTransaction: args.per_transaction_limit,
89
+ daily: args.daily_limit,
90
+ monthly: args.monthly_limit,
91
+ currency: args.limit_currency ?? "EUR",
92
+ } : undefined,
93
+ addedAt: new Date().toISOString(),
94
+ });
95
+
96
+ return {
97
+ success: true,
98
+ message: `Card "${args.nickname}" added successfully`,
99
+ card_id: cardId,
100
+ security: "Card number and CVV encrypted. CVV protected by your master PIN.",
101
+ };
102
+ } catch (error) {
103
+ return {
104
+ success: false,
105
+ error: error instanceof Error ? error.message : "Failed to add card",
106
+ };
107
+ }
108
+ }