lsh-framework 2.2.5 → 2.3.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.
@@ -0,0 +1,139 @@
1
+ /**
2
+ * LSH Configuration Manager
3
+ *
4
+ * Manages encryption keys and configuration for different repositories.
5
+ * Keys are stored in ~/.lsh/config.json separate from the .env files being synced.
6
+ *
7
+ * This prevents sync conflicts where different hosts overwrite each other's keys.
8
+ */
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import * as os from 'os';
12
+ import { logger } from './logger.js';
13
+ export class LshConfigManager {
14
+ configPath;
15
+ config;
16
+ constructor(configPath) {
17
+ this.configPath = configPath || path.join(os.homedir(), '.lsh', 'config.json');
18
+ this.config = this.loadConfig();
19
+ }
20
+ /**
21
+ * Load config from disk, or create default if it doesn't exist
22
+ */
23
+ loadConfig() {
24
+ try {
25
+ if (fs.existsSync(this.configPath)) {
26
+ const data = fs.readFileSync(this.configPath, 'utf-8');
27
+ return JSON.parse(data);
28
+ }
29
+ }
30
+ catch (error) {
31
+ const err = error;
32
+ logger.warn(`Failed to load config: ${err.message}`);
33
+ }
34
+ // Return default config
35
+ return {
36
+ version: '1.0.0',
37
+ keys: {},
38
+ };
39
+ }
40
+ /**
41
+ * Save config to disk
42
+ */
43
+ saveConfig() {
44
+ try {
45
+ const dir = path.dirname(this.configPath);
46
+ if (!fs.existsSync(dir)) {
47
+ fs.mkdirSync(dir, { recursive: true });
48
+ }
49
+ fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8');
50
+ }
51
+ catch (error) {
52
+ const err = error;
53
+ logger.error(`Failed to save config: ${err.message}`);
54
+ throw error;
55
+ }
56
+ }
57
+ /**
58
+ * Get encryption key for a specific repository
59
+ */
60
+ getKey(repoName) {
61
+ // Check config file first
62
+ if (this.config.keys[repoName]) {
63
+ // Update last used timestamp
64
+ this.config.keys[repoName].lastUsed = new Date().toISOString();
65
+ this.saveConfig();
66
+ return this.config.keys[repoName].key;
67
+ }
68
+ // Fall back to environment variables
69
+ const envKey = process.env.LSH_SECRETS_KEY || process.env.LSH_MASTER_KEY;
70
+ if (envKey) {
71
+ logger.debug(`Using encryption key from environment for ${repoName}`);
72
+ return envKey;
73
+ }
74
+ return null;
75
+ }
76
+ /**
77
+ * Set encryption key for a specific repository
78
+ */
79
+ setKey(repoName, key) {
80
+ this.config.keys[repoName] = {
81
+ key,
82
+ createdAt: this.config.keys[repoName]?.createdAt || new Date().toISOString(),
83
+ lastUsed: new Date().toISOString(),
84
+ };
85
+ this.saveConfig();
86
+ logger.debug(`Saved encryption key for ${repoName}`);
87
+ }
88
+ /**
89
+ * Check if a key exists for a repository
90
+ */
91
+ hasKey(repoName) {
92
+ return !!this.config.keys[repoName];
93
+ }
94
+ /**
95
+ * Remove encryption key for a repository
96
+ */
97
+ removeKey(repoName) {
98
+ delete this.config.keys[repoName];
99
+ this.saveConfig();
100
+ logger.debug(`Removed encryption key for ${repoName}`);
101
+ }
102
+ /**
103
+ * List all repositories with stored keys
104
+ */
105
+ listKeys() {
106
+ return Object.entries(this.config.keys).map(([repoName, data]) => ({
107
+ repoName,
108
+ createdAt: data.createdAt,
109
+ lastUsed: data.lastUsed,
110
+ }));
111
+ }
112
+ /**
113
+ * Export key for sharing with other hosts
114
+ */
115
+ exportKey(repoName) {
116
+ const key = this.getKey(repoName);
117
+ if (!key) {
118
+ return null;
119
+ }
120
+ return `export LSH_SECRETS_KEY='${key}'`;
121
+ }
122
+ /**
123
+ * Get config file path (for debugging/migration)
124
+ */
125
+ getConfigPath() {
126
+ return this.configPath;
127
+ }
128
+ }
129
+ // Singleton instance
130
+ let _configManager = null;
131
+ /**
132
+ * Get the global LSH config manager instance
133
+ */
134
+ export function getLshConfig() {
135
+ if (!_configManager) {
136
+ _configManager = new LshConfigManager();
137
+ }
138
+ return _configManager;
139
+ }
@@ -112,6 +112,32 @@ export class SecretsManager {
112
112
  }
113
113
  return env;
114
114
  }
115
+ /**
116
+ * Filter out LSH-internal keys that should not be synced
117
+ * These keys are host-specific and syncing them would cause conflicts
118
+ */
119
+ filterLshInternalKeys(env) {
120
+ const filtered = {};
121
+ const excludedKeys = new Set([
122
+ 'LSH_SECRETS_KEY', // Encryption key - host-specific, managed separately
123
+ 'LSH_MASTER_KEY', // Alternative encryption key name
124
+ 'LSH_INTERNAL_', // Any other LSH internal keys
125
+ ]);
126
+ for (const [key, value] of Object.entries(env)) {
127
+ // Skip exact matches
128
+ if (excludedKeys.has(key)) {
129
+ logger.debug(`Filtering out LSH-internal key: ${key}`);
130
+ continue;
131
+ }
132
+ // Skip keys starting with LSH_INTERNAL_
133
+ if (key.startsWith('LSH_INTERNAL_')) {
134
+ logger.debug(`Filtering out LSH-internal key: ${key}`);
135
+ continue;
136
+ }
137
+ filtered[key] = value;
138
+ }
139
+ return filtered;
140
+ }
115
141
  /**
116
142
  * Format env vars as .env file content
117
143
  */
@@ -186,13 +212,20 @@ export class SecretsManager {
186
212
  }
187
213
  logger.info(`Pushing ${envFilePath} to IPFS (${effectiveEnv})...`);
188
214
  const content = fs.readFileSync(envFilePath, 'utf8');
189
- const env = this.parseEnvFile(content);
215
+ const envParsed = this.parseEnvFile(content);
216
+ // Filter out LSH-internal keys (encryption keys, etc.) that should not be synced
217
+ const env = this.filterLshInternalKeys(envParsed);
218
+ const filteredCount = Object.keys(envParsed).length - Object.keys(env).length;
219
+ if (filteredCount > 0) {
220
+ logger.debug(`Filtered out ${filteredCount} LSH-internal key(s) from sync`);
221
+ }
190
222
  // Check for destructive changes unless force is true
191
223
  if (!force) {
192
224
  try {
193
225
  // Check if secrets already exist for this environment
194
- if (this.storage.exists(effectiveEnv, this.gitInfo?.repoName)) {
195
- const existingSecrets = await this.storage.pull(effectiveEnv, this.encryptionKey, this.gitInfo?.repoName);
226
+ // Use raw environment to match storage.push() behavior
227
+ if (this.storage.exists(environment, this.gitInfo?.repoName)) {
228
+ const existingSecrets = await this.storage.pull(environment, this.encryptionKey, this.gitInfo?.repoName);
196
229
  const cloudEnv = {};
197
230
  existingSecrets.forEach(s => {
198
231
  cloudEnv[s.key] = s.value;
@@ -240,22 +273,50 @@ export class SecretsManager {
240
273
  const effectiveEnv = this.getRepoAwareEnvironment(environment);
241
274
  logger.info(`Pulling ${filename} (${effectiveEnv}) from IPFS...`);
242
275
  // Get secrets from IPFS storage
243
- const secrets = await this.storage.pull(effectiveEnv, this.encryptionKey, this.gitInfo?.repoName);
276
+ // Use raw environment parameter to match how push() stores them
277
+ const secrets = await this.storage.pull(environment, // Use raw environment, not effectiveEnv
278
+ this.encryptionKey, this.gitInfo?.repoName);
244
279
  if (secrets.length === 0) {
245
280
  throw new Error(`No secrets found for environment: ${effectiveEnv}\n\n` +
246
281
  `💡 Tip: Check available environments with: lsh env\n` +
247
282
  ` Or push secrets first with: lsh push --env ${environment}`);
248
283
  }
249
- // Backup existing .env if it exists (unless force is true)
250
- if (fs.existsSync(envFilePath) && !force) {
251
- const backup = `${envFilePath}.backup.${Date.now()}`;
252
- fs.copyFileSync(envFilePath, backup);
253
- logger.info(`Backed up existing .env to ${backup}`);
254
- }
255
- // Convert secrets back to .env format
256
- const envContent = secrets
257
- .map(s => `${s.key}=${s.value}`)
258
- .join('\n') + '\n';
284
+ // Preserve local LSH-internal keys before overwriting
285
+ let localLshKeys = {};
286
+ if (fs.existsSync(envFilePath)) {
287
+ const existingContent = fs.readFileSync(envFilePath, 'utf8');
288
+ const existingEnv = this.parseEnvFile(existingContent);
289
+ // Extract LSH-internal keys to preserve
290
+ const lshKeyNames = ['LSH_SECRETS_KEY', 'LSH_MASTER_KEY'];
291
+ for (const keyName of lshKeyNames) {
292
+ if (existingEnv[keyName]) {
293
+ localLshKeys[keyName] = existingEnv[keyName];
294
+ logger.debug(`Preserving local ${keyName}`);
295
+ }
296
+ }
297
+ // Also preserve any LSH_INTERNAL_* keys
298
+ for (const [key, value] of Object.entries(existingEnv)) {
299
+ if (key.startsWith('LSH_INTERNAL_')) {
300
+ localLshKeys[key] = value;
301
+ logger.debug(`Preserving local ${key}`);
302
+ }
303
+ }
304
+ // Backup existing .env (unless force is true)
305
+ if (!force) {
306
+ const backup = `${envFilePath}.backup.${Date.now()}`;
307
+ fs.copyFileSync(envFilePath, backup);
308
+ logger.info(`Backed up existing ${filename} to ${backup}`);
309
+ }
310
+ }
311
+ // Convert secrets back to env object
312
+ const pulledEnv = {};
313
+ secrets.forEach(s => {
314
+ pulledEnv[s.key] = s.value;
315
+ });
316
+ // Merge pulled secrets with preserved local LSH keys
317
+ const finalEnv = { ...pulledEnv, ...localLshKeys };
318
+ // Convert to .env format
319
+ const envContent = this.formatEnvFile(finalEnv);
259
320
  // Write new .env
260
321
  fs.writeFileSync(envFilePath, envContent, 'utf8');
261
322
  logger.info(`✅ Pulled ${secrets.length} secrets from IPFS`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "2.2.5",
3
+ "version": "2.3.0",
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
5
  "main": "dist/app.js",
6
6
  "bin": {