@useconductor/conductor 1.0.0 → 2.0.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.
Files changed (145) hide show
  1. package/.github/README.md +374 -7
  2. package/.github/workflows/ci.yml +3 -1
  3. package/.github/workflows/claude-code-review.yml +1 -15
  4. package/.github/workflows/publish.yml +43 -0
  5. package/README.md +290 -121
  6. package/dist/cli/commands/audit.d.ts +40 -0
  7. package/dist/cli/commands/audit.d.ts.map +1 -0
  8. package/dist/cli/commands/audit.js +272 -0
  9. package/dist/cli/commands/audit.js.map +1 -0
  10. package/dist/cli/commands/circuit.d.ts +13 -0
  11. package/dist/cli/commands/circuit.d.ts.map +1 -0
  12. package/dist/cli/commands/circuit.js +53 -0
  13. package/dist/cli/commands/circuit.js.map +1 -0
  14. package/dist/cli/commands/config.d.ts +31 -0
  15. package/dist/cli/commands/config.d.ts.map +1 -0
  16. package/dist/cli/commands/config.js +152 -0
  17. package/dist/cli/commands/config.js.map +1 -0
  18. package/dist/cli/commands/init.d.ts +5 -8
  19. package/dist/cli/commands/init.d.ts.map +1 -1
  20. package/dist/cli/commands/init.js +86 -123
  21. package/dist/cli/commands/init.js.map +1 -1
  22. package/dist/cli/commands/marketplace.js +1 -1
  23. package/dist/cli/commands/onboard.d.ts.map +1 -1
  24. package/dist/cli/commands/onboard.js +33 -11
  25. package/dist/cli/commands/onboard.js.map +1 -1
  26. package/dist/cli/commands/release.d.ts.map +1 -1
  27. package/dist/cli/commands/release.js +1 -1
  28. package/dist/cli/commands/release.js.map +1 -1
  29. package/dist/cli/index.js +146 -10
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/core/audit.d.ts.map +1 -1
  32. package/dist/core/audit.js +5 -2
  33. package/dist/core/audit.js.map +1 -1
  34. package/dist/core/conductor.d.ts.map +1 -1
  35. package/dist/core/conductor.js +12 -0
  36. package/dist/core/conductor.js.map +1 -1
  37. package/dist/core/config.d.ts +3 -0
  38. package/dist/core/config.d.ts.map +1 -1
  39. package/dist/core/config.js +46 -2
  40. package/dist/core/config.js.map +1 -1
  41. package/dist/core/database.d.ts +3 -0
  42. package/dist/core/database.d.ts.map +1 -1
  43. package/dist/core/database.js +26 -0
  44. package/dist/core/database.js.map +1 -1
  45. package/dist/core/encryption.d.ts +34 -0
  46. package/dist/core/encryption.d.ts.map +1 -0
  47. package/dist/core/encryption.js +96 -0
  48. package/dist/core/encryption.js.map +1 -0
  49. package/dist/core/zero-config.d.ts.map +1 -1
  50. package/dist/core/zero-config.js +1 -4
  51. package/dist/core/zero-config.js.map +1 -1
  52. package/dist/dashboard/server.d.ts.map +1 -1
  53. package/dist/dashboard/server.js +112 -16
  54. package/dist/dashboard/server.js.map +1 -1
  55. package/dist/mcp/server.d.ts.map +1 -1
  56. package/dist/mcp/server.js +30 -2
  57. package/dist/mcp/server.js.map +1 -1
  58. package/dist/plugins/builtin/aws.d.ts +31 -0
  59. package/dist/plugins/builtin/aws.d.ts.map +1 -0
  60. package/dist/plugins/builtin/aws.js +149 -0
  61. package/dist/plugins/builtin/aws.js.map +1 -0
  62. package/dist/plugins/builtin/database.d.ts +1 -0
  63. package/dist/plugins/builtin/database.d.ts.map +1 -1
  64. package/dist/plugins/builtin/database.js +26 -1
  65. package/dist/plugins/builtin/database.js.map +1 -1
  66. package/dist/plugins/builtin/docker.d.ts +4 -0
  67. package/dist/plugins/builtin/docker.d.ts.map +1 -1
  68. package/dist/plugins/builtin/docker.js +20 -1
  69. package/dist/plugins/builtin/docker.js.map +1 -1
  70. package/dist/plugins/builtin/gcp.d.ts +28 -0
  71. package/dist/plugins/builtin/gcp.d.ts.map +1 -0
  72. package/dist/plugins/builtin/gcp.js +135 -0
  73. package/dist/plugins/builtin/gcp.js.map +1 -0
  74. package/dist/plugins/builtin/index.d.ts.map +1 -1
  75. package/dist/plugins/builtin/index.js +4 -0
  76. package/dist/plugins/builtin/index.js.map +1 -1
  77. package/dist/plugins/builtin/jira.d.ts.map +1 -1
  78. package/dist/plugins/builtin/jira.js +4 -2
  79. package/dist/plugins/builtin/jira.js.map +1 -1
  80. package/dist/plugins/builtin/linear.js +1 -1
  81. package/dist/plugins/builtin/linear.js.map +1 -1
  82. package/dist/plugins/builtin/shell.js +1 -1
  83. package/dist/plugins/builtin/shell.js.map +1 -1
  84. package/dist/plugins/builtin/slack.d.ts +1 -0
  85. package/dist/plugins/builtin/slack.d.ts.map +1 -1
  86. package/dist/plugins/builtin/slack.js +9 -1
  87. package/dist/plugins/builtin/slack.js.map +1 -1
  88. package/dist/plugins/builtin/spotify.js +1 -1
  89. package/dist/plugins/builtin/spotify.js.map +1 -1
  90. package/dist/plugins/builtin/vercel.d.ts.map +1 -1
  91. package/dist/plugins/builtin/vercel.js +3 -1
  92. package/dist/plugins/builtin/vercel.js.map +1 -1
  93. package/dist/security/sso.d.ts +37 -0
  94. package/dist/security/sso.d.ts.map +1 -0
  95. package/dist/security/sso.js +92 -0
  96. package/dist/security/sso.js.map +1 -0
  97. package/docs/deployment.md +201 -0
  98. package/docs/plugin-sdk.md +212 -0
  99. package/package.json +11 -8
  100. package/src/cli/commands/audit.ts +318 -0
  101. package/src/cli/commands/circuit.ts +63 -0
  102. package/src/cli/commands/config.ts +176 -0
  103. package/src/cli/commands/init.ts +87 -145
  104. package/src/cli/commands/marketplace.ts +1 -1
  105. package/src/cli/commands/onboard.ts +33 -11
  106. package/src/cli/commands/release.ts +13 -6
  107. package/src/cli/index.ts +165 -11
  108. package/src/core/audit.ts +5 -2
  109. package/src/core/conductor.ts +11 -0
  110. package/src/core/config.ts +47 -2
  111. package/src/core/database.ts +32 -0
  112. package/src/core/encryption.ts +110 -0
  113. package/src/core/zero-config.ts +1 -5
  114. package/src/dashboard/server.ts +135 -16
  115. package/src/mcp/server.ts +40 -2
  116. package/src/plugins/builtin/aws.ts +162 -0
  117. package/src/plugins/builtin/database.ts +19 -1
  118. package/src/plugins/builtin/docker.ts +17 -1
  119. package/src/plugins/builtin/gcp.ts +145 -0
  120. package/src/plugins/builtin/index.ts +4 -0
  121. package/src/plugins/builtin/jira.ts +23 -19
  122. package/src/plugins/builtin/linear.ts +1 -1
  123. package/src/plugins/builtin/shell.ts +1 -1
  124. package/src/plugins/builtin/slack.ts +6 -1
  125. package/src/plugins/builtin/spotify.ts +1 -1
  126. package/src/plugins/builtin/vercel.ts +3 -1
  127. package/src/security/sso.ts +124 -0
  128. package/tests/audit.test.ts +185 -0
  129. package/tests/circuit-breaker.test.ts +125 -0
  130. package/tests/docker.test.ts +244 -39
  131. package/tests/errors.test.ts +122 -0
  132. package/tests/github.test.ts.skip +392 -0
  133. package/tests/jira.test.ts +310 -0
  134. package/tests/linear.test.ts +366 -0
  135. package/tests/mcp.test.ts.skip +243 -0
  136. package/tests/notion.test.ts +257 -0
  137. package/tests/retry.test.ts +104 -0
  138. package/tests/shell.test.ts +262 -30
  139. package/tests/slack.test.ts +250 -0
  140. package/tests/stripe.test.ts +272 -0
  141. package/tests/validation.test.ts +173 -0
  142. package/tests/vercel.test.ts +368 -0
  143. package/tests/zero-config.test.ts +566 -0
  144. package/C.png +0 -0
  145. package/tests/mcp.test.ts +0 -14
@@ -52,9 +52,16 @@ async function run(cmd: string, args: string[], cwd?: string): Promise<string> {
52
52
 
53
53
  function bumpVersion(current: string, bump: 'patch' | 'minor' | 'major'): string {
54
54
  const parts = current.replace(/^v/, '').split('.').map(Number);
55
- if (bump === 'major') { parts[0]++; parts[1] = 0; parts[2] = 0; }
56
- else if (bump === 'minor') { parts[1]++; parts[2] = 0; }
57
- else { parts[2]++; }
55
+ if (bump === 'major') {
56
+ parts[0]++;
57
+ parts[1] = 0;
58
+ parts[2] = 0;
59
+ } else if (bump === 'minor') {
60
+ parts[1]++;
61
+ parts[2] = 0;
62
+ } else {
63
+ parts[2]++;
64
+ }
58
65
  return parts.join('.');
59
66
  }
60
67
 
@@ -168,7 +175,7 @@ export async function release(): Promise<void> {
168
175
  try {
169
176
  await run('npx', ['vitest', 'run', '--reporter=dot']);
170
177
  ok();
171
- } catch (e: any) {
178
+ } catch {
172
179
  const { skipTests } = await inquirer.prompt<{ skipTests: boolean }>([
173
180
  {
174
181
  type: 'confirm',
@@ -241,8 +248,8 @@ export async function release(): Promise<void> {
241
248
  console.log('');
242
249
  console.log(
243
250
  chalk.bold.white(` ✓ Published `) +
244
- chalk.hex('#FF8C00')(`${pkg.name}@${newVersion}`) +
245
- chalk.bold.white(` to npm`),
251
+ chalk.hex('#FF8C00')(`${pkg.name}@${newVersion}`) +
252
+ chalk.bold.white(` to npm`),
246
253
  );
247
254
  console.log('');
248
255
  console.log(chalk.dim(` npm i -g ${pkg.name}`));
package/src/cli/index.ts CHANGED
@@ -10,7 +10,7 @@ const _require = createRequire(import.meta.url);
10
10
  const { version: pkgVersion } = _require('../../package.json') as { version: string };
11
11
 
12
12
  const program = new Command();
13
- const conductor = new Conductor();
13
+ const conductor = new Conductor(undefined, { quiet: true });
14
14
 
15
15
  program
16
16
  .name('conductor')
@@ -161,6 +161,15 @@ function registerPluginCommands(parent: Command, cmdName: string): void {
161
161
  const { onboard } = await import('./commands/onboard.js');
162
162
  await onboard(conductor);
163
163
  });
164
+
165
+ cmd
166
+ .command('create')
167
+ .argument('<name>', 'Plugin name')
168
+ .description('Scaffold a new plugin with tests')
169
+ .action(async (name: string) => {
170
+ const { pluginCreate } = await import('./commands/plugin-create.js');
171
+ await pluginCreate(name);
172
+ });
164
173
  }
165
174
 
166
175
  registerPluginCommands(program, 'plugins');
@@ -374,16 +383,6 @@ program
374
383
  await doctor(conductor);
375
384
  });
376
385
 
377
- // ── Plugin Create ─────────────────────────────────────────────────────
378
- program
379
- .command('plugin create')
380
- .argument('<name>', 'Plugin name')
381
- .description('Scaffold a new plugin with tests')
382
- .action(async (name: string) => {
383
- const { pluginCreate } = await import('./commands/plugin-create.js');
384
- await pluginCreate(name);
385
- });
386
-
387
386
  // ── Health ────────────────────────────────────────────────────────────
388
387
  program
389
388
  .command('health')
@@ -446,5 +445,160 @@ program
446
445
  await release();
447
446
  });
448
447
 
448
+ // ── Config ────────────────────────────────────────────────────────────────────
449
+ const config = program.command('config').description('Read and write configuration');
450
+
451
+ config
452
+ .command('list')
453
+ .description('Show all configuration keys and values')
454
+ .option('--json', 'Output as JSON')
455
+ .option('--show-secrets', 'Include secret values (use with care)')
456
+ .action(async (opts: { json?: boolean; showSecrets?: boolean }) => {
457
+ const { configList } = await import('./commands/config.js');
458
+ await configList(conductor, { json: opts.json, show_secrets: opts.showSecrets });
459
+ });
460
+
461
+ config
462
+ .command('get')
463
+ .argument('<key>', 'Dot-separated config key, e.g. ai.provider')
464
+ .description('Get a configuration value')
465
+ .option('--json', 'Output as JSON')
466
+ .action(async (key: string, opts: { json?: boolean }) => {
467
+ const { configGet } = await import('./commands/config.js');
468
+ await configGet(conductor, key, opts);
469
+ });
470
+
471
+ config
472
+ .command('set')
473
+ .argument('<key>', 'Dot-separated config key')
474
+ .argument('<value>', 'Value to set (JSON or string)')
475
+ .description('Set a configuration value')
476
+ .action(async (key: string, value: string) => {
477
+ const { configSet } = await import('./commands/config.js');
478
+ await configSet(conductor, key, value);
479
+ });
480
+
481
+ config
482
+ .command('path')
483
+ .description('Print the config file path')
484
+ .action(async () => {
485
+ const { configPath } = await import('./commands/config.js');
486
+ await configPath(conductor);
487
+ });
488
+
489
+ config
490
+ .command('export')
491
+ .description('Export configuration as JSON')
492
+ .option('-o, --output <file>', 'Output file (default: stdout)')
493
+ .action(async (opts: { output?: string }) => {
494
+ const { configExport } = await import('./commands/config.js');
495
+ await configExport(conductor, { output: opts.output });
496
+ });
497
+
498
+ config
499
+ .command('reset')
500
+ .description('Reset configuration to defaults')
501
+ .option('-y, --yes', 'Skip confirmation prompt')
502
+ .action(async (opts: { yes?: boolean }) => {
503
+ const { configReset } = await import('./commands/config.js');
504
+ await configReset(conductor, opts);
505
+ });
506
+
507
+ config
508
+ .command('validate')
509
+ .description('Validate configuration structure')
510
+ .action(async () => {
511
+ const { configValidate } = await import('./commands/config.js');
512
+ await configValidate(conductor);
513
+ });
514
+
515
+ // ── Audit ─────────────────────────────────────────────────────────────────────
516
+ const audit = program.command('audit').description('Query and verify the tamper-evident audit log');
517
+
518
+ audit
519
+ .command('list')
520
+ .description('List audit log entries')
521
+ .option('--actor <actor>', 'Filter by actor')
522
+ .option('--action <action>', 'Filter by action (tool_call, config_set, auth_login, ...)')
523
+ .option('--tool <tool>', 'Filter by tool/resource name')
524
+ .option('--result <result>', 'Filter by result (success, failure, denied, timeout)')
525
+ .option('--since <iso>', 'Show entries after this ISO timestamp')
526
+ .option('--until <iso>', 'Show entries before this ISO timestamp')
527
+ .option('-n, --limit <n>', 'Max entries to show', '100')
528
+ .option('--json', 'Output as JSON')
529
+ .action(async (opts) => {
530
+ const { auditList } = await import('./commands/audit.js');
531
+ await auditList(conductor, opts);
532
+ });
533
+
534
+ audit
535
+ .command('verify')
536
+ .description('Verify SHA-256 chain integrity — detect tampering')
537
+ .option('--json', 'Output as JSON')
538
+ .action(async (opts: { json?: boolean }) => {
539
+ const { auditVerify } = await import('./commands/audit.js');
540
+ await auditVerify(conductor, opts);
541
+ });
542
+
543
+ audit
544
+ .command('tail')
545
+ .description('Stream the audit log in real time')
546
+ .option('-n, --lines <n>', 'Initial lines to show', '20')
547
+ .option('--json', 'Output as NDJSON')
548
+ .action(async (opts: { lines?: string; json?: boolean }) => {
549
+ const { auditTail } = await import('./commands/audit.js');
550
+ await auditTail(conductor, opts);
551
+ });
552
+
553
+ audit
554
+ .command('export')
555
+ .description('Export audit log entries to a file')
556
+ .option('-o, --output <file>', 'Output file path')
557
+ .option('--format <fmt>', 'Output format: json or ndjson', 'json')
558
+ .option('--since <iso>', 'Export entries after this ISO timestamp')
559
+ .option('--until <iso>', 'Export entries before this ISO timestamp')
560
+ .action(async (opts) => {
561
+ const { auditExport } = await import('./commands/audit.js');
562
+ await auditExport(conductor, opts);
563
+ });
564
+
565
+ audit
566
+ .command('stats')
567
+ .description('Show audit log summary statistics')
568
+ .option('--json', 'Output as JSON')
569
+ .action(async (opts: { json?: boolean }) => {
570
+ const { auditStats } = await import('./commands/audit.js');
571
+ await auditStats(conductor, opts);
572
+ });
573
+
574
+ audit
575
+ .command('rotate')
576
+ .description('Manually rotate the current audit log file')
577
+ .action(async () => {
578
+ const { auditRotate } = await import('./commands/audit.js');
579
+ await auditRotate(conductor);
580
+ });
581
+
582
+ // ── Circuit ───────────────────────────────────────────────────────────────────
583
+ const circuit = program.command('circuit').description('View and manage circuit breaker state');
584
+
585
+ circuit
586
+ .command('list')
587
+ .description('Show state of all circuit breakers')
588
+ .option('--json', 'Output as JSON')
589
+ .action(async (opts: { json?: boolean }) => {
590
+ const { circuitList } = await import('./commands/circuit.js');
591
+ await circuitList(conductor, opts);
592
+ });
593
+
594
+ circuit
595
+ .command('reset')
596
+ .argument('<tool>', 'Tool name to reset (e.g. shell.exec)')
597
+ .description('Reset a circuit breaker to closed state')
598
+ .action(async (tool: string) => {
599
+ const { circuitReset } = await import('./commands/circuit.js');
600
+ await circuitReset(conductor, tool);
601
+ });
602
+
449
603
  // ── Run ──────────────────────────────────────────────────────────────
450
604
  program.parse();
package/src/core/audit.ts CHANGED
@@ -200,10 +200,13 @@ export class AuditLogger {
200
200
  for (const line of lines) {
201
201
  const entry = JSON.parse(line) as AuditEntry;
202
202
 
203
- // Reconstruct what the hash should be
203
+ // Reconstruct what the hash should be — must match log() exactly:
204
+ // log() hashes: sha256(lastHash + JSON.stringify({...entryFields, timestamp, previousHash: ''}))
205
+ // where entryFields = {actor, action, resource, result, metadata} (no hash field)
206
+ const { hash: _hash, previousHash: _prev, ...entryFields } = entry;
204
207
  const expectedHash = crypto
205
208
  .createHash('sha256')
206
- .update(prevHash + JSON.stringify({ ...entry, previousHash: entry.previousHash }))
209
+ .update(prevHash + JSON.stringify({ ...entryFields, previousHash: '' }))
207
210
  .digest('hex');
208
211
 
209
212
  if (entry.hash !== expectedHash) {
@@ -1,3 +1,4 @@
1
+ import path from 'path';
1
2
  import { ConfigManager } from './config.js';
2
3
  import { DatabaseManager } from './database.js';
3
4
  import { PluginManager } from '../plugins/manager.js';
@@ -39,6 +40,16 @@ export class Conductor {
39
40
  await this.config.initialize();
40
41
  await this.db.initialize();
41
42
  await this.plugins.loadBuiltins();
43
+
44
+ // Reset audit log on fresh init (avoid tampered chain issues)
45
+ const { default: fs } = await import('fs/promises');
46
+ const auditLog = path.join(this.config.getConfigDir(), 'audit', 'audit.log');
47
+ try {
48
+ const stat = await fs.stat(auditLog);
49
+ if (stat.size === 0) await fs.writeFile(auditLog, '', 'utf-8');
50
+ } catch {
51
+ // First run
52
+ }
42
53
 
43
54
  this.initialized = true;
44
55
 
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { homedir } from 'os';
4
+ import { EncryptionManager } from './encryption.js';
4
5
 
5
6
  export interface ConductorConfig {
6
7
  user?: {
@@ -59,11 +60,13 @@ export class ConfigManager {
59
60
  private configDir: string;
60
61
  private configPath: string;
61
62
  private config: ConductorConfig;
63
+ private encryption: EncryptionManager;
62
64
 
63
65
  constructor(customPath?: string) {
64
66
  this.configDir = customPath || path.join(homedir(), '.conductor');
65
67
  this.configPath = path.join(this.configDir, 'config.json');
66
68
  this.config = {};
69
+ this.encryption = new EncryptionManager(this.configDir);
67
70
  }
68
71
 
69
72
  async initialize(): Promise<void> {
@@ -80,10 +83,51 @@ export class ConfigManager {
80
83
  await this.load();
81
84
  }
82
85
 
86
+ // Encrypt sensitive values before saving
87
+ private async encryptSensitive(obj: Record<string, unknown>): Promise<Record<string, unknown>> {
88
+ const SENSITIVE_KEYS = ['key', 'secret', 'token', 'password', 'api_key', 'access_token'];
89
+ const result: Record<string, unknown> = {};
90
+
91
+ for (const [key, value] of Object.entries(obj)) {
92
+ if (typeof value === 'string' && SENSITIVE_KEYS.some((k) => key.toLowerCase().includes(k))) {
93
+ result[key] = await this.encryption.encrypt(value);
94
+ } else if (typeof value === 'object') {
95
+ result[key] = await this.encryptSensitive(value as Record<string, unknown>);
96
+ } else {
97
+ result[key] = value;
98
+ }
99
+ }
100
+
101
+ return result;
102
+ }
103
+
104
+ // Decrypt sensitive values after loading
105
+ private async decryptSensitive(obj: Record<string, unknown>): Promise<Record<string, unknown>> {
106
+ const SENSITIVE_KEYS = ['key', 'secret', 'token', 'password', 'api_key', 'access_token'];
107
+ const result: Record<string, unknown> = {};
108
+
109
+ for (const [key, value] of Object.entries(obj)) {
110
+ if (typeof value === 'string' && SENSITIVE_KEYS.some((k) => key.toLowerCase().includes(k))) {
111
+ try {
112
+ result[key] = await this.encryption.decrypt(value);
113
+ } catch {
114
+ result[key] = value; // Not encrypted, use as-is
115
+ }
116
+ } else if (typeof value === 'object') {
117
+ result[key] = await this.decryptSensitive(value as Record<string, unknown>);
118
+ } else {
119
+ result[key] = value;
120
+ }
121
+ }
122
+
123
+ return result;
124
+ }
125
+
83
126
  async load(): Promise<void> {
84
127
  try {
85
128
  const data = await fs.readFile(this.configPath, 'utf-8');
86
- this.config = JSON.parse(data);
129
+ const parsed = JSON.parse(data);
130
+ this.config = await this.decryptSensitive(parsed) as ConductorConfig;
87
131
  } catch {
88
132
  // Config doesn't exist yet — use defaults
89
133
  this.config = {
@@ -106,8 +150,9 @@ export class ConfigManager {
106
150
  }
107
151
 
108
152
  async save(): Promise<void> {
153
+ const encrypted = await this.encryptSensitive(this.config as unknown as Record<string, unknown>);
109
154
  const tmp = this.configPath + '.tmp';
110
- await fs.writeFile(tmp, JSON.stringify(this.config, null, 2), 'utf-8');
155
+ await fs.writeFile(tmp, JSON.stringify(encrypted, null, 2), 'utf-8');
111
156
  await fs.rename(tmp, this.configPath);
112
157
  }
113
158
 
@@ -92,6 +92,38 @@ export class DatabaseManager {
92
92
  this.db = new SQL.Database();
93
93
  await this.createTables();
94
94
  }
95
+
96
+ // Always run migrations — safe to run on both new and existing databases
97
+ this.runMigrations();
98
+ }
99
+
100
+ /** Current schema version — bump this whenever tables change. */
101
+ private static readonly SCHEMA_VERSION = 1;
102
+
103
+ private runMigrations(): void {
104
+ if (!this.db) throw new Error('Database not initialized');
105
+
106
+ // Ensure schema_version table exists
107
+ this.db.run(`
108
+ CREATE TABLE IF NOT EXISTS schema_version (
109
+ version INTEGER NOT NULL,
110
+ applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
111
+ )
112
+ `);
113
+
114
+ const stmt = this.db.prepare('SELECT MAX(version) AS v FROM schema_version');
115
+ stmt.step();
116
+ const row = stmt.getAsObject() as { v: number | null };
117
+ stmt.free();
118
+ const current = row.v ?? 0;
119
+
120
+ if (current < 1) {
121
+ // Migration 1: initial schema (all existing tables)
122
+ this.db.run(`INSERT INTO schema_version (version) VALUES (1)`);
123
+ }
124
+
125
+ // Future migrations go here:
126
+ // if (current < 2) { this.db.run(`ALTER TABLE ...`); this.db.run(`INSERT INTO schema_version (version) VALUES (2)`); }
95
127
  }
96
128
 
97
129
  private async createTables(): Promise<void> {
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Encryption at rest for sensitive config data.
3
+ * Uses AES-256-GCM with a machine-derived key.
4
+ */
5
+
6
+ import crypto from 'crypto';
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import os from 'os';
10
+
11
+ const ALGORITHM = 'aes-256-gcm';
12
+ const KEY_LENGTH = 32;
13
+ const IV_LENGTH = 12;
14
+ const AUTH_TAG_LENGTH = 16;
15
+
16
+ /**
17
+ * Derive an encryption key from machine-specific data.
18
+ * The key is stored in the config directory and tied to the machine.
19
+ */
20
+ export class EncryptionManager {
21
+ private keyPath: string;
22
+ private key: Buffer | null = null;
23
+
24
+ constructor(configDir: string) {
25
+ this.keyPath = path.join(configDir, '.key');
26
+ }
27
+
28
+ /**
29
+ * Get or create the encryption key.
30
+ */
31
+ private async getKey(): Promise<Buffer> {
32
+ if (this.key) return this.key;
33
+
34
+ try {
35
+ const existing = await fs.readFile(this.keyPath);
36
+ if (existing.length === KEY_LENGTH) {
37
+ this.key = existing;
38
+ return this.key;
39
+ }
40
+ } catch {
41
+ /* key doesn't exist - generate new one */
42
+ }
43
+
44
+ // Generate new key
45
+ this.key = crypto.randomBytes(KEY_LENGTH);
46
+ await fs.writeFile(this.keyPath, this.key, { mode: 0o600 });
47
+ return this.key;
48
+ }
49
+
50
+ /**
51
+ * Encrypt data using AES-256-GCM.
52
+ */
53
+ async encrypt(plaintext: string): Promise<string> {
54
+ const key = await this.getKey();
55
+ const iv = crypto.randomBytes(IV_LENGTH);
56
+
57
+ const cipher = crypto.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
+ // Format: iv + authTag + ciphertext (all base64)
65
+ const result = Buffer.concat([iv, authTag, encrypted]);
66
+ return result.toString('base64');
67
+ }
68
+
69
+ /**
70
+ * Decrypt data using AES-256-GCM.
71
+ */
72
+ async decrypt(ciphertext: string): Promise<string> {
73
+ const key = await this.getKey();
74
+ const data = Buffer.from(ciphertext, 'base64');
75
+
76
+ const iv = data.subarray(0, IV_LENGTH);
77
+ const authTag = data.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
78
+ const encrypted = data.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
79
+
80
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
81
+ decipher.setAuthTag(authTag);
82
+
83
+ const decrypted = Buffer.concat([
84
+ decipher.update(encrypted),
85
+ decipher.final(),
86
+ ]);
87
+
88
+ return decrypted.toString('utf8');
89
+ }
90
+
91
+ /**
92
+ * Check if encryption has been initialized.
93
+ */
94
+ async isInitialized(): Promise<boolean> {
95
+ try {
96
+ await fs.access(this.keyPath);
97
+ return true;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Encrypt a value if it looks sensitive.
106
+ */
107
+ export function looksEncrypted(value: string): boolean {
108
+ // Base64 strings that look like encryption output
109
+ return /^[A-Za-z0-9+/]{40,}={0,2}$/.test(value);
110
+ }
@@ -32,12 +32,8 @@ export const ZERO_CONFIG_PLUGINS = [
32
32
 
33
33
  // Infrastructure - local tools
34
34
  'shell', // Safe shell commands (whitelist)
35
- 'docker', // Local Docker (if installed)
36
35
 
37
- // GitHub - public data works without auth
38
- 'github', // Public repos, user info
39
-
40
- // Weather - uses free Open-Meteo API
36
+ // Weather - uses free Open-Meteo API (no key required)
41
37
  'weather', // Weather/current forecasts
42
38
 
43
39
  // File system tools are built into Shell plugin