0nmcp 1.4.0 → 1.5.0

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/connections.js CHANGED
@@ -244,8 +244,12 @@ export class ConnectionManager {
244
244
 
245
245
  /**
246
246
  * Get credentials for a service.
247
+ * Checks vault unsealed cache first for sealed connections.
247
248
  */
248
249
  getCredentials(serviceKey) {
250
+ // Check if vault has unsealed credentials in memory
251
+ const unsealed = this._vaultCache?.get(serviceKey);
252
+ if (unsealed) return unsealed;
249
253
  return this.connections[serviceKey]?.credentials || null;
250
254
  }
251
255
 
package/index.js CHANGED
@@ -27,15 +27,18 @@ import { Orchestrator } from "./orchestrator.js";
27
27
  import { WorkflowRunner } from "./workflow.js";
28
28
  import { registerAllTools } from "./tools.js";
29
29
  import { registerCrmTools } from "./crm/index.js";
30
+ import { registerVaultTools, autoUnseal } from "./vault/index.js";
31
+ import { unsealedCache } from "./vault/cache.js";
30
32
 
31
33
  // ── Initialize ─────────────────────────────────────────────
32
34
  const connections = new ConnectionManager();
35
+ connections._vaultCache = unsealedCache;
33
36
  const orchestrator = new Orchestrator(connections);
34
37
  const workflowRunner = new WorkflowRunner(connections);
35
38
 
36
39
  const server = new McpServer({
37
40
  name: "0nMCP",
38
- version: "1.4.0",
41
+ version: "1.5.0",
39
42
  });
40
43
 
41
44
  // ============================================================
@@ -51,6 +54,18 @@ registerAllTools(server, connections, orchestrator, workflowRunner);
51
54
  import { z } from "zod";
52
55
  registerCrmTools(server, z);
53
56
 
57
+ // ============================================================
58
+ // VAULT TOOLS (machine-bound credential encryption)
59
+ // ============================================================
60
+
61
+ registerVaultTools(server, z);
62
+
63
+ // Auto-unseal sealed connections if ON_VAULT_PASSPHRASE is set
64
+ const vaultResult = autoUnseal();
65
+ if (vaultResult.unsealed.length > 0) {
66
+ console.error(`Vault: auto-unsealed ${vaultResult.unsealed.length} connection(s)`);
67
+ }
68
+
54
69
  // ============================================================
55
70
  // START SERVER (stdio transport)
56
71
  // ============================================================
package/lib/badges.json CHANGED
@@ -26,7 +26,7 @@
26
26
  "total": {
27
27
  "schemaVersion": 1,
28
28
  "label": "capabilities",
29
- "message": "693",
29
+ "message": "697",
30
30
  "color": "00ff88"
31
31
  }
32
32
  }
package/lib/stats.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "generated": "2026-02-15T00:13:31.784Z",
2
+ "generated": "2026-02-15T00:30:00.808Z",
3
3
  "catalogVersion": "1.2.2",
4
4
  "services": 26,
5
5
  "tools": 290,
@@ -792,6 +792,7 @@
792
792
  "mongodb"
793
793
  ],
794
794
  "crmTools": 245,
795
- "totalTools": 535,
796
- "totalCapabilities": 693
795
+ "vaultTools": 4,
796
+ "totalTools": 539,
797
+ "totalCapabilities": 697
797
798
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "0nmcp",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "mcpName": "io.github.0nork/0nMCP",
5
- "description": "Universal AI API Orchestrator — 535 tools, 26 services, natural language interface. The most comprehensive MCP server available. Free and open source from 0nORK.",
5
+ "description": "Universal AI API Orchestrator — 539 tools, 26 services, machine-bound vault encryption. The most comprehensive MCP server available. Free and open source from 0nORK.",
6
6
  "type": "module",
7
7
  "main": "index.js",
8
8
  "types": "types/index.d.ts",
@@ -43,6 +43,9 @@
43
43
  },
44
44
  "./server": {
45
45
  "import": "./server.js"
46
+ },
47
+ "./vault": {
48
+ "import": "./vault/index.js"
46
49
  }
47
50
  },
48
51
  "scripts": {
@@ -107,6 +110,10 @@
107
110
  "teams",
108
111
  "onedrive",
109
112
  "mongodb",
113
+ "vault",
114
+ "encryption",
115
+ "machine-bound",
116
+ "credential-storage",
110
117
  "0n",
111
118
  "0nork",
112
119
  "0nmcp"
@@ -156,6 +163,7 @@
156
163
  "crm/",
157
164
  "webhooks.js",
158
165
  "ratelimit.js",
166
+ "vault/",
159
167
  "lib/",
160
168
  "types/",
161
169
  "README.md",
@@ -164,12 +172,13 @@
164
172
  "0nmcp-stats": {
165
173
  "tools": 290,
166
174
  "crmTools": 245,
167
- "totalTools": 535,
175
+ "vaultTools": 4,
176
+ "totalTools": 539,
168
177
  "services": 26,
169
178
  "actions": 65,
170
179
  "triggers": 93,
171
- "totalCapabilities": 693,
180
+ "totalCapabilities": 697,
172
181
  "categories": 13,
173
- "lastUpdated": "2026-02-15T00:13:31.784Z"
182
+ "lastUpdated": "2026-02-15T00:30:00.808Z"
174
183
  }
175
184
  }
package/server.js CHANGED
@@ -20,6 +20,8 @@ import { Orchestrator } from "./orchestrator.js";
20
20
  import { WorkflowRunner } from "./workflow.js";
21
21
  import { registerAllTools } from "./tools.js";
22
22
  import { registerCrmTools } from "./crm/index.js";
23
+ import { registerVaultTools, autoUnseal } from "./vault/index.js";
24
+ import { unsealedCache } from "./vault/cache.js";
23
25
  import { z } from "zod";
24
26
  import {
25
27
  verifyStripeSignature,
@@ -45,11 +47,18 @@ export async function createApp() {
45
47
  }
46
48
 
47
49
  const connections = new ConnectionManager();
50
+ connections._vaultCache = unsealedCache;
48
51
  const orchestrator = new Orchestrator(connections);
49
52
  const workflowRunner = new WorkflowRunner(connections);
50
53
 
51
54
  const app = express();
52
55
 
56
+ // Auto-unseal vault if passphrase is set
57
+ const vaultResult = autoUnseal();
58
+ if (vaultResult.unsealed.length > 0) {
59
+ console.log(`Vault: auto-unsealed ${vaultResult.unsealed.length} connection(s)`);
60
+ }
61
+
53
62
  // ── Raw body capture for webhooks (before json parsing) ──
54
63
  app.use("/webhooks", express.raw({ type: "*/*" }));
55
64
  app.use(express.json());
@@ -82,9 +91,10 @@ export async function createApp() {
82
91
 
83
92
  // New session
84
93
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() });
85
- const server = new McpServer({ name: "0nMCP", version: "1.4.0" });
94
+ const server = new McpServer({ name: "0nMCP", version: "1.5.0" });
86
95
  registerAllTools(server, connections, orchestrator, workflowRunner);
87
96
  registerCrmTools(server, z);
97
+ registerVaultTools(server, z);
88
98
 
89
99
  await server.connect(transport);
90
100
 
package/vault/cache.js ADDED
@@ -0,0 +1,28 @@
1
+ // ============================================================
2
+ // 0nMCP — Vault: Shared Credential Cache
3
+ // ============================================================
4
+ // In-memory store for unsealed credentials. Shared between
5
+ // vault/index.js (writes) and connections.js (reads).
6
+ // Never written to disk — credentials exist only in memory.
7
+ // ============================================================
8
+
9
+ /** @type {Map<string, object>} */
10
+ export const unsealedCache = new Map();
11
+
12
+ /**
13
+ * Get unsealed credentials from memory cache.
14
+ * @param {string} service - Service key
15
+ * @returns {object|null}
16
+ */
17
+ export function getUnsealedCredentials(service) {
18
+ return unsealedCache.get(service) || null;
19
+ }
20
+
21
+ /**
22
+ * Check if a service has unsealed credentials.
23
+ * @param {string} service - Service key
24
+ * @returns {boolean}
25
+ */
26
+ export function isUnsealed(service) {
27
+ return unsealedCache.has(service);
28
+ }
@@ -0,0 +1,147 @@
1
+ // ============================================================
2
+ // 0nMCP — Vault: Cipher Engine
3
+ // ============================================================
4
+ // AES-256-GCM encryption with PBKDF2 key derivation.
5
+ // Keys are derived from passphrase + machine fingerprint,
6
+ // making sealed files machine-bound — they can ONLY be
7
+ // unsealed on the same hardware they were sealed on.
8
+ //
9
+ // Zero external dependencies — Node.js built-in `crypto` only.
10
+ //
11
+ // Patent Pending: US Provisional Patent Application #63/968,814
12
+ // ============================================================
13
+
14
+ import { randomBytes, createCipheriv, createDecipheriv, pbkdf2Sync } from "crypto";
15
+ import { getFingerprint } from "./fingerprint.js";
16
+
17
+ const ALGORITHM = "aes-256-gcm";
18
+ const KEY_LENGTH = 32; // 256 bits
19
+ const IV_LENGTH = 16; // 128 bits
20
+ const SALT_LENGTH = 32; // 256 bits
21
+ const TAG_LENGTH = 16; // 128 bits (GCM auth tag)
22
+ const PBKDF2_ITERATIONS = 100000;
23
+ const PBKDF2_DIGEST = "sha512";
24
+
25
+ /**
26
+ * Derive an encryption key from passphrase + machine fingerprint.
27
+ *
28
+ * The fingerprint is mixed into the key derivation so that
29
+ * the same passphrase on a different machine produces a
30
+ * completely different key — making sealed files machine-bound.
31
+ *
32
+ * @param {string} passphrase - User-provided passphrase
33
+ * @param {Buffer} salt - Random salt for PBKDF2
34
+ * @returns {Buffer} 32-byte AES-256 key
35
+ */
36
+ function deriveKey(passphrase, salt) {
37
+ const fingerprint = getFingerprint();
38
+ const material = `${passphrase}:${fingerprint}`;
39
+ return pbkdf2Sync(material, salt, PBKDF2_ITERATIONS, KEY_LENGTH, PBKDF2_DIGEST);
40
+ }
41
+
42
+ /**
43
+ * Encrypt plaintext data using AES-256-GCM.
44
+ *
45
+ * Output format (binary concat):
46
+ * [salt:32][iv:16][authTag:16][ciphertext:*]
47
+ *
48
+ * @param {string} plaintext - Data to encrypt (JSON string)
49
+ * @param {string} passphrase - Encryption passphrase
50
+ * @returns {{ sealed: string, fingerprint: string }}
51
+ */
52
+ export function seal(plaintext, passphrase) {
53
+ const salt = randomBytes(SALT_LENGTH);
54
+ const iv = randomBytes(IV_LENGTH);
55
+ const key = deriveKey(passphrase, salt);
56
+
57
+ const cipher = createCipheriv(ALGORITHM, key, iv);
58
+ const encrypted = Buffer.concat([
59
+ cipher.update(plaintext, "utf8"),
60
+ cipher.final(),
61
+ ]);
62
+ const authTag = cipher.getAuthTag();
63
+
64
+ // Combine: salt + iv + authTag + ciphertext → base64
65
+ const combined = Buffer.concat([salt, iv, authTag, encrypted]);
66
+ const sealed = combined.toString("base64");
67
+
68
+ return {
69
+ sealed,
70
+ fingerprint: getFingerprint(),
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Decrypt sealed data using AES-256-GCM.
76
+ *
77
+ * @param {string} sealedData - Base64-encoded sealed data
78
+ * @param {string} passphrase - Decryption passphrase
79
+ * @returns {string} Decrypted plaintext
80
+ * @throws {Error} If passphrase is wrong or file is not machine-bound to this hardware
81
+ */
82
+ export function unseal(sealedData, passphrase) {
83
+ const combined = Buffer.from(sealedData, "base64");
84
+
85
+ if (combined.length < SALT_LENGTH + IV_LENGTH + TAG_LENGTH + 1) {
86
+ throw new Error("Invalid sealed data: too short");
87
+ }
88
+
89
+ // Extract components
90
+ const salt = combined.subarray(0, SALT_LENGTH);
91
+ const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
92
+ const authTag = combined.subarray(SALT_LENGTH + IV_LENGTH, SALT_LENGTH + IV_LENGTH + TAG_LENGTH);
93
+ const ciphertext = combined.subarray(SALT_LENGTH + IV_LENGTH + TAG_LENGTH);
94
+
95
+ const key = deriveKey(passphrase, salt);
96
+
97
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
98
+ decipher.setAuthTag(authTag);
99
+
100
+ try {
101
+ const decrypted = Buffer.concat([
102
+ decipher.update(ciphertext),
103
+ decipher.final(),
104
+ ]);
105
+ return decrypted.toString("utf8");
106
+ } catch {
107
+ throw new Error(
108
+ "Unseal failed — wrong passphrase or file was sealed on a different machine. " +
109
+ "Vault files are machine-bound and can only be unsealed on the same hardware."
110
+ );
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Verify that sealed data can be unsealed on this machine.
116
+ * Also checks stored fingerprint against current hardware.
117
+ *
118
+ * @param {string} sealedData - Base64-encoded sealed data
119
+ * @param {string} storedFingerprint - Fingerprint stored when file was sealed
120
+ * @param {string} passphrase - Passphrase to test
121
+ * @returns {{ valid: boolean, machineBound: boolean, error?: string }}
122
+ */
123
+ export function verify(sealedData, storedFingerprint, passphrase) {
124
+ const currentFingerprint = getFingerprint();
125
+ const machineBound = currentFingerprint === storedFingerprint;
126
+
127
+ if (!machineBound) {
128
+ return {
129
+ valid: false,
130
+ machineBound: false,
131
+ error: "Machine fingerprint mismatch — this file was sealed on a different machine.",
132
+ currentFingerprint,
133
+ storedFingerprint,
134
+ };
135
+ }
136
+
137
+ try {
138
+ unseal(sealedData, passphrase);
139
+ return { valid: true, machineBound: true };
140
+ } catch (err) {
141
+ return {
142
+ valid: false,
143
+ machineBound: true,
144
+ error: err.message,
145
+ };
146
+ }
147
+ }
@@ -0,0 +1,58 @@
1
+ // ============================================================
2
+ // 0nMCP — Vault: Machine Fingerprint
3
+ // ============================================================
4
+ // Generates a deterministic, machine-bound hardware fingerprint
5
+ // using SHA-256 hash of system identifiers. Zero dependencies —
6
+ // uses only Node.js built-in modules.
7
+ //
8
+ // Patent Pending: US Provisional Patent Application #63/968,814
9
+ // ============================================================
10
+
11
+ import { createHash } from "crypto";
12
+ import { hostname, cpus, platform, arch, totalmem } from "os";
13
+
14
+ /**
15
+ * Generate a deterministic machine fingerprint.
16
+ *
17
+ * Components:
18
+ * - hostname
19
+ * - CPU model (first core)
20
+ * - CPU core count
21
+ * - OS platform
22
+ * - CPU architecture
23
+ * - Total system memory
24
+ *
25
+ * @returns {{ fingerprint: string, components: object }}
26
+ */
27
+ export function generateFingerprint() {
28
+ const cpuInfo = cpus();
29
+ const components = {
30
+ hostname: hostname(),
31
+ cpuModel: cpuInfo.length > 0 ? cpuInfo[0].model : "unknown",
32
+ cpuCores: cpuInfo.length,
33
+ platform: platform(),
34
+ arch: arch(),
35
+ totalMemory: totalmem(),
36
+ };
37
+
38
+ const raw = [
39
+ components.hostname,
40
+ components.cpuModel,
41
+ components.cpuCores,
42
+ components.platform,
43
+ components.arch,
44
+ components.totalMemory,
45
+ ].join("|");
46
+
47
+ const fingerprint = createHash("sha256").update(raw).digest("hex");
48
+
49
+ return { fingerprint, components };
50
+ }
51
+
52
+ /**
53
+ * Get just the fingerprint string.
54
+ * @returns {string} 64-char hex SHA-256 hash
55
+ */
56
+ export function getFingerprint() {
57
+ return generateFingerprint().fingerprint;
58
+ }
package/vault/index.js ADDED
@@ -0,0 +1,314 @@
1
+ // ============================================================
2
+ // 0nMCP — Vault Module
3
+ // ============================================================
4
+ // Machine-bound encrypted credential storage for .0n files.
5
+ // Seals credentials using AES-256-GCM with PBKDF2 key derivation
6
+ // from passphrase + hardware fingerprint.
7
+ //
8
+ // Zero external dependencies — Node.js built-in `crypto` only.
9
+ // Backward compatible — plaintext .0n files keep working.
10
+ //
11
+ // 4 MCP Tools:
12
+ // vault_seal — Encrypt a service's credentials on disk
13
+ // vault_unseal — Decrypt credentials into memory only
14
+ // vault_verify — Check machine binding + file integrity
15
+ // vault_fingerprint — Show your machine's hardware fingerprint
16
+ //
17
+ // Patent Pending: US Provisional Patent Application #63/968,814
18
+ // ============================================================
19
+
20
+ import { readFileSync, writeFileSync, existsSync, readdirSync } from "fs";
21
+ import { join } from "path";
22
+ import { seal, unseal, verify } from "./cipher.js";
23
+ import { generateFingerprint, getFingerprint } from "./fingerprint.js";
24
+ import { CONNECTIONS_PATH } from "../connections.js";
25
+ import { unsealedCache, getUnsealedCredentials, isUnsealed } from "./cache.js";
26
+
27
+ // Auto-unseal passphrase from environment
28
+ const AUTO_PASSPHRASE = process.env.ON_VAULT_PASSPHRASE || null;
29
+
30
+ /**
31
+ * Seal a service's .0n connection file — encrypts credentials on disk.
32
+ *
33
+ * @param {string} service - Service key (e.g., "stripe", "openai")
34
+ * @param {string} passphrase - Encryption passphrase
35
+ * @returns {{ success: boolean, service?: string, error?: string }}
36
+ */
37
+ export function sealConnection(service, passphrase) {
38
+ const filePath = join(CONNECTIONS_PATH, `${service}.0n`);
39
+
40
+ if (!existsSync(filePath)) {
41
+ return { success: false, error: `No connection file found for service: ${service}` };
42
+ }
43
+
44
+ try {
45
+ const raw = readFileSync(filePath, "utf-8");
46
+ const data = JSON.parse(raw);
47
+
48
+ // Already sealed?
49
+ if (data.$0n?.sealed) {
50
+ return { success: false, error: `Service "${service}" is already sealed. Unseal first to re-seal.` };
51
+ }
52
+
53
+ if (!data.auth?.credentials) {
54
+ return { success: false, error: `No credentials found in ${service}.0n to seal.` };
55
+ }
56
+
57
+ // Seal only the credentials
58
+ const credentialsJson = JSON.stringify(data.auth.credentials);
59
+ const { sealed, fingerprint } = seal(credentialsJson, passphrase);
60
+
61
+ // Replace credentials with sealed envelope
62
+ data.auth.credentials = {};
63
+ data.$0n.sealed = true;
64
+ data.$0n.vault = {
65
+ sealed_at: new Date().toISOString(),
66
+ fingerprint,
67
+ algorithm: "aes-256-gcm",
68
+ kdf: "pbkdf2-sha512-100k",
69
+ };
70
+ data.vault = { data: sealed };
71
+
72
+ writeFileSync(filePath, JSON.stringify(data, null, 2));
73
+
74
+ return {
75
+ success: true,
76
+ service,
77
+ fingerprint,
78
+ message: `Credentials for "${service}" have been sealed. They can only be unsealed on this machine.`,
79
+ };
80
+ } catch (err) {
81
+ return { success: false, error: `Failed to seal ${service}: ${err.message}` };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Unseal a service's credentials — decrypts into memory only.
87
+ * The .0n file on disk remains encrypted.
88
+ *
89
+ * @param {string} service - Service key
90
+ * @param {string} passphrase - Decryption passphrase
91
+ * @returns {{ success: boolean, service?: string, error?: string }}
92
+ */
93
+ export function unsealConnection(service, passphrase) {
94
+ const filePath = join(CONNECTIONS_PATH, `${service}.0n`);
95
+
96
+ if (!existsSync(filePath)) {
97
+ return { success: false, error: `No connection file found for service: ${service}` };
98
+ }
99
+
100
+ try {
101
+ const raw = readFileSync(filePath, "utf-8");
102
+ const data = JSON.parse(raw);
103
+
104
+ if (!data.$0n?.sealed || !data.vault?.data) {
105
+ return { success: false, error: `Service "${service}" is not sealed. Nothing to unseal.` };
106
+ }
107
+
108
+ const decrypted = unseal(data.vault.data, passphrase);
109
+ const credentials = JSON.parse(decrypted);
110
+
111
+ // Store in memory only — never write back to disk
112
+ unsealedCache.set(service, credentials);
113
+
114
+ return {
115
+ success: true,
116
+ service,
117
+ message: `Credentials for "${service}" unsealed into memory. File on disk remains encrypted.`,
118
+ credential_keys: Object.keys(credentials),
119
+ };
120
+ } catch (err) {
121
+ return { success: false, error: err.message };
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Verify a sealed connection's integrity and machine binding.
127
+ *
128
+ * @param {string} service - Service key
129
+ * @param {string} passphrase - Passphrase to verify
130
+ * @returns {{ success: boolean, valid?: boolean, machineBound?: boolean, error?: string }}
131
+ */
132
+ export function verifyConnection(service, passphrase) {
133
+ const filePath = join(CONNECTIONS_PATH, `${service}.0n`);
134
+
135
+ if (!existsSync(filePath)) {
136
+ return { success: false, error: `No connection file found for service: ${service}` };
137
+ }
138
+
139
+ try {
140
+ const raw = readFileSync(filePath, "utf-8");
141
+ const data = JSON.parse(raw);
142
+
143
+ if (!data.$0n?.sealed || !data.vault?.data) {
144
+ return {
145
+ success: true,
146
+ sealed: false,
147
+ message: `Service "${service}" is not sealed — credentials are stored in plaintext.`,
148
+ };
149
+ }
150
+
151
+ const storedFingerprint = data.$0n.vault?.fingerprint;
152
+ const result = verify(data.vault.data, storedFingerprint, passphrase);
153
+
154
+ return {
155
+ success: true,
156
+ sealed: true,
157
+ valid: result.valid,
158
+ machineBound: result.machineBound,
159
+ algorithm: data.$0n.vault?.algorithm,
160
+ sealed_at: data.$0n.vault?.sealed_at,
161
+ error: result.error,
162
+ };
163
+ } catch (err) {
164
+ return { success: false, error: err.message };
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Auto-unseal all sealed connections using ON_VAULT_PASSPHRASE env var.
170
+ * Called during MCP server startup when env var is set.
171
+ *
172
+ * @returns {{ unsealed: string[], failed: string[], skipped: string[] }}
173
+ */
174
+ export function autoUnseal() {
175
+ if (!AUTO_PASSPHRASE) {
176
+ return { unsealed: [], failed: [], skipped: [], message: "ON_VAULT_PASSPHRASE not set" };
177
+ }
178
+
179
+ const results = { unsealed: [], failed: [], skipped: [] };
180
+
181
+ if (!existsSync(CONNECTIONS_PATH)) return results;
182
+
183
+ const files = readdirSync(CONNECTIONS_PATH);
184
+
185
+ for (const file of files) {
186
+ if (!file.endsWith(".0n") && !file.endsWith(".0n.json")) continue;
187
+
188
+ const service = file.replace(/\.0n(\.json)?$/, "");
189
+
190
+ try {
191
+ const raw = readFileSync(join(CONNECTIONS_PATH, file), "utf-8");
192
+ const data = JSON.parse(raw);
193
+
194
+ if (!data.$0n?.sealed || !data.vault?.data) {
195
+ results.skipped.push(service);
196
+ continue;
197
+ }
198
+
199
+ const result = unsealConnection(service, AUTO_PASSPHRASE);
200
+ if (result.success) {
201
+ results.unsealed.push(service);
202
+ } else {
203
+ results.failed.push(service);
204
+ }
205
+ } catch {
206
+ results.failed.push(service);
207
+ }
208
+ }
209
+
210
+ return results;
211
+ }
212
+
213
+ /**
214
+ * Register vault tools on an MCP server instance.
215
+ *
216
+ * @param {import("@modelcontextprotocol/sdk/server/mcp.js").McpServer} server
217
+ * @param {import("zod").ZodType} z - Zod instance for parameter validation
218
+ */
219
+ export function registerVaultTools(server, z) {
220
+ // ─── vault_seal ──────────────────────────────────────────
221
+ server.tool(
222
+ "vault_seal",
223
+ `Encrypt a service's credentials on disk using AES-256-GCM.
224
+ The sealed file is machine-bound — it can ONLY be unsealed on the same hardware.
225
+ Credentials are replaced with an encrypted envelope in the .0n file.
226
+
227
+ Example: vault_seal({ service: "stripe", passphrase: "my-secret-phrase" })`,
228
+ {
229
+ service: z.string().describe("Service key to seal (e.g., stripe, openai, slack)"),
230
+ passphrase: z.string().describe("Passphrase for encryption — remember this to unseal later"),
231
+ },
232
+ async ({ service, passphrase }) => {
233
+ const result = sealConnection(service, passphrase);
234
+ return {
235
+ content: [{
236
+ type: "text",
237
+ text: JSON.stringify(result, null, 2),
238
+ }],
239
+ };
240
+ }
241
+ );
242
+
243
+ // ─── vault_unseal ────────────────────────────────────────
244
+ server.tool(
245
+ "vault_unseal",
246
+ `Decrypt a service's sealed credentials into memory only.
247
+ The .0n file on disk remains encrypted — credentials exist only in process memory.
248
+ Must be run on the same machine where the file was sealed.
249
+
250
+ Example: vault_unseal({ service: "stripe", passphrase: "my-secret-phrase" })`,
251
+ {
252
+ service: z.string().describe("Service key to unseal"),
253
+ passphrase: z.string().describe("Passphrase used when sealing"),
254
+ },
255
+ async ({ service, passphrase }) => {
256
+ const result = unsealConnection(service, passphrase);
257
+ return {
258
+ content: [{
259
+ type: "text",
260
+ text: JSON.stringify(result, null, 2),
261
+ }],
262
+ };
263
+ }
264
+ );
265
+
266
+ // ─── vault_verify ────────────────────────────────────────
267
+ server.tool(
268
+ "vault_verify",
269
+ `Check a sealed connection's integrity and machine binding.
270
+ Verifies the passphrase is correct AND the file was sealed on this machine.
271
+
272
+ Example: vault_verify({ service: "stripe", passphrase: "my-secret-phrase" })`,
273
+ {
274
+ service: z.string().describe("Service key to verify"),
275
+ passphrase: z.string().describe("Passphrase to test"),
276
+ },
277
+ async ({ service, passphrase }) => {
278
+ const result = verifyConnection(service, passphrase);
279
+ return {
280
+ content: [{
281
+ type: "text",
282
+ text: JSON.stringify(result, null, 2),
283
+ }],
284
+ };
285
+ }
286
+ );
287
+
288
+ // ─── vault_fingerprint ──────────────────────────────────
289
+ server.tool(
290
+ "vault_fingerprint",
291
+ `Show your machine's hardware fingerprint.
292
+ This is the unique identifier used for machine-binding sealed vault files.
293
+ The fingerprint is a SHA-256 hash of: hostname, CPU model, cores, platform, arch, and total memory.`,
294
+ {},
295
+ async () => {
296
+ const { fingerprint, components } = generateFingerprint();
297
+ return {
298
+ content: [{
299
+ type: "text",
300
+ text: JSON.stringify({
301
+ fingerprint,
302
+ components,
303
+ message: "This fingerprint is used to bind sealed vault files to this machine.",
304
+ }, null, 2),
305
+ }],
306
+ };
307
+ }
308
+ );
309
+ }
310
+
311
+ // ── Exports ────────────────────────────────────────────────
312
+ export { generateFingerprint, getFingerprint } from "./fingerprint.js";
313
+ export { seal, unseal, verify } from "./cipher.js";
314
+ export { getUnsealedCredentials, isUnsealed } from "./cache.js";