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