lsh-framework 3.5.1 → 3.7.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/README.md CHANGED
@@ -10,15 +10,13 @@
10
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)
11
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
12
12
 
13
- ## What's New in v3.1.19
13
+ ## What's New in v3.5.x
14
14
 
15
- - **Type Safety Milestone** - All `@typescript-eslint/no-explicit-any` warnings eliminated (51+ 0)
16
- - **Constants Centralization** - Hardcoded strings moved to constants modules
17
- - **API Response Builder** - Standardized API responses with `sendSuccess`, `sendError`, `ApiErrors`
18
- - **Test Coverage** - 59 new tests for constants modules (142 total constants tests)
19
- - **Code Quality** - Lint warnings reduced from 744 to 550 (26.1% reduction)
15
+ - **Focused on secrets** - Removed the dormant pre-pivot platform code (SaaS multi-tenant, job/cron daemon, Supabase/Postgres persistence). LSH is now purely an encrypted `.env` sync tool over IPFS.
16
+ - **Dependency modernization** - Express 5, TypeScript 6, ESLint 10, Jest 30.
17
+ - **Hardening** - Bounded network calls (no hangs), command-injection fix in the Kubo installer, reliable npm publishing.
20
18
 
21
- See [Release Notes](docs/releases/3.1.19.md) for full details.
19
+ See [Release Notes](docs/releases/3.5.0.md) for full details.
22
20
 
23
21
  ## Quick Start
24
22
 
@@ -117,6 +115,21 @@ lsh sync push --env dev
117
115
 
118
116
  If exactly one remote service is configured, `lsh` uses it automatically and `LSH_PIN_SERVICE` is optional.
119
117
 
118
+ ### Quickest: bundled pinner (just a token)
119
+
120
+ Skip the manual `ipfs pin remote service add`. Set **`LSH_PIN_TOKEN`** and `lsh` auto-registers a
121
+ remote pinning service for you on first push — defaulting to **4EVERLAND** (free 5GB, standard
122
+ Pinning Service API):
123
+
124
+ ```bash
125
+ # Get a free accessToken from the 4EVERLAND "4EVER Pin" page (https://4everland.org)
126
+ export LSH_PIN_TOKEN=<your-4everland-accessToken>
127
+ lsh push --env dev # auto-registers "lsh-pin" → https://api.4everland.dev, then pins
128
+ ```
129
+
130
+ Use a different provider by overriding the endpoint: `export LSH_PIN_ENDPOINT=<psa-endpoint>`.
131
+ (Note: Pinata's pin-by-CID PSA is paid-only; 4EVERLAND and Filebase offer it free.)
132
+
120
133
  ## Installation
121
134
 
122
135
  ### Prerequisites
@@ -211,22 +224,17 @@ lsh pull --env prod
211
224
 
212
225
  ## Automatic Secret Rotation
213
226
 
214
- Use the built-in daemon for automated rotation:
227
+ Schedule rotation with any external scheduler (system `cron`, a CI job, etc.) that
228
+ runs your rotation script and then pushes the updated secrets:
215
229
 
216
230
  ```bash
217
- # Start daemon
218
- lsh daemon start
219
-
220
- # Schedule monthly key rotation
221
- lsh cron add \
222
- --name "rotate-keys" \
223
- --schedule "0 0 1 * *" \
224
- --command "./scripts/rotate.sh && lsh push"
225
-
226
- # List scheduled jobs
227
- lsh cron list
231
+ # Example: monthly rotation via crontab — `crontab -e`
232
+ 0 0 1 * * cd /path/to/project && ./scripts/rotate.sh && lsh push
228
233
  ```
229
234
 
235
+ Or as a scheduled CI job (GitHub Actions, etc.) that runs the script and `lsh push`.
236
+ LSH focuses on encrypting and syncing the `.env`; the rotation policy/schedule is yours.
237
+
230
238
  ## Export Formats
231
239
 
232
240
  Export secrets in multiple formats:
@@ -322,27 +330,6 @@ lsh push
322
330
  - **[Installation](docs/deployment/INSTALL.md)** - Detailed installation
323
331
  - **[Developer Guide](CLAUDE.md)** - Contributing to LSH
324
332
 
325
- ## Advanced Features
326
-
327
- LSH includes a full automation platform:
328
-
329
- - **Persistent Daemon** - Background job execution
330
- - **Cron Scheduling** - Time-based job scheduling
331
- - **REST API** - HTTP API for integration
332
- - **CI/CD Webhooks** - GitHub/GitLab webhook support
333
- - **POSIX Shell** - Interactive shell with ZSH features
334
-
335
- ```bash
336
- # Start daemon
337
- lsh daemon start
338
-
339
- # API server
340
- LSH_API_KEY=xxx lsh api start --port 3030
341
-
342
- # Interactive shell
343
- lsh -i
344
- ```
345
-
346
333
  ## Configuration
347
334
 
348
335
  ### Environment Variables
@@ -355,14 +342,15 @@ LSH_SECRETS_KEY=<your-encryption-key>
355
342
  # (configure once with: ipfs pin remote service add <name> <endpoint> <key>)
356
343
  LSH_PIN_SERVICE=<service-name>
357
344
 
358
- # Optional - Supabase backend
359
- SUPABASE_URL=https://xxx.supabase.co
360
- SUPABASE_ANON_KEY=<key>
345
+ # Optional - pointer discovery backends, comma-separated in priority order.
346
+ # Default 'w3name,ipns': durable w3name (signed IPNS via name.web3.storage, no
347
+ # account, no DHT TTL) with IPNS-over-DHT fallback. Set 'ipns' for DHT-only.
348
+ LSH_DISCOVERY=w3name,ipns
361
349
 
362
- # Optional - API server
363
- LSH_API_ENABLED=true
364
- LSH_API_PORT=3030
365
- LSH_API_KEY=<key>
350
+ # Optional - bundled pinner: with a token set, lsh auto-registers a remote pin
351
+ # service so pushed content is durable. Endpoint defaults to 4EVERLAND (free 5GB).
352
+ LSH_PIN_TOKEN=<psa-access-token>
353
+ LSH_PIN_ENDPOINT=https://api.4everland.dev # override for another PSA provider
366
354
  ```
367
355
 
368
356
  ### Configuration Files
@@ -33,6 +33,15 @@ export const ENV_VARS = {
33
33
  // Name of the kubo remote pinning service to use for durable sync
34
34
  // (configured via `ipfs pin remote service add <name> <endpoint> <key>`).
35
35
  LSH_PIN_SERVICE: 'LSH_PIN_SERVICE',
36
+ // Discovery backend(s) for the key→CID pointer, comma-separated in priority
37
+ // order. Supported: 'w3name' (durable, hosted), 'ipns' (DHT). Default: 'w3name,ipns'.
38
+ LSH_DISCOVERY: 'LSH_DISCOVERY',
39
+ // Access token for a bundled IPFS remote pinning service. When set (and no
40
+ // remote pin service is already configured), lsh auto-registers one so pushed
41
+ // content is durably pinned. Endpoint defaults to 4EVERLAND (free 5GB).
42
+ LSH_PIN_TOKEN: 'LSH_PIN_TOKEN',
43
+ // Override the PSA endpoint used with LSH_PIN_TOKEN (any IPFS Pinning Service).
44
+ LSH_PIN_ENDPOINT: 'LSH_PIN_ENDPOINT',
36
45
  // Feature flags
37
46
  LSH_LOCAL_STORAGE_QUIET: 'LSH_LOCAL_STORAGE_QUIET',
38
47
  LSH_V1_COMPAT: 'LSH_V1_COMPAT',
@@ -143,4 +152,10 @@ export const DEFAULTS = {
143
152
  IPNS_RESOLVE_TIMEOUT_MS: 30000, // 30s for DHT lookup
144
153
  IPNS_KEY_PREFIX: 'lsh-',
145
154
  IPNS_KEY_DERIVATION_CONTEXT: 'lsh-ipns-v1',
155
+ // Default discovery backends (priority order): durable w3name, then DHT-IPNS fallback.
156
+ DISCOVERY_BACKENDS: 'w3name,ipns',
157
+ // Bundled remote pinning service (used with LSH_PIN_TOKEN). 4EVERLAND free tier (5GB),
158
+ // standard IPFS Pinning Service API. Override the endpoint via LSH_PIN_ENDPOINT.
159
+ DEFAULT_PIN_ENDPOINT: 'https://api.4everland.dev',
160
+ DEFAULT_PIN_SERVICE_NAME: 'lsh-pin',
146
161
  };
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Discovery backend seam.
3
+ *
4
+ * "Discovery" = mapping a key-derived pointer to the latest content CID
5
+ * (publish on push, resolve on pull). Today the only backend is IPNS over the
6
+ * DHT (`IpnsDiscoveryBackend`), which is what LSH has always used — this module
7
+ * just extracts that behind an interface so additional backends (e.g. a durable
8
+ * w3name pointer; see issue #194) can be added without touching the storage layer.
9
+ *
10
+ * This is a behaviour-preserving refactor: `getDiscoveryBackend()` returns the
11
+ * IPNS backend, identical to the previous inline logic.
12
+ */
13
+ import { deriveKeyInfo as defaultDeriveKeyInfo, ensureKeyImported as defaultEnsureKeyImported, } from './ipns-key-manager.js';
14
+ import { w3namePublish as defaultW3namePublish, w3nameResolve as defaultW3nameResolve, } from './w3name-pointer.js';
15
+ import { ENV_VARS, DEFAULTS } from '../constants/config.js';
16
+ import { createLogger } from './logger.js';
17
+ const logger = createLogger('Discovery');
18
+ /**
19
+ * IPNS-over-DHT discovery: derive a deterministic ed25519 key from the secrets
20
+ * key, import it into the local Kubo keystore, and publish/resolve via IPNS.
21
+ */
22
+ export class IpnsDiscoveryBackend {
23
+ ipfsSync;
24
+ deps;
25
+ id = 'ipns';
26
+ constructor(ipfsSync, deps = {
27
+ deriveKeyInfo: defaultDeriveKeyInfo,
28
+ ensureKeyImported: defaultEnsureKeyImported,
29
+ }) {
30
+ this.ipfsSync = ipfsSync;
31
+ this.deps = deps;
32
+ }
33
+ async publish({ secretsKey, repoName, env, cid }) {
34
+ const keyInfo = this.deps.deriveKeyInfo(secretsKey, repoName, env);
35
+ const ipnsName = await this.deps.ensureKeyImported(this.ipfsSync.getApiUrl(), keyInfo);
36
+ if (!ipnsName) {
37
+ return null;
38
+ }
39
+ return this.ipfsSync.publishToIPNS(cid, keyInfo.keyName);
40
+ }
41
+ async resolve({ secretsKey, repoName, env }) {
42
+ const keyInfo = this.deps.deriveKeyInfo(secretsKey, repoName, env);
43
+ const ipnsName = await this.deps.ensureKeyImported(this.ipfsSync.getApiUrl(), keyInfo);
44
+ if (!ipnsName) {
45
+ return { cid: null, name: null };
46
+ }
47
+ const cid = await this.ipfsSync.resolveIPNS(ipnsName);
48
+ return { cid, name: ipnsName };
49
+ }
50
+ }
51
+ /**
52
+ * Durable discovery via w3name (Storacha). Publishes/resolves a signed IPNS
53
+ * record hosted at name.web3.storage — survives offline nodes, no DHT TTL, no
54
+ * account. Uses the SAME seed-derived IPNS name as the `ipns` backend.
55
+ */
56
+ export class W3nameDiscoveryBackend {
57
+ deps;
58
+ id = 'w3name';
59
+ constructor(deps = {
60
+ deriveKeyInfo: defaultDeriveKeyInfo,
61
+ publish: defaultW3namePublish,
62
+ resolve: defaultW3nameResolve,
63
+ }) {
64
+ this.deps = deps;
65
+ }
66
+ async publish({ secretsKey, repoName, env, cid }) {
67
+ const { seed } = this.deps.deriveKeyInfo(secretsKey, repoName, env);
68
+ return this.deps.publish(seed, cid);
69
+ }
70
+ async resolve({ secretsKey, repoName, env }) {
71
+ const { seed } = this.deps.deriveKeyInfo(secretsKey, repoName, env);
72
+ return this.deps.resolve(seed);
73
+ }
74
+ }
75
+ /**
76
+ * Composes backends in priority order. Publish dual-writes to all (best-effort —
77
+ * a failure in one doesn't block the others). Resolve tries each in order and
78
+ * returns the first hit, so a durable backend wins but a fallback still works.
79
+ */
80
+ export class CompositeDiscoveryBackend {
81
+ backends;
82
+ id;
83
+ constructor(backends) {
84
+ this.backends = backends;
85
+ this.id = backends.map((b) => b.id).join('+');
86
+ }
87
+ async publish(params) {
88
+ let firstName = null;
89
+ for (const backend of this.backends) {
90
+ try {
91
+ const name = await backend.publish(params);
92
+ if (name && !firstName) {
93
+ firstName = name;
94
+ }
95
+ }
96
+ catch (error) {
97
+ logger.warn(`Discovery publish via "${backend.id}" failed: ${error.message}`);
98
+ }
99
+ }
100
+ return firstName;
101
+ }
102
+ async resolve(params) {
103
+ let firstName = null;
104
+ for (const backend of this.backends) {
105
+ try {
106
+ const result = await backend.resolve(params);
107
+ if (result.name && !firstName) {
108
+ firstName = result.name;
109
+ }
110
+ if (result.cid) {
111
+ return result;
112
+ }
113
+ }
114
+ catch (error) {
115
+ logger.warn(`Discovery resolve via "${backend.id}" failed: ${error.message}`);
116
+ }
117
+ }
118
+ return { cid: null, name: firstName };
119
+ }
120
+ }
121
+ /** Construct a single backend by id, or null if unknown. */
122
+ function buildBackend(id, ipfsSync) {
123
+ switch (id) {
124
+ case 'ipns':
125
+ return new IpnsDiscoveryBackend(ipfsSync);
126
+ case 'w3name':
127
+ return new W3nameDiscoveryBackend();
128
+ default:
129
+ logger.warn(`Unknown discovery backend "${id}" — ignoring.`);
130
+ return null;
131
+ }
132
+ }
133
+ /**
134
+ * Select discovery backend(s) from `LSH_DISCOVERY` (comma-separated, priority
135
+ * order; default 'w3name,ipns'). Returns a single backend or a composite.
136
+ * Falls back to IPNS if the setting resolves to nothing valid.
137
+ */
138
+ export function getDiscoveryBackend(ipfsSync) {
139
+ const setting = process.env[ENV_VARS.LSH_DISCOVERY] || DEFAULTS.DISCOVERY_BACKENDS;
140
+ const backends = setting
141
+ .split(',')
142
+ .map((s) => s.trim().toLowerCase())
143
+ .filter(Boolean)
144
+ .map((id) => buildBackend(id, ipfsSync))
145
+ .filter((b) => b !== null);
146
+ if (backends.length === 0) {
147
+ return new IpnsDiscoveryBackend(ipfsSync);
148
+ }
149
+ return backends.length === 1 ? backends[0] : new CompositeDiscoveryBackend(backends);
150
+ }
@@ -13,7 +13,7 @@ import * as os from 'os';
13
13
  import * as crypto from 'crypto';
14
14
  import { createLogger } from './logger.js';
15
15
  import { getIPFSSync } from './ipfs-sync.js';
16
- import { deriveKeyInfo, ensureKeyImported } from './ipns-key-manager.js';
16
+ import { getDiscoveryBackend } from './discovery-backend.js';
17
17
  import { ENV_VARS, DEFAULTS } from '../constants/index.js';
18
18
  import { extractErrorMessage } from './lsh-error.js';
19
19
  const logger = createLogger('IPFSSecretsStorage');
@@ -95,16 +95,13 @@ export class IPFSSecretsStorage {
95
95
  try {
96
96
  const repoName = gitRepo || DEFAULTS.DEFAULT_ENVIRONMENT;
97
97
  const env = environment || DEFAULTS.DEFAULT_ENVIRONMENT;
98
- const keyInfo = deriveKeyInfo(encryptionKey, repoName, env);
99
- const ipnsName = await ensureKeyImported(ipfsSync.getApiUrl(), keyInfo);
100
- if (ipnsName) {
101
- const publishedName = await ipfsSync.publishToIPNS(cid, keyInfo.keyName);
102
- if (publishedName) {
103
- metadata.ipns_name = publishedName;
104
- this.metadata[this.getMetadataKey(gitRepo, environment)] = metadata;
105
- await this.saveMetadata();
106
- logger.info(` 🔗 Published to IPNS: ${publishedName}`);
107
- }
98
+ const discovery = getDiscoveryBackend(ipfsSync);
99
+ const publishedName = await discovery.publish({ secretsKey: encryptionKey, repoName, env, cid });
100
+ if (publishedName) {
101
+ metadata.ipns_name = publishedName;
102
+ this.metadata[this.getMetadataKey(gitRepo, environment)] = metadata;
103
+ await this.saveMetadata();
104
+ logger.info(` 🔗 Published to ${discovery.id}: ${publishedName}`);
108
105
  }
109
106
  }
110
107
  catch (error) {
@@ -140,11 +137,12 @@ export class IPFSSecretsStorage {
140
137
  try {
141
138
  const repoName = gitRepo || DEFAULTS.DEFAULT_ENVIRONMENT;
142
139
  const env = environment || DEFAULTS.DEFAULT_ENVIRONMENT;
143
- const keyInfo = deriveKeyInfo(encryptionKey, repoName, env);
144
- const ipnsName = await ensureKeyImported(ipfsSync.getApiUrl(), keyInfo);
140
+ const discovery = getDiscoveryBackend(ipfsSync);
141
+ const resolved = await discovery.resolve({ secretsKey: encryptionKey, repoName, env });
142
+ const ipnsName = resolved.name;
145
143
  if (ipnsName) {
146
144
  logger.info(` 🔍 Resolving via IPNS: ${ipnsName.substring(0, 20)}...`);
147
- resolvedCid = await ipfsSync.resolveIPNS(ipnsName);
145
+ resolvedCid = resolved.cid;
148
146
  if (resolvedCid) {
149
147
  logger.info(` ✅ IPNS resolved to CID: ${resolvedCid}`);
150
148
  // Update local metadata
@@ -15,7 +15,23 @@ import * as path from 'path';
15
15
  import * as os from 'os';
16
16
  import { createLogger } from './logger.js';
17
17
  import { extractErrorMessage } from './lsh-error.js';
18
- import { ENV_VARS } from '../constants/config.js';
18
+ import { ENV_VARS, DEFAULTS } from '../constants/config.js';
19
+ /**
20
+ * Pure helper: choose which configured remote pinning service to use.
21
+ * - explicit `LSH_PIN_SERVICE` wins, but only if it's actually configured;
22
+ * - else the bundled default service (`lsh-pin`) if present;
23
+ * - else the sole configured service;
24
+ * - else null (none / ambiguous).
25
+ */
26
+ export function chooseRemoteService(services, explicit, defaultName) {
27
+ if (explicit) {
28
+ return services.includes(explicit) ? explicit : null;
29
+ }
30
+ if (services.includes(defaultName)) {
31
+ return defaultName;
32
+ }
33
+ return services.length === 1 ? services[0] : null;
34
+ }
19
35
  const logger = createLogger('IPFSSync');
20
36
  /**
21
37
  * Native IPFS Sync
@@ -369,12 +385,43 @@ export class IPFSSync {
369
385
  * - Returns null when nothing is configured or the choice is ambiguous.
370
386
  */
371
387
  async resolveRemoteService() {
388
+ await this.ensureDefaultPinService();
372
389
  const services = await this.listRemoteServices();
373
- const explicit = process.env[ENV_VARS.LSH_PIN_SERVICE];
374
- if (explicit) {
375
- return services.includes(explicit) ? explicit : null;
390
+ return chooseRemoteService(services, process.env[ENV_VARS.LSH_PIN_SERVICE], DEFAULTS.DEFAULT_PIN_SERVICE_NAME);
391
+ }
392
+ /**
393
+ * Bundled pinner: when LSH_PIN_TOKEN is set and no remote pin service named
394
+ * `lsh-pin` is registered yet, auto-register one (endpoint defaults to
395
+ * 4EVERLAND, override via LSH_PIN_ENDPOINT). Lets users get durable pinning
396
+ * with just a token — no manual `ipfs pin remote service add`. Best-effort;
397
+ * never throws. Respects an existing service of the same name and the
398
+ * explicit LSH_PIN_SERVICE (handled by chooseRemoteService).
399
+ */
400
+ async ensureDefaultPinService() {
401
+ const token = process.env[ENV_VARS.LSH_PIN_TOKEN];
402
+ if (!token)
403
+ return;
404
+ const name = DEFAULTS.DEFAULT_PIN_SERVICE_NAME;
405
+ try {
406
+ const existing = await this.listRemoteServices();
407
+ if (existing.includes(name))
408
+ return;
409
+ const endpoint = process.env[ENV_VARS.LSH_PIN_ENDPOINT] || DEFAULTS.DEFAULT_PIN_ENDPOINT;
410
+ const url = `${this.LOCAL_IPFS_API}/pin/remote/service/add` +
411
+ `?arg=${encodeURIComponent(name)}` +
412
+ `&arg=${encodeURIComponent(endpoint)}` +
413
+ `&arg=${encodeURIComponent(token)}`;
414
+ const response = await fetch(url, { method: 'POST', signal: AbortSignal.timeout(10000) });
415
+ if (response.ok) {
416
+ logger.info(`📌 Registered bundled pin service "${name}" → ${endpoint}`);
417
+ }
418
+ else {
419
+ logger.warn(`Could not register pin service "${name}": ${await response.text()}`);
420
+ }
421
+ }
422
+ catch (error) {
423
+ logger.warn(`Pin service registration error: ${extractErrorMessage(error)}`);
376
424
  }
377
- return services.length === 1 ? services[0] : null;
378
425
  }
379
426
  /**
380
427
  * Pin a CID to a configured remote pinning service so the content survives
@@ -0,0 +1,72 @@
1
+ /**
2
+ * w3name pointer ops (issue #194, Phase 2).
3
+ *
4
+ * Durable IPNS via Storacha's w3name: publish a signed record mapping the
5
+ * key-derived IPNS name → `/ipfs/<cid>`, hosted at name.web3.storage (no account,
6
+ * no auth). The name is IDENTICAL to the one Kubo derives from the same seed
7
+ * (verified in the Phase-2 spike), so this shares one logical pointer with the
8
+ * IPNS-over-DHT backend.
9
+ *
10
+ * w3name + @libp2p/crypto are heavy ESM deps; they are **lazily imported** inside
11
+ * the functions so the CLI / unit tests don't load them unless w3name is actually
12
+ * used.
13
+ */
14
+ import { createLogger } from './logger.js';
15
+ const logger = createLogger('W3namePointer');
16
+ const PUBLISH_TIMEOUT_MS = 15000;
17
+ const RESOLVE_TIMEOUT_MS = 10000;
18
+ function withTimeout(p, ms, label) {
19
+ return Promise.race([
20
+ p,
21
+ new Promise((_, reject) => {
22
+ const timer = setTimeout(() => reject(new Error(`w3name ${label} timed out after ${ms}ms`)), ms);
23
+ timer.unref?.();
24
+ }),
25
+ ]);
26
+ }
27
+ /** Build a w3name WritableName from a 32-byte ed25519 seed (deterministic). */
28
+ async function writableFromSeed(seed) {
29
+ const [{ generateKeyPairFromSeed, privateKeyToProtobuf }, Name] = await Promise.all([
30
+ import('@libp2p/crypto/keys'),
31
+ import('w3name'),
32
+ ]);
33
+ const priv = await generateKeyPairFromSeed('Ed25519', new Uint8Array(seed));
34
+ const name = await Name.from(privateKeyToProtobuf(priv));
35
+ return { Name, name };
36
+ }
37
+ /**
38
+ * Publish `cid` under the seed-derived w3name pointer. Returns the IPNS name.
39
+ * Handles IPNS sequencing: increments from the current revision if one exists,
40
+ * else publishes v0.
41
+ */
42
+ export async function w3namePublish(seed, cid) {
43
+ const { Name, name } = await writableFromSeed(seed);
44
+ const value = `/ipfs/${cid}`;
45
+ let revision;
46
+ try {
47
+ const current = await withTimeout(Name.resolve(name), RESOLVE_TIMEOUT_MS, 'resolve(pre-publish)');
48
+ revision = await Name.increment(current, value);
49
+ }
50
+ catch {
51
+ // No existing record (or unresolvable) → start a fresh revision.
52
+ revision = await Name.v0(name, value);
53
+ }
54
+ await withTimeout(Name.publish(revision, name.key), PUBLISH_TIMEOUT_MS, 'publish');
55
+ logger.debug(`Published to w3name: ${name.toString()} → ${value}`);
56
+ return name.toString();
57
+ }
58
+ /** Resolve the latest CID for the seed-derived w3name pointer. */
59
+ export async function w3nameResolve(seed) {
60
+ const { Name, name } = await writableFromSeed(seed);
61
+ const nameStr = name.toString();
62
+ try {
63
+ const revision = await withTimeout(Name.resolve(Name.parse(nameStr)), RESOLVE_TIMEOUT_MS, 'resolve');
64
+ const value = revision.value; // "/ipfs/<cid>"
65
+ const cid = value.startsWith('/ipfs/') ? value.slice('/ipfs/'.length) : null;
66
+ return { cid, name: nameStr };
67
+ }
68
+ catch (error) {
69
+ logger.debug(`w3name resolve failed for ${nameStr}: ${error.message}`);
70
+ return { cid: null, name: nameStr };
71
+ }
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "3.5.1",
3
+ "version": "3.7.0",
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/cli.js",
6
6
  "bin": {
@@ -63,6 +63,7 @@
63
63
  "package.json"
64
64
  ],
65
65
  "dependencies": {
66
+ "@libp2p/crypto": "^5.1.19",
66
67
  "@supabase/supabase-js": "^2.57.4",
67
68
  "bcrypt": "^6.0.0",
68
69
  "chalk": "^5.3.0",
@@ -80,7 +81,8 @@
80
81
  "ora": "^9.0.0",
81
82
  "pg": "^8.16.3",
82
83
  "proper-lockfile": "^4.1.2",
83
- "smol-toml": "^1.3.1"
84
+ "smol-toml": "^1.3.1",
85
+ "w3name": "^1.1.3"
84
86
  },
85
87
  "devDependencies": {
86
88
  "@eslint/js": "^10.0.1",