lsh-framework 3.2.5 → 3.5.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.
- package/LICENSE +21 -0
- package/README.md +72 -34
- package/dist/commands/ipfs.js +7 -12
- package/dist/commands/sync.js +49 -38
- package/dist/constants/config.js +3 -0
- package/dist/lib/floating-point-arithmetic.js +2 -2
- package/dist/lib/ipfs-client-manager.js +51 -13
- package/dist/lib/ipfs-secrets-storage.js +21 -16
- package/dist/lib/ipfs-sync.js +88 -14
- package/dist/lib/secrets-manager.js +117 -47
- package/dist/lib/sync-key-store.js +87 -0
- package/dist/services/secrets/secrets.js +77 -39
- package/package.json +16 -16
- package/dist/__tests__/fixtures/job-fixtures.js +0 -204
- package/dist/__tests__/fixtures/supabase-mocks.js +0 -252
- package/dist/daemon/job-registry.js +0 -556
- package/dist/daemon/lshd.js +0 -968
- package/dist/daemon/saas-api-routes.js +0 -599
- package/dist/daemon/saas-api-server.js +0 -231
- package/dist/examples/supabase-integration.js +0 -106
- package/dist/lib/api-response.js +0 -226
- package/dist/lib/base-command-registrar.js +0 -287
- package/dist/lib/base-job-manager.js +0 -295
- package/dist/lib/cloud-config-manager.js +0 -348
- package/dist/lib/cron-job-manager.js +0 -368
- package/dist/lib/daemon-client-helper.js +0 -145
- package/dist/lib/daemon-client.js +0 -513
- package/dist/lib/database-persistence.js +0 -727
- package/dist/lib/database-schema.js +0 -259
- package/dist/lib/database-types.js +0 -90
- package/dist/lib/enhanced-history-system.js +0 -247
- package/dist/lib/history-system.js +0 -246
- package/dist/lib/job-manager.js +0 -436
- package/dist/lib/job-storage-database.js +0 -164
- package/dist/lib/job-storage-memory.js +0 -73
- package/dist/lib/local-storage-adapter.js +0 -507
- package/dist/lib/optimized-job-scheduler.js +0 -356
- package/dist/lib/saas-audit.js +0 -215
- package/dist/lib/saas-auth.js +0 -465
- package/dist/lib/saas-billing.js +0 -503
- package/dist/lib/saas-email.js +0 -403
- package/dist/lib/saas-encryption.js +0 -221
- package/dist/lib/saas-organizations.js +0 -662
- package/dist/lib/saas-secrets.js +0 -408
- package/dist/lib/saas-types.js +0 -165
- package/dist/lib/supabase-client.js +0 -125
- package/dist/lib/supabase-utils.js +0 -396
- package/dist/services/cron/cron-registrar.js +0 -240
- package/dist/services/cron/cron.js +0 -9
- package/dist/services/daemon/daemon-registrar.js +0 -585
- package/dist/services/daemon/daemon.js +0 -9
- package/dist/services/supabase/supabase-registrar.js +0 -375
- package/dist/services/supabase/supabase.js +0 -9
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 gwicho38
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
# LSH
|
|
1
|
+
# LSH — Encrypted Secrets Manager
|
|
2
2
|
|
|
3
3
|
**The simplest way to sync `.env` files across all your machines.**
|
|
4
4
|
|
|
5
|
-
`lsh` is an encrypted secrets manager that syncs your environment files across development machines with AES-256 encryption
|
|
5
|
+
`lsh` is an encrypted secrets manager that syncs your environment files across development machines with AES-256 encryption over the IPFS network. Secrets are encrypted locally, addressed by content (CID), and published under a deterministic IPNS name derived from your shared key — so a teammate with the same key can pull the latest version with no account or server.
|
|
6
|
+
|
|
7
|
+
> **Durability note:** by default the encrypted content is pinned **only on the machine that pushed it** and served peer-to-peer. Another machine can pull as long as a node that holds the content is online and the IPNS record is still live. For "pull anywhere, anytime" durability, configure a remote pinning service (see [Durable sync](#durable-sync-remote-pinning)).
|
|
6
8
|
|
|
7
9
|
[](https://badge.fury.io/js/lsh-framework)
|
|
8
10
|
[](https://github.com/gwicho38/lsh/actions/workflows/node.js.yml)
|
|
@@ -71,25 +73,49 @@ lsh pull --env staging
|
|
|
71
73
|
## How It Works
|
|
72
74
|
|
|
73
75
|
```
|
|
74
|
-
|
|
75
|
-
┌─────────────┐
|
|
76
|
-
│ .env │
|
|
77
|
-
│ (secrets) │
|
|
78
|
-
└─────────────┘ └─────────────────────┘
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
┌─────────────┐ AES-256
|
|
83
|
-
│ .env │ ◄──decrypt──── │
|
|
84
|
-
│ (secrets) │ └─────────────────────┘
|
|
85
|
-
└─────────────┘
|
|
76
|
+
Machine A (push) Local Kubo (IPFS) node IPFS DHT / swarm
|
|
77
|
+
┌─────────────┐ AES-256 ┌─────────────────────┐ ┌──────────────────┐
|
|
78
|
+
│ .env │ ───encrypt───► │ ipfs add (pin local)│ ──────► │ IPNS record: │
|
|
79
|
+
│ (secrets) │ │ → CID │ publish │ name → CID │
|
|
80
|
+
└─────────────┘ └─────────────────────┘ │ (key-derived) │
|
|
81
|
+
└──────────────────┘
|
|
82
|
+
│ resolve
|
|
83
|
+
Machine B (pull) ▼
|
|
84
|
+
┌─────────────┐ AES-256 ┌─────────────────────┐ fetch ┌──────────────────┐
|
|
85
|
+
│ .env │ ◄──decrypt──── │ ipfs cat <CID> │ ◄─────── │ a node holding │
|
|
86
|
+
│ (secrets) │ └─────────────────────┘ swarm │ the block (A or │
|
|
87
|
+
└─────────────┘ │ a pinning svc) │
|
|
88
|
+
└──────────────────┘
|
|
86
89
|
```
|
|
87
90
|
|
|
88
|
-
1. Your `.env` is encrypted locally with AES-256
|
|
89
|
-
2.
|
|
90
|
-
3.
|
|
91
|
-
4.
|
|
92
|
-
5. Decryption happens locally with
|
|
91
|
+
1. Your `.env` is encrypted locally with AES-256 (the key never leaves the machine).
|
|
92
|
+
2. The ciphertext is added to your **local Kubo (IPFS) daemon** and pinned there, producing a content ID (CID).
|
|
93
|
+
3. The CID is published to **IPNS** under a name derived deterministically from `LSH_SECRETS_KEY` + repo + environment (`HMAC-SHA256`), so teammates need only the shared key.
|
|
94
|
+
4. Another machine derives the same IPNS name, resolves it to the latest CID over the network, and fetches the ciphertext over the IPFS swarm.
|
|
95
|
+
5. Decryption happens locally with the shared key.
|
|
96
|
+
|
|
97
|
+
**What this means:** the encrypted block is only guaranteed to exist where it was pushed. Cross-machine pull works while a node holding the block is online (the publisher, a peer that cached it, or — recommended — a [remote pinning service](#durable-sync-remote-pinning)).
|
|
98
|
+
|
|
99
|
+
## Durable sync (remote pinning)
|
|
100
|
+
|
|
101
|
+
Out of the box, `lsh sync` is zero-config but **not durable**: the encrypted content lives only on the machine that pushed it. If that machine sleeps or goes offline before a teammate pulls — and no peer has cached the block — the pull will stall. `lsh sync push` warns you when no durable pin is configured.
|
|
102
|
+
|
|
103
|
+
To make secrets available "anytime, anywhere", point `lsh` at any IPFS **remote pinning service** (Pinata, Filebase, 4EVERLAND, web3.storage, an IPFS Cluster, etc.). `lsh` uses your local Kubo daemon's remote-pinning support — no extra dependency, and your encryption key never leaves your machine (the service only ever stores ciphertext).
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# 1. Register a pinning service with your local Kubo daemon (one-time)
|
|
107
|
+
ipfs pin remote service add pinata https://api.pinata.cloud/psa <YOUR_JWT>
|
|
108
|
+
|
|
109
|
+
# 2. Tell lsh which service to use (only needed if more than one is configured)
|
|
110
|
+
export LSH_SECRETS_KEY=<your-key>
|
|
111
|
+
export LSH_PIN_SERVICE=pinata
|
|
112
|
+
|
|
113
|
+
# 3. Push — content is now pinned remotely and survives this machine going offline
|
|
114
|
+
lsh sync push --env dev
|
|
115
|
+
# → "Pinned: pinata (durable)"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
If exactly one remote service is configured, `lsh` uses it automatically and `LSH_PIN_SERVICE` is optional.
|
|
93
119
|
|
|
94
120
|
## Installation
|
|
95
121
|
|
|
@@ -138,16 +164,18 @@ lsh pull
|
|
|
138
164
|
# 1. Install LSH
|
|
139
165
|
npm install -g lsh-framework
|
|
140
166
|
|
|
141
|
-
# 2.
|
|
142
|
-
lsh
|
|
167
|
+
# 2. Install + start a local IPFS (Kubo) daemon (one-time)
|
|
168
|
+
lsh sync init
|
|
143
169
|
|
|
144
|
-
# 3. Add your encryption key
|
|
170
|
+
# 3. Add your encryption key (shared with your other machines / team)
|
|
145
171
|
echo "LSH_SECRETS_KEY=your-shared-key" > .env
|
|
146
172
|
|
|
147
|
-
# 4. Pull secrets
|
|
148
|
-
lsh pull
|
|
173
|
+
# 4. Pull secrets (resolves the latest version via IPNS)
|
|
174
|
+
lsh sync pull
|
|
149
175
|
```
|
|
150
176
|
|
|
177
|
+
> Requires a local IPFS (Kubo) daemon — `lsh sync init` installs and starts one. The pushing machine must be online (or a pinning service configured) for others to fetch the content.
|
|
178
|
+
|
|
151
179
|
## Multi-Environment Support
|
|
152
180
|
|
|
153
181
|
```bash
|
|
@@ -215,10 +243,10 @@ eval "$(lsh list --format export)"
|
|
|
215
243
|
|
|
216
244
|
## Security
|
|
217
245
|
|
|
218
|
-
- **AES-256
|
|
246
|
+
- **AES-256** encryption for all secrets (the key never leaves your machine)
|
|
219
247
|
- **Content-addressed storage** - tamper-proof IPFS CIDs
|
|
220
|
-
- **Zero-knowledge** -
|
|
221
|
-
- **Local-first** -
|
|
248
|
+
- **Zero-knowledge** - the IPFS network (and any pinning service) only ever sees ciphertext
|
|
249
|
+
- **Local-first** - works offline with cached secrets
|
|
222
250
|
|
|
223
251
|
### Best Practices
|
|
224
252
|
|
|
@@ -258,16 +286,25 @@ lsh key
|
|
|
258
286
|
lsh push --force
|
|
259
287
|
```
|
|
260
288
|
|
|
261
|
-
### "
|
|
289
|
+
### Pull hangs or "Could not resolve secrets from network"
|
|
290
|
+
|
|
291
|
+
The IPNS name resolved but no online node is serving the content (or the IPNS record expired). Either:
|
|
262
292
|
|
|
263
293
|
```bash
|
|
264
|
-
|
|
265
|
-
|
|
294
|
+
# On the machine that pushed: make sure its daemon is running, then re-push
|
|
295
|
+
lsh sync status
|
|
296
|
+
lsh sync push --env dev
|
|
297
|
+
|
|
298
|
+
# Better: configure a remote pinning service so content stays available
|
|
299
|
+
# even when the pushing machine is offline (see "Durable sync" below)
|
|
266
300
|
```
|
|
267
301
|
|
|
268
|
-
###
|
|
302
|
+
### "IPFS daemon not running"
|
|
269
303
|
|
|
270
|
-
|
|
304
|
+
```bash
|
|
305
|
+
lsh sync init # install + start a local Kubo daemon
|
|
306
|
+
lsh sync status # verify it is up
|
|
307
|
+
```
|
|
271
308
|
|
|
272
309
|
```bash
|
|
273
310
|
# If secrets were pushed before, pull should auto-recover
|
|
@@ -314,8 +351,9 @@ lsh -i
|
|
|
314
351
|
# Required
|
|
315
352
|
LSH_SECRETS_KEY=<your-encryption-key>
|
|
316
353
|
|
|
317
|
-
# Optional -
|
|
318
|
-
|
|
354
|
+
# Optional - name of a kubo remote pinning service for durable sync
|
|
355
|
+
# (configure once with: ipfs pin remote service add <name> <endpoint> <key>)
|
|
356
|
+
LSH_PIN_SERVICE=<service-name>
|
|
319
357
|
|
|
320
358
|
# Optional - Supabase backend
|
|
321
359
|
SUPABASE_URL=https://xxx.supabase.co
|
package/dist/commands/ipfs.js
CHANGED
|
@@ -9,6 +9,7 @@ import { getIPFSSync } from '../lib/ipfs-sync.js';
|
|
|
9
9
|
import { deriveKeyInfo, ensureKeyImported } from '../lib/ipns-key-manager.js';
|
|
10
10
|
import { getGitRepoInfo } from '../lib/git-utils.js';
|
|
11
11
|
import { ENV_VARS, DEFAULTS } from '../constants/index.js';
|
|
12
|
+
import { extractErrorMessage } from '../lib/lsh-error.js';
|
|
12
13
|
/**
|
|
13
14
|
* Register IPFS commands
|
|
14
15
|
*/
|
|
@@ -107,8 +108,7 @@ export function registerIPFSCommands(program) {
|
|
|
107
108
|
console.log('');
|
|
108
109
|
}
|
|
109
110
|
catch (error) {
|
|
110
|
-
|
|
111
|
-
console.error(chalk.red('\n❌ Failed to check status:'), err.message);
|
|
111
|
+
console.error(chalk.red('\n❌ Failed to check status:'), extractErrorMessage(error));
|
|
112
112
|
process.exit(1);
|
|
113
113
|
}
|
|
114
114
|
});
|
|
@@ -134,9 +134,8 @@ export function registerIPFSCommands(program) {
|
|
|
134
134
|
console.log('');
|
|
135
135
|
}
|
|
136
136
|
catch (error) {
|
|
137
|
-
const err = error;
|
|
138
137
|
spinner.fail(chalk.red('Installation failed'));
|
|
139
|
-
console.error(chalk.red(
|
|
138
|
+
console.error(chalk.red(extractErrorMessage(error)));
|
|
140
139
|
process.exit(1);
|
|
141
140
|
}
|
|
142
141
|
});
|
|
@@ -150,8 +149,7 @@ export function registerIPFSCommands(program) {
|
|
|
150
149
|
await manager.uninstall();
|
|
151
150
|
}
|
|
152
151
|
catch (error) {
|
|
153
|
-
|
|
154
|
-
console.error(chalk.red('\n❌ Uninstallation failed:'), err.message);
|
|
152
|
+
console.error(chalk.red('\n❌ Uninstallation failed:'), extractErrorMessage(error));
|
|
155
153
|
process.exit(1);
|
|
156
154
|
}
|
|
157
155
|
});
|
|
@@ -171,9 +169,8 @@ export function registerIPFSCommands(program) {
|
|
|
171
169
|
console.log('');
|
|
172
170
|
}
|
|
173
171
|
catch (error) {
|
|
174
|
-
const err = error;
|
|
175
172
|
spinner.fail(chalk.red('Initialization failed'));
|
|
176
|
-
console.error(chalk.red(
|
|
173
|
+
console.error(chalk.red(extractErrorMessage(error)));
|
|
177
174
|
process.exit(1);
|
|
178
175
|
}
|
|
179
176
|
});
|
|
@@ -187,8 +184,7 @@ export function registerIPFSCommands(program) {
|
|
|
187
184
|
await manager.start();
|
|
188
185
|
}
|
|
189
186
|
catch (error) {
|
|
190
|
-
|
|
191
|
-
console.error(chalk.red('\n❌ Failed to start daemon:'), err.message);
|
|
187
|
+
console.error(chalk.red('\n❌ Failed to start daemon:'), extractErrorMessage(error));
|
|
192
188
|
process.exit(1);
|
|
193
189
|
}
|
|
194
190
|
});
|
|
@@ -202,8 +198,7 @@ export function registerIPFSCommands(program) {
|
|
|
202
198
|
await manager.stop();
|
|
203
199
|
}
|
|
204
200
|
catch (error) {
|
|
205
|
-
|
|
206
|
-
console.error(chalk.red('\n❌ Failed to stop daemon:'), err.message);
|
|
201
|
+
console.error(chalk.red('\n❌ Failed to stop daemon:'), extractErrorMessage(error));
|
|
207
202
|
process.exit(1);
|
|
208
203
|
}
|
|
209
204
|
});
|
package/dist/commands/sync.js
CHANGED
|
@@ -14,6 +14,7 @@ import { IPFSClientManager } from '../lib/ipfs-client-manager.js';
|
|
|
14
14
|
import { getGitRepoInfo } from '../lib/git-utils.js';
|
|
15
15
|
import { deriveKeyInfo, ensureKeyImported } from '../lib/ipns-key-manager.js';
|
|
16
16
|
import { ENV_VARS, DEFAULTS } from '../constants/index.js';
|
|
17
|
+
import { extractErrorMessage } from '../lib/lsh-error.js';
|
|
17
18
|
/**
|
|
18
19
|
* Register sync commands
|
|
19
20
|
*/
|
|
@@ -81,9 +82,8 @@ export function registerSyncCommands(program) {
|
|
|
81
82
|
installSpinner.succeed(chalk.green('IPFS client installed'));
|
|
82
83
|
}
|
|
83
84
|
catch (error) {
|
|
84
|
-
const err = error;
|
|
85
85
|
installSpinner.fail(chalk.red('Failed to install IPFS'));
|
|
86
|
-
console.error(chalk.red(
|
|
86
|
+
console.error(chalk.red(extractErrorMessage(error)));
|
|
87
87
|
process.exit(1);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
@@ -98,14 +98,14 @@ export function registerSyncCommands(program) {
|
|
|
98
98
|
initSpinner.succeed(chalk.green('IPFS repository initialized'));
|
|
99
99
|
}
|
|
100
100
|
catch (error) {
|
|
101
|
-
const
|
|
101
|
+
const msg = extractErrorMessage(error);
|
|
102
102
|
// Check if already initialized
|
|
103
|
-
if (
|
|
103
|
+
if (msg.includes('already') || msg.includes('exists')) {
|
|
104
104
|
initSpinner.succeed(chalk.green('IPFS repository already initialized'));
|
|
105
105
|
}
|
|
106
106
|
else {
|
|
107
107
|
initSpinner.fail(chalk.red('Failed to initialize IPFS'));
|
|
108
|
-
console.error(chalk.red(
|
|
108
|
+
console.error(chalk.red(msg));
|
|
109
109
|
process.exit(1);
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -116,9 +116,8 @@ export function registerSyncCommands(program) {
|
|
|
116
116
|
startSpinner.succeed(chalk.green('IPFS daemon started'));
|
|
117
117
|
}
|
|
118
118
|
catch (error) {
|
|
119
|
-
const err = error;
|
|
120
119
|
startSpinner.fail(chalk.red('Failed to start daemon'));
|
|
121
|
-
console.error(chalk.red(
|
|
120
|
+
console.error(chalk.red(extractErrorMessage(error)));
|
|
122
121
|
process.exit(1);
|
|
123
122
|
}
|
|
124
123
|
// Final status
|
|
@@ -148,8 +147,7 @@ export function registerSyncCommands(program) {
|
|
|
148
147
|
console.log(chalk.green('✓ IPFS daemon running'));
|
|
149
148
|
}
|
|
150
149
|
catch (error) {
|
|
151
|
-
|
|
152
|
-
console.error(chalk.red(err.message));
|
|
150
|
+
console.error(chalk.red(extractErrorMessage(error)));
|
|
153
151
|
process.exit(1);
|
|
154
152
|
}
|
|
155
153
|
// Step 2: Read and validate .env file
|
|
@@ -228,9 +226,8 @@ export function registerSyncCommands(program) {
|
|
|
228
226
|
console.log('');
|
|
229
227
|
}
|
|
230
228
|
catch (error) {
|
|
231
|
-
const err = error;
|
|
232
229
|
uploadSpinner.fail(chalk.red('Sync failed'));
|
|
233
|
-
console.error(chalk.red(
|
|
230
|
+
console.error(chalk.red(extractErrorMessage(error)));
|
|
234
231
|
process.exit(1);
|
|
235
232
|
}
|
|
236
233
|
});
|
|
@@ -251,8 +248,7 @@ export function registerSyncCommands(program) {
|
|
|
251
248
|
await ipfsManager.ensureDaemonRunning();
|
|
252
249
|
}
|
|
253
250
|
catch (error) {
|
|
254
|
-
|
|
255
|
-
spinner.fail(chalk.red(err.message));
|
|
251
|
+
spinner.fail(chalk.red(extractErrorMessage(error)));
|
|
256
252
|
process.exit(1);
|
|
257
253
|
}
|
|
258
254
|
// Read .env file
|
|
@@ -306,35 +302,59 @@ export function registerSyncCommands(program) {
|
|
|
306
302
|
spinner.succeed(chalk.green('Uploaded to IPFS!'));
|
|
307
303
|
console.log('');
|
|
308
304
|
console.log(chalk.bold('CID:'), chalk.cyan(cid));
|
|
309
|
-
|
|
305
|
+
const repoName = gitInfo?.repoName || DEFAULTS.DEFAULT_ENVIRONMENT;
|
|
306
|
+
const env = options.env || DEFAULTS.DEFAULT_ENVIRONMENT;
|
|
307
|
+
// Publish to IPNS so teammates can `lsh sync pull` without a CID.
|
|
308
|
+
let ipnsPublished = false;
|
|
310
309
|
if (encryptionKey) {
|
|
311
310
|
try {
|
|
312
|
-
const repoName = gitInfo?.repoName || DEFAULTS.DEFAULT_ENVIRONMENT;
|
|
313
|
-
const env = options.env || DEFAULTS.DEFAULT_ENVIRONMENT;
|
|
314
311
|
const keyInfo = deriveKeyInfo(encryptionKey, repoName, env);
|
|
315
312
|
const ipnsName = await ensureKeyImported(ipfsSync.getApiUrl(), keyInfo);
|
|
316
313
|
if (ipnsName) {
|
|
317
314
|
const publishedName = await ipfsSync.publishToIPNS(cid, keyInfo.keyName);
|
|
318
315
|
if (publishedName) {
|
|
319
316
|
console.log(chalk.bold('IPNS:'), chalk.cyan(publishedName));
|
|
317
|
+
ipnsPublished = true;
|
|
320
318
|
}
|
|
321
319
|
}
|
|
322
320
|
}
|
|
323
|
-
catch {
|
|
324
|
-
|
|
321
|
+
catch (error) {
|
|
322
|
+
console.error(chalk.yellow(`IPNS publish error: ${extractErrorMessage(error)}`));
|
|
325
323
|
}
|
|
326
324
|
}
|
|
325
|
+
// Durable remote pin (best-effort): makes the content survive this
|
|
326
|
+
// machine going offline. No-op unless a pinning service is configured.
|
|
327
|
+
const pinnedService = await ipfsSync.addRemotePin(cid, `lsh-${repoName}-${env}`);
|
|
328
|
+
if (pinnedService) {
|
|
329
|
+
console.log(chalk.bold('Pinned:'), chalk.cyan(`${pinnedService} (durable)`));
|
|
330
|
+
}
|
|
327
331
|
console.log('');
|
|
328
332
|
console.log(chalk.gray('Teammates can pull with just:'));
|
|
329
333
|
console.log(chalk.cyan(' lsh sync pull'));
|
|
330
334
|
console.log(chalk.gray('Or by specific CID:'));
|
|
331
335
|
console.log(chalk.cyan(` lsh sync pull ${cid}`));
|
|
332
336
|
console.log('');
|
|
337
|
+
// Honest durability reporting — do not claim success the user cannot rely on.
|
|
338
|
+
if (!pinnedService) {
|
|
339
|
+
console.log(chalk.yellow('⚠️ No remote pin — this content lives ONLY on this machine.'));
|
|
340
|
+
console.log(chalk.gray(' If this machine goes offline, teammates cannot fetch it.'));
|
|
341
|
+
console.log(chalk.gray(' Enable durable pinning (one-time):'));
|
|
342
|
+
console.log(chalk.gray(' ipfs pin remote service add <name> <endpoint> <key>'));
|
|
343
|
+
console.log(chalk.gray(' export LSH_PIN_SERVICE=<name>'));
|
|
344
|
+
console.log('');
|
|
345
|
+
}
|
|
346
|
+
if (!ipnsPublished) {
|
|
347
|
+
spinner.warn(chalk.yellow('IPNS publish failed — teammates CANNOT `lsh sync pull` (no CID) until you re-push.'));
|
|
348
|
+
console.log(chalk.gray(` They can still pull by explicit CID: lsh sync pull ${cid}`));
|
|
349
|
+
console.log('');
|
|
350
|
+
// The headline promise ("teammates can pull with just: lsh sync pull") failed,
|
|
351
|
+
// so exit non-zero rather than reporting a success the user cannot rely on.
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
333
354
|
}
|
|
334
355
|
catch (error) {
|
|
335
|
-
const err = error;
|
|
336
356
|
spinner.fail(chalk.red('Push failed'));
|
|
337
|
-
console.error(chalk.red(
|
|
357
|
+
console.error(chalk.red(extractErrorMessage(error)));
|
|
338
358
|
process.exit(1);
|
|
339
359
|
}
|
|
340
360
|
});
|
|
@@ -357,8 +377,7 @@ export function registerSyncCommands(program) {
|
|
|
357
377
|
await ipfsManager.ensureDaemonRunning();
|
|
358
378
|
}
|
|
359
379
|
catch (error) {
|
|
360
|
-
|
|
361
|
-
spinner.fail(chalk.red(err.message));
|
|
380
|
+
spinner.fail(chalk.red(extractErrorMessage(error)));
|
|
362
381
|
process.exit(1);
|
|
363
382
|
}
|
|
364
383
|
// Get encryption key
|
|
@@ -400,8 +419,7 @@ export function registerSyncCommands(program) {
|
|
|
400
419
|
spinner.start('Downloading from IPFS...');
|
|
401
420
|
}
|
|
402
421
|
catch (error) {
|
|
403
|
-
|
|
404
|
-
spinner.fail(chalk.red(`IPNS resolution failed: ${err.message}`));
|
|
422
|
+
spinner.fail(chalk.red(`IPNS resolution failed: ${extractErrorMessage(error)}`));
|
|
405
423
|
process.exit(1);
|
|
406
424
|
}
|
|
407
425
|
}
|
|
@@ -480,9 +498,8 @@ export function registerSyncCommands(program) {
|
|
|
480
498
|
console.log('');
|
|
481
499
|
}
|
|
482
500
|
catch (error) {
|
|
483
|
-
const err = error;
|
|
484
501
|
spinner.fail(chalk.red('Pull failed'));
|
|
485
|
-
console.error(chalk.red(
|
|
502
|
+
console.error(chalk.red(extractErrorMessage(error)));
|
|
486
503
|
process.exit(1);
|
|
487
504
|
}
|
|
488
505
|
});
|
|
@@ -565,8 +582,7 @@ export function registerSyncCommands(program) {
|
|
|
565
582
|
console.log('');
|
|
566
583
|
}
|
|
567
584
|
catch (error) {
|
|
568
|
-
|
|
569
|
-
console.error(chalk.red('Failed to check status:'), err.message);
|
|
585
|
+
console.error(chalk.red('Failed to check status:'), extractErrorMessage(error));
|
|
570
586
|
process.exit(1);
|
|
571
587
|
}
|
|
572
588
|
});
|
|
@@ -615,8 +631,7 @@ export function registerSyncCommands(program) {
|
|
|
615
631
|
console.log('');
|
|
616
632
|
}
|
|
617
633
|
catch (error) {
|
|
618
|
-
|
|
619
|
-
console.error(chalk.red('Failed to get history:'), err.message);
|
|
634
|
+
console.error(chalk.red('Failed to get history:'), extractErrorMessage(error));
|
|
620
635
|
process.exit(1);
|
|
621
636
|
}
|
|
622
637
|
});
|
|
@@ -648,9 +663,8 @@ export function registerSyncCommands(program) {
|
|
|
648
663
|
}
|
|
649
664
|
}
|
|
650
665
|
catch (error) {
|
|
651
|
-
const err = error;
|
|
652
666
|
spinner.fail(chalk.red('Verification failed'));
|
|
653
|
-
console.error(chalk.red(
|
|
667
|
+
console.error(chalk.red(extractErrorMessage(error)));
|
|
654
668
|
process.exit(1);
|
|
655
669
|
}
|
|
656
670
|
});
|
|
@@ -665,8 +679,7 @@ export function registerSyncCommands(program) {
|
|
|
665
679
|
console.log(chalk.green('✅ Sync history cleared'));
|
|
666
680
|
}
|
|
667
681
|
catch (error) {
|
|
668
|
-
|
|
669
|
-
console.error(chalk.red('Failed to clear history:'), err.message);
|
|
682
|
+
console.error(chalk.red('Failed to clear history:'), extractErrorMessage(error));
|
|
670
683
|
process.exit(1);
|
|
671
684
|
}
|
|
672
685
|
});
|
|
@@ -690,8 +703,7 @@ export function registerSyncCommands(program) {
|
|
|
690
703
|
await manager.start();
|
|
691
704
|
}
|
|
692
705
|
catch (error) {
|
|
693
|
-
|
|
694
|
-
console.error(chalk.red('Failed to start daemon:'), err.message);
|
|
706
|
+
console.error(chalk.red('Failed to start daemon:'), extractErrorMessage(error));
|
|
695
707
|
process.exit(1);
|
|
696
708
|
}
|
|
697
709
|
});
|
|
@@ -705,8 +717,7 @@ export function registerSyncCommands(program) {
|
|
|
705
717
|
await manager.stop();
|
|
706
718
|
}
|
|
707
719
|
catch (error) {
|
|
708
|
-
|
|
709
|
-
console.error(chalk.red('Failed to stop daemon:'), err.message);
|
|
720
|
+
console.error(chalk.red('Failed to stop daemon:'), extractErrorMessage(error));
|
|
710
721
|
process.exit(1);
|
|
711
722
|
}
|
|
712
723
|
});
|
package/dist/constants/config.js
CHANGED
|
@@ -30,6 +30,9 @@ export const ENV_VARS = {
|
|
|
30
30
|
// Secrets management
|
|
31
31
|
LSH_SECRETS_KEY: 'LSH_SECRETS_KEY',
|
|
32
32
|
LSH_MASTER_KEY: 'LSH_MASTER_KEY',
|
|
33
|
+
// Name of the kubo remote pinning service to use for durable sync
|
|
34
|
+
// (configured via `ipfs pin remote service add <name> <endpoint> <key>`).
|
|
35
|
+
LSH_PIN_SERVICE: 'LSH_PIN_SERVICE',
|
|
33
36
|
// Feature flags
|
|
34
37
|
LSH_LOCAL_STORAGE_QUIET: 'LSH_LOCAL_STORAGE_QUIET',
|
|
35
38
|
LSH_V1_COMPAT: 'LSH_V1_COMPAT',
|
|
@@ -25,7 +25,7 @@ export class FloatingPointArithmetic {
|
|
|
25
25
|
return this.roundToPrecision(result);
|
|
26
26
|
}
|
|
27
27
|
catch (error) {
|
|
28
|
-
throw new Error(`Arithmetic error: ${error.message}
|
|
28
|
+
throw new Error(`Arithmetic error: ${error.message}`, { cause: error });
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
@@ -193,7 +193,7 @@ export class FloatingPointArithmetic {
|
|
|
193
193
|
return result;
|
|
194
194
|
}
|
|
195
195
|
catch (error) {
|
|
196
|
-
throw new Error(`Expression evaluation failed: ${error.message}
|
|
196
|
+
throw new Error(`Expression evaluation failed: ${error.message}`, { cause: error });
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
/**
|
|
@@ -5,14 +5,20 @@
|
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import * as os from 'os';
|
|
8
|
-
import { exec, spawn } from 'child_process';
|
|
8
|
+
import { exec, execFile, spawn } from 'child_process';
|
|
9
9
|
import { promisify } from 'util';
|
|
10
10
|
import * as readline from 'readline';
|
|
11
11
|
import { createLogger } from './logger.js';
|
|
12
12
|
import { getPlatformInfo } from './platform-utils.js';
|
|
13
13
|
import { getLshConfig } from './lsh-config.js';
|
|
14
14
|
const execAsync = promisify(exec);
|
|
15
|
+
// execFile does NOT spawn a shell: arguments are passed literally to the binary,
|
|
16
|
+
// so interpolated values (e.g. a Kubo version) cannot inject shell commands.
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
15
18
|
const logger = createLogger('IPFSClientManager');
|
|
19
|
+
// Kubo releases are strict semver (e.g. "0.26.0"). Reject anything else before it
|
|
20
|
+
// reaches a download URL or subprocess — defense in depth against command injection.
|
|
21
|
+
const KUBO_VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
16
22
|
/**
|
|
17
23
|
* IPFS Client Manager
|
|
18
24
|
*
|
|
@@ -91,6 +97,9 @@ export class IPFSClientManager {
|
|
|
91
97
|
logger.info('📦 Installing IPFS client (Kubo)...');
|
|
92
98
|
// Determine version to install
|
|
93
99
|
const version = options.version || await this.getLatestKuboVersion();
|
|
100
|
+
if (!KUBO_VERSION_RE.test(version)) {
|
|
101
|
+
throw new Error(`Invalid Kubo version "${version}": expected semver like 0.26.0`);
|
|
102
|
+
}
|
|
94
103
|
logger.info(` Version: ${version}`);
|
|
95
104
|
logger.info(` Platform: ${platformInfo.platformName} ${platformInfo.arch}`);
|
|
96
105
|
// Download and install based on platform
|
|
@@ -200,7 +209,7 @@ export class IPFSClientManager {
|
|
|
200
209
|
}
|
|
201
210
|
catch (initError) {
|
|
202
211
|
const err = initError;
|
|
203
|
-
throw new Error(`Failed to auto-initialize IPFS repo: ${err.message}
|
|
212
|
+
throw new Error(`Failed to auto-initialize IPFS repo: ${err.message}`, { cause: initError });
|
|
204
213
|
}
|
|
205
214
|
}
|
|
206
215
|
logger.info('🚀 Starting IPFS daemon...');
|
|
@@ -267,6 +276,24 @@ export class IPFSClientManager {
|
|
|
267
276
|
*/
|
|
268
277
|
async stop() {
|
|
269
278
|
const pidPath = path.join(this.ipfsDir, 'daemon.pid');
|
|
279
|
+
// Try graceful shutdown via IPFS API first (works even without PID file)
|
|
280
|
+
try {
|
|
281
|
+
const response = await fetch('http://127.0.0.1:5001/api/v0/shutdown', {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
signal: AbortSignal.timeout(5000),
|
|
284
|
+
});
|
|
285
|
+
if (response.ok) {
|
|
286
|
+
// Clean up PID file if it exists
|
|
287
|
+
if (fs.existsSync(pidPath)) {
|
|
288
|
+
fs.unlinkSync(pidPath);
|
|
289
|
+
}
|
|
290
|
+
logger.info('✅ IPFS daemon stopped');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// API shutdown failed, fall back to PID-based kill
|
|
296
|
+
}
|
|
270
297
|
if (!fs.existsSync(pidPath)) {
|
|
271
298
|
logger.info('ℹ️ IPFS daemon not running (no PID file)');
|
|
272
299
|
return;
|
|
@@ -280,6 +307,13 @@ export class IPFSClientManager {
|
|
|
280
307
|
}
|
|
281
308
|
catch (error) {
|
|
282
309
|
const err = error;
|
|
310
|
+
// Process already gone — clean up stale PID file
|
|
311
|
+
if (err.code === 'ESRCH') {
|
|
312
|
+
if (fs.existsSync(pidPath))
|
|
313
|
+
fs.unlinkSync(pidPath);
|
|
314
|
+
logger.info('✅ IPFS daemon already stopped (stale PID file cleaned)');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
283
317
|
logger.error(`❌ Failed to stop daemon: ${err.message}`);
|
|
284
318
|
throw error;
|
|
285
319
|
}
|
|
@@ -289,8 +323,12 @@ export class IPFSClientManager {
|
|
|
289
323
|
*/
|
|
290
324
|
async getLatestKuboVersion() {
|
|
291
325
|
try {
|
|
292
|
-
// Use GitHub API to get latest release
|
|
293
|
-
|
|
326
|
+
// Use GitHub API to get latest release. Bound the request: an unbounded
|
|
327
|
+
// fetch hangs indefinitely on a blocked/slow network (e.g. CI runners),
|
|
328
|
+
// which would stall install() and time out tests before the fallback.
|
|
329
|
+
const response = await fetch('https://api.github.com/repos/ipfs/kubo/releases/latest', {
|
|
330
|
+
signal: AbortSignal.timeout(3000),
|
|
331
|
+
});
|
|
294
332
|
const data = await response.json();
|
|
295
333
|
// Remove 'v' prefix if present
|
|
296
334
|
return data.tag_name.replace(/^v/, '');
|
|
@@ -308,11 +346,11 @@ export class IPFSClientManager {
|
|
|
308
346
|
const downloadUrl = `https://dist.ipfs.tech/kubo/v${version}/kubo_v${version}_darwin-${arch}.tar.gz`;
|
|
309
347
|
const tarPath = path.join(this.ipfsDir, 'kubo.tar.gz');
|
|
310
348
|
logger.info(' Downloading Kubo...');
|
|
311
|
-
// Download
|
|
312
|
-
await
|
|
349
|
+
// Download (execFile: no shell, args passed literally)
|
|
350
|
+
await execFileAsync('curl', ['-L', '-o', tarPath, downloadUrl]);
|
|
313
351
|
logger.info(' Extracting...');
|
|
314
352
|
// Extract
|
|
315
|
-
await
|
|
353
|
+
await execFileAsync('tar', ['-xzf', tarPath, '-C', this.ipfsDir]);
|
|
316
354
|
// Move binary
|
|
317
355
|
const extractedBinPath = path.join(this.ipfsDir, 'kubo', 'ipfs');
|
|
318
356
|
fs.mkdirSync(this.binDir, { recursive: true });
|
|
@@ -331,11 +369,11 @@ export class IPFSClientManager {
|
|
|
331
369
|
const downloadUrl = `https://dist.ipfs.tech/kubo/v${version}/kubo_v${version}_linux-${arch}.tar.gz`;
|
|
332
370
|
const tarPath = path.join(this.ipfsDir, 'kubo.tar.gz');
|
|
333
371
|
logger.info(' Downloading Kubo...');
|
|
334
|
-
// Download
|
|
335
|
-
await
|
|
372
|
+
// Download (execFile: no shell, args passed literally)
|
|
373
|
+
await execFileAsync('curl', ['-L', '-o', tarPath, downloadUrl]);
|
|
336
374
|
logger.info(' Extracting...');
|
|
337
375
|
// Extract
|
|
338
|
-
await
|
|
376
|
+
await execFileAsync('tar', ['-xzf', tarPath, '-C', this.ipfsDir]);
|
|
339
377
|
// Move binary
|
|
340
378
|
const extractedBinPath = path.join(this.ipfsDir, 'kubo', 'ipfs');
|
|
341
379
|
fs.mkdirSync(this.binDir, { recursive: true });
|
|
@@ -353,11 +391,11 @@ export class IPFSClientManager {
|
|
|
353
391
|
const downloadUrl = `https://dist.ipfs.tech/kubo/v${version}/kubo_v${version}_windows-amd64.zip`;
|
|
354
392
|
const zipPath = path.join(this.ipfsDir, 'kubo.zip');
|
|
355
393
|
logger.info(' Downloading Kubo...');
|
|
356
|
-
// Download
|
|
357
|
-
await
|
|
394
|
+
// Download (execFile: no shell, args passed literally)
|
|
395
|
+
await execFileAsync('curl', ['-L', '-o', zipPath, downloadUrl]);
|
|
358
396
|
logger.info(' Extracting...');
|
|
359
397
|
// Extract (Windows has built-in tar that supports zip)
|
|
360
|
-
await
|
|
398
|
+
await execFileAsync('tar', ['-xf', zipPath, '-C', this.ipfsDir]);
|
|
361
399
|
// Move binary
|
|
362
400
|
const extractedBinPath = path.join(this.ipfsDir, 'kubo', 'ipfs.exe');
|
|
363
401
|
fs.mkdirSync(this.binDir, { recursive: true });
|