0nmcp 2.2.0 → 2.4.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/orchestrator.js CHANGED
@@ -38,9 +38,11 @@ const CAPABILITY_KEYWORDS = {
38
38
  export class Orchestrator {
39
39
  /**
40
40
  * @param {import("./connections.js").ConnectionManager} connectionManager
41
+ * @param {import("./capability-proxy.js").CapabilityProxy} [proxy]
41
42
  */
42
- constructor(connectionManager) {
43
+ constructor(connectionManager, proxy) {
43
44
  this.connections = connectionManager;
45
+ this.proxy = proxy;
44
46
  this.anthropic = null;
45
47
 
46
48
  // Dynamically import Anthropic SDK if key is available
@@ -277,14 +279,7 @@ Rules:
277
279
  }
278
280
 
279
281
  try {
280
- // Build URL
281
- let url = service.baseUrl + endpoint.path;
282
-
283
- // Substitute path params from step.params and credentials
284
- const allParams = { ...creds, ...step.params };
285
- url = url.replace(/\{(\w+)\}/g, (_, key) => allParams[key] || `{${key}}`);
286
-
287
- // Substitute context references {{label.field}}
282
+ // Substitute context references {{label.field}} in params
288
283
  if (step.params) {
289
284
  const paramsStr = JSON.stringify(step.params);
290
285
  const resolved = paramsStr.replace(/\{\{(\w+)\.(\w+)\}\}/g, (_, label, field) => {
@@ -293,32 +288,8 @@ Rules:
293
288
  step.params = JSON.parse(resolved);
294
289
  }
295
290
 
296
- // Build headers
297
- const headers = service.authHeader(creds);
298
-
299
- // Build request options
300
- const options = { method: endpoint.method, headers };
301
-
302
- if (endpoint.method !== "GET" && step.params) {
303
- const contentType = endpoint.contentType || "application/json";
304
- if (contentType === "application/x-www-form-urlencoded") {
305
- headers["Content-Type"] = "application/x-www-form-urlencoded";
306
- options.body = new URLSearchParams(this._flattenParams(step.params)).toString();
307
- } else {
308
- headers["Content-Type"] = "application/json";
309
- options.body = JSON.stringify(step.params);
310
- }
311
- }
312
-
313
- // Add query params for GET requests
314
- if (endpoint.method === "GET" && step.params) {
315
- const queryStr = new URLSearchParams(this._flattenParams(step.params)).toString();
316
- if (queryStr) url += (url.includes("?") ? "&" : "?") + queryStr;
317
- }
318
-
319
- // Execute
320
- const response = await fetch(url, options);
321
- const data = await response.json().catch(() => ({ status: response.status }));
291
+ // Execute through capability proxy (rate-limited, audited, zero-knowledge)
292
+ const { response, data } = await this.proxy.call(step.service, step.endpoint, step.params || {});
322
293
 
323
294
  return {
324
295
  success: response.ok,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "0nmcp",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "mcpName": "io.github.0nork/0nMCP",
5
- "description": "Universal AI API Orchestrator — 550 tools, 26 services, portable AI Brain bundles + machine-bound vault encryption + Application Engine. The most comprehensive MCP server available. Free and open source from 0nORK.",
5
+ "description": "Universal AI API Orchestrator — 819 tools, 48 services, portable AI Brain bundles + machine-bound vault encryption + Application Engine. 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",
@@ -246,6 +246,7 @@
246
246
  "catalog.js",
247
247
  "connections.js",
248
248
  "orchestrator.js",
249
+ "capability-proxy.js",
249
250
  "crm.js",
250
251
  "crm/",
251
252
  "webhooks.js",
@@ -258,17 +259,19 @@
258
259
  "LICENSE"
259
260
  ],
260
261
  "0nmcp-stats": {
261
- "tools": 290,
262
+ "tools": 545,
262
263
  "crmTools": 245,
263
264
  "vaultTools": 4,
265
+ "vaultContainerTools": 8,
266
+ "deedTools": 6,
264
267
  "engineTools": 6,
265
268
  "appTools": 5,
266
- "totalTools": 550,
267
- "services": 26,
268
- "actions": 65,
269
- "triggers": 93,
270
- "totalCapabilities": 708,
271
- "categories": 13,
272
- "lastUpdated": "2026-02-28T10:15:52.734Z"
269
+ "totalTools": 819,
270
+ "services": 48,
271
+ "actions": 104,
272
+ "triggers": 155,
273
+ "totalCapabilities": 1078,
274
+ "categories": 21,
275
+ "lastUpdated": "2026-03-13T00:31:43.573Z"
273
276
  }
274
277
  }
package/tools.js CHANGED
@@ -16,7 +16,7 @@ import { SERVICE_CATALOG, listServices, getService } from "./catalog.js";
16
16
  * @param {import("./orchestrator.js").Orchestrator} orchestrator
17
17
  * @param {import("./workflow.js").WorkflowRunner} [workflowRunner]
18
18
  */
19
- export function registerAllTools(server, connections, orchestrator, workflowRunner) {
19
+ export function registerAllTools(server, connections, orchestrator, workflowRunner, proxy) {
20
20
  // ─── execute ───────────────────────────────────────────
21
21
  server.tool(
22
22
  "execute",
@@ -291,41 +291,8 @@ Examples:
291
291
  }
292
292
 
293
293
  try {
294
- let url = catalog.baseUrl + ep.path;
295
- const allParams = { ...creds, ...(params || {}) };
296
-
297
- // Substitute path params
298
- url = url.replace(/\{(\w+)\}/g, (_, key) => allParams[key] || `{${key}}`);
299
-
300
- const headers = catalog.authHeader(creds);
301
- const options = { method: ep.method, headers };
302
-
303
- if (ep.method !== "GET" && params) {
304
- const contentType = ep.contentType || "application/json";
305
- if (contentType === "application/x-www-form-urlencoded") {
306
- headers["Content-Type"] = "application/x-www-form-urlencoded";
307
- const flat = {};
308
- for (const [k, v] of Object.entries(params)) {
309
- if (typeof v !== "object") flat[k] = String(v);
310
- }
311
- options.body = new URLSearchParams(flat).toString();
312
- } else {
313
- headers["Content-Type"] = "application/json";
314
- options.body = JSON.stringify(params);
315
- }
316
- }
317
-
318
- if (ep.method === "GET" && params) {
319
- const flat = {};
320
- for (const [k, v] of Object.entries(params)) {
321
- if (typeof v !== "object") flat[k] = String(v);
322
- }
323
- const qs = new URLSearchParams(flat).toString();
324
- if (qs) url += (url.includes("?") ? "&" : "?") + qs;
325
- }
326
-
327
- const response = await fetch(url, options);
328
- const data = await response.json().catch(() => ({ status: response.status, statusText: response.statusText }));
294
+ // Execute through capability proxy (rate-limited, audited, zero-knowledge)
295
+ const { response, data } = await proxy.call(service, endpoint, params || {});
329
296
 
330
297
  return {
331
298
  content: [{
package/vault/sync.js ADDED
@@ -0,0 +1,250 @@
1
+ // ============================================================
2
+ // 0nMCP — Vault: Cross-Platform E2E Encrypted Sync
3
+ // ============================================================
4
+ // Syncs vault credentials across CLI, web console, and other
5
+ // platforms using Argon2id key derivation + AES-256-GCM.
6
+ //
7
+ // The server NEVER sees plaintext credentials — only encrypted
8
+ // blobs are stored/transmitted. The sync passphrase never
9
+ // leaves the user's device.
10
+ // ============================================================
11
+
12
+ import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync } from "fs";
13
+ import { join } from "path";
14
+ import { homedir } from "os";
15
+ import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
16
+ import { createRequire } from "module";
17
+
18
+ const require = createRequire(import.meta.url);
19
+
20
+ const DOT_ON = join(homedir(), ".0n");
21
+ const CONNECTIONS_DIR = join(DOT_ON, "connections");
22
+ const SYNC_SALT_FILE = join(DOT_ON, "sync-salt");
23
+ const API_BASE = "https://0nmcp.com";
24
+
25
+ // AES-256-GCM constants
26
+ const ALGORITHM = "aes-256-gcm";
27
+ const KEY_LENGTH = 32;
28
+ const IV_LENGTH = 12; // 96 bits (GCM recommended)
29
+ const TAG_LENGTH = 16;
30
+
31
+ // Argon2id parameters (same as crypto-container.js)
32
+ const ARGON2_MEMORY = 65536; // 64MB
33
+ const ARGON2_ITERATIONS = 4;
34
+ const ARGON2_PARALLELISM = 2;
35
+ const ARGON2_HASH_LENGTH = 32; // 256 bits
36
+
37
+ // ── Key Derivation ──────────────────────────────────────────
38
+
39
+ /**
40
+ * Derive a 256-bit encryption key from passphrase using Argon2id.
41
+ * Memory-hard and GPU-resistant.
42
+ *
43
+ * @param {string} passphrase - User's sync passphrase
44
+ * @param {Buffer} salt - 32-byte salt
45
+ * @returns {Promise<Buffer>} 32-byte AES-256 key
46
+ */
47
+ async function deriveKey(passphrase, salt) {
48
+ let argon2;
49
+ try {
50
+ argon2 = await import("argon2");
51
+ } catch {
52
+ throw new Error("argon2 package required for vault sync. Install: npm i argon2");
53
+ }
54
+
55
+ const mod = argon2.default || argon2;
56
+ const hash = await mod.hash(passphrase, {
57
+ type: 2, // argon2id
58
+ memoryCost: ARGON2_MEMORY,
59
+ timeCost: ARGON2_ITERATIONS,
60
+ parallelism: ARGON2_PARALLELISM,
61
+ hashLength: ARGON2_HASH_LENGTH,
62
+ salt,
63
+ raw: true,
64
+ });
65
+ return hash;
66
+ }
67
+
68
+ // ── Encryption / Decryption ─────────────────────────────────
69
+
70
+ /**
71
+ * Encrypt data for cloud sync.
72
+ * Uses Argon2id-derived key + AES-256-GCM.
73
+ *
74
+ * @param {string} plaintext - Data to encrypt
75
+ * @param {string} passphrase - Sync passphrase
76
+ * @returns {Promise<{ encrypted_data: string, iv: string, salt: string }>}
77
+ */
78
+ export async function encryptForSync(plaintext, passphrase) {
79
+ const salt = randomBytes(32);
80
+ const iv = randomBytes(IV_LENGTH);
81
+ const key = await deriveKey(passphrase, salt);
82
+
83
+ const cipher = createCipheriv(ALGORITHM, key, iv);
84
+ const encrypted = Buffer.concat([
85
+ cipher.update(plaintext, "utf8"),
86
+ cipher.final(),
87
+ ]);
88
+ const tag = cipher.getAuthTag();
89
+
90
+ // Combine tag + ciphertext for storage
91
+ const combined = Buffer.concat([tag, encrypted]);
92
+
93
+ return {
94
+ encrypted_data: combined.toString("base64"),
95
+ iv: iv.toString("base64"),
96
+ salt: salt.toString("base64"),
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Decrypt data from cloud sync.
102
+ *
103
+ * @param {string} encryptedB64 - Base64 encoded [tag + ciphertext]
104
+ * @param {string} ivB64 - Base64 encoded IV
105
+ * @param {string} saltB64 - Base64 encoded salt
106
+ * @param {string} passphrase - Sync passphrase
107
+ * @returns {Promise<string>} Decrypted plaintext
108
+ */
109
+ export async function decryptFromSync(encryptedB64, ivB64, saltB64, passphrase) {
110
+ const combined = Buffer.from(encryptedB64, "base64");
111
+ const iv = Buffer.from(ivB64, "base64");
112
+ const salt = Buffer.from(saltB64, "base64");
113
+
114
+ if (combined.length < TAG_LENGTH + 1) {
115
+ throw new Error("Invalid encrypted data: too short");
116
+ }
117
+
118
+ const tag = combined.subarray(0, TAG_LENGTH);
119
+ const ciphertext = combined.subarray(TAG_LENGTH);
120
+
121
+ const key = await deriveKey(passphrase, salt);
122
+
123
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
124
+ decipher.setAuthTag(tag);
125
+
126
+ try {
127
+ const decrypted = Buffer.concat([
128
+ decipher.update(ciphertext),
129
+ decipher.final(),
130
+ ]);
131
+ return decrypted.toString("utf8");
132
+ } catch {
133
+ throw new Error("Decryption failed — wrong sync passphrase.");
134
+ }
135
+ }
136
+
137
+ // ── Sync Operations ─────────────────────────────────────────
138
+
139
+ async function apiCall(method, path, token, body = null) {
140
+ const headers = {
141
+ "Authorization": `Bearer ${token}`,
142
+ "Content-Type": "application/json",
143
+ };
144
+
145
+ const opts = { method, headers };
146
+ if (body) opts.body = JSON.stringify(body);
147
+
148
+ const res = await fetch(`${API_BASE}${path}`, opts);
149
+ const data = await res.json();
150
+ return { ok: res.ok, status: res.status, data };
151
+ }
152
+
153
+ /**
154
+ * Push all local connections to cloud (E2E encrypted).
155
+ *
156
+ * @param {string} token - Access token
157
+ * @param {string} passphrase - Sync passphrase
158
+ * @returns {Promise<{ pushed: number, errors: string[] }>}
159
+ */
160
+ export async function pushToCloud(token, passphrase) {
161
+ if (!existsSync(CONNECTIONS_DIR)) {
162
+ return { pushed: 0, errors: ["No connections directory found. Run: 0nmcp init"] };
163
+ }
164
+
165
+ const files = readdirSync(CONNECTIONS_DIR).filter(f => f.endsWith(".0n"));
166
+ const errors = [];
167
+ let pushed = 0;
168
+
169
+ for (const file of files) {
170
+ const serviceKey = file.replace(".0n", "");
171
+ try {
172
+ const content = readFileSync(join(CONNECTIONS_DIR, file), "utf8");
173
+ const { encrypted_data, iv, salt } = await encryptForSync(content, passphrase);
174
+
175
+ const { ok, data } = await apiCall("PUT", "/api/vault/sync", token, {
176
+ service_key: serviceKey,
177
+ encrypted_data,
178
+ iv,
179
+ salt,
180
+ });
181
+
182
+ if (ok) {
183
+ pushed++;
184
+ } else {
185
+ errors.push(`${serviceKey}: ${data.error || "Unknown error"}`);
186
+ }
187
+ } catch (err) {
188
+ errors.push(`${serviceKey}: ${err.message}`);
189
+ }
190
+ }
191
+
192
+ return { pushed, errors };
193
+ }
194
+
195
+ /**
196
+ * Pull all connections from cloud and write to local.
197
+ *
198
+ * @param {string} token - Access token
199
+ * @param {string} passphrase - Sync passphrase
200
+ * @returns {Promise<{ pulled: number, errors: string[] }>}
201
+ */
202
+ export async function pullFromCloud(token, passphrase) {
203
+ if (!existsSync(CONNECTIONS_DIR)) {
204
+ mkdirSync(CONNECTIONS_DIR, { recursive: true });
205
+ }
206
+
207
+ const { ok, data } = await apiCall("GET", "/api/vault/sync", token);
208
+ if (!ok) {
209
+ return { pulled: 0, errors: [data.error || "Failed to fetch from cloud"] };
210
+ }
211
+
212
+ const entries = data.entries || [];
213
+ const errors = [];
214
+ let pulled = 0;
215
+
216
+ for (const entry of entries) {
217
+ try {
218
+ const plaintext = await decryptFromSync(
219
+ entry.encrypted_data,
220
+ entry.iv,
221
+ entry.salt,
222
+ passphrase
223
+ );
224
+
225
+ const filePath = join(CONNECTIONS_DIR, `${entry.service_key}.0n`);
226
+ writeFileSync(filePath, plaintext);
227
+ pulled++;
228
+ } catch (err) {
229
+ errors.push(`${entry.service_key}: ${err.message}`);
230
+ }
231
+ }
232
+
233
+ return { pulled, errors };
234
+ }
235
+
236
+ /**
237
+ * Store sync salt locally (so the passphrase prompt
238
+ * can remind the user they've set one up).
239
+ */
240
+ export function hasSyncSetup() {
241
+ return existsSync(SYNC_SALT_FILE);
242
+ }
243
+
244
+ /**
245
+ * Mark that sync has been configured.
246
+ */
247
+ export function markSyncSetup() {
248
+ if (!existsSync(DOT_ON)) mkdirSync(DOT_ON, { recursive: true });
249
+ writeFileSync(SYNC_SALT_FILE, new Date().toISOString());
250
+ }