@mmmbuto/nexuscrew 0.9.35 → 0.9.37
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/CHANGELOG.md +14 -0
- package/frontend/dist/assets/{index-cShz4eZG.js → index-Bg8pqKlu.js} +36 -36
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/fleet/builtin.js +24 -0
- package/lib/fleet/endpoint-probe.js +5 -2
- package/lib/notify/event-feed-client.js +5 -1
- package/lib/proxy/federation.js +37 -5
- package/lib/server.js +3 -0
- package/lib/vl-nodes/broker.js +14 -2
- package/package.json +1 -1
package/frontend/dist/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="NexusCrew" />
|
|
12
12
|
<link rel="manifest" href="/manifest.json" />
|
|
13
13
|
<title>NexusCrew</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-Bg8pqKlu.js"></script>
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-XusWm2UG.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.9.
|
|
1
|
+
{"version":"0.9.37"}
|
package/lib/fleet/builtin.js
CHANGED
|
@@ -72,6 +72,24 @@ function draftFrom(defs) {
|
|
|
72
72
|
...(Array.isArray(defs.models) && defs.models.length
|
|
73
73
|
? { models: defs.models.map((m) => ({ ...m })) }
|
|
74
74
|
: {}),
|
|
75
|
+
// Gli capabilityProfiles dichiarati vanno RIPORTATI come i models: la
|
|
76
|
+
// bozza e' la base di ogni scrittura, e una chiave non copiata o sparisce
|
|
77
|
+
// (nessun riferimento) o rende il file non scrivibile (una cella che
|
|
78
|
+
// riferisce un profilo non piu' dichiarato). Copia profonda: per ogni
|
|
79
|
+
// profilo, per ogni chiave array, array copiato.
|
|
80
|
+
...(defs.capabilityProfiles && Object.keys(defs.capabilityProfiles).length
|
|
81
|
+
? {
|
|
82
|
+
capabilityProfiles: Object.fromEntries(Object.entries(defs.capabilityProfiles).map(([nome, profilo]) => [
|
|
83
|
+
nome,
|
|
84
|
+
{
|
|
85
|
+
...profilo,
|
|
86
|
+
...(Array.isArray(profilo.mcp) ? { mcp: [...profilo.mcp] } : {}),
|
|
87
|
+
...(Array.isArray(profilo.skills) ? { skills: [...profilo.skills] } : {}),
|
|
88
|
+
...(Array.isArray(profilo.ondemand) ? { ondemand: [...profilo.ondemand] } : {}),
|
|
89
|
+
},
|
|
90
|
+
])),
|
|
91
|
+
}
|
|
92
|
+
: {}),
|
|
75
93
|
engines: defs.engines.map((e) => ({
|
|
76
94
|
...e,
|
|
77
95
|
...(e.managed ? { managed: { ...e.managed } } : {}),
|
|
@@ -1433,6 +1451,12 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1433
1451
|
// vuoto — si poteva dichiarare un modello e non vederlo mai, ne' ritirarlo.
|
|
1434
1452
|
// I test CRUD non se ne accorgevano perche' leggevano fleet.json diretto.
|
|
1435
1453
|
...(Array.isArray(defs.models) && defs.models.length ? { models: defs.models } : {}),
|
|
1454
|
+
// I capabilityProfiles dichiarati escono dalla VISTA come dal file: la
|
|
1455
|
+
// finestra li legge da qui per vederli e ritirarli, e la API li conferma
|
|
1456
|
+
// senza rileggere fleet.json.
|
|
1457
|
+
...(defs.capabilityProfiles && Object.keys(defs.capabilityProfiles).length
|
|
1458
|
+
? { capabilityProfiles: defs.capabilityProfiles }
|
|
1459
|
+
: {}),
|
|
1436
1460
|
managedCatalog,
|
|
1437
1461
|
// I NOMI dei server MCP disponibili, per la finestra che sceglie quali
|
|
1438
1462
|
// strumenti dare a una cella. Solo nomi: le definizioni non escono di qui,
|
|
@@ -69,8 +69,11 @@ function createEndpointProbe({
|
|
|
69
69
|
const run = (async () => {
|
|
70
70
|
const controller = new AbortController();
|
|
71
71
|
const timer = setTimer(() => controller.abort(), timeoutMs);
|
|
72
|
-
//
|
|
73
|
-
|
|
72
|
+
// Il timer resta REF per la durata della sonda: e' l'unico handle che
|
|
73
|
+
// puo' chiudere un fetch che non risponde, e con l'unref (senza altri
|
|
74
|
+
// handle) l'event loop si svuota prima che scada — la promise non si
|
|
75
|
+
// risolve mai e il file di test resta appeso. Vive al massimo timeoutMs
|
|
76
|
+
// e viene spento nel finally: nessun processo trattenuto oltre.
|
|
74
77
|
let verdict;
|
|
75
78
|
try {
|
|
76
79
|
const res = await fetchImpl(url, { method: 'GET', signal: controller.signal });
|
|
@@ -136,7 +136,11 @@ function createEventFeedClient(opts = {}) {
|
|
|
136
136
|
opts.eventsHub.broadcast({ type: f.outcome === 'dismissed' ? 'ask-dismissed' : 'ask-answered',
|
|
137
137
|
id: f.askId, ownerId: envelope.ownerId, eventId: envelope.eventId });
|
|
138
138
|
} else if (f.type === 'file-notice') {
|
|
139
|
-
|
|
139
|
+
// Stessa forma della consegna locale (lib/files/routes.js): il nome del
|
|
140
|
+
// file e' SEMPRE nel corpo, con la caption quando c'e'. Prima l'importato
|
|
141
|
+
// senza caption arrivava all'operatore senza corpo, mentre il locale
|
|
142
|
+
// portava il nome: due classi diverse per lo stesso avviso.
|
|
143
|
+
opts.eventsHub.broadcast({ type: 'notify', title: `file: ${f.name}`, body: f.caption ? `${f.name} — ${f.caption}` : f.name,
|
|
140
144
|
originNode: envelope.ownerId, ownerId: envelope.ownerId,
|
|
141
145
|
// Stessa attribuzione di cella del ramo notify: un file e' un avviso
|
|
142
146
|
// DELLA CELLA, e senza questo campo il relay lo contava come scope nodo
|
package/lib/proxy/federation.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const http = require('node:http');
|
|
4
4
|
const net = require('node:net');
|
|
5
5
|
const express = require('express');
|
|
6
|
+
const { WebSocketServer } = require('ws');
|
|
6
7
|
const store = require('../nodes/store.js');
|
|
7
8
|
const topologyCache = require('../nodes/topology-cache.js');
|
|
8
9
|
const { allowedByClass, classifyResource } = require('./resource-acl.js');
|
|
@@ -467,7 +468,7 @@ function liveHostLedgerCapture({ ledger, ownerId, resource }) {
|
|
|
467
468
|
};
|
|
468
469
|
}
|
|
469
470
|
|
|
470
|
-
function proxyHttp(req, res, { port, path, credential, visited = null, hopProof = null, setCookiePrefix = null, panelCookie = null, capture = null }) {
|
|
471
|
+
function proxyHttp(req, res, { port, path, credential, visited = null, hopProof = null, setCookiePrefix = null, panelCookie = null, capture = null, getTimeoutMs = 10000 }) {
|
|
471
472
|
// A body a local middleware has already read is no longer in the stream: the
|
|
472
473
|
// headers would announce bytes that nothing sends, and the peer would wait for
|
|
473
474
|
// a body that never arrives. Say it at once, with a name, instead of letting
|
|
@@ -490,12 +491,24 @@ function proxyHttp(req, res, { port, path, credential, visited = null, hopProof
|
|
|
490
491
|
res.end(body);
|
|
491
492
|
});
|
|
492
493
|
});
|
|
493
|
-
|
|
494
|
+
// La GET federata (decks, fleet/status, sessions…) deve degradare in tempo
|
|
495
|
+
// quando il tunnel del peer è «su a metà» (socket aperto, nessuna risposta):
|
|
496
|
+
// 10 s e 504 — prima era un unico 30 s che chiudeva con 502. Gli altri
|
|
497
|
+
// metodi mantengono il tetto largo di prima.
|
|
498
|
+
const FORWARD_TIMEOUT_MS = req.method === 'GET' ? getTimeoutMs : 30000;
|
|
499
|
+
up.setTimeout(FORWARD_TIMEOUT_MS, () => {
|
|
500
|
+
if (!res.headersSent) {
|
|
501
|
+
res.status(504).json({ error: 'peer non risponde entro il timeout', reason: 'federation-peer-timeout' });
|
|
502
|
+
up.destroy();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
up.destroy();
|
|
506
|
+
});
|
|
494
507
|
up.on('error', () => { if (!res.headersSent) res.status(502).json({ error: 'peer non raggiungibile', reason: 'federation-peer-unreachable' }); else res.destroy(); });
|
|
495
508
|
req.pipe(up);
|
|
496
509
|
}
|
|
497
510
|
|
|
498
|
-
function routeHandler({ nodesPath, localPort, localCredential, ingress = null, readonly = () => false, hopSecret = null, liveHostLedger = null }) {
|
|
511
|
+
function routeHandler({ nodesPath, localPort, localCredential, ingress = null, readonly = () => false, hopSecret = null, liveHostLedger = null, forwardGetTimeoutMs = 10000 }) {
|
|
499
512
|
return (req, res) => {
|
|
500
513
|
const parsed = parseRoute(req.url);
|
|
501
514
|
// Prima dell'allowlist: un peer senza permesso non deve nemmeno sapere se la
|
|
@@ -543,6 +556,7 @@ function routeHandler({ nodesPath, localPort, localCredential, ingress = null, r
|
|
|
543
556
|
visited,
|
|
544
557
|
hopProof: signHop(secret, { method: req.method, path, visited }),
|
|
545
558
|
panelCookie,
|
|
559
|
+
getTimeoutMs: forwardGetTimeoutMs,
|
|
546
560
|
});
|
|
547
561
|
}
|
|
548
562
|
const next = st && store.getNode(st, parsed.route[0]);
|
|
@@ -563,7 +577,7 @@ function routeHandler({ nodesPath, localPort, localCredential, ingress = null, r
|
|
|
563
577
|
const capture = liveHostLedger && (parsed.resource === '/live-host/designate' || parsed.resource === '/live-host/clear')
|
|
564
578
|
? liveHostLedgerCapture({ ledger: liveHostLedger, ownerId: next.nodeId, resource: parsed.resource })
|
|
565
579
|
: null;
|
|
566
|
-
proxyHttp(req, res, { port: next.localPort, path, credential: next.token, visited, setCookiePrefix, panelCookie, capture });
|
|
580
|
+
proxyHttp(req, res, { port: next.localPort, path, credential: next.token, visited, setCookiePrefix, panelCookie, capture, getTimeoutMs: forwardGetTimeoutMs });
|
|
567
581
|
};
|
|
568
582
|
}
|
|
569
583
|
|
|
@@ -1496,7 +1510,7 @@ function forwardUpgrade({ req, socket, head, nodesPath, localPort, localCredenti
|
|
|
1496
1510
|
// and a limited peer must not reach an operator-class channel through it.
|
|
1497
1511
|
{
|
|
1498
1512
|
const gate = classGateCheck(parsed.resource, 'GET', ingress);
|
|
1499
|
-
if (!gate.ok) return reject(socket, gate.status === 404 ? 404 : 403);
|
|
1513
|
+
if (!gate.ok) { if (parsed.resource === '/ws') return rejectAsNoSuchSession(socket, req); return reject(socket, gate.status === 404 ? 404 : 403); }
|
|
1500
1514
|
}
|
|
1501
1515
|
const st = store.loadStore(nodesPath);
|
|
1502
1516
|
if (!st) return reject(socket, 503);
|
|
@@ -1545,6 +1559,24 @@ function forwardUpgrade({ req, socket, head, nodesPath, localPort, localCredenti
|
|
|
1545
1559
|
up.on('error', () => reject(socket, 502)); socket.on('error', () => up.destroy());
|
|
1546
1560
|
}
|
|
1547
1561
|
|
|
1562
|
+
// Indistinguibile da «sessione inesistente» (lib/ws/bridge.js): le stesse
|
|
1563
|
+
// chiamate della libreria ws usata dal bridge (sendJson + close), cosi' i byte
|
|
1564
|
+
// sul canale sono quelli del percorso locale. Un rifiuto HTTP grezzo sul
|
|
1565
|
+
// canale WS rivelerebbe l'esistenza o lo stato della risorsa.
|
|
1566
|
+
const REJECT_WSS = new WebSocketServer({ noServer: true });
|
|
1567
|
+
function rejectAsNoSuchSession(socket, req) {
|
|
1568
|
+
// Solo un VERO upgrade WS ha una chiave: per qualunque altra richiesta su /ws
|
|
1569
|
+
// (non-upgrade) resta il rifiuto di classe di prima — non c'e' un canale
|
|
1570
|
+
// WebSocket da chiudere, e rispondere come il bridge non avrebbe senso.
|
|
1571
|
+
if (!req || !req.headers || !req.headers['sec-websocket-key']) return reject(socket, 403);
|
|
1572
|
+
try {
|
|
1573
|
+
REJECT_WSS.handleUpgrade(req, socket, Buffer.alloc(0), (ws) => {
|
|
1574
|
+
try { ws.send(JSON.stringify({ type: 'error', reason: 'no such session' })); } catch (_) { /* socket gia' morto */ }
|
|
1575
|
+
try { ws.close(4404, 'no such session'); } catch (_) { /* idem */ }
|
|
1576
|
+
});
|
|
1577
|
+
} catch (_) { try { socket.destroy(); } catch (_) { /* idem */ } }
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1548
1580
|
function reject(socket, code) { try { socket.end(`HTTP/1.1 ${code} Error\r\nConnection: close\r\n\r\n`); } catch (_) {} }
|
|
1549
1581
|
|
|
1550
1582
|
module.exports = {
|
package/lib/server.js
CHANGED
|
@@ -1751,6 +1751,9 @@ function createServer(opts = {}) {
|
|
|
1751
1751
|
|
|
1752
1752
|
server.on('close', () => {
|
|
1753
1753
|
fleetP.then((fleet) => (typeof fleet.close === 'function' ? fleet.close() : null)).catch(() => {});
|
|
1754
|
+
// I waiter dei vl-node pendenti si risolvono SUBITO (idle): il processo
|
|
1755
|
+
// non resta in piedi per i boundedWait residui.
|
|
1756
|
+
try { vlNodeBroker.flushPendingWaiters(); } catch (_) { /* broker gia' chiuso */ }
|
|
1754
1757
|
});
|
|
1755
1758
|
|
|
1756
1759
|
return {
|
package/lib/vl-nodes/broker.js
CHANGED
|
@@ -283,7 +283,8 @@ function createBroker(options = {}) {
|
|
|
283
283
|
waiter.cleanup?.();
|
|
284
284
|
resolve({ type: 'idle', generation: state.generation });
|
|
285
285
|
}, boundedWait);
|
|
286
|
-
|
|
286
|
+
// Nessun unref: il timer e' REF perche' la promise del poll si risolva
|
|
287
|
+
// sempre (test e shutdown); il flush alla chiusura evita attese residue.
|
|
287
288
|
if (signal) {
|
|
288
289
|
const onAbort = () => {
|
|
289
290
|
if (state.waiter === waiter) state.waiter = null;
|
|
@@ -316,6 +317,17 @@ function createBroker(options = {}) {
|
|
|
316
317
|
return { ok: true, id, status: 'submitted', generation: state.generation };
|
|
317
318
|
}
|
|
318
319
|
|
|
320
|
+
// Chiusura/shutdown: i waiter pendenti si risolvono SUBITO (idle) cosi' il
|
|
321
|
+
// processo puo' terminare senza aspettare i boundedWait residui.
|
|
322
|
+
function flushPendingWaiters() {
|
|
323
|
+
let flushed = 0;
|
|
324
|
+
for (const state of states.values()) {
|
|
325
|
+
if (state.waiter) { finishWaiter(state, { type: 'idle', generation: state.generation }); flushed += 1; }
|
|
326
|
+
}
|
|
327
|
+
return flushed;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
|
|
319
331
|
function forget(nodeId) {
|
|
320
332
|
const state = states.get(nodeId);
|
|
321
333
|
if (state) finishWaiter(state, { type: 'revoked' });
|
|
@@ -343,7 +355,7 @@ function createBroker(options = {}) {
|
|
|
343
355
|
});
|
|
344
356
|
}
|
|
345
357
|
|
|
346
|
-
return { poll, dispatch, forget, list, observe, events, _states: states };
|
|
358
|
+
return { poll, dispatch, forget, list, observe, events, flushPendingWaiters, _states: states };
|
|
347
359
|
}
|
|
348
360
|
|
|
349
361
|
module.exports = {
|