@mmmbuto/nexuscrew 0.9.53 → 0.9.55

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,8 +11,8 @@
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-DBlFdkZn.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-Da4oBb4f.css">
14
+ <script type="module" crossorigin src="/assets/index-H557UWX-.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-CvMkwRD1.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
@@ -1 +1 @@
1
- {"version":"0.9.53"}
1
+ {"version":"0.9.55"}
@@ -957,8 +957,8 @@ function autoUpdateCommand(args, opts = {}) {
957
957
  const daFile = () => {
958
958
  try {
959
959
  const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
960
- return cfg.autoUpdate !== false;
961
- } catch (_) { return true; } // default ON, come la configurazione
960
+ return cfg.autoUpdate === true;
961
+ } catch (_) { return false; } // default OFF, come la configurazione
962
962
  };
963
963
 
964
964
  if (azione === 'status') {
@@ -974,7 +974,7 @@ function autoUpdateCommand(args, opts = {}) {
974
974
  }).then(async (r) => {
975
975
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
976
976
  const j = await r.json();
977
- const acceso = j.autoUpdate !== false;
977
+ const acceso = j.autoUpdate === true;
978
978
  log(`autoupdate: ${acceso ? 'on' : 'off'}`);
979
979
  if (j.update && j.update.latest) log(` ultima versione vista su npm latest: ${j.update.latest}`);
980
980
  return { code: 0 };
@@ -988,8 +988,16 @@ function autoUpdateCommand(args, opts = {}) {
988
988
  if (!attivo) {
989
989
  // Servizio spento: si scrive il file, e si DICE che vale al prossimo avvio.
990
990
  // Tacerlo lascerebbe credere che abbia gia' effetto.
991
- let cfg = {};
992
- try { cfg = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch (_) { /* file nuovo */ }
991
+ // Se la config esiste ma non si legge, NON si riscrive: partire da un
992
+ // oggetto vuoto cancellerebbe le chiavi dell'operatore (wizardDone e le
993
+ // altre) per salvarne una sola. Motivo categorico, mai il messaggio del
994
+ // parser (contiene un estratto del file).
995
+ const letta = require('../config.js').readConfigForWrite(configPath);
996
+ if (!letta.ok) {
997
+ log(`autoupdate: config illeggibile ${letta.path} (${letta.motivo}) — non la riscrivo; ripara o rimuovi il file`);
998
+ return { code: 1 };
999
+ }
1000
+ const cfg = letta.cfg;
993
1001
  // Scrittura atomica e rifiuto dei symlink, come fa la route che scrive lo
994
1002
  // stesso file: tmp nella stessa directory, 0600, rename. Un `writeFileSync`
995
1003
  // diretto puo' lasciare un config.json troncato se il processo muore a
package/lib/cli/init.js CHANGED
@@ -18,6 +18,7 @@ const {
18
18
  selectProviderModeSync, fleetFileMode,
19
19
  } = require('./fleet-service.js');
20
20
  const { resolveBootPaths } = require('./stable-alias.js');
21
+ const { readConfigForWrite } = require('../config.js');
21
22
  const { aggiornaDefinizioni } = require('../fleet/definitions.js');
22
23
  const { defaultDefinitions } = require('../fleet/managed.js');
23
24
  const { commandExists, resolveCommand } = require('./path.js');
@@ -169,17 +170,26 @@ function runInit(opts = {}) {
169
170
  writeConfigAtomic(configPath, { port });
170
171
  actions.push(`created config ${configPath} (port ${port})`);
171
172
  } else {
172
- let current;
173
- try {
174
- current = JSON.parse(fs.readFileSync(configPath, 'utf8'));
175
- } catch (_) { current = {}; }
176
- if (opts.port) {
173
+ // Fail-closed come per fleet.json (vedi ensureFleetDefaults): un file
174
+ // PRESENTE ma illeggibile NON si riscrive. Riscriverlo con la sola porta
175
+ // cancellerebbe le chiavi dell'operatore (autoUpdate, wizardDone) e
176
+ // riporterebbe i default: e' il guasto osservato sulle macchine, dove
177
+ // l'opzione autoUpdate tornava a true da sola dopo essere stata spenta.
178
+ // La lettura e' la stessa usata dagli altri scrittori di config
179
+ // (readConfigForWrite): una sola regola, un solo motivo categorico.
180
+ const letta = readConfigForWrite(configPath);
181
+ const current = letta.ok ? letta.cfg : null;
182
+ const illeggibile = letta.ok ? null : letta.motivo;
183
+ if (!current) {
184
+ actions.push(`WARN: config illeggibile ${configPath} (${illeggibile}): non la riscrivo, il file resta com'e' (fail-closed)`);
185
+ if (opts.port) actions.push(`WARN: porta ${opts.port} non applicata alla config illeggibile`);
186
+ } else if (opts.port) {
177
187
  current.port = opts.port;
178
188
  port = opts.port;
179
189
  writeConfigAtomic(configPath, current);
180
190
  actions.push(`updated config ${configPath} (port ${port})`);
181
- } else if (current && current.port) port = current.port;
182
- actions.push(`preserved config ${configPath} (port ${port})`);
191
+ } else if (current.port) port = current.port;
192
+ if (current) actions.push(`preserved config ${configPath} (port ${port})`);
183
193
  }
184
194
  }
185
195
 
package/lib/config.js CHANGED
@@ -81,7 +81,9 @@ function baseDefaults() {
81
81
  credentialsPath: path.join(os.homedir(), '.nexuscrew', 'credentials.json'),
82
82
  // Installazioni npm globali controllano periodicamente il dist-tag latest.
83
83
  // Il manager aggiorna solo verso una semver superiore: mai downgrade.
84
- autoUpdate: true,
84
+ // SPENTO di default: chiave assente = false. Si accende da Impostazioni o
85
+ // con `nexuscrew autoupdate on`, e resta spento finche' non lo si accende.
86
+ autoUpdate: false,
85
87
  sessionPresets: {},
86
88
  // Ponte Live (fetta 3, rev5): isolabile — a false il ponte non si
87
89
  // connette mai e ogni avvio di Live resta sul comportamento standard.
@@ -147,6 +149,37 @@ function readConfigJson() {
147
149
  } catch (_) { return {}; }
148
150
  }
149
151
 
152
+ // Lettura della config per una SCRITTURA (merge). Distingue tre casi, e i tre
153
+ // non collassano in uno: chi scrive deve poter dire «assente» (legittimo: si
154
+ // parte da zero) da «presente ma illeggibile» (mai partire da zero: si
155
+ // perderebbero le chiavi dell'operatore, e i default tornerebbero — e' il
156
+ // guasto per cui autoUpdate si riaccendeva da solo).
157
+ // Il motivo e' una CATEGORIA fissa — 'parse-error', 'not-object',
158
+ // 'read-error:<codice errno>' — MAI il messaggio del parser: su Node recenti
159
+ // JSON.parse include un estratto dell'input, cioe' contenuto della config, e
160
+ // questi messaggi finiscono in risposte HTTP e log.
161
+ // -> { ok: true, cfg } | { ok: false, motivo, path }
162
+ function readConfigForWrite(filePath, { fsImpl = fs } = {}) {
163
+ const target = filePath || configJsonPath();
164
+ let testo;
165
+ try {
166
+ testo = fsImpl.readFileSync(target, 'utf8');
167
+ } catch (e) {
168
+ if (e && e.code === 'ENOENT') return { ok: true, cfg: {} }; // assente: si parte da zero
169
+ return { ok: false, motivo: `read-error${e && e.code ? `:${e.code}` : ''}`, path: target };
170
+ }
171
+ let parsed;
172
+ try {
173
+ parsed = JSON.parse(testo);
174
+ } catch (_) {
175
+ return { ok: false, motivo: 'parse-error', path: target };
176
+ }
177
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
178
+ return { ok: false, motivo: 'not-object', path: target };
179
+ }
180
+ return { ok: true, cfg: parsed };
181
+ }
182
+
150
183
  // Override da env (precedence più alta di config.json).
151
184
  function envOverrides() {
152
185
  const e = {};
@@ -259,4 +292,4 @@ function defaults() {
259
292
  return { ...base, ...env, terminal: resolveTerminal(base, env) };
260
293
  }
261
294
 
262
- module.exports = { assertLoopback, baseDefaults, readConfigJson, loadConfig, defaults, LOOPBACK, configJsonPath, eventsEnabledFlag, readFederationEventsEnabled, TERMINAL_DEFAULTS, resolveTerminal };
295
+ module.exports = { assertLoopback, baseDefaults, readConfigJson, readConfigForWrite, loadConfig, defaults, LOOPBACK, configJsonPath, eventsEnabledFlag, readFederationEventsEnabled, TERMINAL_DEFAULTS, resolveTerminal };
@@ -10,6 +10,7 @@
10
10
  const fs = require('node:fs');
11
11
  const path = require('node:path');
12
12
  const crypto = require('node:crypto');
13
+ const { readConfigForWrite } = require('../config.js');
13
14
 
14
15
  const CREDENTIAL_FILES = Object.freeze({
15
16
  identityDaemonCredential: 'daemon.credential',
@@ -61,6 +62,23 @@ function provisionIdentityAuthority({
61
62
  fsImpl = fs,
62
63
  now = new Date,
63
64
  } = {}) {
65
+ // PRIMA di qualunque scrittura (cartella compresa): se la config esiste ma
66
+ // non si legge, si rifiuta senza aver toccato NULLA. Motivo categorico, mai
67
+ // il messaggio del parser: contiene un estratto del file e finirebbe nel
68
+ // referto del comando.
69
+ const resolvedConfigPath = configPath
70
+ || (process.env.NEXUSCREW_CONFIG_FILE
71
+ || path.join(home || require('node:os').homedir(), '.nexuscrew', 'config.json'));
72
+ const lettaConfig = writeConfig
73
+ ? readConfigForWrite(resolvedConfigPath, { fsImpl })
74
+ : { ok: true, cfg: {} };
75
+ if (!lettaConfig.ok) {
76
+ return {
77
+ ok: false, code: 'CONFIG_UNREADABLE', dir: dir || defaultAuthorityDir(home),
78
+ message: `config illeggibile ${lettaConfig.path} (${lettaConfig.motivo}): non la riscrivo, niente e' stato scritto`,
79
+ };
80
+ }
81
+
64
82
  const targetDir = dir || defaultAuthorityDir(home);
65
83
  fsImpl.mkdirSync(targetDir, { recursive: true, mode: 0o700 });
66
84
 
@@ -104,18 +122,10 @@ function provisionIdentityAuthority({
104
122
  for (const filePath of written) fsImpl.chmodSync(filePath, 0o600);
105
123
  for (const filePath of written) assertSafeMode(filePath, 0o600);
106
124
 
107
- const resolvedConfigPath = configPath
108
- || (process.env.NEXUSCREW_CONFIG_FILE
109
- || path.join(home || require('node:os').homedir(), '.nexuscrew', 'config.json'));
110
-
111
125
  let configUpdated = false;
112
126
  let configInstructions = null;
113
127
  if (writeConfig) {
114
- let config = {};
115
- try {
116
- config = JSON.parse(fsImpl.readFileSync(resolvedConfigPath, 'utf8'));
117
- if (!config || typeof config !== 'object' || Array.isArray(config)) config = {};
118
- } catch (_) { config = {}; }
128
+ const config = lettaConfig.cfg;
119
129
  config.fleet = { ...(config.fleet || {}), identity: { ...(config.fleet?.identity || {}), mode: 'authority' } };
120
130
  fsImpl.mkdirSync(path.dirname(resolvedConfigPath), { recursive: true });
121
131
  fsImpl.writeFileSync(resolvedConfigPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
@@ -1620,6 +1620,22 @@ function forwardUpgrade({ req, socket, head, nodesPath, localPort, localCredenti
1620
1620
  }
1621
1621
  }
1622
1622
  const up = net.connect({ host: '127.0.0.1', port });
1623
+ // ECONNREFUSED sull'hop locale (tunnel che si riavvia) arriva PRIMA del
1624
+ // 'connect': nel ramo hubMode il listener 'error' era registrato solo dentro
1625
+ // up.once('connect', …) e l'evento restava senza gestore → Unhandled 'error'
1626
+ // event → crash del processo (caduto 5 volte su un nodo Termux). Lo
1627
+ // registriamo QUI, sincrono, prima che qualunque I/O possa fallire, valido
1628
+ // per entrambi i rami. Idempotente con i listener registrati dopo il connect:
1629
+ // destroy() e upstreamGone() sono gia' protetti da flag.
1630
+ let heldRecord = null;
1631
+ up.on('error', () => {
1632
+ if (hubMode) {
1633
+ if (heldRecord) hubAttaches.destroy(heldRecord);
1634
+ else acceptWsThenClose(socket, req, 4402, 'upstream gone');
1635
+ } else {
1636
+ upstreamGone();
1637
+ }
1638
+ });
1623
1639
  // Osservatore NON consumante della testa di risposta dell'upstream: serve
1624
1640
  // solo a sapere se il 101 e' transitato (client in stato WebSocket) o se
1625
1641
  // l'hop e' morto prima. La pipe resta l'unico trasporto dei byte: il
@@ -1679,6 +1695,7 @@ function forwardUpgrade({ req, socket, head, nodesPath, localPort, localCredenti
1679
1695
  const record = hubAttaches.hold({
1680
1696
  attachId, tokenHash: tokenFingerprint(attachToken), route: attachRoute, session: attachSession, up,
1681
1697
  });
1698
+ heldRecord = record;
1682
1699
  up.on('data', (chunk) => hubAttaches.feed(record, chunk));
1683
1700
  up.on('close', () => hubAttaches.destroy(record));
1684
1701
  up.on('error', () => hubAttaches.destroy(record));
package/lib/server.js CHANGED
@@ -379,7 +379,7 @@ function createServer(opts = {}) {
379
379
  const updater = opts.updateManager || createNpmUpdater({
380
380
  currentVersion: VERSION,
381
381
  home: cfg.home || os.homedir(),
382
- enabled: cfg.autoUpdate !== false,
382
+ enabled: cfg.autoUpdate === true,
383
383
  readonly: bridgeReadonly(),
384
384
  diagnostics,
385
385
  ...(cfg.updateSeams || {}),
@@ -44,6 +44,7 @@ const crypto = require('node:crypto');
44
44
  const express = require('express');
45
45
 
46
46
  const nodesStore = require('../nodes/store.js');
47
+ const { readConfigForWrite } = require('../config.js');
47
48
  const nodeAliases = require('../nodes/aliases.js');
48
49
  const { setConsent: setAudioConsent, isConsent: isAudioConsent } = require('../audio/consent.js');
49
50
  const { describeCapability } = require('../audio/capability.js');
@@ -285,7 +286,7 @@ function settingsRoutes(deps = {}) {
285
286
  platform,
286
287
  service,
287
288
  version: VERSION,
288
- autoUpdate: updateStatus ? updateStatus.enabled : fileCfg.autoUpdate !== false,
289
+ autoUpdate: updateStatus ? updateStatus.enabled : fileCfg.autoUpdate === true,
289
290
  // Valore effettivo: l'env mantiene la precedenza sul file e il runtime
290
291
  // Fleet legge lo stesso oggetto cfg alla prossima creazione di cella.
291
292
  alternateScreen: cfg.alternateScreen === true,
@@ -360,8 +361,14 @@ function settingsRoutes(deps = {}) {
360
361
  return send(res, 400, { error: 'aiDesktop deve essere boolean' });
361
362
  }
362
363
 
363
- // merge sul config esistente (preserva le chiavi non gestite qui)
364
- const { cfg: current } = readConfigFile(configPath);
364
+ // merge sul config esistente (preserva le chiavi non gestite qui).
365
+ // Se il file ESISTE ma non si legge, NON si parte da un oggetto vuoto: si
366
+ // risponderebbe 200 avendo appena cancellato le chiavi dell'operatore.
367
+ const letta = readConfigForWrite(configPath);
368
+ if (!letta.ok) {
369
+ return send(res, 409, { error: 'config-unreadable', reason: letta.motivo, path: letta.path });
370
+ }
371
+ const current = letta.cfg;
365
372
  const next = { ...current };
366
373
  if (b.roles !== undefined) {
367
374
  const prev = (current.roles && typeof current.roles === 'object') ? current.roles : {};
@@ -387,7 +394,7 @@ function settingsRoutes(deps = {}) {
387
394
  saved: true,
388
395
  config: {
389
396
  roles: next.roles, port: next.port, wizardDone: next.wizardDone,
390
- autoUpdate: next.autoUpdate !== false, alternateScreen: next.alternateScreen === true,
397
+ autoUpdate: next.autoUpdate === true, alternateScreen: next.alternateScreen === true,
391
398
  },
392
399
  };
393
400
  // Il server legge la porta SOLO allo startup: il cambio vale al prossimo
@@ -425,9 +432,12 @@ function settingsRoutes(deps = {}) {
425
432
  if (!b || typeof b !== 'object' || Array.isArray(b) || typeof b.enabled !== 'boolean') {
426
433
  return send(res, 400, { error: 'body deve essere {enabled: boolean}' });
427
434
  }
428
- const { cfg: cfgNow } = readConfigFile(configPath);
435
+ const letta = readConfigForWrite(configPath);
436
+ if (!letta.ok) {
437
+ return send(res, 409, { error: 'config-unreadable', reason: letta.motivo, path: letta.path });
438
+ }
429
439
  try {
430
- atomicWriteConfig(configPath, { ...cfgNow, aiDesktop: b.enabled });
440
+ atomicWriteConfig(configPath, { ...letta.cfg, aiDesktop: b.enabled });
431
441
  } catch (e) {
432
442
  return send(res, 500, { error: `config non salvata: ${e.message}` });
433
443
  }
@@ -59,7 +59,10 @@ function createNpmUpdater(opts = {}) {
59
59
  const intervalMs = opts.intervalMs === undefined ? DEFAULT_INTERVAL_MS : opts.intervalMs;
60
60
  const maxLogBytes = opts.maxLogBytes === undefined ? DEFAULT_MAX_LOG_BYTES : opts.maxLogBytes;
61
61
  const diagnostics = opts.diagnostics || null;
62
- let enabled = opts.enabled !== false && !readonly;
62
+ // Spento se non richiesto esplicitamente: l'unico chiamante di produzione
63
+ // passa `enabled: cfg.autoUpdate === true`, quindi la chiave assente non
64
+ // avvia il daemon. Stesso criterio di setEnabled (`value === true`).
65
+ let enabled = opts.enabled === true && !readonly;
63
66
  let checking = null;
64
67
  let timer = null;
65
68
  let state = readState(statusPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.9.53",
3
+ "version": "0.9.55",
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": {