@mmmbuto/nexuscrew 0.8.9 → 0.8.10
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 +39 -18
- package/frontend/dist/assets/index-CbUkgtAz.js +91 -0
- package/frontend/dist/assets/index-ChGJawuv.css +32 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/auth/token.js +1 -1
- package/lib/cli/commands.js +236 -141
- package/lib/cli/doctor.js +17 -14
- package/lib/cli/init.js +2 -2
- package/lib/cli/pidfile.js +3 -2
- package/lib/decks/store.js +5 -2
- package/lib/fleet/builtin.js +12 -1
- package/lib/mcp/server.js +161 -0
- package/lib/nodes/commands.js +95 -123
- package/lib/nodes/health.js +22 -14
- package/lib/nodes/peering.js +72 -10
- package/lib/nodes/store.js +21 -18
- package/lib/nodes/tunnel-supervisor.js +29 -9
- package/lib/nodes/tunnel.js +217 -72
- package/lib/proxy/federation.js +68 -6
- package/lib/server.js +36 -31
- package/lib/settings/routes.js +202 -95
- package/package.json +2 -2
- package/skills/nexuscrew-agent/SKILL.md +6 -2
- package/frontend/dist/assets/index-9Drd8g0k.css +0 -32
- package/frontend/dist/assets/index-DTmT7yhV.js +0 -91
package/lib/server.js
CHANGED
|
@@ -129,30 +129,19 @@ function createServer(opts = {}) {
|
|
|
129
129
|
return { localPort: node.localPort, token: node.token || null };
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const tr = nodesTunnel.startReverse({
|
|
138
|
-
home: cfg.home || os.homedir(), rendezvous: st.rendezvous,
|
|
139
|
-
spawnImpl: cfg.tunnelSpawnImpl, spawnSyncImpl: cfg.tunnelSpawnSyncImpl,
|
|
140
|
-
sshBin: cfg.sshBin, logFd: cfg.tunnelLogFd,
|
|
141
|
-
});
|
|
142
|
-
if (!tr.started && tr.reason !== 'already running') {
|
|
143
|
-
process.stderr.write(`[nexuscrew] reverse tunnel autostart failed: ${tr.reason || 'unknown'}\n`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// New Hydra peers are ordinary local+remote nodes. Only the side that owns
|
|
149
|
-
// the outbound SSH alias dials; inbound records are reached through its -R.
|
|
150
|
-
{
|
|
132
|
+
const runtimePort = () => (server && server.address() ? server.address().port : cfg.port);
|
|
133
|
+
let tunnelsStarted = false;
|
|
134
|
+
function startManagedTunnels() {
|
|
135
|
+
if (tunnelsStarted) return;
|
|
136
|
+
tunnelsStarted = true;
|
|
151
137
|
const st = nodesStore.loadStore(nodesPath);
|
|
138
|
+
// Exactly one connection per configured hub. Legacy `roles.node` /
|
|
139
|
+
// rendezvous data is migration-only and never starts a second hidden SSH
|
|
140
|
+
// process. Publishing this device is the optional -R on its outbound link.
|
|
152
141
|
for (const node of (st && st.nodes) || []) {
|
|
153
142
|
if (node.direction !== 'inbound' && node.autostart === true) {
|
|
154
143
|
const tr = nodesTunnel.startForward({
|
|
155
|
-
home: cfg.home || os.homedir(), node, localAppPort:
|
|
144
|
+
home: cfg.home || os.homedir(), node, localAppPort: runtimePort(),
|
|
156
145
|
spawnImpl: cfg.tunnelSpawnImpl, spawnSyncImpl: cfg.tunnelSpawnSyncImpl,
|
|
157
146
|
sshBin: cfg.sshBin, logFd: cfg.tunnelLogFd,
|
|
158
147
|
});
|
|
@@ -251,15 +240,13 @@ function createServer(opts = {}) {
|
|
|
251
240
|
const h = healths[i] || null;
|
|
252
241
|
return { ...n, tunnel: nodesHealth.tunnelFromHealth(h), health: h };
|
|
253
242
|
});
|
|
254
|
-
|
|
255
|
-
if (view.rendezvous) out.rendezvous = view.rendezvous;
|
|
256
|
-
res.json(out);
|
|
243
|
+
res.json({ nodeId: view.nodeId, nodes });
|
|
257
244
|
} catch (e) { res.status(500).json({ error: String(e.message || e) }); }
|
|
258
245
|
});
|
|
259
246
|
// Settings API B2 (design §4b(6)): read-only GET + mutanti lista chiusa per il
|
|
260
247
|
// wizard/settings UI. Dietro lo stesso requireToken del router /api; il gate
|
|
261
248
|
// READONLY route-level e la redazione token vivono dentro settingsRoutes.
|
|
262
|
-
api.use('/settings', settingsRoutes({ cfg, nodesPath, tokenStore, closeSessions, updater }));
|
|
249
|
+
api.use('/settings', settingsRoutes({ cfg, nodesPath, tokenStore, closeSessions, updater, runtimePort }));
|
|
263
250
|
api.get('/topology', async (_req, res) => {
|
|
264
251
|
try { res.json(await federation.collectLocalTopology({ nodesPath, cachePath: topologyCachePath })); }
|
|
265
252
|
catch (e) { res.status(500).json({ error: String(e.message || e) }); }
|
|
@@ -291,6 +278,7 @@ function createServer(opts = {}) {
|
|
|
291
278
|
app.use('/api', api);
|
|
292
279
|
app.use('/federation', federation.peerRouter({
|
|
293
280
|
nodesPath, localPort: () => (server && server.address() ? server.address().port : cfg.port), localCredential: () => tokenHolder.value, readonly: proxyReadonly, version: VERSION,
|
|
281
|
+
fetchImpl: healthFetch,
|
|
294
282
|
roles: () => require('./cli/commands.js').readRoles(cfg.configPath || configJsonPath()),
|
|
295
283
|
}));
|
|
296
284
|
|
|
@@ -305,14 +293,18 @@ function createServer(opts = {}) {
|
|
|
305
293
|
// per costruire path: validazione ^[a-z0-9-]{1,32}$, nome invalido → 404 secco
|
|
306
294
|
// (niente traversal, niente fallback silenzioso alla SPA su nomi sporchi).
|
|
307
295
|
const DECK_NAME_RE = /^[a-z0-9-]{1,32}$/;
|
|
296
|
+
const DECK_OWNER_RE = /^[a-f0-9]{16,64}$/;
|
|
308
297
|
// Cattura TUTTO cio' che sta sotto /deck/ (anche slash/segmenti extra o encoded)
|
|
309
298
|
// e valida il remainder: qualunque cosa non sia un nome deck strict → 404,
|
|
310
299
|
// senza mai cadere nel catch-all SPA con un nome sporco.
|
|
311
300
|
app.get(/^\/deck\/(.*)$/, (req, res) => {
|
|
312
301
|
// parita' col client: deckFromPath accetta UN trailing slash (/deck/main/),
|
|
313
302
|
// il server deve fare lo stesso; piu' di uno resta 404 (regex strict).
|
|
314
|
-
const
|
|
315
|
-
|
|
303
|
+
const value = req.params[0].replace(/\/$/, '');
|
|
304
|
+
const parts = value.split('/');
|
|
305
|
+
const valid = (parts.length === 1 && DECK_NAME_RE.test(parts[0]))
|
|
306
|
+
|| (parts.length === 2 && DECK_OWNER_RE.test(parts[0]) && DECK_NAME_RE.test(parts[1]));
|
|
307
|
+
if (!valid) {
|
|
316
308
|
return res.status(404).type('text/plain').send('invalid deck name');
|
|
317
309
|
}
|
|
318
310
|
return res.sendFile(path.join(distDir, 'index.html'));
|
|
@@ -323,7 +315,7 @@ function createServer(opts = {}) {
|
|
|
323
315
|
// Close the watcher when the HTTP server closes. Registered HERE (inside
|
|
324
316
|
// createServer) — not in start() — so every createServer consumer is covered,
|
|
325
317
|
// not only the start() path. watcher.close() is idempotent.
|
|
326
|
-
server.on('listening', () => updater.start());
|
|
318
|
+
server.on('listening', () => { updater.start(); startManagedTunnels(); });
|
|
327
319
|
server.on('close', () => { watcher.close(); previews.close(); eventsHub.closeAll(); updater.close(); });
|
|
328
320
|
// noServer: gestiamo l'upgrade a mano per instradare /ws (locale) e /node/*
|
|
329
321
|
// (proxy). Il WS locale resta identico; il proxy WS applica gli STESSI check
|
|
@@ -353,13 +345,13 @@ function createServer(opts = {}) {
|
|
|
353
345
|
let u; try { u = new URL(req.url, 'http://127.0.0.1'); } catch (_) { return socket.destroy(); }
|
|
354
346
|
const given = bearerFrom(req) || u.searchParams.get('token') || '';
|
|
355
347
|
if (!verify(tokenHolder.value, given)) return socket.destroy();
|
|
356
|
-
federation.forwardUpgrade({ req, socket, head, nodesPath, localPort:
|
|
348
|
+
federation.forwardUpgrade({ req, socket, head, nodesPath, localPort: runtimePort, localCredential: () => tokenHolder.value, ingress: null, readonly: proxyReadonly, activeSockets: proxySockets });
|
|
357
349
|
return;
|
|
358
350
|
}
|
|
359
351
|
if (pathname.startsWith('/federation/route/')) {
|
|
360
352
|
const ingress = federation.peerFromToken(nodesPath, bearerFrom(req));
|
|
361
353
|
if (!ingress) return socket.destroy();
|
|
362
|
-
federation.forwardUpgrade({ req, socket, head, nodesPath, localPort:
|
|
354
|
+
federation.forwardUpgrade({ req, socket, head, nodesPath, localPort: runtimePort, localCredential: () => tokenHolder.value, ingress, readonly: proxyReadonly, activeSockets: proxySockets });
|
|
363
355
|
return;
|
|
364
356
|
}
|
|
365
357
|
if (pathname === '/node' || pathname.startsWith('/node/')) {
|
|
@@ -411,12 +403,18 @@ function start(opts = {}) {
|
|
|
411
403
|
const { server, cfg } = createServer(opts);
|
|
412
404
|
const log = opts.log || console.log;
|
|
413
405
|
const requestedPort = cfg.port;
|
|
406
|
+
const nodesPath = opts.nodesPath || nodesStore.defaultNodesPath(cfg.home || os.homedir());
|
|
407
|
+
const pairedPeers = nodesStore.hasPairedPeers(nodesStore.loadStore(nodesPath));
|
|
408
|
+
const listenError = (error) => {
|
|
409
|
+
if (typeof opts.onListenError === 'function') return opts.onListenError(error);
|
|
410
|
+
throw error;
|
|
411
|
+
};
|
|
414
412
|
const onListening = () => {
|
|
415
413
|
cfg.port = server.address().port;
|
|
416
414
|
// Il token NON si stampa allo startup: finirebbe nei log del servizio
|
|
417
415
|
// (journalctl/logfile). L'apertura autenticata passa da `nexuscrew show`.
|
|
418
416
|
log(`nexuscrew on http://${cfg.bind}:${cfg.port} (open with \`nexuscrew show\`)`);
|
|
419
|
-
log('localhost-only — reach it via SSH
|
|
417
|
+
log('localhost-only — reach it via a user-controlled SSH or VPN channel.');
|
|
420
418
|
};
|
|
421
419
|
const persistFallback = () => {
|
|
422
420
|
const selected = server.address().port;
|
|
@@ -442,10 +440,17 @@ function start(opts = {}) {
|
|
|
442
440
|
};
|
|
443
441
|
server.once('error', (error) => {
|
|
444
442
|
if (error && error.code === 'EADDRINUSE' && requestedPort !== 0 && opts.autoPort !== false) {
|
|
443
|
+
if (pairedPeers) {
|
|
444
|
+
const refused = new Error(`preferred port ${requestedPort} is busy; paired peers exist, refusing automatic port change`);
|
|
445
|
+
refused.code = 'EADDRINUSE_PAIRED';
|
|
446
|
+
refused.cause = error;
|
|
447
|
+
listenError(refused);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
445
450
|
tryFallback(requestedPort >= 65535 ? 41820 : requestedPort + 1, 200);
|
|
446
451
|
return;
|
|
447
452
|
}
|
|
448
|
-
|
|
453
|
+
listenError(error);
|
|
449
454
|
});
|
|
450
455
|
server.listen(requestedPort, cfg.bind, onListening);
|
|
451
456
|
return server;
|