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,284 @@
1
+ /**
2
+ * Encryption for sensitive data with KMS support
3
+ *
4
+ * Supports multiple backends:
5
+ * 1. AWS KMS (recommended for production)
6
+ * 2. Local AES-256-GCM (for development)
7
+ *
8
+ * Configuration via environment:
9
+ * - MCP_CRYPTO_BACKEND: "aws-kms" | "local" (default: "local")
10
+ * - AWS_KMS_KEY_ID: ARN or alias of the KMS key
11
+ * - AWS_REGION: AWS region for KMS
12
+ * - MCP_MASTER_KEY: Local master key (if using local backend)
13
+ *
14
+ * Security model with KMS:
15
+ * - Even with root access to VPS, attacker needs AWS credentials
16
+ * - AWS credentials should use IAM role with minimal permissions
17
+ * - KMS provides audit logs of all decrypt operations
18
+ */
19
+
20
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
21
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
22
+ import { homedir } from "os";
23
+ import { join } from "path";
24
+ import { KMSClient, EncryptCommand, DecryptCommand, GenerateDataKeyCommand } from "@aws-sdk/client-kms";
25
+
26
+ // ============================================================================
27
+ // Configuration
28
+ // ============================================================================
29
+
30
+ const ALGORITHM = "aes-256-gcm";
31
+ const KEY_LENGTH = 32;
32
+ const IV_LENGTH = 16;
33
+ const AUTH_TAG_LENGTH = 16;
34
+
35
+ const CONFIG_DIR = join(homedir(), ".mcp-ecosystem");
36
+ const KEY_FILE = join(CONFIG_DIR, ".master-key");
37
+ const DEK_CACHE_FILE = join(CONFIG_DIR, ".dek-cache");
38
+
39
+ // Prefixes to identify encryption version/backend
40
+ const PREFIX_LOCAL = "enc:local:";
41
+ const PREFIX_KMS = "enc:kms:";
42
+
43
+ type CryptoBackend = "local" | "aws-kms";
44
+
45
+ function getBackend(): CryptoBackend {
46
+ const backend = process.env.MCP_CRYPTO_BACKEND?.toLowerCase();
47
+ if (backend === "aws-kms" || backend === "kms") return "aws-kms";
48
+ return "local";
49
+ }
50
+
51
+ // ============================================================================
52
+ // AWS KMS Backend
53
+ // ============================================================================
54
+
55
+ let kmsClient: KMSClient | null = null;
56
+ let cachedDataKey: Buffer | null = null;
57
+ let cachedEncryptedDataKey: Buffer | null = null;
58
+
59
+ function getKMSClient(): KMSClient {
60
+ if (!kmsClient) {
61
+ kmsClient = new KMSClient({
62
+ region: process.env.AWS_REGION || "us-east-1",
63
+ });
64
+ }
65
+ return kmsClient;
66
+ }
67
+
68
+ function getKMSKeyId(): string {
69
+ const keyId = process.env.AWS_KMS_KEY_ID;
70
+ if (!keyId) {
71
+ throw new Error("AWS_KMS_KEY_ID environment variable required for KMS backend");
72
+ }
73
+ return keyId;
74
+ }
75
+
76
+ /**
77
+ * Generate or retrieve Data Encryption Key (DEK) from KMS
78
+ * Uses envelope encryption pattern
79
+ */
80
+ async function getDataKey(): Promise<{ plaintext: Buffer; encrypted: Buffer }> {
81
+ // Check memory cache first
82
+ if (cachedDataKey && cachedEncryptedDataKey) {
83
+ return { plaintext: cachedDataKey, encrypted: cachedEncryptedDataKey };
84
+ }
85
+
86
+ // Check disk cache (encrypted DEK)
87
+ if (existsSync(DEK_CACHE_FILE)) {
88
+ try {
89
+ const encryptedDek = readFileSync(DEK_CACHE_FILE);
90
+ const client = getKMSClient();
91
+
92
+ const response = await client.send(new DecryptCommand({
93
+ CiphertextBlob: encryptedDek,
94
+ KeyId: getKMSKeyId(),
95
+ }));
96
+
97
+ if (response.Plaintext) {
98
+ cachedDataKey = Buffer.from(response.Plaintext);
99
+ cachedEncryptedDataKey = encryptedDek;
100
+ return { plaintext: cachedDataKey, encrypted: cachedEncryptedDataKey };
101
+ }
102
+ } catch (err) {
103
+ console.error("[crypto] Failed to decrypt cached DEK, generating new one");
104
+ }
105
+ }
106
+
107
+ // Generate new DEK
108
+ const client = getKMSClient();
109
+ const response = await client.send(new GenerateDataKeyCommand({
110
+ KeyId: getKMSKeyId(),
111
+ KeySpec: "AES_256",
112
+ }));
113
+
114
+ if (!response.Plaintext || !response.CiphertextBlob) {
115
+ throw new Error("KMS GenerateDataKey failed");
116
+ }
117
+
118
+ cachedDataKey = Buffer.from(response.Plaintext);
119
+ cachedEncryptedDataKey = Buffer.from(response.CiphertextBlob);
120
+
121
+ // Cache encrypted DEK to disk (safe - it's encrypted by KMS)
122
+ ensureConfigDir();
123
+ writeFileSync(DEK_CACHE_FILE, cachedEncryptedDataKey, { mode: 0o600 });
124
+
125
+ console.error("[crypto] Generated new Data Encryption Key via KMS");
126
+ return { plaintext: cachedDataKey, encrypted: cachedEncryptedDataKey };
127
+ }
128
+
129
+ async function encryptWithKMS(plaintext: string): Promise<string> {
130
+ const { plaintext: dek } = await getDataKey();
131
+
132
+ const iv = randomBytes(IV_LENGTH);
133
+ const cipher = createCipheriv(ALGORITHM, dek, iv);
134
+
135
+ let encrypted = cipher.update(plaintext, "utf8", "base64");
136
+ encrypted += cipher.final("base64");
137
+ const authTag = cipher.getAuthTag();
138
+
139
+ // Format: enc:kms:<iv>:<authTag>:<ciphertext>
140
+ return `${PREFIX_KMS}${iv.toString("base64")}:${authTag.toString("base64")}:${encrypted}`;
141
+ }
142
+
143
+ async function decryptWithKMS(encrypted: string): Promise<string> {
144
+ const { plaintext: dek } = await getDataKey();
145
+
146
+ const parts = encrypted.slice(PREFIX_KMS.length).split(":");
147
+ if (parts.length !== 3) {
148
+ throw new Error("Invalid KMS encrypted format");
149
+ }
150
+
151
+ const iv = Buffer.from(parts[0]!, "base64");
152
+ const authTag = Buffer.from(parts[1]!, "base64");
153
+ const ciphertext = parts[2]!;
154
+
155
+ const decipher = createDecipheriv(ALGORITHM, dek, iv);
156
+ decipher.setAuthTag(authTag);
157
+
158
+ let decrypted: string = decipher.update(ciphertext, "base64", "utf8");
159
+ decrypted += decipher.final("utf8");
160
+
161
+ return decrypted;
162
+ }
163
+
164
+ // ============================================================================
165
+ // Local Backend (for development)
166
+ // ============================================================================
167
+
168
+ function ensureConfigDir(): void {
169
+ if (!existsSync(CONFIG_DIR)) {
170
+ mkdirSync(CONFIG_DIR, { recursive: true });
171
+ }
172
+ }
173
+
174
+ function getLocalMasterKey(): Buffer {
175
+ if (process.env.MCP_MASTER_KEY) {
176
+ return scryptSync(process.env.MCP_MASTER_KEY, "mcp-ecosystem-salt-v1", KEY_LENGTH);
177
+ }
178
+
179
+ if (existsSync(KEY_FILE)) {
180
+ const keyData = readFileSync(KEY_FILE, "utf-8").trim();
181
+ return scryptSync(keyData, "mcp-ecosystem-salt-v1", KEY_LENGTH);
182
+ }
183
+
184
+ console.error("[crypto] Generating new local master key...");
185
+ const newKey = randomBytes(32).toString("base64");
186
+
187
+ ensureConfigDir();
188
+ writeFileSync(KEY_FILE, newKey, { mode: 0o600 });
189
+ chmodSync(KEY_FILE, 0o600);
190
+
191
+ console.error(`[crypto] Master key saved to ${KEY_FILE}`);
192
+ return scryptSync(newKey, "mcp-ecosystem-salt-v1", KEY_LENGTH);
193
+ }
194
+
195
+ function encryptLocal(plaintext: string): string {
196
+ const key = getLocalMasterKey();
197
+ const iv = randomBytes(IV_LENGTH);
198
+ const cipher = createCipheriv(ALGORITHM, key, iv);
199
+
200
+ let encrypted = cipher.update(plaintext, "utf8", "base64");
201
+ encrypted += cipher.final("base64");
202
+ const authTag = cipher.getAuthTag();
203
+
204
+ return `${PREFIX_LOCAL}${iv.toString("base64")}:${authTag.toString("base64")}:${encrypted}`;
205
+ }
206
+
207
+ function decryptLocal(encrypted: string): string {
208
+ const key = getLocalMasterKey();
209
+ const parts = encrypted.slice(PREFIX_LOCAL.length).split(":");
210
+
211
+ if (parts.length !== 3) {
212
+ throw new Error("Invalid local encrypted format");
213
+ }
214
+
215
+ const iv = Buffer.from(parts[0]!, "base64");
216
+ const authTag = Buffer.from(parts[1]!, "base64");
217
+ const ciphertext = parts[2]!;
218
+
219
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
220
+ decipher.setAuthTag(authTag);
221
+
222
+ let decrypted: string = decipher.update(ciphertext, "base64", "utf8");
223
+ decrypted += decipher.final("utf8");
224
+
225
+ return decrypted;
226
+ }
227
+
228
+ // ============================================================================
229
+ // Public API
230
+ // ============================================================================
231
+
232
+ /**
233
+ * Check if a value is encrypted
234
+ */
235
+ export function isEncrypted(value: string): boolean {
236
+ return value?.startsWith(PREFIX_LOCAL) || value?.startsWith(PREFIX_KMS) || false;
237
+ }
238
+
239
+ /**
240
+ * Encrypt sensitive data using configured backend
241
+ */
242
+ export async function encrypt(plaintext: string): Promise<string> {
243
+ if (!plaintext || isEncrypted(plaintext)) {
244
+ return plaintext;
245
+ }
246
+
247
+ const backend = getBackend();
248
+
249
+ if (backend === "aws-kms") {
250
+ return encryptWithKMS(plaintext);
251
+ }
252
+
253
+ return encryptLocal(plaintext);
254
+ }
255
+
256
+ /**
257
+ * Decrypt sensitive data (auto-detects backend from prefix)
258
+ */
259
+ export async function decrypt(encrypted: string): Promise<string> {
260
+ if (!encrypted || !isEncrypted(encrypted)) {
261
+ return encrypted;
262
+ }
263
+
264
+ if (encrypted.startsWith(PREFIX_KMS)) {
265
+ return decryptWithKMS(encrypted);
266
+ }
267
+
268
+ if (encrypted.startsWith(PREFIX_LOCAL)) {
269
+ return decryptLocal(encrypted);
270
+ }
271
+
272
+ throw new Error("Unknown encryption format");
273
+ }
274
+
275
+ /**
276
+ * Get current crypto backend info
277
+ */
278
+ export function getCryptoInfo(): { backend: CryptoBackend; kmsKeyId?: string } {
279
+ const backend = getBackend();
280
+ return {
281
+ backend,
282
+ kmsKeyId: backend === "aws-kms" ? process.env.AWS_KMS_KEY_ID : undefined,
283
+ };
284
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * CVV-specific encryption using PIN-derived key
3
+ *
4
+ * CVV is encrypted with a key derived from the master PIN.
5
+ * This means:
6
+ * - CVV is stored encrypted
7
+ * - Without the PIN, CVV cannot be decrypted
8
+ * - Even with system access, attacker needs the PIN
9
+ */
10
+
11
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
12
+ import { getCvvEncryptionKey } from "./pin-manager";
13
+
14
+ const ALGORITHM = "aes-256-gcm";
15
+ const IV_LENGTH = 16;
16
+
17
+ const CVV_PREFIX = "cvv:";
18
+
19
+ /**
20
+ * Check if value is CVV-encrypted
21
+ */
22
+ export function isCvvEncrypted(value: string): boolean {
23
+ return value?.startsWith(CVV_PREFIX) || false;
24
+ }
25
+
26
+ /**
27
+ * Encrypt CVV with PIN-derived key
28
+ * Requires cards to be unlocked
29
+ */
30
+ export function encryptCvv(cvv: string): string | null {
31
+ const key = getCvvEncryptionKey();
32
+ if (!key) {
33
+ return null; // Not unlocked
34
+ }
35
+
36
+ const iv = randomBytes(IV_LENGTH);
37
+ const cipher = createCipheriv(ALGORITHM, key, iv);
38
+
39
+ let encrypted = cipher.update(cvv, "utf8", "base64");
40
+ encrypted += cipher.final("base64");
41
+ const authTag = cipher.getAuthTag();
42
+
43
+ // Format: cvv:<iv>:<authTag>:<ciphertext>
44
+ return `${CVV_PREFIX}${iv.toString("base64")}:${authTag.toString("base64")}:${encrypted}`;
45
+ }
46
+
47
+ /**
48
+ * Decrypt CVV with PIN-derived key
49
+ * Requires cards to be unlocked
50
+ */
51
+ export function decryptCvv(encrypted: string): string | null {
52
+ if (!isCvvEncrypted(encrypted)) {
53
+ return encrypted; // Not encrypted, return as-is
54
+ }
55
+
56
+ const key = getCvvEncryptionKey();
57
+ if (!key) {
58
+ return null; // Not unlocked
59
+ }
60
+
61
+ const parts = encrypted.slice(CVV_PREFIX.length).split(":");
62
+ if (parts.length !== 3) {
63
+ return null; // Invalid format
64
+ }
65
+
66
+ try {
67
+ const iv = Buffer.from(parts[0]!, "base64");
68
+ const authTag = Buffer.from(parts[1]!, "base64");
69
+ const ciphertext = parts[2]!;
70
+
71
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
72
+ decipher.setAuthTag(authTag);
73
+
74
+ let decrypted = decipher.update(ciphertext, "base64", "utf8");
75
+ decrypted += decipher.final("utf8");
76
+
77
+ return decrypted;
78
+ } catch {
79
+ return null; // Decryption failed (wrong PIN or corrupted)
80
+ }
81
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * MCP Core - Shared utilities for all MCP servers
3
+ */
4
+
5
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import {
8
+ CallToolRequestSchema,
9
+ ListToolsRequestSchema,
10
+ type Tool,
11
+ } from "@modelcontextprotocol/sdk/types.js";
12
+
13
+ export interface MCPServerConfig {
14
+ name: string;
15
+ version: string;
16
+ description?: string;
17
+ }
18
+
19
+ export interface ToolDefinition {
20
+ name: string;
21
+ description: string;
22
+ inputSchema: Record<string, unknown>;
23
+ handler: (args: Record<string, unknown>) => Promise<ToolResult>;
24
+ }
25
+
26
+ export interface ToolResult {
27
+ content: Array<{ type: "text"; text: string }>;
28
+ isError?: boolean;
29
+ [key: string]: unknown;
30
+ }
31
+
32
+ /**
33
+ * Create and configure an MCP server with tools
34
+ */
35
+ export function createMCPServer(
36
+ config: MCPServerConfig,
37
+ tools: ToolDefinition[]
38
+ ): Server {
39
+ const server = new Server(
40
+ {
41
+ name: config.name,
42
+ version: config.version,
43
+ },
44
+ {
45
+ capabilities: {
46
+ tools: {},
47
+ },
48
+ }
49
+ );
50
+
51
+ // Register tool list handler
52
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
53
+ tools: tools.map((t) => ({
54
+ name: t.name,
55
+ description: t.description,
56
+ inputSchema: t.inputSchema,
57
+ })),
58
+ }));
59
+
60
+ // Register tool call handler
61
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
62
+ const toolName = request.params.name;
63
+ const tool = tools.find((t) => t.name === toolName);
64
+
65
+ if (!tool) {
66
+ return {
67
+ content: [{ type: "text" as const, text: `Unknown tool: ${toolName}` }],
68
+ isError: true,
69
+ };
70
+ }
71
+
72
+ try {
73
+ const args = request.params.arguments ?? {};
74
+ return await tool.handler(args as Record<string, unknown>);
75
+ } catch (error) {
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ return {
78
+ content: [{ type: "text" as const, text: `Error: ${message}` }],
79
+ isError: true,
80
+ };
81
+ }
82
+ });
83
+
84
+ return server;
85
+ }
86
+
87
+ /**
88
+ * Start MCP server with stdio transport
89
+ */
90
+ export async function startMCPServer(server: Server): Promise<void> {
91
+ const transport = new StdioServerTransport();
92
+ await server.connect(transport);
93
+ console.error(`[MCP] Server started`);
94
+ }
95
+
96
+ /**
97
+ * Helper to create a successful tool result
98
+ */
99
+ export function success(text: string): ToolResult {
100
+ return {
101
+ content: [{ type: "text", text }],
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Helper to create a JSON tool result
107
+ */
108
+ export function json(data: unknown): ToolResult {
109
+ return {
110
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Helper to create an error tool result
116
+ */
117
+ export function error(message: string): ToolResult {
118
+ return {
119
+ content: [{ type: "text", text: message }],
120
+ isError: true,
121
+ };
122
+ }
123
+
124
+ export { Server, StdioServerTransport };
125
+
126
+ // CLI execution utilities
127
+ export { execCommand, execGit, execHeroku, parseJsonOutput, type ExecResult, type ExecOptions } from "./cli-runner";