lsh-framework 3.2.4 → 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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -34
  3. package/dist/commands/ipfs.js +7 -12
  4. package/dist/commands/sync.js +51 -39
  5. package/dist/constants/config.js +3 -0
  6. package/dist/lib/floating-point-arithmetic.js +2 -2
  7. package/dist/lib/ipfs-client-manager.js +51 -13
  8. package/dist/lib/ipfs-secrets-storage.js +21 -16
  9. package/dist/lib/ipfs-sync.js +88 -14
  10. package/dist/lib/secrets-manager.js +117 -47
  11. package/dist/lib/sync-key-store.js +87 -0
  12. package/dist/services/secrets/secrets.js +77 -39
  13. package/package.json +16 -16
  14. package/dist/__tests__/fixtures/job-fixtures.js +0 -204
  15. package/dist/__tests__/fixtures/supabase-mocks.js +0 -252
  16. package/dist/daemon/job-registry.js +0 -556
  17. package/dist/daemon/lshd.js +0 -968
  18. package/dist/daemon/saas-api-routes.js +0 -599
  19. package/dist/daemon/saas-api-server.js +0 -231
  20. package/dist/examples/supabase-integration.js +0 -106
  21. package/dist/lib/api-response.js +0 -226
  22. package/dist/lib/base-command-registrar.js +0 -287
  23. package/dist/lib/base-job-manager.js +0 -295
  24. package/dist/lib/cloud-config-manager.js +0 -348
  25. package/dist/lib/cron-job-manager.js +0 -368
  26. package/dist/lib/daemon-client-helper.js +0 -145
  27. package/dist/lib/daemon-client.js +0 -513
  28. package/dist/lib/database-persistence.js +0 -727
  29. package/dist/lib/database-schema.js +0 -259
  30. package/dist/lib/database-types.js +0 -90
  31. package/dist/lib/enhanced-history-system.js +0 -247
  32. package/dist/lib/history-system.js +0 -246
  33. package/dist/lib/job-manager.js +0 -436
  34. package/dist/lib/job-storage-database.js +0 -164
  35. package/dist/lib/job-storage-memory.js +0 -73
  36. package/dist/lib/local-storage-adapter.js +0 -507
  37. package/dist/lib/optimized-job-scheduler.js +0 -356
  38. package/dist/lib/saas-audit.js +0 -215
  39. package/dist/lib/saas-auth.js +0 -465
  40. package/dist/lib/saas-billing.js +0 -503
  41. package/dist/lib/saas-email.js +0 -403
  42. package/dist/lib/saas-encryption.js +0 -221
  43. package/dist/lib/saas-organizations.js +0 -662
  44. package/dist/lib/saas-secrets.js +0 -408
  45. package/dist/lib/saas-types.js +0 -165
  46. package/dist/lib/supabase-client.js +0 -125
  47. package/dist/lib/supabase-utils.js +0 -396
  48. package/dist/services/cron/cron-registrar.js +0 -240
  49. package/dist/services/cron/cron.js +0 -9
  50. package/dist/services/daemon/daemon-registrar.js +0 -585
  51. package/dist/services/daemon/daemon.js +0 -9
  52. package/dist/services/supabase/supabase-registrar.js +0 -375
  53. 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 v3.1.19 - Encrypted Secrets Manager
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 via the IPFS network. Push once, pull anywhere.
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
  [![npm version](https://badge.fury.io/js/lsh-framework.svg)](https://badge.fury.io/js/lsh-framework)
8
10
  [![Node.js CI](https://github.com/gwicho38/lsh/actions/workflows/node.js.yml/badge.svg)](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
- Your Machine Storacha (IPFS Network)
75
- ┌─────────────┐ ┌─────────────────────┐
76
- │ .env │ AES-256 Encrypted Blob
77
- │ (secrets) │ ───encrypt───► (content-addressed)
78
- └─────────────┘ └─────────────────────┘
79
-
80
-
81
- Another Machine ┌─────────────────────┐
82
- ┌─────────────┐ AES-256 Registry │
83
- │ .env │ ◄──decrypt──── │ (points to blob)
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. Encrypted data uploads to IPFS via Storacha
90
- 3. A registry tracks the latest version per repository
91
- 4. Other machines pull via the content ID (CID)
92
- 5. Decryption happens locally with your shared key
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. Authenticate with Storacha (one-time)
142
- lsh storacha login your@email.com
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-CBC** encryption for all secrets
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** - Storacha never sees your unencrypted data
221
- - **Local-first** - Works offline with cached secrets
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
- ### "Storacha authentication required"
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
- lsh storacha login your@email.com
265
- # Check email for verification
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
- ### Pull fails after clearing metadata
302
+ ### "IPFS daemon not running"
269
303
 
270
- v3.0.0 fix: Pull now automatically checks the Storacha registry when local metadata is missing.
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 - Storacha (default enabled)
318
- LSH_STORACHA_ENABLED=true
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
@@ -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
- const err = error;
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(err.message));
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
- const err = error;
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(err.message));
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
- const err = error;
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
- const err = error;
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
  });
@@ -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(err.message));
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 err = error;
101
+ const msg = extractErrorMessage(error);
102
102
  // Check if already initialized
103
- if (err.message.includes('already') || err.message.includes('exists')) {
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(err.message));
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(err.message));
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
- const err = error;
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(err.message));
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
- const err = error;
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
- // Publish to IPNS
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
- // Non-fatal
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(err.message));
357
+ console.error(chalk.red(extractErrorMessage(error)));
338
358
  process.exit(1);
339
359
  }
340
360
  });
@@ -344,6 +364,7 @@ export function registerSyncCommands(program) {
344
364
  .description('⬇️ Pull secrets from IPFS (auto-resolves via IPNS if no CID given)')
345
365
  .option('-o, --output <path>', 'Output file path', '.env')
346
366
  .option('-e, --env <name>', 'Environment name', '')
367
+ .option('-r, --repo <name>', 'Source repo name for IPNS resolution (overrides auto-detected repo)')
347
368
  .option('--force', 'Overwrite existing file without backup')
348
369
  .action(async (cid, options) => {
349
370
  const spinner = ora(cid ? 'Downloading from IPFS...' : 'Resolving latest secrets via IPNS...').start();
@@ -356,8 +377,7 @@ export function registerSyncCommands(program) {
356
377
  await ipfsManager.ensureDaemonRunning();
357
378
  }
358
379
  catch (error) {
359
- const err = error;
360
- spinner.fail(chalk.red(err.message));
380
+ spinner.fail(chalk.red(extractErrorMessage(error)));
361
381
  process.exit(1);
362
382
  }
363
383
  // Get encryption key
@@ -378,7 +398,7 @@ export function registerSyncCommands(program) {
378
398
  process.exit(1);
379
399
  }
380
400
  const gitInfo = getGitRepoInfo();
381
- const repoName = gitInfo?.repoName || DEFAULTS.DEFAULT_ENVIRONMENT;
401
+ const repoName = options.repo || gitInfo?.repoName || DEFAULTS.DEFAULT_ENVIRONMENT;
382
402
  const environment = options.env || DEFAULTS.DEFAULT_ENVIRONMENT;
383
403
  const keyInfo = deriveKeyInfo(ipnsKey, repoName, environment);
384
404
  const ipnsName = await ensureKeyImported(ipfsSync.getApiUrl(), keyInfo);
@@ -399,8 +419,7 @@ export function registerSyncCommands(program) {
399
419
  spinner.start('Downloading from IPFS...');
400
420
  }
401
421
  catch (error) {
402
- const err = error;
403
- spinner.fail(chalk.red(`IPNS resolution failed: ${err.message}`));
422
+ spinner.fail(chalk.red(`IPNS resolution failed: ${extractErrorMessage(error)}`));
404
423
  process.exit(1);
405
424
  }
406
425
  }
@@ -479,9 +498,8 @@ export function registerSyncCommands(program) {
479
498
  console.log('');
480
499
  }
481
500
  catch (error) {
482
- const err = error;
483
501
  spinner.fail(chalk.red('Pull failed'));
484
- console.error(chalk.red(err.message));
502
+ console.error(chalk.red(extractErrorMessage(error)));
485
503
  process.exit(1);
486
504
  }
487
505
  });
@@ -564,8 +582,7 @@ export function registerSyncCommands(program) {
564
582
  console.log('');
565
583
  }
566
584
  catch (error) {
567
- const err = error;
568
- console.error(chalk.red('Failed to check status:'), err.message);
585
+ console.error(chalk.red('Failed to check status:'), extractErrorMessage(error));
569
586
  process.exit(1);
570
587
  }
571
588
  });
@@ -614,8 +631,7 @@ export function registerSyncCommands(program) {
614
631
  console.log('');
615
632
  }
616
633
  catch (error) {
617
- const err = error;
618
- console.error(chalk.red('Failed to get history:'), err.message);
634
+ console.error(chalk.red('Failed to get history:'), extractErrorMessage(error));
619
635
  process.exit(1);
620
636
  }
621
637
  });
@@ -647,9 +663,8 @@ export function registerSyncCommands(program) {
647
663
  }
648
664
  }
649
665
  catch (error) {
650
- const err = error;
651
666
  spinner.fail(chalk.red('Verification failed'));
652
- console.error(chalk.red(err.message));
667
+ console.error(chalk.red(extractErrorMessage(error)));
653
668
  process.exit(1);
654
669
  }
655
670
  });
@@ -664,8 +679,7 @@ export function registerSyncCommands(program) {
664
679
  console.log(chalk.green('✅ Sync history cleared'));
665
680
  }
666
681
  catch (error) {
667
- const err = error;
668
- console.error(chalk.red('Failed to clear history:'), err.message);
682
+ console.error(chalk.red('Failed to clear history:'), extractErrorMessage(error));
669
683
  process.exit(1);
670
684
  }
671
685
  });
@@ -689,8 +703,7 @@ export function registerSyncCommands(program) {
689
703
  await manager.start();
690
704
  }
691
705
  catch (error) {
692
- const err = error;
693
- console.error(chalk.red('Failed to start daemon:'), err.message);
706
+ console.error(chalk.red('Failed to start daemon:'), extractErrorMessage(error));
694
707
  process.exit(1);
695
708
  }
696
709
  });
@@ -704,8 +717,7 @@ export function registerSyncCommands(program) {
704
717
  await manager.stop();
705
718
  }
706
719
  catch (error) {
707
- const err = error;
708
- console.error(chalk.red('Failed to stop daemon:'), err.message);
720
+ console.error(chalk.red('Failed to stop daemon:'), extractErrorMessage(error));
709
721
  process.exit(1);
710
722
  }
711
723
  });
@@ -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
  /**