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,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
|
+
}
|