@mmmbuto/nexuscrew 0.8.48 → 0.8.50

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-C_SyIZ78.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-kj-l-RDc.js"></script>
15
15
  <link rel="stylesheet" crossorigin href="/assets/index-43DFO1EH.css">
16
16
  </head>
17
17
  <body>
@@ -1 +1 @@
1
- {"version":"0.8.48"}
1
+ {"version":"0.8.50"}
@@ -96,4 +96,28 @@ function summarizeStops(results) {
96
96
  return { stoppedAny, quarantinedAny, allClosed: stoppedAny && !quarantinedAny };
97
97
  }
98
98
 
99
- module.exports = { LEASE_MS, GRACE_MS, nextReadySlot, prepareRotation, abortPrepared, commitRotation, settleGrace, quarantineSlot, stopWasDemonstrated, summarizeStops };
99
+ // Esito di un giro di verifica del pool. `verified` richiede TUTTI gli slot:
100
+ // e' una scelta deliberata, perche' rotateRotatableReverse si rifiuta di
101
+ // commutare dentro un pool non interamente provato. La conseguenza pero' va
102
+ // detta, non dedotta: finche' il pool non e' verificato la rotazione
103
+ // automatica resta SPENTA e il watcher non viene nemmeno armato. Un pool
104
+ // "unverifiable" non e' un'etichetta descrittiva, e' autoriparazione assente.
105
+ function summarizePoolVerification(results, slotCount) {
106
+ const list = Array.isArray(results) ? results : [];
107
+ const total = Number.isSafeInteger(slotCount) && slotCount > 0 ? slotCount : 0;
108
+ const verifiedSlots = [...new Set(list.filter((r) => r && r.proven === true)
109
+ .map((r) => r.slot).filter(Number.isSafeInteger))].sort((a, b) => a - b);
110
+ const failures = list.filter((r) => r && r.proven !== true)
111
+ .map((r) => ({ slot: r.slot, code: typeof r.code === 'string' && /^[a-z0-9-]{1,64}$/.test(r.code) ? r.code : 'reverse-slot-verify-failed' }));
112
+ // Zero slot dichiarati non e' "tutto verificato": senza un pool da provare
113
+ // non c'e' nulla da cui ruotare.
114
+ const verified = total > 0 && verifiedSlots.length === total;
115
+ return {
116
+ verifiedSlots,
117
+ failures,
118
+ verification: verified ? 'verified' : 'unverifiable',
119
+ rotationActive: verified,
120
+ };
121
+ }
122
+
123
+ module.exports = { LEASE_MS, GRACE_MS, nextReadySlot, prepareRotation, abortPrepared, commitRotation, settleGrace, quarantineSlot, stopWasDemonstrated, summarizeStops, summarizePoolVerification };
package/lib/server.js CHANGED
@@ -187,9 +187,20 @@ function createServer(opts = {}) {
187
187
  const key = reverseKey(active.port, expectedGeneration);
188
188
  const existing = entries.get(key);
189
189
  if (existing) return existing;
190
+ // Il listener prova la PROPRIA identita' a chi compone la sfida, e l'altro
191
+ // capo la costruisce con l'id di QUESTA installazione (probeReverseOwner usa
192
+ // `peer.nodeId`, cioe' noi visti da lui). `node.nodeId` e' invece l'id del
193
+ // nodo REMOTO — lo store lo impone, rifiutando come self-reference una voce
194
+ // il cui nodeId eguagli il proprio. Registrarlo faceva firmare due tuple
195
+ // diverse ai due capi: la prova non tornava mai e Share si spegneva con un
196
+ // errore definitivo, non ritentabile.
197
+ const localInstanceId = (nodesStore.loadStore(nodesPath) || {}).nodeId || null;
198
+ // Fail-closed: senza identita' locale il listener nascerebbe con una prova
199
+ // che nessuno puo' validare. Meglio non aprirlo che aprirlo indimostrabile.
200
+ if (!localInstanceId) return null;
190
201
  const listener = await reverseSlotListeners.open({
191
202
  nodeName: node.name, remotePort: active.port, generation: expectedGeneration,
192
- instanceId: node.nodeId, secret: node.acceptToken,
203
+ instanceId: localInstanceId, secret: node.acceptToken,
193
204
  });
194
205
  let launched = nodesTunnel.startReverseForward({
195
206
  home: cfg.home || os.homedir(), node, remotePort: active.port, generation: expectedGeneration,
@@ -256,6 +267,10 @@ function createServer(opts = {}) {
256
267
  async function verifyRotatablePool(node) {
257
268
  const pool = node?.reversePool;
258
269
  if (!pool || !node.shared || !node.token || !node.nodeId) return { verified: false, code: 'reverse-pool-not-ready' };
270
+ // Fermarsi al primo slot che non si prova nasconde quali altri fossero
271
+ // sani: l'esito e' identico (servono tutti per 'verified'), ma la diagnosi
272
+ // no. Chi guarda deve poter distinguere "un solo slot non risponde" da
273
+ // "non risponde nessuno", perche' sono due guasti diversi.
259
274
  const proven = [];
260
275
  for (let slot = 0; slot < pool.slots.length; slot += 1) {
261
276
  const candidate = pool.slots[slot];
@@ -264,9 +279,9 @@ function createServer(opts = {}) {
264
279
  await startRotatableReverse(node, { slot, generation: candidate.generation });
265
280
  temporary = slot !== pool.activeSlot;
266
281
  await federation.verifyHubPoolSlot({ node, slot, generation: candidate.generation, fetchImpl: healthFetch });
267
- proven.push(slot);
268
- } catch (_) {
269
- break;
282
+ proven.push({ slot, proven: true });
283
+ } catch (error) {
284
+ proven.push({ slot, proven: false, code: error && error.code });
270
285
  } finally {
271
286
  if (temporary) await stopRotatableReverse(node.name, { remotePort: candidate.port, generation: candidate.generation });
272
287
  }
@@ -274,15 +289,27 @@ function createServer(opts = {}) {
274
289
  const current = nodesStore.loadStoreStrict(nodesPath);
275
290
  const fresh = nodesStore.getNode(current, node.name);
276
291
  if (!fresh?.reversePool) return { verified: false, code: 'reverse-pool-missing' };
277
- const verifiedSlots = [...new Set(proven)].sort((a, b) => a - b);
278
- const verification = verifiedSlots.length === fresh.reversePool.slots.length ? 'verified' : 'unverifiable';
292
+ const outcome = reverseRotation.summarizePoolVerification(proven, fresh.reversePool.slots.length);
293
+ const { verifiedSlots, verification, failures } = outcome;
279
294
  const updatedPool = { ...fresh.reversePool, verifiedSlots, verification };
280
295
  nodesStore.atomicWriteStore(nodesPath, nodesStore.setNodeReversePool(current, fresh.name, updatedPool));
281
296
  diagnostics.record(verification === 'verified' ? 'info' : 'warn', 'reverse-pool',
282
297
  verification === 'verified' ? 'REVERSE_POOL_VERIFIED' : 'REVERSE_POOL_UNVERIFIABLE',
283
- verification === 'verified' ? 'Reverse pool verified' : 'Reverse pool could not be fully verified', { node: fresh.name, verifiedSlots: verifiedSlots.length });
298
+ verification === 'verified' ? 'Reverse pool verified' : 'Reverse pool could not be fully verified',
299
+ { node: fresh.name, verifiedSlots: verifiedSlots.length, slots: fresh.reversePool.slots.length,
300
+ ...(failures.length ? { failedSlots: failures } : {}) });
301
+ // Un pool non verificato non e' solo un'etichetta: la rotazione automatica
302
+ // RIFIUTA di partire senza tutti gli slot provati (scelta deliberata, vedi
303
+ // rotateRotatableReverse) e il watcher non viene nemmeno armato. Detto
304
+ // altrimenti: l'autoriparazione del canale reverse resta SPENTA, e finora
305
+ // nessuno lo diceva. Va dichiarato, non dedotto da un aggettivo.
306
+ if (verification !== 'verified') {
307
+ diagnostics.record('warn', 'reverse-pool', 'REVERSE_POOL_ROTATION_INACTIVE',
308
+ 'Automatic reverse rotation is not active: the pool is not fully verified',
309
+ { node: fresh.name, verifiedSlots: verifiedSlots.length, slots: fresh.reversePool.slots.length });
310
+ }
284
311
  if (verification === 'verified') ensureReverseWatcher(fresh.name);
285
- return { verified: verification === 'verified', verification, verifiedSlots };
312
+ return { verified: verification === 'verified', verification, verifiedSlots, ...(failures.length ? { failures } : {}) };
286
313
  }
287
314
  async function settleRotatableGrace(name, generation) {
288
315
  const current = nodesStore.loadStoreStrict(nodesPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.8.48",
3
+ "version": "0.8.50",
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": {