lsh-framework 3.2.5 → 3.5.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/LICENSE +21 -0
- package/README.md +72 -34
- package/dist/commands/ipfs.js +7 -12
- package/dist/commands/self.js +22 -16
- 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
|
@@ -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 });
|
|
@@ -15,6 +15,7 @@ import { createLogger } from './logger.js';
|
|
|
15
15
|
import { getIPFSSync } from './ipfs-sync.js';
|
|
16
16
|
import { deriveKeyInfo, ensureKeyImported } from './ipns-key-manager.js';
|
|
17
17
|
import { ENV_VARS, DEFAULTS } from '../constants/index.js';
|
|
18
|
+
import { extractErrorMessage } from './lsh-error.js';
|
|
18
19
|
const logger = createLogger('IPFSSecretsStorage');
|
|
19
20
|
/**
|
|
20
21
|
* IPFS Secrets Storage
|
|
@@ -107,16 +108,23 @@ export class IPFSSecretsStorage {
|
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
catch (error) {
|
|
110
|
-
|
|
111
|
-
logger.error(`Content uploaded (CID: ${cid}) but IPNS publish failed: ${err.message}\n` +
|
|
111
|
+
logger.error(`Content uploaded (CID: ${cid}) but IPNS publish failed: ${extractErrorMessage(error)}\n` +
|
|
112
112
|
`Other machines won't find it via 'lsh pull' until you re-push.`);
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
+
// Durable remote pin (best-effort): survives this machine going offline.
|
|
116
|
+
// No-op unless a kubo remote pinning service is configured.
|
|
117
|
+
const pinnedService = await ipfsSync.addRemotePin(cid, `lsh-${gitRepo || DEFAULTS.DEFAULT_ENVIRONMENT}-${environment}`);
|
|
118
|
+
if (pinnedService) {
|
|
119
|
+
logger.info(` 📌 Remote-pinned to "${pinnedService}" (durable)`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
logger.warn('No remote pin — content lives only on this machine until a peer caches it. Configure LSH_PIN_SERVICE for durability.');
|
|
123
|
+
}
|
|
115
124
|
return cid;
|
|
116
125
|
}
|
|
117
126
|
catch (error) {
|
|
118
|
-
|
|
119
|
-
logger.error(`Failed to push secrets to IPFS: ${err.message}`);
|
|
127
|
+
logger.error(`Failed to push secrets to IPFS: ${extractErrorMessage(error)}`);
|
|
120
128
|
throw error;
|
|
121
129
|
}
|
|
122
130
|
}
|
|
@@ -155,8 +163,7 @@ export class IPFSSecretsStorage {
|
|
|
155
163
|
}
|
|
156
164
|
}
|
|
157
165
|
catch (error) {
|
|
158
|
-
|
|
159
|
-
logger.debug(` IPNS resolution error: ${err.message}`);
|
|
166
|
+
logger.debug(` IPNS resolution error: ${extractErrorMessage(error)}`);
|
|
160
167
|
}
|
|
161
168
|
}
|
|
162
169
|
// No fallback to local metadata — IPNS is the source of truth
|
|
@@ -185,8 +192,7 @@ export class IPFSSecretsStorage {
|
|
|
185
192
|
}
|
|
186
193
|
}
|
|
187
194
|
catch (error) {
|
|
188
|
-
|
|
189
|
-
logger.debug(` IPFS download failed: ${err.message}`);
|
|
195
|
+
logger.debug(` IPFS download failed: ${extractErrorMessage(error)}`);
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
if (!cachedData) {
|
|
@@ -201,8 +207,7 @@ export class IPFSSecretsStorage {
|
|
|
201
207
|
return secrets;
|
|
202
208
|
}
|
|
203
209
|
catch (error) {
|
|
204
|
-
|
|
205
|
-
logger.error(`Failed to pull secrets from IPFS: ${err.message}`);
|
|
210
|
+
logger.error(`Failed to pull secrets from IPFS: ${extractErrorMessage(error)}`);
|
|
206
211
|
throw error;
|
|
207
212
|
}
|
|
208
213
|
}
|
|
@@ -275,19 +280,19 @@ export class IPFSSecretsStorage {
|
|
|
275
280
|
return JSON.parse(decrypted);
|
|
276
281
|
}
|
|
277
282
|
catch (error) {
|
|
278
|
-
const
|
|
283
|
+
const msg = extractErrorMessage(error);
|
|
279
284
|
// Catch crypto errors (bad decrypt, wrong block length) AND JSON parse errors
|
|
280
285
|
// (wrong key can produce garbage that fails JSON.parse)
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
286
|
+
if (msg.includes('bad decrypt') ||
|
|
287
|
+
msg.includes('wrong final block length') ||
|
|
288
|
+
msg.includes('Unexpected token') ||
|
|
289
|
+
msg.includes('JSON')) {
|
|
285
290
|
throw new Error('Decryption failed. This usually means:\n' +
|
|
286
291
|
' 1. You need to set LSH_SECRETS_KEY environment variable\n' +
|
|
287
292
|
' 2. The key must match the one used during encryption\n' +
|
|
288
293
|
' 3. Generate a shared key with: lsh key\n' +
|
|
289
294
|
' 4. Add it to your .env: LSH_SECRETS_KEY=<key>\n' +
|
|
290
|
-
'\nOriginal error: ' +
|
|
295
|
+
'\nOriginal error: ' + msg, { cause: error });
|
|
291
296
|
}
|
|
292
297
|
throw error;
|
|
293
298
|
}
|
package/dist/lib/ipfs-sync.js
CHANGED
|
@@ -14,6 +14,8 @@ import * as fsPromises from 'fs/promises';
|
|
|
14
14
|
import * as path from 'path';
|
|
15
15
|
import * as os from 'os';
|
|
16
16
|
import { createLogger } from './logger.js';
|
|
17
|
+
import { extractErrorMessage } from './lsh-error.js';
|
|
18
|
+
import { ENV_VARS } from '../constants/config.js';
|
|
17
19
|
const logger = createLogger('IPFSSync');
|
|
18
20
|
/**
|
|
19
21
|
* Native IPFS Sync
|
|
@@ -123,8 +125,7 @@ export class IPFSSync {
|
|
|
123
125
|
return cid;
|
|
124
126
|
}
|
|
125
127
|
catch (error) {
|
|
126
|
-
|
|
127
|
-
logger.error(`IPFS upload error: ${err.message}`);
|
|
128
|
+
logger.error(`IPFS upload error: ${extractErrorMessage(error)}`);
|
|
128
129
|
return null;
|
|
129
130
|
}
|
|
130
131
|
}
|
|
@@ -151,6 +152,7 @@ export class IPFSSync {
|
|
|
151
152
|
logger.debug('Local daemon download failed, trying gateways...');
|
|
152
153
|
}
|
|
153
154
|
// Fall back to public gateways
|
|
155
|
+
let backoffMs = 1000;
|
|
154
156
|
for (const gatewayTemplate of this.GATEWAYS) {
|
|
155
157
|
const gatewayUrl = gatewayTemplate.replace('{cid}', cid);
|
|
156
158
|
try {
|
|
@@ -165,6 +167,8 @@ export class IPFSSync {
|
|
|
165
167
|
}
|
|
166
168
|
catch {
|
|
167
169
|
logger.debug(`Gateway ${gatewayUrl} failed, trying next...`);
|
|
170
|
+
await new Promise((resolve) => { setTimeout(resolve, backoffMs); });
|
|
171
|
+
backoffMs = Math.min(backoffMs * 2, 10000);
|
|
168
172
|
}
|
|
169
173
|
}
|
|
170
174
|
logger.error(`Failed to download CID: ${cid}`);
|
|
@@ -229,8 +233,7 @@ export class IPFSSync {
|
|
|
229
233
|
return false;
|
|
230
234
|
}
|
|
231
235
|
catch (error) {
|
|
232
|
-
|
|
233
|
-
logger.error(`Pin failed: ${err.message}`);
|
|
236
|
+
logger.error(`Pin failed: ${extractErrorMessage(error)}`);
|
|
234
237
|
return false;
|
|
235
238
|
}
|
|
236
239
|
}
|
|
@@ -255,8 +258,7 @@ export class IPFSSync {
|
|
|
255
258
|
return false;
|
|
256
259
|
}
|
|
257
260
|
catch (error) {
|
|
258
|
-
|
|
259
|
-
logger.error(`Unpin failed: ${err.message}`);
|
|
261
|
+
logger.error(`Unpin failed: ${extractErrorMessage(error)}`);
|
|
260
262
|
return false;
|
|
261
263
|
}
|
|
262
264
|
}
|
|
@@ -297,8 +299,7 @@ export class IPFSSync {
|
|
|
297
299
|
await fsPromises.writeFile(this.historyPath, JSON.stringify(history, null, 2), 'utf-8');
|
|
298
300
|
}
|
|
299
301
|
catch (error) {
|
|
300
|
-
|
|
301
|
-
logger.debug(`Failed to save history: ${err.message}`);
|
|
302
|
+
logger.debug(`Failed to save history: ${extractErrorMessage(error)}`);
|
|
302
303
|
}
|
|
303
304
|
}
|
|
304
305
|
/**
|
|
@@ -327,8 +328,7 @@ export class IPFSSync {
|
|
|
327
328
|
}
|
|
328
329
|
}
|
|
329
330
|
catch (error) {
|
|
330
|
-
|
|
331
|
-
logger.error(`Failed to clear history: ${err.message}`);
|
|
331
|
+
logger.error(`Failed to clear history: ${extractErrorMessage(error)}`);
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
334
|
/**
|
|
@@ -343,6 +343,82 @@ export class IPFSSync {
|
|
|
343
343
|
getApiUrl() {
|
|
344
344
|
return this.LOCAL_IPFS_API;
|
|
345
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* List the names of remote pinning services configured in the local Kubo
|
|
348
|
+
* node (via `ipfs pin remote service add`). Returns [] on any error.
|
|
349
|
+
*/
|
|
350
|
+
async listRemoteServices() {
|
|
351
|
+
try {
|
|
352
|
+
const response = await fetch(`${this.LOCAL_IPFS_API}/pin/remote/service/ls`, {
|
|
353
|
+
method: 'POST',
|
|
354
|
+
signal: AbortSignal.timeout(5000),
|
|
355
|
+
});
|
|
356
|
+
if (!response.ok)
|
|
357
|
+
return [];
|
|
358
|
+
const data = await response.json();
|
|
359
|
+
return (data.RemoteServices || []).map((s) => s.Service).filter(Boolean);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Decide which remote pinning service to pin to.
|
|
367
|
+
* - If LSH_PIN_SERVICE is set, use it only when it is actually configured.
|
|
368
|
+
* - Otherwise, use the sole configured service when exactly one exists.
|
|
369
|
+
* - Returns null when nothing is configured or the choice is ambiguous.
|
|
370
|
+
*/
|
|
371
|
+
async resolveRemoteService() {
|
|
372
|
+
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;
|
|
376
|
+
}
|
|
377
|
+
return services.length === 1 ? services[0] : null;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Pin a CID to a configured remote pinning service so the content survives
|
|
381
|
+
* this machine going offline. This is what makes "pull anywhere, anytime"
|
|
382
|
+
* real: without it, blocks live only on the pushing node.
|
|
383
|
+
*
|
|
384
|
+
* Returns the service name on success, or null when no service is
|
|
385
|
+
* configured (the common zero-config case) or the pin request failed.
|
|
386
|
+
* Never throws — durable pinning is best-effort and the caller decides how
|
|
387
|
+
* loudly to warn.
|
|
388
|
+
*/
|
|
389
|
+
async addRemotePin(cid, pinName) {
|
|
390
|
+
try {
|
|
391
|
+
const service = await this.resolveRemoteService();
|
|
392
|
+
if (!service)
|
|
393
|
+
return null;
|
|
394
|
+
const url = `${this.LOCAL_IPFS_API}/pin/remote/add` +
|
|
395
|
+
`?arg=${encodeURIComponent(cid)}` +
|
|
396
|
+
`&service=${encodeURIComponent(service)}` +
|
|
397
|
+
`&name=${encodeURIComponent(pinName)}` +
|
|
398
|
+
`&background=true`;
|
|
399
|
+
const response = await fetch(url, {
|
|
400
|
+
method: 'POST',
|
|
401
|
+
signal: AbortSignal.timeout(30000),
|
|
402
|
+
});
|
|
403
|
+
if (!response.ok) {
|
|
404
|
+
const errorText = await response.text();
|
|
405
|
+
// An already-present pin is reported as an error by some services; treat
|
|
406
|
+
// a "already pinned"/"duplicate" message as success rather than a failure.
|
|
407
|
+
if (/already|duplicate|exists/i.test(errorText)) {
|
|
408
|
+
logger.info(`📌 Already remote-pinned on "${service}": ${cid}`);
|
|
409
|
+
return service;
|
|
410
|
+
}
|
|
411
|
+
logger.warn(`Remote pin failed on "${service}": ${errorText}`);
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
logger.info(`📌 Remote-pinned to "${service}" (durable): ${cid}`);
|
|
415
|
+
return service;
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
logger.warn(`Remote pin error: ${extractErrorMessage(error)}`);
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
346
422
|
/**
|
|
347
423
|
* Publish a CID to IPNS under the given key name.
|
|
348
424
|
* The key must already be imported into Kubo.
|
|
@@ -371,8 +447,7 @@ export class IPFSSync {
|
|
|
371
447
|
return data.Name;
|
|
372
448
|
}
|
|
373
449
|
catch (error) {
|
|
374
|
-
|
|
375
|
-
logger.warn(`IPNS publish error (attempt ${attempt}): ${err.message}`);
|
|
450
|
+
logger.warn(`IPNS publish error (attempt ${attempt}): ${extractErrorMessage(error)}`);
|
|
376
451
|
if (attempt === 2) {
|
|
377
452
|
logger.error(`IPNS publish failed after retry. Content is uploaded (CID: ${cid}) ` +
|
|
378
453
|
`but other machines won't find it via 'lsh pull' until you re-push.`);
|
|
@@ -402,8 +477,7 @@ export class IPFSSync {
|
|
|
402
477
|
return resolvedCid;
|
|
403
478
|
}
|
|
404
479
|
catch (error) {
|
|
405
|
-
|
|
406
|
-
logger.debug(`IPNS resolve error: ${err.message}`);
|
|
480
|
+
logger.debug(`IPNS resolve error: ${extractErrorMessage(error)}`);
|
|
407
481
|
return null;
|
|
408
482
|
}
|
|
409
483
|
}
|