lsh-framework 2.0.2 β†’ 2.1.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.
package/README.md CHANGED
@@ -8,21 +8,22 @@
8
8
 
9
9
  Traditional secret management tools are either too complex, too expensive, or require vendor lock-in. LSH gives you:
10
10
 
11
- - **Encrypted sync** across all your machines using IPFS content-addressed storage
11
+ - **True multi-host sync** via IPFS network (Storacha) - enabled by default in v2.1.0!
12
+ - **Encrypted sync** with AES-256 encryption before upload
12
13
  - **Automatic rotation** with built-in daemon scheduling
13
14
  - **Team collaboration** with shared encryption keys
14
15
  - **Multi-environment** support (dev/staging/prod)
15
- - **Local-first** - works offline, your data stays on your machine
16
- - **Free & Open Source** - no per-seat pricing, no cloud dependencies
16
+ - **Local-first** - works offline, graceful fallback to local cache
17
+ - **Free & Open Source** - no per-seat pricing, 5GB free tier on Storacha
17
18
 
18
19
  **Plus, you get a complete shell automation platform as a bonus.**
19
20
 
20
21
  ## Quick Start
21
22
 
22
- **New to LSH?** LSH uses IPFS-based local storage - zero configuration needed!
23
- - **Local-first** - All secrets stored encrypted on your machine at `~/.lsh/secrets-cache/`
24
- - **No cloud required** - Works completely offline
25
- - **Team sync** - Share encryption key to sync across team members
23
+ **New to LSH?** Choose your sync method:
24
+ - **Storacha (IPFS network)** - One-time email auth, automatic multi-host sync (NEW in v2.1.0!)
25
+ - **Local storage** - Zero config, works offline, encrypted at `~/.lsh/secrets-cache/`
26
+ - **Supabase** - Team collaboration with audit logs and role-based access
26
27
 
27
28
  ### Quick Install (Works Immediately!)
28
29
 
@@ -196,6 +197,63 @@ lsh sync-history show
196
197
 
197
198
  See [IPFS Sync Records Documentation](docs/features/IPFS_SYNC_RECORDS.md) for complete details.
198
199
 
200
+ ### 🌐 Multi-Host IPFS Network Sync (New in v2.1.0!)
201
+
202
+ **True multi-host sync via Storacha (IPFS network) - enabled by default!**
203
+
204
+ LSH now syncs secrets across all your machines via the IPFS network with zero configuration after one-time email authentication.
205
+
206
+ ```bash
207
+ # On Host A (MacBook) - One-time setup
208
+ lsh storacha login [email protected]
209
+ # βœ… Email verification β†’ payment plan β†’ space created automatically
210
+
211
+ # Push secrets (automatic network sync)
212
+ cd ~/repos/my-project
213
+ lsh push --env dev
214
+ # πŸ“€ Uploaded to Storacha: bafkrei...
215
+ # ☁️ Synced to IPFS network
216
+
217
+ # On Host B (Linux server) - One-time setup
218
+ lsh storacha login [email protected]
219
+
220
+ # Pull secrets (automatic network download)
221
+ cd ~/repos/my-project
222
+ lsh pull --env dev
223
+ # πŸ“₯ Downloading from Storacha: bafkrei...
224
+ # βœ… Downloaded from IPFS network
225
+ ```
226
+
227
+ **Features:**
228
+ - βœ… **Default enabled** - Works automatically after authentication
229
+ - βœ… **One-time setup** - Just authenticate with email per machine
230
+ - βœ… **Encrypted before upload** - AES-256 encryption (secrets never leave your machine unencrypted)
231
+ - βœ… **Graceful fallback** - Uses local cache if network unavailable
232
+ - βœ… **Content-addressed** - IPFS CIDs ensure tamper-proof integrity
233
+ - βœ… **Free tier** - 5GB storage on Storacha's free plan
234
+
235
+ **How it works:**
236
+ 1. Your secrets are encrypted locally with `LSH_SECRETS_KEY`
237
+ 2. Encrypted data is uploaded to IPFS via Storacha
238
+ 3. On another machine, LSH downloads from IPFS using the content ID (CID)
239
+ 4. Secrets are decrypted locally with the same encryption key
240
+ 5. Local cache speeds up offline access
241
+
242
+ **Check status:**
243
+ ```bash
244
+ lsh storacha status
245
+ # πŸ” Authentication: βœ… Authenticated
246
+ # 🌐 Network Sync: βœ… Enabled
247
+ # πŸ“¦ Spaces: lsh-secrets (current)
248
+ ```
249
+
250
+ **Disable network sync (local cache only):**
251
+ ```bash
252
+ lsh storacha disable
253
+ # Or set environment variable:
254
+ export LSH_STORACHA_ENABLED=false
255
+ ```
256
+
199
257
  ### πŸ” Secrets Management
200
258
 
201
259
  - **AES-256 Encryption** - Military-grade encryption for all secrets
package/dist/cli.js CHANGED
@@ -12,6 +12,7 @@ import { registerConfigCommands } from './commands/config.js';
12
12
  import { registerSyncHistoryCommands } from './commands/sync-history.js';
13
13
  import { registerIPFSCommands } from './commands/ipfs.js';
14
14
  import { registerMigrateCommand } from './commands/migrate.js';
15
+ import { registerStorachaCommands } from './commands/storacha.js';
15
16
  import { init_daemon } from './services/daemon/daemon.js';
16
17
  import { init_supabase } from './services/supabase/supabase.js';
17
18
  import { init_cron } from './services/cron/cron.js';
@@ -148,6 +149,7 @@ function findSimilarCommands(input, validCommands) {
148
149
  registerSyncHistoryCommands(program);
149
150
  registerIPFSCommands(program);
150
151
  registerMigrateCommand(program);
152
+ registerStorachaCommands(program);
151
153
  // Secrets management (primary feature)
152
154
  await init_secrets(program);
153
155
  // Supporting services
@@ -18,6 +18,7 @@ export function registerInitCommands(program) {
18
18
  .command('init')
19
19
  .description('Interactive setup wizard (first-time configuration)')
20
20
  .option('--local', 'Use local-only encryption (no cloud sync)')
21
+ .option('--storacha', 'Use Storacha IPFS network sync (recommended)')
21
22
  .option('--supabase', 'Use Supabase cloud storage')
22
23
  .option('--postgres', 'Use self-hosted PostgreSQL')
23
24
  .option('--skip-test', 'Skip connection testing')
@@ -58,7 +59,10 @@ async function runSetupWizard(options) {
58
59
  }
59
60
  // Determine storage type
60
61
  let storageType;
61
- if (options.local) {
62
+ if (options.storacha) {
63
+ storageType = 'storacha';
64
+ }
65
+ else if (options.local) {
62
66
  storageType = 'local';
63
67
  }
64
68
  else if (options.postgres) {
@@ -76,19 +80,23 @@ async function runSetupWizard(options) {
76
80
  message: 'Choose storage backend:',
77
81
  choices: [
78
82
  {
79
- name: 'Supabase (free, cloud-hosted, recommended)',
83
+ name: '🌐 Storacha (IPFS network, zero-config, recommended)',
84
+ value: 'storacha',
85
+ },
86
+ {
87
+ name: '☁️ Supabase (cloud-hosted, team collaboration)',
80
88
  value: 'supabase',
81
89
  },
82
90
  {
83
- name: 'Local encryption (file-based, no cloud sync)',
91
+ name: 'πŸ’Ύ Local encryption (file-based, no cloud sync)',
84
92
  value: 'local',
85
93
  },
86
94
  {
87
- name: 'Self-hosted PostgreSQL',
95
+ name: '🐘 Self-hosted PostgreSQL',
88
96
  value: 'postgres',
89
97
  },
90
98
  ],
91
- default: 'supabase',
99
+ default: 'storacha',
92
100
  },
93
101
  ]);
94
102
  storageType = storage;
@@ -98,7 +106,10 @@ async function runSetupWizard(options) {
98
106
  encryptionKey: generateEncryptionKey(),
99
107
  };
100
108
  // Configure based on storage type
101
- if (storageType === 'supabase') {
109
+ if (storageType === 'storacha') {
110
+ await configureStoracha(config);
111
+ }
112
+ else if (storageType === 'supabase') {
102
113
  await configureSupabase(config, options.skipTest);
103
114
  }
104
115
  else if (storageType === 'postgres') {
@@ -195,6 +206,34 @@ async function testSupabaseConnection(url, key) {
195
206
  throw new Error(`Could not connect to Supabase: ${err.message}`);
196
207
  }
197
208
  }
209
+ /**
210
+ * Configure Storacha IPFS network sync
211
+ */
212
+ async function configureStoracha(config) {
213
+ console.log(chalk.cyan('\n🌐 Storacha IPFS Network Sync'));
214
+ console.log(chalk.gray('Zero-config multi-host secrets sync via IPFS network'));
215
+ console.log('');
216
+ const answers = await inquirer.prompt([
217
+ {
218
+ type: 'input',
219
+ name: 'email',
220
+ message: 'Enter your email for Storacha authentication:',
221
+ validate: (input) => {
222
+ if (!input.trim())
223
+ return 'Email is required';
224
+ if (!input.includes('@'))
225
+ return 'Must be a valid email address';
226
+ return true;
227
+ },
228
+ },
229
+ ]);
230
+ config.storachaEmail = answers.email.trim();
231
+ console.log('');
232
+ console.log(chalk.yellow('πŸ“§ Please check your email to complete authentication.'));
233
+ console.log(chalk.gray(' After setup completes, run:'));
234
+ console.log(chalk.cyan(' lsh storacha login ' + config.storachaEmail));
235
+ console.log('');
236
+ }
198
237
  /**
199
238
  * Configure self-hosted PostgreSQL
200
239
  */
@@ -270,6 +309,9 @@ async function saveConfiguration(config) {
270
309
  if (config.storageType === 'local') {
271
310
  updates.LSH_STORAGE_MODE = 'local';
272
311
  }
312
+ if (config.storageType === 'storacha') {
313
+ updates.LSH_STORACHA_ENABLED = 'true';
314
+ }
273
315
  // Update .env content
274
316
  for (const [key, value] of Object.entries(updates)) {
275
317
  const regex = new RegExp(`^${key}=.*$`, 'm');
@@ -335,7 +377,10 @@ function showSuccessMessage(config) {
335
377
  console.log(chalk.gray(' Share it with your team to sync secrets.'));
336
378
  console.log('');
337
379
  // Storage info
338
- if (config.storageType === 'supabase') {
380
+ if (config.storageType === 'storacha') {
381
+ console.log(chalk.cyan('🌐 Using Storacha IPFS network sync'));
382
+ }
383
+ else if (config.storageType === 'supabase') {
339
384
  console.log(chalk.cyan('☁️ Using Supabase cloud storage'));
340
385
  }
341
386
  else if (config.storageType === 'postgres') {
@@ -348,21 +393,37 @@ function showSuccessMessage(config) {
348
393
  // Next steps
349
394
  console.log(chalk.bold('πŸš€ Next steps:'));
350
395
  console.log('');
351
- console.log(chalk.gray(' 1. Verify your setup:'));
352
- console.log(chalk.cyan(' lsh doctor'));
353
- console.log('');
354
- if (config.storageType !== 'local') {
396
+ if (config.storageType === 'storacha') {
397
+ console.log(chalk.gray(' 1. Authenticate with Storacha:'));
398
+ console.log(chalk.cyan(` lsh storacha login ${config.storachaEmail || 'your@email.com'}`));
399
+ console.log('');
355
400
  console.log(chalk.gray(' 2. Push your secrets:'));
356
401
  console.log(chalk.cyan(' lsh push --env dev'));
402
+ console.log(chalk.gray(' (Automatically uploads to IPFS network)'));
357
403
  console.log('');
358
404
  console.log(chalk.gray(' 3. On another machine:'));
359
- console.log(chalk.cyan(' lsh init ') + chalk.gray('# Use the same credentials'));
405
+ console.log(chalk.cyan(' lsh init --storacha'));
406
+ console.log(chalk.cyan(' lsh storacha login your@email.com'));
360
407
  console.log(chalk.cyan(' lsh pull --env dev'));
408
+ console.log(chalk.gray(' (Automatically downloads from IPFS network)'));
361
409
  }
362
410
  else {
363
- console.log(chalk.gray(' 2. Start managing secrets:'));
364
- console.log(chalk.cyan(' lsh set API_KEY myvalue'));
365
- console.log(chalk.cyan(' lsh list'));
411
+ console.log(chalk.gray(' 1. Verify your setup:'));
412
+ console.log(chalk.cyan(' lsh doctor'));
413
+ console.log('');
414
+ if (config.storageType !== 'local') {
415
+ console.log(chalk.gray(' 2. Push your secrets:'));
416
+ console.log(chalk.cyan(' lsh push --env dev'));
417
+ console.log('');
418
+ console.log(chalk.gray(' 3. On another machine:'));
419
+ console.log(chalk.cyan(' lsh init ') + chalk.gray('# Use the same credentials'));
420
+ console.log(chalk.cyan(' lsh pull --env dev'));
421
+ }
422
+ else {
423
+ console.log(chalk.gray(' 2. Start managing secrets:'));
424
+ console.log(chalk.cyan(' lsh set API_KEY myvalue'));
425
+ console.log(chalk.cyan(' lsh list'));
426
+ }
366
427
  }
367
428
  console.log('');
368
429
  console.log(chalk.gray('πŸ“– Documentation: https://github.com/gwicho38/lsh'));
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Storacha Commands
3
+ * Manage Storacha (IPFS network) authentication and configuration
4
+ */
5
+ import { getStorachaClient } from '../lib/storacha-client.js';
6
+ export function registerStorachaCommands(program) {
7
+ const storacha = program
8
+ .command('storacha')
9
+ .description('Manage Storacha (IPFS network) sync');
10
+ // Login command
11
+ storacha
12
+ .command('login <email>')
13
+ .description('Authenticate with Storacha via email')
14
+ .action(async (email) => {
15
+ try {
16
+ const client = getStorachaClient();
17
+ console.log('\nπŸ” Storacha Authentication\n');
18
+ console.log('━'.repeat(60));
19
+ console.log('');
20
+ await client.login(email);
21
+ }
22
+ catch (error) {
23
+ const err = error;
24
+ console.error('\n❌ Authentication failed:', err.message);
25
+ console.error('');
26
+ console.error('πŸ’‘ Troubleshooting:');
27
+ console.error(' - Check your email for the verification link');
28
+ console.error(' - Complete signup at: https://console.storacha.network/');
29
+ console.error(' - Ensure you have an active internet connection');
30
+ console.error('');
31
+ process.exit(1);
32
+ }
33
+ });
34
+ // Status command
35
+ storacha
36
+ .command('status')
37
+ .description('Show Storacha authentication and configuration status')
38
+ .action(async () => {
39
+ try {
40
+ const client = getStorachaClient();
41
+ const status = await client.getStatus();
42
+ console.log('\n☁️ Storacha Status\n');
43
+ console.log('━'.repeat(60));
44
+ console.log('');
45
+ // Authentication status
46
+ console.log('πŸ” Authentication:');
47
+ if (status.authenticated) {
48
+ console.log(` Status: βœ… Authenticated`);
49
+ if (status.email) {
50
+ console.log(` Email: ${status.email}`);
51
+ }
52
+ }
53
+ else {
54
+ console.log(` Status: ❌ Not authenticated`);
55
+ console.log('');
56
+ console.log('πŸ’‘ To authenticate:');
57
+ console.log(' lsh storacha login [email protected]');
58
+ }
59
+ console.log('');
60
+ // Network sync status
61
+ console.log('🌐 Network Sync:');
62
+ console.log(` Enabled: ${status.enabled ? 'βœ… Yes' : '❌ No'}`);
63
+ if (!status.enabled) {
64
+ console.log('');
65
+ console.log('πŸ’‘ To enable:');
66
+ console.log(' export LSH_STORACHA_ENABLED=true');
67
+ console.log(' # Add to ~/.bashrc or ~/.zshrc for persistence');
68
+ }
69
+ console.log('');
70
+ // Spaces status
71
+ if (status.authenticated) {
72
+ console.log('πŸ“¦ Spaces:');
73
+ if (status.spaces.length === 0) {
74
+ console.log(' No spaces found');
75
+ console.log('');
76
+ console.log('πŸ’‘ Create a space:');
77
+ console.log(' lsh storacha space create my-space');
78
+ }
79
+ else {
80
+ console.log(` Total: ${status.spaces.length}`);
81
+ if (status.currentSpace) {
82
+ console.log(` Current: ${status.currentSpace}`);
83
+ }
84
+ console.log('');
85
+ console.log(' Available spaces:');
86
+ status.spaces.forEach(space => {
87
+ const marker = space.name === status.currentSpace ? 'β†’' : ' ';
88
+ console.log(` ${marker} ${space.name}`);
89
+ });
90
+ }
91
+ console.log('');
92
+ }
93
+ // Quick actions
94
+ console.log('πŸ’‘ Quick Actions:');
95
+ if (!status.authenticated) {
96
+ console.log(' lsh storacha login [email protected]');
97
+ }
98
+ else if (!status.enabled) {
99
+ console.log(' export LSH_STORACHA_ENABLED=true');
100
+ }
101
+ else {
102
+ console.log(' lsh push # Will sync to Storacha network');
103
+ console.log(' lsh pull # Will download from Storacha if needed');
104
+ }
105
+ console.log('');
106
+ console.log('━'.repeat(60));
107
+ console.log('');
108
+ }
109
+ catch (error) {
110
+ const err = error;
111
+ console.error('❌ Failed to get status:', err.message);
112
+ process.exit(1);
113
+ }
114
+ });
115
+ // Space commands
116
+ const space = storacha
117
+ .command('space')
118
+ .description('Manage Storacha spaces');
119
+ space
120
+ .command('create <name>')
121
+ .description('Create a new space')
122
+ .action(async (name) => {
123
+ try {
124
+ const client = getStorachaClient();
125
+ const status = await client.getStatus();
126
+ if (!status.authenticated) {
127
+ console.error('❌ Not authenticated');
128
+ console.error('');
129
+ console.error('πŸ’‘ First, authenticate:');
130
+ console.error(' lsh storacha login [email protected]');
131
+ process.exit(1);
132
+ }
133
+ console.log(`\nπŸ†• Creating space: ${name}...\n`);
134
+ await client.createSpace(name);
135
+ console.log('');
136
+ }
137
+ catch (error) {
138
+ const err = error;
139
+ console.error('\n❌ Failed to create space:', err.message);
140
+ process.exit(1);
141
+ }
142
+ });
143
+ space
144
+ .command('list')
145
+ .description('List all spaces')
146
+ .action(async () => {
147
+ try {
148
+ const client = getStorachaClient();
149
+ const status = await client.getStatus();
150
+ if (!status.authenticated) {
151
+ console.error('❌ Not authenticated');
152
+ console.error('');
153
+ console.error('πŸ’‘ First, authenticate:');
154
+ console.error(' lsh storacha login [email protected]');
155
+ process.exit(1);
156
+ }
157
+ console.log('\nπŸ“¦ Storacha Spaces\n');
158
+ console.log('━'.repeat(60));
159
+ console.log('');
160
+ if (status.spaces.length === 0) {
161
+ console.log('No spaces found');
162
+ console.log('');
163
+ console.log('πŸ’‘ Create a space:');
164
+ console.log(' lsh storacha space create my-space');
165
+ }
166
+ else {
167
+ status.spaces.forEach((space, index) => {
168
+ const marker = space.name === status.currentSpace ? 'β†’' : ' ';
169
+ console.log(`${marker} ${index + 1}. ${space.name}`);
170
+ console.log(` DID: ${space.did}`);
171
+ console.log(` Registered: ${space.registered}`);
172
+ console.log('');
173
+ });
174
+ if (status.currentSpace) {
175
+ console.log(`Current space: ${status.currentSpace}`);
176
+ }
177
+ }
178
+ console.log('━'.repeat(60));
179
+ console.log('');
180
+ }
181
+ catch (error) {
182
+ const err = error;
183
+ console.error('❌ Failed to list spaces:', err.message);
184
+ process.exit(1);
185
+ }
186
+ });
187
+ // Enable/disable commands
188
+ storacha
189
+ .command('enable')
190
+ .description('Enable Storacha network sync')
191
+ .action(() => {
192
+ const client = getStorachaClient();
193
+ client.enable();
194
+ console.log('');
195
+ console.log('πŸ’‘ For persistence, add to your shell profile:');
196
+ console.log(' echo "export LSH_STORACHA_ENABLED=true" >> ~/.bashrc');
197
+ console.log(' # or ~/.zshrc for zsh');
198
+ console.log('');
199
+ });
200
+ storacha
201
+ .command('disable')
202
+ .description('Disable Storacha network sync (local cache only)')
203
+ .action(() => {
204
+ const client = getStorachaClient();
205
+ client.disable();
206
+ console.log('');
207
+ });
208
+ }
@@ -7,6 +7,7 @@ import * as path from 'path';
7
7
  import * as os from 'os';
8
8
  import * as crypto from 'crypto';
9
9
  import { createLogger } from './logger.js';
10
+ import { getStorachaClient } from './storacha-client.js';
10
11
  const logger = createLogger('IPFSSecretsStorage');
11
12
  /**
12
13
  * IPFS Secrets Storage
@@ -63,8 +64,21 @@ export class IPFSSecretsStorage {
63
64
  if (gitRepo) {
64
65
  logger.info(` Repository: ${gitRepo}/${gitBranch || 'main'}`);
65
66
  }
66
- // TODO: In future, upload to real IPFS network via Storacha
67
- // For now, using local storage with IPFS-compatible CIDs
67
+ // Upload to Storacha network if enabled
68
+ const storacha = getStorachaClient();
69
+ if (storacha.isEnabled()) {
70
+ try {
71
+ const filename = `lsh-secrets-${environment}-${cid}.encrypted`;
72
+ // encryptedData is already a Buffer, pass it directly
73
+ await storacha.upload(Buffer.from(encryptedData), filename);
74
+ logger.info(` ☁️ Synced to Storacha network`);
75
+ }
76
+ catch (error) {
77
+ const err = error;
78
+ logger.warn(` ⚠️ Storacha upload failed: ${err.message}`);
79
+ logger.warn(` Secrets are still cached locally`);
80
+ }
81
+ }
68
82
  return cid;
69
83
  }
70
84
  catch (error) {
@@ -84,9 +98,34 @@ export class IPFSSecretsStorage {
84
98
  throw new Error(`No secrets found for environment: ${environment}`);
85
99
  }
86
100
  // Try to load from local cache
87
- const cachedData = await this.loadLocally(metadata.cid);
101
+ let cachedData = await this.loadLocally(metadata.cid);
102
+ // If not in cache, try downloading from Storacha
103
+ if (!cachedData) {
104
+ const storacha = getStorachaClient();
105
+ if (storacha.isEnabled()) {
106
+ try {
107
+ logger.info(` ☁️ Downloading from Storacha network...`);
108
+ const downloadedData = await storacha.download(metadata.cid);
109
+ // Store in local cache for future use
110
+ await this.storeLocally(metadata.cid, downloadedData.toString('utf-8'), environment);
111
+ cachedData = downloadedData.toString('utf-8');
112
+ logger.info(` βœ… Downloaded and cached from Storacha`);
113
+ }
114
+ catch (error) {
115
+ const err = error;
116
+ throw new Error(`Secrets not in cache and Storacha download failed: ${err.message}`);
117
+ }
118
+ }
119
+ else {
120
+ throw new Error(`Secrets not found in cache. CID: ${metadata.cid}\n\n` +
121
+ `πŸ’‘ Tip: Enable Storacha network sync:\n` +
122
+ ` export LSH_STORACHA_ENABLED=true\n` +
123
+ ` Or set up Supabase: lsh supabase init`);
124
+ }
125
+ }
126
+ // At this point cachedData is guaranteed to be a string
88
127
  if (!cachedData) {
89
- throw new Error(`Secrets not found in cache. CID: ${metadata.cid}`);
128
+ throw new Error(`Failed to retrieve secrets for environment: ${environment}`);
90
129
  }
91
130
  // Decrypt secrets
92
131
  const secrets = this.decryptSecrets(cachedData, encryptionKey);
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Storacha Client Wrapper
3
+ * Provides IPFS network sync via Storacha (formerly web3.storage)
4
+ */
5
+ import * as Storacha from '@storacha/client';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { homedir } from 'os';
9
+ import { logger } from './logger.js';
10
+ export class StorachaClient {
11
+ client = null;
12
+ configPath;
13
+ config;
14
+ constructor() {
15
+ const lshDir = path.join(homedir(), '.lsh');
16
+ this.configPath = path.join(lshDir, 'storacha-config.json');
17
+ // Ensure directory exists
18
+ if (!fs.existsSync(lshDir)) {
19
+ fs.mkdirSync(lshDir, { recursive: true });
20
+ }
21
+ // Load config
22
+ this.config = this.loadConfig();
23
+ }
24
+ /**
25
+ * Load Storacha configuration
26
+ */
27
+ loadConfig() {
28
+ try {
29
+ if (fs.existsSync(this.configPath)) {
30
+ const data = fs.readFileSync(this.configPath, 'utf-8');
31
+ return JSON.parse(data);
32
+ }
33
+ }
34
+ catch (_error) {
35
+ logger.warn('Failed to load Storacha config, using defaults');
36
+ }
37
+ // Default to enabled unless explicitly disabled
38
+ const envDisabled = process.env.LSH_STORACHA_ENABLED === 'false';
39
+ return {
40
+ enabled: !envDisabled,
41
+ };
42
+ }
43
+ /**
44
+ * Save Storacha configuration
45
+ */
46
+ saveConfig() {
47
+ try {
48
+ fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
49
+ }
50
+ catch (error) {
51
+ logger.error('Failed to save Storacha config:', error);
52
+ }
53
+ }
54
+ /**
55
+ * Check if Storacha is enabled
56
+ * Default: enabled (unless explicitly disabled via LSH_STORACHA_ENABLED=false)
57
+ */
58
+ isEnabled() {
59
+ // Explicitly disabled via env var
60
+ if (process.env.LSH_STORACHA_ENABLED === 'false') {
61
+ return false;
62
+ }
63
+ // Use config setting (defaults to true)
64
+ return this.config.enabled;
65
+ }
66
+ /**
67
+ * Get or create Storacha client
68
+ */
69
+ async getClient() {
70
+ if (this.client) {
71
+ return this.client;
72
+ }
73
+ try {
74
+ // Create client (it will use default store for Node.js)
75
+ this.client = await Storacha.create();
76
+ return this.client;
77
+ }
78
+ catch (error) {
79
+ const err = error;
80
+ throw new Error(`Failed to create Storacha client: ${err.message}`);
81
+ }
82
+ }
83
+ /**
84
+ * Check if user is authenticated
85
+ */
86
+ async isAuthenticated() {
87
+ try {
88
+ const client = await this.getClient();
89
+ const accountsRecord = await client.accounts();
90
+ // Convert Record to array
91
+ const accounts = Object.values(accountsRecord);
92
+ return accounts.length > 0;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ /**
99
+ * Login with email (triggers email verification)
100
+ */
101
+ async login(email) {
102
+ try {
103
+ const client = await this.getClient();
104
+ logger.info(`πŸ“§ Sending verification email to ${email}...`);
105
+ logger.info(' Click the link in your email to complete authentication.');
106
+ // This will wait for email confirmation
107
+ const account = await client.login(email);
108
+ logger.info('βœ… Email verified!');
109
+ // Wait for payment plan selection (15 min timeout)
110
+ logger.info('⏳ Waiting for payment plan selection...');
111
+ logger.info(' Please complete the signup at: https://console.storacha.network/');
112
+ await account.plan.wait();
113
+ logger.info('βœ… Payment plan confirmed!');
114
+ // Save email to config
115
+ this.config.email = email;
116
+ this.saveConfig();
117
+ // Check if space exists, create default if not
118
+ const spaces = await client.spaces();
119
+ if (spaces.length === 0) {
120
+ logger.info('πŸ†• Creating default space: "lsh-secrets"...');
121
+ const space = await client.createSpace('lsh-secrets', { account });
122
+ await client.setCurrentSpace(space.did());
123
+ this.config.spaceName = 'lsh-secrets';
124
+ this.saveConfig();
125
+ logger.info('βœ… Default space created and activated!');
126
+ }
127
+ else {
128
+ logger.info(`βœ… Found ${spaces.length} existing space(s)`);
129
+ // Set first space as current if none selected
130
+ const currentSpace = await client.currentSpace();
131
+ if (!currentSpace) {
132
+ await client.setCurrentSpace(spaces[0].did());
133
+ logger.info(` Activated space: ${spaces[0].name || 'unnamed'}`);
134
+ }
135
+ }
136
+ logger.info('');
137
+ logger.info('πŸŽ‰ Storacha setup complete!');
138
+ logger.info(' Enable network sync: export LSH_STORACHA_ENABLED=true');
139
+ logger.info(' Or add to ~/.bashrc or ~/.zshrc');
140
+ }
141
+ catch (error) {
142
+ const err = error;
143
+ throw new Error(`Login failed: ${err.message}`);
144
+ }
145
+ }
146
+ /**
147
+ * Get current authentication status
148
+ */
149
+ async getStatus() {
150
+ const authenticated = await this.isAuthenticated();
151
+ if (!authenticated) {
152
+ return {
153
+ authenticated: false,
154
+ spaces: [],
155
+ enabled: this.isEnabled(),
156
+ };
157
+ }
158
+ const client = await this.getClient();
159
+ const spacesRecord = await client.spaces();
160
+ // Convert Record to array
161
+ const spacesArray = Object.values(spacesRecord);
162
+ const currentSpace = await client.currentSpace();
163
+ return {
164
+ authenticated: true,
165
+ email: this.config.email,
166
+ currentSpace: currentSpace?.name || currentSpace?.did(),
167
+ spaces: spacesArray.map(s => ({
168
+ did: s.did(),
169
+ name: s.name || 'unnamed',
170
+ registered: new Date().toISOString(), // Space doesn't have registered field
171
+ })),
172
+ enabled: this.isEnabled(),
173
+ };
174
+ }
175
+ /**
176
+ * Create a new space
177
+ */
178
+ async createSpace(name) {
179
+ const client = await this.getClient();
180
+ const accountsRecord = await client.accounts();
181
+ const accounts = Object.values(accountsRecord);
182
+ if (accounts.length === 0) {
183
+ throw new Error('Not authenticated. Run: lsh storacha login <email>');
184
+ }
185
+ const space = await client.createSpace(name, { account: accounts[0] });
186
+ await client.setCurrentSpace(space.did());
187
+ this.config.spaceName = name;
188
+ this.saveConfig();
189
+ logger.info(`βœ… Space "${name}" created and activated`);
190
+ }
191
+ /**
192
+ * Upload data to Storacha
193
+ * Returns CID of uploaded content
194
+ */
195
+ async upload(data, filename) {
196
+ if (!this.isEnabled()) {
197
+ throw new Error('Storacha is not enabled. Set LSH_STORACHA_ENABLED=true');
198
+ }
199
+ if (!await this.isAuthenticated()) {
200
+ throw new Error('Not authenticated. Run: lsh storacha login <email>');
201
+ }
202
+ const client = await this.getClient();
203
+ // Create File from buffer using Uint8Array view
204
+ const uint8Array = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
205
+ // TypeScript has issues with ArrayBufferLike vs ArrayBuffer, use type assertion
206
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
207
+ const file = new File([uint8Array], filename, { type: 'application/octet-stream' });
208
+ // Upload file
209
+ const cid = await client.uploadFile(file);
210
+ logger.info(`πŸ“€ Uploaded to Storacha: ${cid}`);
211
+ logger.info(` Gateway: https://${cid}.ipfs.storacha.link`);
212
+ return cid.toString();
213
+ }
214
+ /**
215
+ * Download data from Storacha via IPFS gateway
216
+ * Returns data buffer
217
+ */
218
+ async download(cid) {
219
+ const gatewayUrl = `https://${cid}.ipfs.storacha.link`;
220
+ try {
221
+ logger.info(`πŸ“₯ Downloading from Storacha: ${cid}...`);
222
+ const response = await fetch(gatewayUrl);
223
+ if (!response.ok) {
224
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
225
+ }
226
+ const arrayBuffer = await response.arrayBuffer();
227
+ const buffer = Buffer.from(arrayBuffer);
228
+ logger.info(`βœ… Downloaded ${buffer.length} bytes from Storacha`);
229
+ return buffer;
230
+ }
231
+ catch (error) {
232
+ const err = error;
233
+ throw new Error(`Failed to download from Storacha: ${err.message}`);
234
+ }
235
+ }
236
+ /**
237
+ * Enable Storacha network sync
238
+ */
239
+ enable() {
240
+ this.config.enabled = true;
241
+ this.saveConfig();
242
+ logger.info('βœ… Storacha network sync enabled');
243
+ }
244
+ /**
245
+ * Disable Storacha network sync
246
+ */
247
+ disable() {
248
+ this.config.enabled = false;
249
+ this.saveConfig();
250
+ logger.info('⏸️ Storacha network sync disabled (using local cache only)');
251
+ }
252
+ }
253
+ // Singleton instance
254
+ let storachaClient = null;
255
+ /**
256
+ * Get singleton Storacha client instance
257
+ */
258
+ export function getStorachaClient() {
259
+ if (!storachaClient) {
260
+ storachaClient = new StorachaClient();
261
+ }
262
+ return storachaClient;
263
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "2.0.2",
3
+ "version": "2.1.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
5
  "main": "dist/app.js",
6
6
  "bin": {