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 +4 -0
- package/index.js +16 -1
- package/lib/badges.json +1 -1
- package/lib/stats.json +4 -3
- package/package.json +14 -5
- package/server.js +11 -1
- package/vault/cache.js +28 -0
- package/vault/cipher.js +147 -0
- package/vault/fingerprint.js +58 -0
- package/vault/index.js +314 -0
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.
|
|
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
package/lib/stats.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated": "2026-02-15T00:
|
|
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
|
-
"
|
|
796
|
-
"
|
|
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.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"mcpName": "io.github.0nork/0nMCP",
|
|
5
|
-
"description": "Universal AI API Orchestrator —
|
|
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
|
-
"
|
|
175
|
+
"vaultTools": 4,
|
|
176
|
+
"totalTools": 539,
|
|
168
177
|
"services": 26,
|
|
169
178
|
"actions": 65,
|
|
170
179
|
"triggers": 93,
|
|
171
|
-
"totalCapabilities":
|
|
180
|
+
"totalCapabilities": 697,
|
|
172
181
|
"categories": 13,
|
|
173
|
-
"lastUpdated": "2026-02-15T00:
|
|
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.
|
|
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
|
+
}
|
package/vault/cipher.js
ADDED
|
@@ -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";
|