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 +65 -7
- package/dist/cli.js +2 -0
- package/dist/commands/init.js +76 -15
- package/dist/commands/storacha.js +208 -0
- package/dist/lib/ipfs-secrets-storage.js +43 -4
- package/dist/lib/storacha-client.js +263 -0
- package/package.json +1 -1
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
|
-
- **
|
|
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,
|
|
16
|
-
- **Free & Open Source** - no per-seat pricing,
|
|
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?**
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
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
|
package/dist/commands/init.js
CHANGED
|
@@ -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.
|
|
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: '
|
|
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: '
|
|
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 === '
|
|
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 === '
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
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('
|
|
364
|
-
console.log(chalk.cyan(' lsh
|
|
365
|
-
console.log(
|
|
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
|
-
//
|
|
67
|
-
|
|
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
|
-
|
|
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(`
|
|
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.
|
|
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": {
|