lsh-framework 3.1.8 → 3.1.14

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,365 @@
1
+ /**
2
+ * Sync Commands
3
+ * Native IPFS sync for secrets management (mirrors mcli pattern)
4
+ *
5
+ * Usage: lsh sync <command>
6
+ */
7
+ import chalk from 'chalk';
8
+ import ora from 'ora';
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import * as crypto from 'crypto';
12
+ import { getIPFSSync } from '../lib/ipfs-sync.js';
13
+ import { getGitRepoInfo } from '../lib/git-utils.js';
14
+ import { ENV_VARS } from '../constants/index.js';
15
+ /**
16
+ * Register sync commands
17
+ */
18
+ export function registerSyncCommands(program) {
19
+ const syncCommand = program
20
+ .command('sync')
21
+ .description('Sync secrets via native IPFS')
22
+ .action(() => {
23
+ // Show help when running `lsh sync` without subcommand
24
+ console.log(chalk.bold.cyan('\nšŸ”„ LSH Sync - IPFS Secrets Sync\n'));
25
+ console.log(chalk.gray('Sync encrypted secrets via native IPFS (no auth required)\n'));
26
+ console.log(chalk.bold('Commands:'));
27
+ console.log(` ${chalk.cyan('init')} šŸš€ Initialize and start the IPFS daemon`);
28
+ console.log(` ${chalk.cyan('push')} ā¬†ļø Push encrypted secrets to IPFS`);
29
+ console.log(` ${chalk.cyan('pull')} ā¬‡ļø Pull secrets from IPFS by CID`);
30
+ console.log(` ${chalk.cyan('status')} šŸ“Š Show IPFS daemon and sync status`);
31
+ console.log(` ${chalk.cyan('history')} šŸ“œ Show IPFS sync history`);
32
+ console.log(` ${chalk.cyan('verify')} āœ… Verify that a CID is accessible on IPFS`);
33
+ console.log(` ${chalk.cyan('clear')} šŸ—‘ļø Clear sync history`);
34
+ console.log('');
35
+ console.log(chalk.bold('Examples:'));
36
+ console.log(chalk.gray(' lsh sync init # Set up IPFS for first time'));
37
+ console.log(chalk.gray(' lsh sync push # Push secrets, get CID'));
38
+ console.log(chalk.gray(' lsh sync pull <cid> # Pull secrets by CID'));
39
+ console.log('');
40
+ console.log(chalk.gray('Run "lsh sync <command> --help" for more info.'));
41
+ console.log('');
42
+ });
43
+ // lsh sync init
44
+ syncCommand
45
+ .command('init')
46
+ .description('šŸš€ Initialize and start the IPFS daemon')
47
+ .action(async () => {
48
+ console.log(chalk.bold.cyan('\nšŸš€ Initializing IPFS for sync...\n'));
49
+ const ipfsSync = getIPFSSync();
50
+ // Check if daemon is already running
51
+ if (await ipfsSync.checkDaemon()) {
52
+ const info = await ipfsSync.getDaemonInfo();
53
+ console.log(chalk.green('āœ… IPFS daemon is already running!'));
54
+ if (info) {
55
+ console.log(chalk.gray(` Peer ID: ${info.peerId.substring(0, 16)}...`));
56
+ console.log(chalk.gray(` Version: ${info.version}`));
57
+ }
58
+ console.log('');
59
+ console.log(chalk.gray('You can now sync secrets:'));
60
+ console.log(chalk.cyan(' lsh sync push'));
61
+ console.log('');
62
+ return;
63
+ }
64
+ // Daemon not running, show instructions
65
+ console.log(chalk.yellow('āš ļø IPFS daemon not running'));
66
+ console.log('');
67
+ console.log(chalk.gray('To start IPFS:'));
68
+ console.log('');
69
+ console.log(chalk.bold('1. Install IPFS (if not installed):'));
70
+ console.log(chalk.cyan(' lsh ipfs install'));
71
+ console.log('');
72
+ console.log(chalk.bold('2. Initialize IPFS repository:'));
73
+ console.log(chalk.cyan(' lsh ipfs init'));
74
+ console.log('');
75
+ console.log(chalk.bold('3. Start the daemon:'));
76
+ console.log(chalk.cyan(' lsh ipfs start'));
77
+ console.log('');
78
+ console.log(chalk.gray('Or run all at once:'));
79
+ console.log(chalk.cyan(' lsh ipfs install && lsh ipfs init && lsh ipfs start'));
80
+ console.log('');
81
+ });
82
+ // lsh sync push
83
+ syncCommand
84
+ .command('push')
85
+ .description('ā¬†ļø Push encrypted secrets to IPFS, returns CID')
86
+ .option('-f, --file <path>', 'Path to .env file', '.env')
87
+ .option('-e, --env <name>', 'Environment name', '')
88
+ .action(async (options) => {
89
+ const spinner = ora('Uploading to IPFS...').start();
90
+ try {
91
+ const ipfsSync = getIPFSSync();
92
+ const gitInfo = getGitRepoInfo();
93
+ // Check daemon
94
+ if (!await ipfsSync.checkDaemon()) {
95
+ spinner.fail(chalk.red('IPFS daemon not running'));
96
+ console.log('');
97
+ console.log(chalk.gray('Initialize IPFS first:'));
98
+ console.log(chalk.cyan(' lsh sync init'));
99
+ process.exit(1);
100
+ }
101
+ // Read .env file
102
+ const envPath = path.resolve(options.file);
103
+ if (!fs.existsSync(envPath)) {
104
+ spinner.fail(chalk.red(`File not found: ${envPath}`));
105
+ process.exit(1);
106
+ }
107
+ const content = fs.readFileSync(envPath, 'utf-8');
108
+ // Get encryption key
109
+ const encryptionKey = process.env[ENV_VARS.LSH_SECRETS_KEY];
110
+ if (!encryptionKey) {
111
+ spinner.fail(chalk.red('LSH_SECRETS_KEY not set'));
112
+ console.log('');
113
+ console.log(chalk.gray('Generate a key with:'));
114
+ console.log(chalk.cyan(' lsh key'));
115
+ console.log('');
116
+ console.log(chalk.gray('Then set it:'));
117
+ console.log(chalk.cyan(' export LSH_SECRETS_KEY=<your-key>'));
118
+ process.exit(1);
119
+ }
120
+ // Encrypt content
121
+ const key = crypto.createHash('sha256').update(encryptionKey).digest();
122
+ const iv = crypto.randomBytes(16);
123
+ const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
124
+ let encrypted = cipher.update(content, 'utf8', 'hex');
125
+ encrypted += cipher.final('hex');
126
+ const encryptedData = iv.toString('hex') + ':' + encrypted;
127
+ // Upload to IPFS
128
+ const filename = `lsh-secrets-${options.env || 'default'}.encrypted`;
129
+ const cid = await ipfsSync.upload(Buffer.from(encryptedData, 'utf-8'), filename, {
130
+ environment: options.env || undefined,
131
+ gitRepo: gitInfo?.repoName || undefined,
132
+ });
133
+ if (!cid) {
134
+ spinner.fail(chalk.red('Upload failed'));
135
+ process.exit(1);
136
+ }
137
+ spinner.succeed(chalk.green('Uploaded to IPFS!'));
138
+ console.log('');
139
+ console.log(chalk.bold('CID:'), chalk.cyan(cid));
140
+ console.log('');
141
+ console.log(chalk.gray('Share this CID with teammates to pull secrets:'));
142
+ console.log(chalk.cyan(` lsh sync pull ${cid}`));
143
+ console.log('');
144
+ console.log(chalk.gray('Public gateway URLs:'));
145
+ ipfsSync.getGatewayUrls(cid).slice(0, 2).forEach(url => {
146
+ console.log(chalk.gray(` ${url}`));
147
+ });
148
+ console.log('');
149
+ }
150
+ catch (error) {
151
+ const err = error;
152
+ spinner.fail(chalk.red('Push failed'));
153
+ console.error(chalk.red(err.message));
154
+ process.exit(1);
155
+ }
156
+ });
157
+ // lsh sync pull <cid>
158
+ syncCommand
159
+ .command('pull <cid>')
160
+ .description('ā¬‡ļø Pull secrets from IPFS by CID')
161
+ .option('-o, --output <path>', 'Output file path', '.env')
162
+ .option('--force', 'Overwrite existing file without backup')
163
+ .action(async (cid, options) => {
164
+ const spinner = ora('Downloading from IPFS...').start();
165
+ try {
166
+ const ipfsSync = getIPFSSync();
167
+ // Download from IPFS
168
+ const data = await ipfsSync.download(cid);
169
+ if (!data) {
170
+ spinner.fail(chalk.red('Download failed'));
171
+ console.log('');
172
+ console.log(chalk.gray('The CID might not be available on public gateways yet.'));
173
+ console.log(chalk.gray('Make sure the source machine is online with IPFS daemon running.'));
174
+ process.exit(1);
175
+ }
176
+ // Get encryption key
177
+ const encryptionKey = process.env[ENV_VARS.LSH_SECRETS_KEY];
178
+ if (!encryptionKey) {
179
+ spinner.fail(chalk.red('LSH_SECRETS_KEY not set'));
180
+ console.log('');
181
+ console.log(chalk.gray('You need the same encryption key used to push.'));
182
+ console.log(chalk.gray('Set it in your environment:'));
183
+ console.log(chalk.cyan(' export LSH_SECRETS_KEY=<key-from-teammate>'));
184
+ process.exit(1);
185
+ }
186
+ // Decrypt content
187
+ const encryptedData = data.toString('utf-8');
188
+ const [ivHex, encrypted] = encryptedData.split(':');
189
+ if (!ivHex || !encrypted) {
190
+ spinner.fail(chalk.red('Invalid encrypted data format'));
191
+ process.exit(1);
192
+ }
193
+ const key = crypto.createHash('sha256').update(encryptionKey).digest();
194
+ const iv = Buffer.from(ivHex, 'hex');
195
+ let decrypted;
196
+ try {
197
+ const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
198
+ decrypted = decipher.update(encrypted, 'hex', 'utf8');
199
+ decrypted += decipher.final('utf8');
200
+ }
201
+ catch {
202
+ spinner.fail(chalk.red('Decryption failed'));
203
+ console.log('');
204
+ console.log(chalk.red('Wrong encryption key!'));
205
+ console.log(chalk.gray('Make sure LSH_SECRETS_KEY matches the key used to push.'));
206
+ process.exit(1);
207
+ }
208
+ // Write output file
209
+ const outputPath = path.resolve(options.output);
210
+ // Backup existing file if it exists (unless --force)
211
+ if (fs.existsSync(outputPath) && !options.force) {
212
+ const backupPath = `${outputPath}.backup.${Date.now()}`;
213
+ fs.copyFileSync(outputPath, backupPath);
214
+ console.log(chalk.gray(`Backed up existing file to: ${backupPath}`));
215
+ }
216
+ fs.writeFileSync(outputPath, decrypted, 'utf-8');
217
+ spinner.succeed(chalk.green('Downloaded and decrypted!'));
218
+ console.log('');
219
+ console.log(chalk.bold('Output:'), chalk.cyan(outputPath));
220
+ console.log(chalk.bold('CID:'), chalk.gray(cid));
221
+ console.log('');
222
+ }
223
+ catch (error) {
224
+ const err = error;
225
+ spinner.fail(chalk.red('Pull failed'));
226
+ console.error(chalk.red(err.message));
227
+ process.exit(1);
228
+ }
229
+ });
230
+ // lsh sync status
231
+ syncCommand
232
+ .command('status')
233
+ .description('šŸ“Š Show IPFS daemon and sync status')
234
+ .action(async () => {
235
+ try {
236
+ const ipfsSync = getIPFSSync();
237
+ const daemonInfo = await ipfsSync.getDaemonInfo();
238
+ console.log(chalk.bold.cyan('\nšŸ“Š Sync Status\n'));
239
+ console.log(chalk.gray('━'.repeat(50)));
240
+ console.log('');
241
+ if (daemonInfo) {
242
+ console.log(chalk.green('āœ… IPFS daemon running'));
243
+ console.log(` Peer ID: ${daemonInfo.peerId.substring(0, 16)}...`);
244
+ console.log(` Version: ${daemonInfo.version}`);
245
+ console.log('');
246
+ console.log(chalk.gray('Ready to sync:'));
247
+ console.log(chalk.cyan(' lsh sync push # Push secrets'));
248
+ console.log(chalk.cyan(' lsh sync pull <cid> # Pull by CID'));
249
+ }
250
+ else {
251
+ console.log(chalk.yellow('āš ļø IPFS daemon not running'));
252
+ console.log('');
253
+ console.log(chalk.gray('Start with:'));
254
+ console.log(chalk.cyan(' lsh sync init'));
255
+ }
256
+ console.log('');
257
+ }
258
+ catch (error) {
259
+ const err = error;
260
+ console.error(chalk.red('Failed to check status:'), err.message);
261
+ process.exit(1);
262
+ }
263
+ });
264
+ // lsh sync history
265
+ syncCommand
266
+ .command('history')
267
+ .description('šŸ“œ Show IPFS sync history')
268
+ .option('-n, --limit <number>', 'Number of entries to show', '10')
269
+ .option('--json', 'Output as JSON')
270
+ .action(async (options) => {
271
+ try {
272
+ const ipfsSync = getIPFSSync();
273
+ const limit = parseInt(options.limit, 10);
274
+ const history = await ipfsSync.getHistory(limit);
275
+ if (options.json) {
276
+ console.log(JSON.stringify(history, null, 2));
277
+ return;
278
+ }
279
+ console.log(chalk.bold.cyan('\nšŸ“œ Sync History\n'));
280
+ console.log(chalk.gray('━'.repeat(60)));
281
+ console.log('');
282
+ if (history.length === 0) {
283
+ console.log(chalk.gray('No sync history found.'));
284
+ console.log('');
285
+ console.log(chalk.gray('Push your first secrets with:'));
286
+ console.log(chalk.cyan(' lsh sync push'));
287
+ console.log('');
288
+ return;
289
+ }
290
+ for (const entry of history) {
291
+ const date = new Date(entry.timestamp);
292
+ const dateStr = date.toLocaleString();
293
+ console.log(chalk.bold(`${entry.cid.substring(0, 16)}...`));
294
+ console.log(` File: ${entry.filename}`);
295
+ console.log(` Size: ${entry.size} bytes`);
296
+ console.log(` Time: ${dateStr}`);
297
+ if (entry.gitRepo) {
298
+ console.log(` Repo: ${entry.gitRepo}`);
299
+ }
300
+ if (entry.environment) {
301
+ console.log(` Env: ${entry.environment}`);
302
+ }
303
+ console.log('');
304
+ }
305
+ console.log(chalk.gray(`Showing ${history.length} entries. Use -n to show more.`));
306
+ console.log('');
307
+ }
308
+ catch (error) {
309
+ const err = error;
310
+ console.error(chalk.red('Failed to get history:'), err.message);
311
+ process.exit(1);
312
+ }
313
+ });
314
+ // lsh sync verify <cid>
315
+ syncCommand
316
+ .command('verify <cid>')
317
+ .description('āœ… Verify that a CID is accessible on IPFS')
318
+ .action(async (cid) => {
319
+ const spinner = ora('Verifying CID accessibility...').start();
320
+ try {
321
+ const ipfsSync = getIPFSSync();
322
+ const result = await ipfsSync.verifyCid(cid);
323
+ if (result.available) {
324
+ spinner.succeed(chalk.green('CID is accessible!'));
325
+ console.log('');
326
+ console.log(chalk.bold('CID:'), chalk.cyan(cid));
327
+ console.log(chalk.bold('Source:'), chalk.gray(result.source));
328
+ console.log('');
329
+ }
330
+ else {
331
+ spinner.fail(chalk.red('CID not accessible'));
332
+ console.log('');
333
+ console.log(chalk.gray('The CID could not be found on the network.'));
334
+ console.log(chalk.gray('Possible reasons:'));
335
+ console.log(chalk.gray(' - Source machine is offline'));
336
+ console.log(chalk.gray(' - Content not yet propagated to gateways'));
337
+ console.log(chalk.gray(' - Invalid CID'));
338
+ console.log('');
339
+ }
340
+ }
341
+ catch (error) {
342
+ const err = error;
343
+ spinner.fail(chalk.red('Verification failed'));
344
+ console.error(chalk.red(err.message));
345
+ process.exit(1);
346
+ }
347
+ });
348
+ // lsh sync clear
349
+ syncCommand
350
+ .command('clear')
351
+ .description('šŸ—‘ļø Clear sync history')
352
+ .action(async () => {
353
+ try {
354
+ const ipfsSync = getIPFSSync();
355
+ await ipfsSync.clearHistory();
356
+ console.log(chalk.green('āœ… Sync history cleared'));
357
+ }
358
+ catch (error) {
359
+ const err = error;
360
+ console.error(chalk.red('Failed to clear history:'), err.message);
361
+ process.exit(1);
362
+ }
363
+ });
364
+ }
365
+ export default registerSyncCommands;
@@ -33,7 +33,6 @@ export const ENV_VARS = {
33
33
  // Feature flags
34
34
  LSH_LOCAL_STORAGE_QUIET: 'LSH_LOCAL_STORAGE_QUIET',
35
35
  LSH_V1_COMPAT: 'LSH_V1_COMPAT',
36
- LSH_STORACHA_ENABLED: 'LSH_STORACHA_ENABLED',
37
36
  DISABLE_IPFS_SYNC: 'DISABLE_IPFS_SYNC',
38
37
  // Logging
39
38
  LSH_LOG_LEVEL: 'LSH_LOG_LEVEL',
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * IPFS Secrets Storage Adapter
3
- * Stores encrypted secrets on IPFS using Storacha (formerly web3.storage)
3
+ * Stores encrypted secrets on IPFS via native Kubo daemon
4
+ *
5
+ * Priority order:
6
+ * 1. Native IPFS (Kubo daemon on port 5001) - zero-config, no auth
7
+ * 2. Local cache - always available for offline access
4
8
  */
5
9
  import * as fs from 'fs';
6
10
  import * as fsPromises from 'fs/promises';
@@ -8,7 +12,7 @@ import * as path from 'path';
8
12
  import * as os from 'os';
9
13
  import * as crypto from 'crypto';
10
14
  import { createLogger } from './logger.js';
11
- import { getStorachaClient } from './storacha-client.js';
15
+ import { getIPFSSync } from './ipfs-sync.js';
12
16
  import { ENV_VARS } from '../constants/index.js';
13
17
  const logger = createLogger('IPFSSecretsStorage');
14
18
  /**
@@ -31,8 +35,14 @@ export class IPFSSecretsStorage {
31
35
  const lshDir = path.join(homeDir, '.lsh');
32
36
  this.cacheDir = path.join(lshDir, 'secrets-cache');
33
37
  this.metadataPath = path.join(lshDir, 'secrets-metadata.json');
34
- // Initialize metadata - will be loaded on first use
35
- this.metadata = {};
38
+ // Load metadata synchronously to ensure we have all existing entries
39
+ // This fixes the bug where sequential pushes from different repos
40
+ // would overwrite each other's metadata
41
+ this.metadata = this.loadMetadata();
42
+ // Ensure cache directory exists
43
+ if (!fs.existsSync(this.cacheDir)) {
44
+ fs.mkdirSync(this.cacheDir, { recursive: true });
45
+ }
36
46
  }
37
47
  /**
38
48
  * Initialize async parts
@@ -73,36 +83,36 @@ export class IPFSSecretsStorage {
73
83
  if (gitRepo) {
74
84
  logger.info(` Repository: ${gitRepo}/${gitBranch || 'main'}`);
75
85
  }
76
- // Upload to Storacha network if enabled
77
- const storacha = getStorachaClient();
78
- if (storacha.isEnabled()) {
86
+ // Try native IPFS upload
87
+ const ipfsSync = getIPFSSync();
88
+ let uploadedToNetwork = false;
89
+ let realCid = null;
90
+ if (await ipfsSync.checkDaemon()) {
79
91
  try {
80
- const filename = `lsh-secrets-${environment}-${cid}.encrypted`;
81
- // encryptedData is already a Buffer, pass it directly
82
- await storacha.upload(Buffer.from(encryptedData), filename);
83
- logger.info(` ā˜ļø Synced to Storacha network`);
84
- // Upload registry file if this is a git repo
85
- // This allows detection on new machines without local metadata
86
- // Include the secrets CID so other hosts can fetch the latest version
87
- if (gitRepo) {
88
- try {
89
- await storacha.uploadRegistry(gitRepo, environment, cid);
90
- logger.debug(` šŸ“ Registry uploaded for ${gitRepo} (CID: ${cid})`);
91
- }
92
- catch (regError) {
93
- // Registry upload failed, but secrets are still uploaded
94
- const _regErr = regError;
95
- logger.debug(` Registry upload failed: ${_regErr.message}`);
92
+ const filename = `lsh-secrets-${environment}.encrypted`;
93
+ realCid = await ipfsSync.upload(Buffer.from(encryptedData, 'utf-8'), filename, { environment, gitRepo });
94
+ if (realCid) {
95
+ // Update CID to the real IPFS CID
96
+ logger.info(` 🌐 Synced to IPFS (CID: ${realCid})`);
97
+ uploadedToNetwork = true;
98
+ // Update metadata with real CID if different
99
+ if (realCid !== cid) {
100
+ metadata.cid = realCid;
101
+ this.metadata[this.getMetadataKey(gitRepo, environment)] = metadata;
102
+ await this.saveMetadata();
96
103
  }
97
104
  }
98
105
  }
99
106
  catch (error) {
100
107
  const err = error;
101
- logger.warn(` āš ļø Storacha upload failed: ${err.message}`);
102
- logger.warn(` Secrets are still cached locally`);
108
+ logger.warn(` āš ļø IPFS upload failed: ${err.message}`);
103
109
  }
104
110
  }
105
- return cid;
111
+ if (!uploadedToNetwork) {
112
+ logger.warn(` šŸ“ Secrets cached locally only (no network sync)`);
113
+ logger.warn(` šŸ’” Start IPFS daemon for network sync: lsh ipfs start`);
114
+ }
115
+ return realCid || cid;
106
116
  }
107
117
  catch (error) {
108
118
  const err = error;
@@ -121,94 +131,62 @@ export class IPFSSecretsStorage {
121
131
  const displayEnv = gitRepo
122
132
  ? (environment ? `${gitRepo}_${environment}` : gitRepo)
123
133
  : (environment || 'default');
124
- // If no local metadata, try to fetch from Storacha registry first (for git repos)
134
+ // If no local metadata, check IPFS sync history
125
135
  if (!metadata && gitRepo) {
126
136
  try {
127
- const storacha = getStorachaClient();
128
- if (storacha.isEnabled() && await storacha.isAuthenticated()) {
129
- logger.info(` šŸ” No local metadata found, checking Storacha registry...`);
130
- const latestCid = await storacha.getLatestCID(gitRepo);
131
- if (latestCid) {
132
- logger.info(` āœ… Found secrets in registry (CID: ${latestCid})`);
133
- // Create metadata from registry
134
- metadata = {
135
- environment,
136
- git_repo: gitRepo,
137
- cid: latestCid,
138
- timestamp: new Date().toISOString(),
139
- keys_count: 0, // Unknown until decrypted
140
- encrypted: true,
141
- };
142
- this.metadata[metadataKey] = metadata;
143
- await this.saveMetadata();
144
- }
137
+ const ipfsSync = getIPFSSync();
138
+ const latestCid = await ipfsSync.getLatestCid(gitRepo, environment);
139
+ if (latestCid) {
140
+ logger.info(` āœ… Found secrets in IPFS history (CID: ${latestCid})`);
141
+ // Create metadata from history
142
+ metadata = {
143
+ environment,
144
+ git_repo: gitRepo,
145
+ cid: latestCid,
146
+ timestamp: new Date().toISOString(),
147
+ keys_count: 0, // Unknown until decrypted
148
+ encrypted: true,
149
+ };
150
+ this.metadata[metadataKey] = metadata;
151
+ await this.saveMetadata();
145
152
  }
146
153
  }
147
154
  catch (error) {
148
- // Registry check failed, continue to error
155
+ // History check failed, continue to error
149
156
  const err = error;
150
- logger.debug(` Registry check failed: ${err.message}`);
157
+ logger.debug(` IPFS history check failed: ${err.message}`);
151
158
  }
152
159
  }
153
160
  if (!metadata) {
154
161
  throw new Error(`No secrets found for environment: ${displayEnv}\n\n` +
155
162
  `šŸ’” Tip: Check available environments with: lsh env\n` +
156
- ` Or push secrets first with: lsh push`);
157
- }
158
- // Check if there's a newer version in the registry (for git repos)
159
- if (gitRepo) {
160
- try {
161
- const storacha = getStorachaClient();
162
- if (storacha.isEnabled() && await storacha.isAuthenticated()) {
163
- const latestCid = await storacha.getLatestCID(gitRepo);
164
- if (latestCid && latestCid !== metadata.cid) {
165
- logger.info(` šŸ”„ Found newer version in registry (CID: ${latestCid})`);
166
- // Update metadata with latest CID
167
- metadata = {
168
- ...metadata,
169
- cid: latestCid,
170
- timestamp: new Date().toISOString(),
171
- };
172
- this.metadata[metadataKey] = metadata;
173
- await this.saveMetadata();
174
- }
175
- }
176
- }
177
- catch (error) {
178
- // Registry check failed, continue with local metadata
179
- const err = error;
180
- logger.debug(` Registry check failed: ${err.message}`);
181
- }
163
+ ` Or push secrets first with: lsh push\n` +
164
+ ` Or pull by CID with: lsh sync pull <cid>`);
182
165
  }
183
166
  // Try to load from local cache
184
167
  let cachedData = await this.loadLocally(metadata.cid);
185
- // If not in cache, try downloading from Storacha
168
+ // If not in cache, try downloading from IPFS
186
169
  if (!cachedData) {
187
- const storacha = getStorachaClient();
188
- if (storacha.isEnabled()) {
189
- try {
190
- logger.info(` ā˜ļø Downloading from Storacha network...`);
191
- const downloadedData = await storacha.download(metadata.cid);
170
+ const ipfsSync = getIPFSSync();
171
+ try {
172
+ logger.info(` 🌐 Downloading from IPFS...`);
173
+ const downloadedData = await ipfsSync.download(metadata.cid);
174
+ if (downloadedData) {
192
175
  // Store in local cache for future use
193
176
  await this.storeLocally(metadata.cid, downloadedData.toString('utf-8'), environment);
194
177
  cachedData = downloadedData.toString('utf-8');
195
- logger.info(` āœ… Downloaded and cached from Storacha`);
196
- }
197
- catch (error) {
198
- const err = error;
199
- throw new Error(`Secrets not in cache and Storacha download failed: ${err.message}`);
178
+ logger.info(` āœ… Downloaded and cached from IPFS`);
200
179
  }
201
180
  }
202
- else {
203
- throw new Error(`Secrets not found in cache. CID: ${metadata.cid}\n\n` +
204
- `šŸ’” Tip: Enable Storacha network sync:\n` +
205
- ` export LSH_STORACHA_ENABLED=true\n` +
206
- ` Or set up Supabase: lsh supabase init`);
181
+ catch (error) {
182
+ const err = error;
183
+ logger.debug(` IPFS download failed: ${err.message}`);
207
184
  }
208
185
  }
209
- // At this point cachedData is guaranteed to be a string
210
186
  if (!cachedData) {
211
- throw new Error(`Failed to retrieve secrets for environment: ${environment}`);
187
+ throw new Error(`Secrets not found in cache or IPFS. CID: ${metadata.cid}\n\n` +
188
+ `šŸ’” Tip: Start IPFS daemon: lsh ipfs start\n` +
189
+ ` Or pull directly by CID: lsh sync pull <cid>`);
212
190
  }
213
191
  // Decrypt secrets
214
192
  const secrets = this.decryptSecrets(cachedData, encryptionKey);
@@ -302,7 +280,7 @@ export class IPFSSecretsStorage {
302
280
  throw new Error('Decryption failed. This usually means:\n' +
303
281
  ' 1. You need to set LSH_SECRETS_KEY environment variable\n' +
304
282
  ' 2. The key must match the one used during encryption\n' +
305
- ' 3. Generate a shared key with: lsh secrets key\n' +
283
+ ' 3. Generate a shared key with: lsh key\n' +
306
284
  ' 4. Add it to your .env: LSH_SECRETS_KEY=<key>\n' +
307
285
  '\nOriginal error: ' + err.message);
308
286
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * IPFS Sync Logger
3
- * Records immutable sync operations to IPFS using Storacha (formerly web3.storage)
3
+ * Records immutable sync operations to IPFS via native Kubo daemon
4
4
  */
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
@@ -11,12 +11,11 @@ import { ENV_VARS } from '../constants/index.js';
11
11
  /**
12
12
  * IPFS Sync Logger
13
13
  *
14
- * Stores immutable sync records on IPFS using Storacha (storacha.network)
14
+ * Stores immutable sync records on IPFS via native daemon
15
15
  *
16
16
  * Features:
17
- * - Zero-config: Works automatically with embedded token
17
+ * - Zero-config: Works with local IPFS daemon
18
18
  * - Immutable: Content-addressed storage on IPFS
19
- * - Free: 5GB storage forever via Storacha
20
19
  * - Privacy: Only metadata stored, no secrets
21
20
  * - Opt-out: Can be disabled via DISABLE_IPFS_SYNC config
22
21
  */
@@ -63,9 +62,8 @@ export class IPFSSyncLogger {
63
62
  user: os.userInfo().username,
64
63
  lsh_version: version,
65
64
  };
66
- // For now, use simple file-based storage with IPFS-like CIDs
67
- // This avoids requiring Storacha authentication
68
- // In production, you'd upload to actual IPFS/Storacha
65
+ // Use file-based storage with IPFS-like CIDs
66
+ // Records are stored locally and can optionally be uploaded to IPFS
69
67
  const cid = this.generateContentId(record);
70
68
  const repoEnv = this.getRepoEnvKey(record.git_repo, record.environment);
71
69
  // Store the record locally