0nmcp 1.4.0 → 1.6.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/cli.js +256 -0
- package/connections.js +4 -0
- package/engine/bundler.js +395 -0
- package/engine/cipher-portable.js +94 -0
- package/engine/index.js +390 -0
- package/engine/mapper.js +292 -0
- package/engine/parser.js +221 -0
- package/engine/platforms.js +254 -0
- package/engine/validator.js +257 -0
- package/index.js +23 -1
- package/lib/badges.json +1 -1
- package/lib/stats.json +5 -3
- package/package.json +23 -5
- package/server.js +14 -2
- 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/cli.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* npx 0nmcp connect Interactive connection setup
|
|
13
13
|
* npx 0nmcp list List connected services
|
|
14
14
|
* npx 0nmcp migrate Migrate from ~/.0nmcp to ~/.0n
|
|
15
|
+
* npx 0nmcp engine Engine commands (import, verify, platforms, export, open)
|
|
15
16
|
*
|
|
16
17
|
* ═══════════════════════════════════════════════════════════════════════════
|
|
17
18
|
*/
|
|
@@ -78,6 +79,14 @@ ${c.bright}Usage:${c.reset}
|
|
|
78
79
|
${c.cyan}npx 0nmcp list${c.reset} List connected services
|
|
79
80
|
${c.cyan}npx 0nmcp migrate${c.reset} Migrate from ~/.0nmcp to ~/.0n
|
|
80
81
|
|
|
82
|
+
${c.bright}Engine commands:${c.reset}
|
|
83
|
+
|
|
84
|
+
${c.cyan}npx 0nmcp engine import <file>${c.reset} Import credentials from .env/CSV/JSON
|
|
85
|
+
${c.cyan}npx 0nmcp engine verify${c.reset} Verify all connected API keys
|
|
86
|
+
${c.cyan}npx 0nmcp engine platforms${c.reset} Generate AI platform configs
|
|
87
|
+
${c.cyan}npx 0nmcp engine export${c.reset} Export .0n bundle (AI Brain)
|
|
88
|
+
${c.cyan}npx 0nmcp engine open <bundle>${c.reset} Open/inspect a .0n bundle
|
|
89
|
+
|
|
81
90
|
${c.bright}Serve options:${c.reset}
|
|
82
91
|
|
|
83
92
|
${c.cyan}npx 0nmcp serve --port 3000 --host 0.0.0.0${c.reset}
|
|
@@ -208,6 +217,13 @@ ${c.bright}Links:${c.reset}
|
|
|
208
217
|
}
|
|
209
218
|
}
|
|
210
219
|
|
|
220
|
+
// Engine
|
|
221
|
+
if (command === 'engine') {
|
|
222
|
+
console.log(BANNER);
|
|
223
|
+
await handleEngine(args.slice(1));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
211
227
|
// Migrate
|
|
212
228
|
if (command === 'migrate') {
|
|
213
229
|
console.log(BANNER);
|
|
@@ -421,4 +437,244 @@ async function interactiveConnect() {
|
|
|
421
437
|
console.log(` Saved to: ${filePath}`);
|
|
422
438
|
}
|
|
423
439
|
|
|
440
|
+
async function handleEngine(args) {
|
|
441
|
+
const sub = args[0];
|
|
442
|
+
|
|
443
|
+
if (!sub || sub === 'help') {
|
|
444
|
+
console.log(`${c.bright}Engine — .0n Conversion Engine${c.reset}\n`);
|
|
445
|
+
console.log(` ${c.cyan}import <file>${c.reset} Import credentials from .env, CSV, or JSON`);
|
|
446
|
+
console.log(` ${c.cyan}verify${c.reset} Verify all connected API keys`);
|
|
447
|
+
console.log(` ${c.cyan}platforms${c.reset} Generate AI platform configs`);
|
|
448
|
+
console.log(` ${c.cyan}export${c.reset} Export .0n bundle (AI Brain)`);
|
|
449
|
+
console.log(` ${c.cyan}open <bundle>${c.reset} Open/inspect a .0n bundle file\n`);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (sub === 'import') {
|
|
454
|
+
const source = args[1];
|
|
455
|
+
if (!source) {
|
|
456
|
+
console.log(`${c.red}Usage: npx 0nmcp engine import <file>${c.reset}`);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const { parseFile } = await import('./engine/parser.js');
|
|
461
|
+
const { mapEnvVars, groupByService, validateMapping } = await import('./engine/mapper.js');
|
|
462
|
+
const { entries } = parseFile(source);
|
|
463
|
+
const { mapped, unmapped } = mapEnvVars(entries);
|
|
464
|
+
const groups = groupByService(mapped);
|
|
465
|
+
|
|
466
|
+
console.log(`${c.bright}Import Results:${c.reset}\n`);
|
|
467
|
+
console.log(` Entries found: ${entries.length}`);
|
|
468
|
+
console.log(` Mapped: ${mapped.length}`);
|
|
469
|
+
console.log(` Unmapped: ${unmapped.length}\n`);
|
|
470
|
+
|
|
471
|
+
console.log(`${c.bright}Services Detected:${c.reset}\n`);
|
|
472
|
+
for (const [service, group] of Object.entries(groups)) {
|
|
473
|
+
const validation = validateMapping(service, group.credentials);
|
|
474
|
+
const status = validation.valid ? `${c.green}complete${c.reset}` : `${c.yellow}missing: ${validation.missing.join(', ')}${c.reset}`;
|
|
475
|
+
console.log(` ${c.green}●${c.reset} ${c.bright}${service}${c.reset} — ${Object.keys(group.credentials).length} credentials (${status})`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (unmapped.length > 0) {
|
|
479
|
+
console.log(`\n${c.yellow}Unmapped variables:${c.reset} ${unmapped.map(u => u.key).join(', ')}`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
console.log(`\n${c.bright}Next:${c.reset} Run ${c.cyan}npx 0nmcp engine export${c.reset} to create a portable .0n bundle.`);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
console.log(`${c.red}Error: ${err.message}${c.reset}`);
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (sub === 'verify') {
|
|
491
|
+
try {
|
|
492
|
+
const { verifyAll } = await import('./engine/validator.js');
|
|
493
|
+
const { existsSync, readdirSync, readFileSync } = await import('fs');
|
|
494
|
+
const connectionsDir = path.join(DOT_ON_DIR, 'connections');
|
|
495
|
+
|
|
496
|
+
if (!existsSync(connectionsDir)) {
|
|
497
|
+
console.log(`${c.yellow}No connections found. Run ${c.cyan}npx 0nmcp connect${c.reset} first.`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const files = readdirSync(connectionsDir).filter(f => f.endsWith('.0n') || f.endsWith('.0n.json'));
|
|
502
|
+
const connections = {};
|
|
503
|
+
for (const file of files) {
|
|
504
|
+
try {
|
|
505
|
+
const data = JSON.parse(readFileSync(path.join(connectionsDir, file), 'utf8'));
|
|
506
|
+
if (data.$0n?.sealed) continue;
|
|
507
|
+
connections[data.service] = { credentials: data.auth?.credentials || {} };
|
|
508
|
+
} catch { /* skip */ }
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (Object.keys(connections).length === 0) {
|
|
512
|
+
console.log(`${c.yellow}No unsealed connections to verify.${c.reset}`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
console.log(`${c.bright}Verifying ${Object.keys(connections).length} connections...${c.reset}\n`);
|
|
517
|
+
|
|
518
|
+
const { results, summary } = await verifyAll(connections);
|
|
519
|
+
for (const [service, result] of Object.entries(results)) {
|
|
520
|
+
const icon = result.valid ? `${c.green}✓` : `${c.red}✗`;
|
|
521
|
+
const latency = result.latency_ms ? ` (${result.latency_ms}ms)` : '';
|
|
522
|
+
console.log(` ${icon}${c.reset} ${c.bright}${service}${c.reset}${latency}${result.error ? ` — ${result.error}` : ''}`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
console.log(`\n${c.bright}Summary:${c.reset} ${summary.valid}/${summary.total} valid`);
|
|
526
|
+
} catch (err) {
|
|
527
|
+
console.log(`${c.red}Error: ${err.message}${c.reset}`);
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (sub === 'platforms') {
|
|
534
|
+
try {
|
|
535
|
+
const { getPlatformInfo, generatePlatformConfig } = await import('./engine/platforms.js');
|
|
536
|
+
const platform = args[1];
|
|
537
|
+
|
|
538
|
+
if (platform) {
|
|
539
|
+
const config = generatePlatformConfig(platform);
|
|
540
|
+
console.log(`${c.bright}${config.name} Configuration:${c.reset}\n`);
|
|
541
|
+
console.log(` Config path: ${config.path || '(HTTP only)'}`);
|
|
542
|
+
console.log(` Format: ${config.format}\n`);
|
|
543
|
+
console.log(typeof config.config === 'string' ? config.config : JSON.stringify(config.config, null, 2));
|
|
544
|
+
} else {
|
|
545
|
+
const info = getPlatformInfo();
|
|
546
|
+
console.log(`${c.bright}Supported AI Platforms:${c.reset}\n`);
|
|
547
|
+
for (const p of info) {
|
|
548
|
+
const status = p.installed ? `${c.green}installed${c.reset}` : `${c.yellow}not installed${c.reset}`;
|
|
549
|
+
console.log(` ${p.installed ? c.green + '●' : c.blue + '○'}${c.reset} ${c.bright}${p.name}${c.reset} (${status})`);
|
|
550
|
+
if (p.configPath) console.log(` ${p.configPath}`);
|
|
551
|
+
}
|
|
552
|
+
console.log(`\n${c.bright}Tip:${c.reset} Run ${c.cyan}npx 0nmcp engine platforms <name>${c.reset} to see config for a specific platform.`);
|
|
553
|
+
console.log(` Names: claude_desktop, cursor, windsurf, gemini, continue, cline, openai`);
|
|
554
|
+
}
|
|
555
|
+
} catch (err) {
|
|
556
|
+
console.log(`${c.red}Error: ${err.message}${c.reset}`);
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (sub === 'export') {
|
|
563
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
564
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
const passphrase = await ask('Bundle passphrase: ');
|
|
568
|
+
if (!passphrase) {
|
|
569
|
+
console.log(`${c.red}Passphrase required.${c.reset}`);
|
|
570
|
+
rl.close();
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
const name = await ask(`Bundle name (default: 0n AI Brain): `) || '0n AI Brain';
|
|
574
|
+
rl.close();
|
|
575
|
+
|
|
576
|
+
const { createBundle } = await import('./engine/bundler.js');
|
|
577
|
+
const { existsSync, readdirSync, readFileSync } = await import('fs');
|
|
578
|
+
const connectionsDir = path.join(DOT_ON_DIR, 'connections');
|
|
579
|
+
|
|
580
|
+
if (!existsSync(connectionsDir)) {
|
|
581
|
+
console.log(`${c.yellow}No connections found.${c.reset}`);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const files = readdirSync(connectionsDir).filter(f => f.endsWith('.0n') || f.endsWith('.0n.json'));
|
|
586
|
+
const connections = {};
|
|
587
|
+
for (const file of files) {
|
|
588
|
+
try {
|
|
589
|
+
const data = JSON.parse(readFileSync(path.join(connectionsDir, file), 'utf8'));
|
|
590
|
+
if (data.$0n?.sealed) continue;
|
|
591
|
+
connections[data.service] = {
|
|
592
|
+
credentials: data.auth?.credentials || {},
|
|
593
|
+
name: data.$0n?.name || data.service,
|
|
594
|
+
authType: data.auth?.type || 'api_key',
|
|
595
|
+
environment: data.environment || 'production',
|
|
596
|
+
};
|
|
597
|
+
} catch { /* skip */ }
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (Object.keys(connections).length === 0) {
|
|
601
|
+
console.log(`${c.yellow}No exportable connections found.${c.reset}`);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
console.log(`\n${c.bright}Creating AI Brain bundle...${c.reset}\n`);
|
|
606
|
+
|
|
607
|
+
const result = createBundle({ connections, passphrase, name, platforms: 'all' });
|
|
608
|
+
|
|
609
|
+
console.log(`${c.green}${c.bright}Bundle created!${c.reset}\n`);
|
|
610
|
+
console.log(` Path: ${result.path}`);
|
|
611
|
+
console.log(` Services: ${result.manifest.services.join(', ')}`);
|
|
612
|
+
console.log(` Connections: ${result.manifest.connection_count}`);
|
|
613
|
+
console.log(` Platforms: ${result.manifest.platform_count}`);
|
|
614
|
+
console.log(` Encryption: ${result.manifest.encryption.method}`);
|
|
615
|
+
console.log(`\n${c.bright}Share this file — recipient opens with:${c.reset} ${c.cyan}npx 0nmcp engine open <bundle>${c.reset}`);
|
|
616
|
+
} catch (err) {
|
|
617
|
+
console.log(`${c.red}Error: ${err.message}${c.reset}`);
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (sub === 'open') {
|
|
624
|
+
const bundlePath = args[1];
|
|
625
|
+
if (!bundlePath) {
|
|
626
|
+
console.log(`${c.red}Usage: npx 0nmcp engine open <bundle.0n>${c.reset}`);
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
const { inspectBundle, openBundle } = await import('./engine/bundler.js');
|
|
632
|
+
|
|
633
|
+
// First inspect
|
|
634
|
+
const info = inspectBundle(bundlePath);
|
|
635
|
+
console.log(`${c.bright}Bundle: ${info.name}${c.reset}\n`);
|
|
636
|
+
console.log(` Created: ${info.created}`);
|
|
637
|
+
console.log(` Services: ${info.services.map(s => s.service).join(', ')}`);
|
|
638
|
+
console.log(` Platforms: ${info.platforms.join(', ')}`);
|
|
639
|
+
if (info.includes.length > 0) {
|
|
640
|
+
console.log(` Includes: ${info.includes.map(i => i.name).join(', ')}`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
644
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
645
|
+
|
|
646
|
+
const passphrase = await ask('\nPassphrase to decrypt (leave empty to inspect only): ');
|
|
647
|
+
rl.close();
|
|
648
|
+
|
|
649
|
+
if (!passphrase) {
|
|
650
|
+
console.log(`\n${c.yellow}Inspect only — no credentials extracted.${c.reset}`);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const result = openBundle(bundlePath, passphrase);
|
|
655
|
+
|
|
656
|
+
console.log(`\n${c.green}${c.bright}Bundle opened!${c.reset}\n`);
|
|
657
|
+
console.log(` Connections imported: ${result.connections.join(', ')}`);
|
|
658
|
+
if (result.includes.length > 0) {
|
|
659
|
+
console.log(` Files extracted: ${result.includes.join(', ')}`);
|
|
660
|
+
}
|
|
661
|
+
if (result.errors.length > 0) {
|
|
662
|
+
console.log(`\n${c.red}Errors:${c.reset}`);
|
|
663
|
+
for (const err of result.errors) {
|
|
664
|
+
console.log(` ${c.red}●${c.reset} ${err}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
console.log(`\n${c.bright}Tip:${c.reset} Run ${c.cyan}npx 0nmcp engine platforms${c.reset} to install AI platform configs.`);
|
|
668
|
+
} catch (err) {
|
|
669
|
+
console.log(`${c.red}Error: ${err.message}${c.reset}`);
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
console.log(`${c.red}Unknown engine command: ${sub}${c.reset}`);
|
|
676
|
+
console.log(`Run ${c.cyan}npx 0nmcp engine help${c.reset} for usage`);
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
|
|
424
680
|
main().catch(console.error);
|
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
|
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — Engine: Bundle Creator/Opener
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Creates and opens portable .0n bundle files — the "Alpha
|
|
5
|
+
// AI Brain" format. Bundles contain encrypted connections,
|
|
6
|
+
// platform configs, and optional include files.
|
|
7
|
+
//
|
|
8
|
+
// Portable encryption: passphrase-only (no machine binding).
|
|
9
|
+
// After import, optionally re-seal with vault for machine lock.
|
|
10
|
+
//
|
|
11
|
+
// Patent Pending: US Provisional Patent Application #63/968,814
|
|
12
|
+
// ============================================================
|
|
13
|
+
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
15
|
+
import { join, basename } from "path";
|
|
16
|
+
import { createHash } from "crypto";
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
import { sealPortable, unsealPortable } from "./cipher-portable.js";
|
|
19
|
+
import { generateAllPlatformConfigs } from "./platforms.js";
|
|
20
|
+
import { CONNECTIONS_PATH } from "../connections.js";
|
|
21
|
+
|
|
22
|
+
const BUNDLES_DIR = join(homedir(), ".0n", "bundles");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compute SHA-256 checksum of a string.
|
|
26
|
+
*/
|
|
27
|
+
function sha256(data) {
|
|
28
|
+
return createHash("sha256").update(data).digest("hex");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a .0n bundle file.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} options
|
|
35
|
+
* @param {Record<string, { credentials: Record<string, string>, authType?: string }>} options.connections
|
|
36
|
+
* @param {string} options.passphrase
|
|
37
|
+
* @param {string} [options.outputPath]
|
|
38
|
+
* @param {string} [options.name]
|
|
39
|
+
* @param {string} [options.description]
|
|
40
|
+
* @param {string[]|"all"} [options.platforms] - Platform keys or "all"
|
|
41
|
+
* @param {Array<{ path: string, type?: string, name?: string }>} [options.includes]
|
|
42
|
+
* @param {boolean} [options.seal] - Default true
|
|
43
|
+
* @returns {{ bundle: object, path: string, manifest: object }}
|
|
44
|
+
*/
|
|
45
|
+
export function createBundle(options) {
|
|
46
|
+
const {
|
|
47
|
+
connections,
|
|
48
|
+
passphrase,
|
|
49
|
+
outputPath,
|
|
50
|
+
name = "0n Bundle",
|
|
51
|
+
description = "",
|
|
52
|
+
platforms = "all",
|
|
53
|
+
includes = [],
|
|
54
|
+
seal = true,
|
|
55
|
+
} = options;
|
|
56
|
+
|
|
57
|
+
const now = new Date().toISOString();
|
|
58
|
+
|
|
59
|
+
// Build connections array
|
|
60
|
+
const bundleConnections = [];
|
|
61
|
+
for (const [service, conn] of Object.entries(connections)) {
|
|
62
|
+
const credJson = JSON.stringify(conn.credentials);
|
|
63
|
+
|
|
64
|
+
if (seal) {
|
|
65
|
+
const { sealed } = sealPortable(credJson, passphrase);
|
|
66
|
+
bundleConnections.push({
|
|
67
|
+
service,
|
|
68
|
+
name: conn.name || service,
|
|
69
|
+
environment: conn.environment || "production",
|
|
70
|
+
auth_type: conn.authType || "api_key",
|
|
71
|
+
credential_keys: Object.keys(conn.credentials),
|
|
72
|
+
sealed: true,
|
|
73
|
+
vault: {
|
|
74
|
+
data: sealed,
|
|
75
|
+
algorithm: "aes-256-gcm",
|
|
76
|
+
kdf: "pbkdf2-sha512-100k",
|
|
77
|
+
portable: true,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
bundleConnections.push({
|
|
82
|
+
service,
|
|
83
|
+
name: conn.name || service,
|
|
84
|
+
environment: conn.environment || "production",
|
|
85
|
+
auth_type: conn.authType || "api_key",
|
|
86
|
+
credential_keys: Object.keys(conn.credentials),
|
|
87
|
+
sealed: false,
|
|
88
|
+
credentials: conn.credentials,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Generate platform configs
|
|
94
|
+
let platformConfigs = {};
|
|
95
|
+
if (platforms === "all" || (Array.isArray(platforms) && platforms.length > 0)) {
|
|
96
|
+
const allConfigs = generateAllPlatformConfigs({});
|
|
97
|
+
if (platforms === "all") {
|
|
98
|
+
platformConfigs = {};
|
|
99
|
+
for (const [key, cfg] of Object.entries(allConfigs)) {
|
|
100
|
+
platformConfigs[key] = {
|
|
101
|
+
format: cfg.format,
|
|
102
|
+
config_path: cfg.path,
|
|
103
|
+
config: cfg.config,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
for (const key of platforms) {
|
|
108
|
+
if (allConfigs[key]) {
|
|
109
|
+
platformConfigs[key] = {
|
|
110
|
+
format: allConfigs[key].format,
|
|
111
|
+
config_path: allConfigs[key].path,
|
|
112
|
+
config: allConfigs[key].config,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Process include files
|
|
120
|
+
const bundleIncludes = [];
|
|
121
|
+
for (const inc of includes) {
|
|
122
|
+
if (!existsSync(inc.path)) continue;
|
|
123
|
+
const content = readFileSync(inc.path);
|
|
124
|
+
const data = content.toString("base64");
|
|
125
|
+
bundleIncludes.push({
|
|
126
|
+
name: inc.name || basename(inc.path),
|
|
127
|
+
type: inc.type || detectIncludeType(inc.path),
|
|
128
|
+
target: inc.target || detectTarget(inc.path),
|
|
129
|
+
data,
|
|
130
|
+
checksum: `sha256:${sha256(content.toString())}`,
|
|
131
|
+
size: content.length,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Build manifest
|
|
136
|
+
const connectionsStr = JSON.stringify(bundleConnections);
|
|
137
|
+
const platformsStr = JSON.stringify(platformConfigs);
|
|
138
|
+
const includesStr = JSON.stringify(bundleIncludes);
|
|
139
|
+
|
|
140
|
+
const manifest = {
|
|
141
|
+
bundle_version: "1.0.0",
|
|
142
|
+
generator: "0nmcp-engine/1.6.0",
|
|
143
|
+
connection_count: bundleConnections.length,
|
|
144
|
+
platform_count: Object.keys(platformConfigs).length,
|
|
145
|
+
include_count: bundleIncludes.length,
|
|
146
|
+
services: bundleConnections.map(c => c.service),
|
|
147
|
+
encryption: seal
|
|
148
|
+
? { method: "portable", algorithm: "aes-256-gcm", kdf: "pbkdf2-sha512-100k" }
|
|
149
|
+
: { method: "none" },
|
|
150
|
+
checksums: {
|
|
151
|
+
connections: `sha256:${sha256(connectionsStr)}`,
|
|
152
|
+
platforms: `sha256:${sha256(platformsStr)}`,
|
|
153
|
+
includes: `sha256:${sha256(includesStr)}`,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Assemble bundle
|
|
158
|
+
const bundle = {
|
|
159
|
+
$0n: {
|
|
160
|
+
type: "bundle",
|
|
161
|
+
version: "1.0.0",
|
|
162
|
+
created: now,
|
|
163
|
+
updated: now,
|
|
164
|
+
name,
|
|
165
|
+
description,
|
|
166
|
+
},
|
|
167
|
+
connections: bundleConnections,
|
|
168
|
+
platforms: platformConfigs,
|
|
169
|
+
includes: bundleIncludes,
|
|
170
|
+
manifest,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Write to file
|
|
174
|
+
if (!existsSync(BUNDLES_DIR)) mkdirSync(BUNDLES_DIR, { recursive: true });
|
|
175
|
+
const ts = now.replace(/[:.]/g, "-").slice(0, 19);
|
|
176
|
+
const outPath = outputPath || join(BUNDLES_DIR, `bundle-${ts}.0n`);
|
|
177
|
+
const outDir = join(outPath, "..");
|
|
178
|
+
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
179
|
+
|
|
180
|
+
writeFileSync(outPath, JSON.stringify(bundle, null, 2));
|
|
181
|
+
|
|
182
|
+
return { bundle, path: outPath, manifest };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Open a .0n bundle and extract connections.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} bundlePath
|
|
189
|
+
* @param {string} passphrase
|
|
190
|
+
* @param {object} [options]
|
|
191
|
+
* @param {boolean} [options.installConnections] - Write to ~/.0n/connections/ (default true)
|
|
192
|
+
* @param {boolean} [options.dryRun] - Preview without writing (default false)
|
|
193
|
+
* @returns {{ connections: string[], platforms: string[], includes: string[], errors: string[] }}
|
|
194
|
+
*/
|
|
195
|
+
export function openBundle(bundlePath, passphrase, options = {}) {
|
|
196
|
+
const { installConnections = true, dryRun = false } = options;
|
|
197
|
+
|
|
198
|
+
const raw = readFileSync(bundlePath, "utf-8");
|
|
199
|
+
const bundle = JSON.parse(raw);
|
|
200
|
+
|
|
201
|
+
if (!bundle.$0n || bundle.$0n.type !== "bundle") {
|
|
202
|
+
throw new Error("Not a valid .0n bundle file.");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const results = { connections: [], platforms: [], includes: [], errors: [] };
|
|
206
|
+
|
|
207
|
+
// Extract connections
|
|
208
|
+
for (const conn of bundle.connections || []) {
|
|
209
|
+
try {
|
|
210
|
+
let credentials;
|
|
211
|
+
|
|
212
|
+
if (conn.sealed && conn.vault?.data) {
|
|
213
|
+
const decrypted = unsealPortable(conn.vault.data, passphrase);
|
|
214
|
+
credentials = JSON.parse(decrypted);
|
|
215
|
+
} else if (conn.credentials) {
|
|
216
|
+
credentials = conn.credentials;
|
|
217
|
+
} else {
|
|
218
|
+
results.errors.push(`${conn.service}: No credentials found`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (installConnections && !dryRun) {
|
|
223
|
+
// Write as standard .0n connection file
|
|
224
|
+
const connFile = {
|
|
225
|
+
$0n: {
|
|
226
|
+
type: "connection",
|
|
227
|
+
version: "1.0.0",
|
|
228
|
+
created: new Date().toISOString(),
|
|
229
|
+
name: conn.name || conn.service,
|
|
230
|
+
},
|
|
231
|
+
service: conn.service,
|
|
232
|
+
environment: conn.environment || "production",
|
|
233
|
+
auth: {
|
|
234
|
+
type: conn.auth_type || "api_key",
|
|
235
|
+
credentials,
|
|
236
|
+
},
|
|
237
|
+
metadata: {
|
|
238
|
+
imported_from: basename(bundlePath),
|
|
239
|
+
imported_at: new Date().toISOString(),
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
if (!existsSync(CONNECTIONS_PATH)) mkdirSync(CONNECTIONS_PATH, { recursive: true });
|
|
244
|
+
const filePath = join(CONNECTIONS_PATH, `${conn.service}.0n`);
|
|
245
|
+
writeFileSync(filePath, JSON.stringify(connFile, null, 2));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
results.connections.push(conn.service);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
results.errors.push(`${conn.service}: ${err.message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Extract includes
|
|
255
|
+
for (const inc of bundle.includes || []) {
|
|
256
|
+
try {
|
|
257
|
+
if (dryRun) {
|
|
258
|
+
results.includes.push(inc.name);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const target = inc.target
|
|
263
|
+
? inc.target.replace("~", homedir())
|
|
264
|
+
: join(homedir(), ".0n", inc.type === "workflow" ? "workflows" : "plugins", inc.name);
|
|
265
|
+
|
|
266
|
+
const dir = join(target, "..");
|
|
267
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
268
|
+
|
|
269
|
+
const data = Buffer.from(inc.data, "base64");
|
|
270
|
+
|
|
271
|
+
// Verify checksum
|
|
272
|
+
if (inc.checksum) {
|
|
273
|
+
const expected = inc.checksum.replace("sha256:", "");
|
|
274
|
+
const actual = sha256(data.toString());
|
|
275
|
+
if (actual !== expected) {
|
|
276
|
+
results.errors.push(`${inc.name}: Checksum mismatch`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
writeFileSync(target, data);
|
|
282
|
+
results.includes.push(inc.name);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
results.errors.push(`${inc.name}: ${err.message}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// List platforms
|
|
289
|
+
for (const key of Object.keys(bundle.platforms || {})) {
|
|
290
|
+
results.platforms.push(key);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return results;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Inspect a bundle without passphrase — shows metadata only, no credentials.
|
|
298
|
+
* @param {string} bundlePath
|
|
299
|
+
* @returns {object} Bundle metadata and service list
|
|
300
|
+
*/
|
|
301
|
+
export function inspectBundle(bundlePath) {
|
|
302
|
+
const raw = readFileSync(bundlePath, "utf-8");
|
|
303
|
+
const bundle = JSON.parse(raw);
|
|
304
|
+
|
|
305
|
+
if (!bundle.$0n || bundle.$0n.type !== "bundle") {
|
|
306
|
+
throw new Error("Not a valid .0n bundle file.");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
name: bundle.$0n.name,
|
|
311
|
+
description: bundle.$0n.description,
|
|
312
|
+
created: bundle.$0n.created,
|
|
313
|
+
version: bundle.$0n.version,
|
|
314
|
+
services: (bundle.connections || []).map(c => ({
|
|
315
|
+
service: c.service,
|
|
316
|
+
name: c.name,
|
|
317
|
+
sealed: c.sealed,
|
|
318
|
+
credential_keys: c.credential_keys,
|
|
319
|
+
})),
|
|
320
|
+
platforms: Object.keys(bundle.platforms || {}),
|
|
321
|
+
includes: (bundle.includes || []).map(i => ({
|
|
322
|
+
name: i.name,
|
|
323
|
+
type: i.type,
|
|
324
|
+
size: i.size,
|
|
325
|
+
})),
|
|
326
|
+
manifest: bundle.manifest,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Verify bundle integrity: checksums + passphrase test.
|
|
332
|
+
* @param {string} bundlePath
|
|
333
|
+
* @param {string} passphrase
|
|
334
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
335
|
+
*/
|
|
336
|
+
export function verifyBundle(bundlePath, passphrase) {
|
|
337
|
+
const raw = readFileSync(bundlePath, "utf-8");
|
|
338
|
+
const bundle = JSON.parse(raw);
|
|
339
|
+
const errors = [];
|
|
340
|
+
|
|
341
|
+
if (!bundle.$0n || bundle.$0n.type !== "bundle") {
|
|
342
|
+
return { valid: false, errors: ["Not a valid .0n bundle file."] };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Verify manifest checksums
|
|
346
|
+
const checksums = bundle.manifest?.checksums || {};
|
|
347
|
+
if (checksums.connections) {
|
|
348
|
+
const actual = sha256(JSON.stringify(bundle.connections));
|
|
349
|
+
const expected = checksums.connections.replace("sha256:", "");
|
|
350
|
+
if (actual !== expected) errors.push("Connections checksum mismatch");
|
|
351
|
+
}
|
|
352
|
+
if (checksums.platforms) {
|
|
353
|
+
const actual = sha256(JSON.stringify(bundle.platforms));
|
|
354
|
+
const expected = checksums.platforms.replace("sha256:", "");
|
|
355
|
+
if (actual !== expected) errors.push("Platforms checksum mismatch");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Try to unseal each connection
|
|
359
|
+
for (const conn of bundle.connections || []) {
|
|
360
|
+
if (conn.sealed && conn.vault?.data) {
|
|
361
|
+
try {
|
|
362
|
+
unsealPortable(conn.vault.data, passphrase);
|
|
363
|
+
} catch {
|
|
364
|
+
errors.push(`${conn.service}: Failed to unseal — wrong passphrase`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return { valid: errors.length === 0, errors };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Detect include file type from path.
|
|
374
|
+
*/
|
|
375
|
+
function detectIncludeType(filePath) {
|
|
376
|
+
if (filePath.endsWith(".0n")) return "workflow";
|
|
377
|
+
if (filePath.endsWith(".js") || filePath.endsWith(".mjs")) return "skill";
|
|
378
|
+
if (filePath.endsWith(".json")) return "config";
|
|
379
|
+
if (filePath.endsWith(".md")) return "doc";
|
|
380
|
+
return "file";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Detect target directory for include files.
|
|
385
|
+
*/
|
|
386
|
+
function detectTarget(filePath) {
|
|
387
|
+
const type = detectIncludeType(filePath);
|
|
388
|
+
const name = basename(filePath);
|
|
389
|
+
switch (type) {
|
|
390
|
+
case "workflow": return `~/.0n/workflows/${name}`;
|
|
391
|
+
case "skill": return `~/.0n/plugins/${name}`;
|
|
392
|
+
case "config": return `~/.0n/${name}`;
|
|
393
|
+
default: return `~/.0n/includes/${name}`;
|
|
394
|
+
}
|
|
395
|
+
}
|