@mmmbuto/nexuscrew 0.9.36 → 0.9.38

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.
@@ -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-DS1VyIuo.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-DDuZWnkm.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.36"}
1
+ {"version":"0.9.38"}
@@ -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
- // Un timer di sonda non tiene vivo il processo.
73
- if (timer && typeof timer.unref === 'function') timer.unref();
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
- opts.eventsHub.broadcast({ type: 'notify', title: `file: ${f.name}`, ...(f.caption ? { body: f.caption } : {}),
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
@@ -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
- up.setTimeout(30000, () => up.destroy());
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 {
@@ -283,7 +283,8 @@ function createBroker(options = {}) {
283
283
  waiter.cleanup?.();
284
284
  resolve({ type: 'idle', generation: state.generation });
285
285
  }, boundedWait);
286
- waiter.timer.unref?.();
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 = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.9.36",
3
+ "version": "0.9.38",
4
4
  "description": "Faithful browser tmux client — attach to live sessions over a real PTY, localhost-only, mobile-easy",
5
5
  "main": "lib/server.js",
6
6
  "bin": {