lsh-framework 3.2.5 → 3.5.1

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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -34
  3. package/dist/commands/ipfs.js +7 -12
  4. package/dist/commands/self.js +22 -16
  5. package/dist/commands/sync.js +49 -38
  6. package/dist/constants/config.js +3 -0
  7. package/dist/lib/floating-point-arithmetic.js +2 -2
  8. package/dist/lib/ipfs-client-manager.js +51 -13
  9. package/dist/lib/ipfs-secrets-storage.js +21 -16
  10. package/dist/lib/ipfs-sync.js +88 -14
  11. package/dist/lib/secrets-manager.js +117 -47
  12. package/dist/lib/sync-key-store.js +87 -0
  13. package/dist/services/secrets/secrets.js +77 -39
  14. package/package.json +16 -16
  15. package/dist/__tests__/fixtures/job-fixtures.js +0 -204
  16. package/dist/__tests__/fixtures/supabase-mocks.js +0 -252
  17. package/dist/daemon/job-registry.js +0 -556
  18. package/dist/daemon/lshd.js +0 -968
  19. package/dist/daemon/saas-api-routes.js +0 -599
  20. package/dist/daemon/saas-api-server.js +0 -231
  21. package/dist/examples/supabase-integration.js +0 -106
  22. package/dist/lib/api-response.js +0 -226
  23. package/dist/lib/base-command-registrar.js +0 -287
  24. package/dist/lib/base-job-manager.js +0 -295
  25. package/dist/lib/cloud-config-manager.js +0 -348
  26. package/dist/lib/cron-job-manager.js +0 -368
  27. package/dist/lib/daemon-client-helper.js +0 -145
  28. package/dist/lib/daemon-client.js +0 -513
  29. package/dist/lib/database-persistence.js +0 -727
  30. package/dist/lib/database-schema.js +0 -259
  31. package/dist/lib/database-types.js +0 -90
  32. package/dist/lib/enhanced-history-system.js +0 -247
  33. package/dist/lib/history-system.js +0 -246
  34. package/dist/lib/job-manager.js +0 -436
  35. package/dist/lib/job-storage-database.js +0 -164
  36. package/dist/lib/job-storage-memory.js +0 -73
  37. package/dist/lib/local-storage-adapter.js +0 -507
  38. package/dist/lib/optimized-job-scheduler.js +0 -356
  39. package/dist/lib/saas-audit.js +0 -215
  40. package/dist/lib/saas-auth.js +0 -465
  41. package/dist/lib/saas-billing.js +0 -503
  42. package/dist/lib/saas-email.js +0 -403
  43. package/dist/lib/saas-encryption.js +0 -221
  44. package/dist/lib/saas-organizations.js +0 -662
  45. package/dist/lib/saas-secrets.js +0 -408
  46. package/dist/lib/saas-types.js +0 -165
  47. package/dist/lib/supabase-client.js +0 -125
  48. package/dist/lib/supabase-utils.js +0 -396
  49. package/dist/services/cron/cron-registrar.js +0 -240
  50. package/dist/services/cron/cron.js +0 -9
  51. package/dist/services/daemon/daemon-registrar.js +0 -585
  52. package/dist/services/daemon/daemon.js +0 -9
  53. package/dist/services/supabase/supabase-registrar.js +0 -375
  54. package/dist/services/supabase/supabase.js +0 -9
@@ -3,6 +3,7 @@
3
3
  * Sync .env files across machines using encrypted Supabase storage
4
4
  */
5
5
  import * as fs from 'fs';
6
+ import * as os from 'os';
6
7
  import * as path from 'path';
7
8
  import * as crypto from 'crypto';
8
9
  import { createLogger, LogLevel } from './logger.js';
@@ -10,7 +11,53 @@ import { getGitRepoInfo, hasEnvExample, ensureEnvInGitignore } from './git-utils
10
11
  import { IPFSSyncLogger } from './ipfs-sync-logger.js';
11
12
  import { IPFSSecretsStorage } from './ipfs-secrets-storage.js';
12
13
  import { ENV_VARS } from '../constants/index.js';
14
+ import { extractErrorMessage } from './lsh-error.js';
15
+ import { SyncKeyStore } from './sync-key-store.js';
13
16
  const logger = createLogger('SecretsManager');
17
+ /**
18
+ * Read LSH_SECRETS_KEY from a .env file without loading the entire file into process.env
19
+ */
20
+ function readKeyFromEnvFile(envPath) {
21
+ try {
22
+ if (fs.existsSync(envPath)) {
23
+ const content = fs.readFileSync(envPath, 'utf8');
24
+ const match = content.match(/^LSH_SECRETS_KEY=['"]?([^'"\n]+)['"]?/m);
25
+ if (match)
26
+ return match[1];
27
+ }
28
+ }
29
+ catch {
30
+ // Ignore read errors
31
+ }
32
+ return null;
33
+ }
34
+ /**
35
+ * Find LSH_SECRETS_KEY from environment, local .env, global ~/.env,
36
+ * or the persistent SyncKeyStore at $LSH_HOME/sync_key.json.
37
+ * Returns null if no explicit key is found (does not generate a fallback).
38
+ */
39
+ export function findEncryptionKey() {
40
+ // 1. Check environment variable
41
+ const envKey = process.env[ENV_VARS.LSH_SECRETS_KEY];
42
+ if (envKey)
43
+ return envKey;
44
+ // 2. Check local .env
45
+ const localKey = readKeyFromEnvFile(path.join(process.cwd(), '.env'));
46
+ if (localKey)
47
+ return localKey;
48
+ // 3. Check global ~/.env
49
+ const home = process.env[ENV_VARS.HOME] || process.env[ENV_VARS.USERPROFILE] || '';
50
+ if (home) {
51
+ const globalKey = readKeyFromEnvFile(path.join(home, '.env'));
52
+ if (globalKey)
53
+ return globalKey;
54
+ }
55
+ // 4. Check persistent sync key store ($LSH_HOME/sync_key.json)
56
+ const stored = new SyncKeyStore().get();
57
+ if (stored)
58
+ return stored;
59
+ return null;
60
+ }
14
61
  export class SecretsManager {
15
62
  storage;
16
63
  encryptionKey;
@@ -73,57 +120,85 @@ export class SecretsManager {
73
120
  * Get default encryption key from environment or machine
74
121
  */
75
122
  getDefaultEncryptionKey() {
76
- // Check for explicit key
77
- const envKey = process.env[ENV_VARS.LSH_SECRETS_KEY];
78
- if (envKey) {
79
- return envKey;
80
- }
81
- // Generate from machine ID and user
82
- const machineId = process.env[ENV_VARS.HOSTNAME] || 'localhost';
83
- const user = process.env[ENV_VARS.USER] || 'unknown';
84
- const seed = `${machineId}-${user}-lsh-secrets`;
123
+ // Check environment, local .env, and global ~/.env
124
+ const explicitKey = findEncryptionKey();
125
+ if (explicitKey)
126
+ return explicitKey;
127
+ logger.warn('No explicit LSH_SECRETS_KEY found. Generating machine-derived fallback key.');
128
+ logger.warn('Set LSH_SECRETS_KEY in your environment for portable, secure encryption.');
129
+ // Generate from multiple machine-specific entropy sources
130
+ const hostname = os.hostname();
131
+ const uid = String(os.userInfo().uid);
132
+ const homedir = os.homedir();
133
+ // Try to read /etc/machine-id (Linux) for additional entropy
134
+ let machineSpecific;
135
+ try {
136
+ machineSpecific = fs.readFileSync('/etc/machine-id', 'utf8').trim();
137
+ }
138
+ catch {
139
+ // Fallback to CPU model on macOS / systems without machine-id
140
+ machineSpecific = os.cpus()[0]?.model || 'no-cpu-info';
141
+ }
142
+ const seed = `${hostname}-${uid}-${homedir}-${machineSpecific}-lsh-secrets`;
85
143
  // Create deterministic key
86
144
  return crypto.createHash('sha256').update(seed).digest('hex');
87
145
  }
88
146
  /**
89
- * Encrypt a value
147
+ * Encrypt a value using AES-256-GCM with authentication tag
90
148
  */
91
149
  encrypt(text) {
92
- const iv = crypto.randomBytes(16);
150
+ const iv = crypto.randomBytes(12); // 96-bit IV recommended for GCM
93
151
  const key = Buffer.from(this.encryptionKey, 'hex');
94
- const cipher = crypto.createCipheriv('aes-256-cbc', key.slice(0, 32), iv);
152
+ const cipher = crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
95
153
  let encrypted = cipher.update(text, 'utf8', 'hex');
96
154
  encrypted += cipher.final('hex');
97
- return iv.toString('hex') + ':' + encrypted;
155
+ const authTag = cipher.getAuthTag();
156
+ // Format: iv:authTag:encrypted (3-part GCM format)
157
+ return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
98
158
  }
99
159
  /**
100
- * Decrypt a value
160
+ * Decrypt a value (supports both AES-256-GCM and legacy AES-256-CBC)
101
161
  */
102
162
  decrypt(text) {
103
163
  try {
104
164
  const parts = text.split(':');
105
- if (parts.length !== 2) {
165
+ const key = Buffer.from(this.encryptionKey, 'hex');
166
+ if (parts.length === 3) {
167
+ // AES-256-GCM format: iv:authTag:encrypted
168
+ const iv = Buffer.from(parts[0], 'hex');
169
+ const authTag = Buffer.from(parts[1], 'hex');
170
+ const encryptedText = parts[2];
171
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), iv);
172
+ decipher.setAuthTag(authTag);
173
+ let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
174
+ decrypted += decipher.final('utf8');
175
+ return decrypted;
176
+ }
177
+ else if (parts.length === 2) {
178
+ // Legacy AES-256-CBC format: iv:encrypted
179
+ const iv = Buffer.from(parts[0], 'hex');
180
+ const encryptedText = parts[1];
181
+ const decipher = crypto.createDecipheriv('aes-256-cbc', key.slice(0, 32), iv);
182
+ let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
183
+ decrypted += decipher.final('utf8');
184
+ return decrypted;
185
+ }
186
+ else {
106
187
  throw new Error('Invalid encrypted format');
107
188
  }
108
- const iv = Buffer.from(parts[0], 'hex');
109
- const encryptedText = parts[1];
110
- const key = Buffer.from(this.encryptionKey, 'hex');
111
- const decipher = crypto.createDecipheriv('aes-256-cbc', key.slice(0, 32), iv);
112
- let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
113
- decrypted += decipher.final('utf8');
114
- return decrypted;
115
189
  }
116
190
  catch (error) {
117
- const err = error;
118
- if (err.message.includes('bad decrypt') || err.message.includes('wrong final block length')) {
191
+ const msg = extractErrorMessage(error);
192
+ if (msg.includes('bad decrypt') || msg.includes('wrong final block length') ||
193
+ msg.includes('Unsupported state') || msg.includes('unable to authenticate')) {
119
194
  throw new Error('Decryption failed. This usually means:\n' +
120
195
  ' 1. You need to set LSH_SECRETS_KEY environment variable\n' +
121
196
  ' 2. The key must match the one used during encryption\n' +
122
197
  ' 3. Generate a shared key with: lsh secrets key\n' +
123
198
  ' 4. Add it to your .env: LSH_SECRETS_KEY=<key>\n' +
124
- '\nOriginal error: ' + err.message);
199
+ '\nOriginal error: ' + msg, { cause: error });
125
200
  }
126
- throw err;
201
+ throw error;
127
202
  }
128
203
  }
129
204
  /**
@@ -243,8 +318,8 @@ export class SecretsManager {
243
318
  }
244
319
  // Get the effective environment name (repo-aware)
245
320
  const effectiveEnv = this.getRepoAwareEnvironment(environment);
246
- // Warn if using default key
247
- if (!process.env[ENV_VARS.LSH_SECRETS_KEY]) {
321
+ // Warn if using machine-derived fallback key (no explicit key found anywhere)
322
+ if (!findEncryptionKey()) {
248
323
  logger.warn('⚠️ Warning: No LSH_SECRETS_KEY set. Using machine-specific key.');
249
324
  logger.warn(' To share secrets across machines, generate a key with: lsh key');
250
325
  logger.warn(' Then add LSH_SECRETS_KEY=<key> to your .env on all machines');
@@ -277,10 +352,9 @@ export class SecretsManager {
277
352
  }
278
353
  }
279
354
  catch (error) {
280
- const err = error;
281
355
  // Re-throw destructive change errors
282
- if (err.message.includes('Destructive change')) {
283
- throw err;
356
+ if (extractErrorMessage(error).includes('Destructive change')) {
357
+ throw error;
284
358
  }
285
359
  // Ignore other errors (like missing secrets) and proceed
286
360
  }
@@ -357,8 +431,8 @@ export class SecretsManager {
357
431
  const finalEnv = { ...pulledEnv, ...localLshKeys };
358
432
  // Convert to .env format
359
433
  const envContent = this.formatEnvFile(finalEnv);
360
- // Write new .env
361
- fs.writeFileSync(envFilePath, envContent, 'utf8');
434
+ // Write new .env with restrictive permissions (owner read/write only)
435
+ fs.writeFileSync(envFilePath, envContent, { encoding: 'utf8', mode: 0o600 });
362
436
  logger.info(`✅ Pulled ${secrets.length} secrets from IPFS`);
363
437
  // Get metadata for CID display
364
438
  const metadata = this.storage.getMetadata(environment, this.gitInfo?.repoName);
@@ -430,7 +504,7 @@ export class SecretsManager {
430
504
  cloudExists: false,
431
505
  cloudKeys: 0,
432
506
  cloudModified: undefined,
433
- keySet: !!process.env[ENV_VARS.LSH_SECRETS_KEY],
507
+ keySet: !!findEncryptionKey(),
434
508
  keyMatches: undefined,
435
509
  suggestions: [],
436
510
  };
@@ -518,7 +592,7 @@ export class SecretsManager {
518
592
  * Generate encryption key if not set
519
593
  */
520
594
  async ensureEncryptionKey() {
521
- if (process.env[ENV_VARS.LSH_SECRETS_KEY]) {
595
+ if (findEncryptionKey()) {
522
596
  return true; // Key already set
523
597
  }
524
598
  logger.warn('⚠️ No encryption key found. Generating a new key...');
@@ -540,7 +614,7 @@ export class SecretsManager {
540
614
  else {
541
615
  content += `\n# LSH Secrets Encryption Key (do not commit!)\nLSH_SECRETS_KEY=${key}\n`;
542
616
  }
543
- fs.writeFileSync(envPath, content, 'utf8');
617
+ fs.writeFileSync(envPath, content, { encoding: 'utf8', mode: 0o600 });
544
618
  // Set in current process
545
619
  process.env[ENV_VARS.LSH_SECRETS_KEY] = key;
546
620
  this.encryptionKey = key;
@@ -549,8 +623,7 @@ export class SecretsManager {
549
623
  return true;
550
624
  }
551
625
  catch (error) {
552
- const _err = error;
553
- logger.error(`Failed to save encryption key: ${_err.message}`);
626
+ logger.error(`Failed to save encryption key: ${extractErrorMessage(error)}`);
554
627
  logger.info('Please set it manually:');
555
628
  logger.info(`export LSH_SECRETS_KEY=${key}`);
556
629
  return false;
@@ -581,13 +654,12 @@ LSH_SECRETS_KEY=${this.encryptionKey}
581
654
  # Add your environment variables below
582
655
  `;
583
656
  try {
584
- fs.writeFileSync(envFilePath, template, 'utf8');
657
+ fs.writeFileSync(envFilePath, template, { encoding: 'utf8', mode: 0o600 });
585
658
  logger.info(`✅ Created ${envFilePath} from template`);
586
659
  return true;
587
660
  }
588
661
  catch (error) {
589
- const _err = error;
590
- logger.error(`Failed to create ${envFilePath}: ${_err.message}`);
662
+ logger.error(`Failed to create ${envFilePath}: ${extractErrorMessage(error)}`);
591
663
  return false;
592
664
  }
593
665
  }
@@ -599,13 +671,12 @@ LSH_SECRETS_KEY=${this.encryptionKey}
599
671
  if (!content.includes('LSH_SECRETS_KEY')) {
600
672
  newContent += `\n# LSH Secrets Encryption Key (auto-generated)\nLSH_SECRETS_KEY=${this.encryptionKey}\n`;
601
673
  }
602
- fs.writeFileSync(envFilePath, newContent, 'utf8');
674
+ fs.writeFileSync(envFilePath, newContent, { encoding: 'utf8', mode: 0o600 });
603
675
  logger.info(`✅ Created ${envFilePath} from ${path.basename(examplePath)}`);
604
676
  return true;
605
677
  }
606
678
  catch (error) {
607
- const _err = error;
608
- logger.error(`Failed to create ${envFilePath}: ${_err.message}`);
679
+ logger.error(`Failed to create ${envFilePath}: ${extractErrorMessage(error)}`);
609
680
  return false;
610
681
  }
611
682
  }
@@ -679,7 +750,7 @@ LSH_SECRETS_KEY=${this.encryptionKey}
679
750
  out();
680
751
  }
681
752
  // Step 1: Ensure encryption key exists
682
- if (!process.env[ENV_VARS.LSH_SECRETS_KEY]) {
753
+ if (!findEncryptionKey()) {
683
754
  logger.info('🔑 No encryption key found...');
684
755
  await this.ensureEncryptionKey();
685
756
  out();
@@ -982,8 +1053,7 @@ LSH_SECRETS_KEY=${this.encryptionKey}
982
1053
  }
983
1054
  catch (error) {
984
1055
  // Don't fail operation if IPFS logging fails
985
- const err = error;
986
- logger.warn(`⚠️ Could not log to IPFS: ${err.message}`);
1056
+ logger.warn(`⚠️ Could not log to IPFS: ${extractErrorMessage(error)}`);
987
1057
  }
988
1058
  }
989
1059
  }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Persistent storage for the LSH sync secret.
3
+ *
4
+ * Mirrors the design of mcli's `mcli sync key` store: a single 64-char
5
+ * hex value kept in ``$LSH_HOME/sync_key.json`` (default
6
+ * ``~/.config/lsh/sync_key.json``) with mode 0600. The
7
+ * ``LSH_SECRETS_KEY`` environment variable still wins when set; the
8
+ * store is only consulted as a fallback so users do not have to re-paste
9
+ * their secret on every shell.
10
+ */
11
+ import * as crypto from 'crypto';
12
+ import * as fs from 'fs';
13
+ import * as os from 'os';
14
+ import * as path from 'path';
15
+ export const HEX64_REGEX = /^[0-9a-fA-F]{64}$/;
16
+ const FILENAME = 'sync_key.json';
17
+ /** Resolve the directory where lsh stores its config (`$LSH_HOME` or `~/.config/lsh`). */
18
+ export function getLshHome() {
19
+ const overridden = process.env.LSH_HOME;
20
+ if (overridden && overridden.trim().length > 0)
21
+ return overridden;
22
+ return path.join(os.homedir(), '.config', 'lsh');
23
+ }
24
+ /** Return a freshly-generated 64-char hex secret without persisting it. */
25
+ export function generateKey() {
26
+ return crypto.randomBytes(32).toString('hex');
27
+ }
28
+ /**
29
+ * Persistent on-disk store for the LSH sync secret.
30
+ */
31
+ export class SyncKeyStore {
32
+ /** Absolute path to the on-disk key file. */
33
+ get path() {
34
+ return path.join(getLshHome(), FILENAME);
35
+ }
36
+ /** Read the persisted key, or `null` if none is configured / file is malformed. */
37
+ get() {
38
+ const file = this.path;
39
+ if (!fs.existsSync(file))
40
+ return null;
41
+ try {
42
+ const raw = fs.readFileSync(file, 'utf-8');
43
+ const parsed = JSON.parse(raw);
44
+ if (typeof parsed.key === 'string' && HEX64_REGEX.test(parsed.key)) {
45
+ return parsed.key;
46
+ }
47
+ }
48
+ catch {
49
+ // fall through to null
50
+ }
51
+ return null;
52
+ }
53
+ /** Validate and persist a key. Throws on invalid input. */
54
+ set(key) {
55
+ if (typeof key !== 'string' || !HEX64_REGEX.test(key)) {
56
+ throw new Error('sync key must be a 64-char hex string');
57
+ }
58
+ this.write(key);
59
+ }
60
+ /** Generate a new key and persist it. Refuses to overwrite unless `force=true`. */
61
+ generate(force = false) {
62
+ if (fs.existsSync(this.path) && !force) {
63
+ throw new Error(`sync key already exists at ${this.path}; use force=true to overwrite`);
64
+ }
65
+ const key = generateKey();
66
+ this.write(key);
67
+ return key;
68
+ }
69
+ /** Delete the persisted key, if any. No-op when the file is absent. */
70
+ clear() {
71
+ try {
72
+ fs.unlinkSync(this.path);
73
+ }
74
+ catch (err) {
75
+ const e = err;
76
+ if (e.code !== 'ENOENT')
77
+ throw err;
78
+ }
79
+ }
80
+ write(key) {
81
+ const dir = path.dirname(this.path);
82
+ fs.mkdirSync(dir, { recursive: true });
83
+ // Write first, then chmod, so the secret is never world-readable.
84
+ fs.writeFileSync(this.path, JSON.stringify({ key }));
85
+ fs.chmodSync(this.path, 0o600);
86
+ }
87
+ }
@@ -2,53 +2,25 @@
2
2
  * Secrets Management Commands
3
3
  * Sync .env files across development environments
4
4
  */
5
- import SecretsManager from '../../lib/secrets-manager.js';
5
+ import SecretsManager, { findEncryptionKey } from '../../lib/secrets-manager.js';
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import * as readline from 'readline';
9
9
  import { getGitRepoInfo } from '../../lib/git-utils.js';
10
10
  import { ENV_VARS } from '../../constants/index.js';
11
11
  import { IPFSClientManager } from '../../lib/ipfs-client-manager.js';
12
+ import { SyncKeyStore } from '../../lib/sync-key-store.js';
13
+ function maskKey(key) {
14
+ if (key.length <= 12)
15
+ return '*'.repeat(key.length);
16
+ return `${key.slice(0, 4)}...${key.slice(-4)}`;
17
+ }
12
18
  /**
13
19
  * Type guard to check if a string is a valid OutputFormat.
14
20
  */
15
21
  function isOutputFormat(value) {
16
22
  return ['env', 'json', 'yaml', 'toml', 'export'].includes(value);
17
23
  }
18
- /**
19
- * Find existing LSH_SECRETS_KEY from environment, local .env, or global ~/.env
20
- */
21
- function findExistingKey() {
22
- // 1. Check environment variable
23
- const envKey = process.env[ENV_VARS.LSH_SECRETS_KEY];
24
- if (envKey)
25
- return envKey;
26
- // 2. Check local .env
27
- const localEnvPath = path.join(process.cwd(), '.env');
28
- const localKey = readKeyFromEnvFile(localEnvPath);
29
- if (localKey)
30
- return localKey;
31
- // 3. Check global ~/.env
32
- const globalEnvPath = path.join(process.env.HOME || '~', '.env');
33
- const globalKey = readKeyFromEnvFile(globalEnvPath);
34
- if (globalKey)
35
- return globalKey;
36
- return null;
37
- }
38
- function readKeyFromEnvFile(envPath) {
39
- try {
40
- if (fs.existsSync(envPath)) {
41
- const content = fs.readFileSync(envPath, 'utf-8');
42
- const match = content.match(/^LSH_SECRETS_KEY=['"]?([^'"\n]+)['"]?/m);
43
- if (match)
44
- return match[1];
45
- }
46
- }
47
- catch {
48
- // Ignore read errors
49
- }
50
- return null;
51
- }
52
24
  export async function init_secrets(program) {
53
25
  // Push secrets to cloud
54
26
  program
@@ -305,7 +277,7 @@ export async function init_secrets(program) {
305
277
  // Default action: show existing key or prompt to generate
306
278
  keyCmd
307
279
  .action(async () => {
308
- const existingKey = findExistingKey();
280
+ const existingKey = findEncryptionKey();
309
281
  if (existingKey) {
310
282
  const masked = existingKey.slice(0, 8) + '...' + existingKey.slice(-4);
311
283
  console.log(`\n🔑 LSH_SECRETS_KEY=${masked}\n`);
@@ -326,7 +298,7 @@ export async function init_secrets(program) {
326
298
  .option('--no-mask', 'Show the full key (default: masked)')
327
299
  .option('--export', 'Output in export format for shell evaluation')
328
300
  .action(async (options) => {
329
- const existingKey = findExistingKey();
301
+ const existingKey = findEncryptionKey();
330
302
  if (!existingKey) {
331
303
  console.error('❌ No encryption key found. Run: lsh key generate');
332
304
  process.exit(1);
@@ -350,7 +322,7 @@ export async function init_secrets(program) {
350
322
  .option('--force', 'Overwrite existing key')
351
323
  .option('--export', 'Output in export format for shell evaluation')
352
324
  .action(async (options) => {
353
- const existingKey = findExistingKey();
325
+ const existingKey = findEncryptionKey();
354
326
  if (existingKey && !options.force) {
355
327
  console.error('❌ An encryption key already exists. Use --force to overwrite.');
356
328
  console.error('⚠️ Warning: existing secrets will NOT be decryptable with a new key.\n');
@@ -400,7 +372,7 @@ export async function init_secrets(program) {
400
372
  process.exit(1);
401
373
  }
402
374
  // Check for existing key
403
- const existingKey = findExistingKey();
375
+ const existingKey = findEncryptionKey();
404
376
  if (existingKey) {
405
377
  if (existingKey === keyValue) {
406
378
  console.log('\n✅ This key is already configured.\n');
@@ -447,6 +419,72 @@ export async function init_secrets(program) {
447
419
  process.exit(1);
448
420
  }
449
421
  });
422
+ // lsh key gen — generate + persist to ~/.config/lsh/sync_key.json
423
+ keyCmd
424
+ .command('gen')
425
+ .description('Generate a new key and persist it to the LSH sync key store')
426
+ .option('-f, --force', 'Overwrite an existing stored key')
427
+ .option('--show', 'Print the full key (default masks it)')
428
+ .action((options) => {
429
+ const store = new SyncKeyStore();
430
+ let key;
431
+ try {
432
+ key = store.generate(options.force === true);
433
+ }
434
+ catch (err) {
435
+ const e = err;
436
+ console.error(`❌ ${e.message}`);
437
+ console.error("💡 Use --force to overwrite, or run 'lsh key show' to view the existing key.");
438
+ process.exit(1);
439
+ return; // for the type-checker
440
+ }
441
+ console.log(`✅ Generated sync key at ${store.path}`);
442
+ if (options.show) {
443
+ console.log(`🔑 ${key}`);
444
+ }
445
+ else {
446
+ console.log(`🔑 ${maskKey(key)} (use --show to print full)`);
447
+ }
448
+ console.log("💡 Share this key with teammates / your other hosts. Then run `lsh key set <key>` on each peer.");
449
+ });
450
+ // lsh key set — paste a 64-char hex key from another host into the store
451
+ keyCmd
452
+ .command('set <key>')
453
+ .description('Persist an existing 64-char hex key to the LSH sync key store')
454
+ .action((keyValue) => {
455
+ const store = new SyncKeyStore();
456
+ try {
457
+ store.set(keyValue.trim());
458
+ }
459
+ catch (err) {
460
+ const e = err;
461
+ console.error(`❌ ${e.message}`);
462
+ process.exit(1);
463
+ }
464
+ console.log('✅ Sync key stored.');
465
+ });
466
+ // lsh key clear — delete the persisted key (env var, if set, is untouched)
467
+ keyCmd
468
+ .command('clear')
469
+ .description('Delete the persisted LSH sync key (env var is untouched)')
470
+ .option('-y, --yes', 'Skip confirmation prompt')
471
+ .action(async (options) => {
472
+ if (!options.yes) {
473
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
474
+ const answer = await new Promise((resolve) => {
475
+ rl.question('Remove the stored sync key? [y/N] ', (ans) => {
476
+ rl.close();
477
+ resolve(ans.trim().toLowerCase());
478
+ });
479
+ });
480
+ if (answer !== 'y' && answer !== 'yes') {
481
+ console.log('Aborted.');
482
+ return;
483
+ }
484
+ }
485
+ new SyncKeyStore().clear();
486
+ console.log('✅ Stored sync key removed.');
487
+ });
450
488
  // Create .env file
451
489
  program
452
490
  .command('create')
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "3.2.5",
3
+ "version": "3.5.1",
4
4
  "description": "Simple, cross-platform encrypted secrets manager with automatic sync, IPFS audit logs, and multi-environment support. Just run lsh sync and start managing your secrets.",
5
- "main": "dist/app.js",
5
+ "main": "dist/cli.js",
6
6
  "bin": {
7
7
  "lsh": "./dist/cli.js"
8
8
  },
@@ -64,39 +64,39 @@
64
64
  ],
65
65
  "dependencies": {
66
66
  "@supabase/supabase-js": "^2.57.4",
67
- "@types/proper-lockfile": "^4.1.4",
68
67
  "bcrypt": "^6.0.0",
69
68
  "chalk": "^5.3.0",
70
69
  "chokidar": "^5.0.0",
71
- "commander": "^14.0.2",
70
+ "commander": "^15.0.0",
72
71
  "cors": "^2.8.5",
73
72
  "dotenv": "^17.2.3",
74
- "express": "^4.22.1",
73
+ "express": "^5.2.1",
75
74
  "express-rate-limit": "^8.2.1",
76
75
  "glob": "^13.0.0",
77
- "inquirer": "^9.2.12",
76
+ "inquirer": "^14.0.1",
78
77
  "js-yaml": "^4.1.0",
79
78
  "jsonwebtoken": "^9.0.2",
80
79
  "node-cron": "^4.2.1",
81
80
  "ora": "^9.0.0",
82
81
  "pg": "^8.16.3",
83
82
  "proper-lockfile": "^4.1.2",
84
- "smol-toml": "^1.3.1",
85
- "uuid": "^13.0.0"
83
+ "smol-toml": "^1.3.1"
86
84
  },
87
85
  "devDependencies": {
88
- "@types/bcrypt": "^5.0.2",
86
+ "@eslint/js": "^10.0.1",
87
+ "@types/bcrypt": "^6.0.0",
89
88
  "@types/cors": "^2.8.17",
90
- "@types/express": "^4.17.21",
89
+ "@types/express": "^5.0.6",
91
90
  "@types/jest": "^30.0.0",
92
91
  "@types/js-yaml": "^4.0.9",
93
92
  "@types/jsonwebtoken": "^9.0.5",
94
- "@types/node": "^20.19.27",
95
- "@typescript-eslint/eslint-plugin": "^8.44.1",
96
- "@typescript-eslint/parser": "^8.44.1",
97
- "eslint": "^9.36.0",
98
- "jest": "^29.7.0",
93
+ "@types/node": "^25.9.1",
94
+ "@types/proper-lockfile": "^4.1.4",
95
+ "@typescript-eslint/eslint-plugin": "^8.60.0",
96
+ "@typescript-eslint/parser": "^8.60.0",
97
+ "eslint": "^10.4.1",
98
+ "jest": "^30.4.2",
99
99
  "ts-jest": "^29.2.5",
100
- "typescript": "^5.4.5"
100
+ "typescript": "^6.0.3"
101
101
  }
102
102
  }