@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.
- package/CHANGELOG.md +19 -0
- package/frontend/dist/assets/index-CvMkwRD1.css +32 -0
- package/frontend/dist/assets/index-H557UWX-.js +169 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +13 -5
- package/lib/cli/init.js +17 -7
- package/lib/config.js +35 -2
- package/lib/fleet/identity-provision.js +19 -9
- package/lib/proxy/federation.js +17 -0
- package/lib/server.js +1 -1
- package/lib/settings/routes.js +16 -6
- package/lib/update/manager.js +4 -1
- package/package.json +1 -1
- package/frontend/dist/assets/index-DBlFdkZn.js +0 -169
- package/frontend/dist/assets/index-Da4oBb4f.css +0 -32
package/frontend/dist/index.html
CHANGED
|
@@ -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-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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.
|
|
1
|
+
{"version":"0.9.55"}
|
package/lib/cli/commands.js
CHANGED
|
@@ -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
|
|
961
|
-
} catch (_) { return
|
|
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
|
|
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
|
-
|
|
992
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 });
|
package/lib/proxy/federation.js
CHANGED
|
@@ -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
|
|
382
|
+
enabled: cfg.autoUpdate === true,
|
|
383
383
|
readonly: bridgeReadonly(),
|
|
384
384
|
diagnostics,
|
|
385
385
|
...(cfg.updateSeams || {}),
|
package/lib/settings/routes.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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, { ...
|
|
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
|
}
|
package/lib/update/manager.js
CHANGED
|
@@ -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
|
-
|
|
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);
|