@mmmbuto/nexuscrew 0.9.5 → 0.9.7
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 +97 -0
- package/frontend/dist/assets/index-DDAwkwKR.css +32 -0
- package/frontend/dist/assets/index-YpDRVy-I.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/init.js +22 -2
- package/lib/cli/service.js +11 -3
- package/lib/cli/stable-alias.js +117 -0
- package/lib/nodes/pubkey-format.js +69 -0
- package/lib/nodes/tunnel-supervisor.js +14 -17
- package/lib/nodes/tunnel.js +66 -12
- package/lib/settings/pairing-coordinator.js +10 -4
- package/lib/settings/routes.js +11 -0
- package/package.json +1 -1
- package/frontend/dist/assets/index-BUbgwJsU.js +0 -93
- package/frontend/dist/assets/index-BzmGsXBy.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-YpDRVy-I.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DDAwkwKR.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.9.
|
|
1
|
+
{"version":"0.9.7"}
|
package/lib/cli/init.js
CHANGED
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
generateFleetService, installFleetService, migrationGate,
|
|
18
18
|
selectProviderModeSync, fleetFileMode,
|
|
19
19
|
} = require('./fleet-service.js');
|
|
20
|
+
const { resolveBootPaths } = require('./stable-alias.js');
|
|
20
21
|
const { aggiornaDefinizioni } = require('../fleet/definitions.js');
|
|
21
22
|
const { defaultDefinitions } = require('../fleet/managed.js');
|
|
22
23
|
const { commandExists, resolveCommand } = require('./path.js');
|
|
@@ -234,6 +235,17 @@ function runInit(opts = {}) {
|
|
|
234
235
|
port, home, uid: uid(),
|
|
235
236
|
installPath: opts.installPath,
|
|
236
237
|
};
|
|
238
|
+
// 1-bis (R23): la STESSA classe di difetto del companion di boot — nodeBin
|
|
239
|
+
// e entry finiscono versionati nell'unit/plist del servizio per-nodo. Si
|
|
240
|
+
// risolve qui, alla scrittura, e si dichiara cio' che non ha alias stabile:
|
|
241
|
+
// i due punti di scrittura non devono divergere (correggerne uno solo
|
|
242
|
+
// lascerebbe il difetto vivo).
|
|
243
|
+
const svcBoot = resolveBootPaths({
|
|
244
|
+
nodeBin: svcCtx.nodeBin,
|
|
245
|
+
entryPath: path.join(svcCtx.repoRoot, 'bin', 'nexuscrew.js'),
|
|
246
|
+
});
|
|
247
|
+
svcCtx.nodeBin = svcBoot.nodeBin;
|
|
248
|
+
svcCtx.entryPath = svcBoot.entryPath;
|
|
237
249
|
const content = generateService(platform, svcCtx);
|
|
238
250
|
|
|
239
251
|
if (!installBoot) {
|
|
@@ -250,6 +262,7 @@ function runInit(opts = {}) {
|
|
|
250
262
|
: 'tmux non trovato su PATH (installa tmux)';
|
|
251
263
|
actions.push(`WARN: ${reason} -> service NON installato (ri-runna init)`);
|
|
252
264
|
} else {
|
|
265
|
+
for (const w of svcBoot.warnings) actions.push(`WARN service: ${w}`);
|
|
253
266
|
try {
|
|
254
267
|
const r = installService(platform, content, svcCtx, { execImpl: opts.execImpl });
|
|
255
268
|
if (r.failures && r.failures.length) {
|
|
@@ -301,10 +314,17 @@ function runInit(opts = {}) {
|
|
|
301
314
|
} else if (dryRun) {
|
|
302
315
|
actions.push('DRY-RUN fleet companion generato, NON installato');
|
|
303
316
|
} else {
|
|
317
|
+
// R23: i path del boot si risolvono alla SCRITTURA del servizio (mai
|
|
318
|
+
// a ogni avvio): un alias stabile che risolva allo stesso file, se
|
|
319
|
+
// c'e'; altrimenti il path attuale, DICHIARATO — un avviso adesso e'
|
|
320
|
+
// meglio di un path che scade in silenzio (vedi stable-alias.js).
|
|
321
|
+
const bootEntry = path.join(svcCtx.repoRoot, 'bin', 'nexuscrew.js');
|
|
322
|
+
const boot = resolveBootPaths({ nodeBin: svcCtx.nodeBin, entryPath: bootEntry });
|
|
323
|
+
for (const w of boot.warnings) actions.push(`WARN fleet companion: ${w}`);
|
|
304
324
|
const fleetContent = generateFleetService({
|
|
305
325
|
platform,
|
|
306
|
-
nodeBin:
|
|
307
|
-
entryPath:
|
|
326
|
+
nodeBin: boot.nodeBin,
|
|
327
|
+
entryPath: boot.entryPath,
|
|
308
328
|
repoRoot: svcCtx.repoRoot,
|
|
309
329
|
home,
|
|
310
330
|
});
|
package/lib/cli/service.js
CHANGED
|
@@ -53,6 +53,14 @@ function assertSystemdSafe(label, p) {
|
|
|
53
53
|
|
|
54
54
|
// --- Templates ---
|
|
55
55
|
|
|
56
|
+
// Entry del servizio: di norma derivato da repoRoot; i punti di scrittura
|
|
57
|
+
// che risolvono l'alias stabile (R23/1-bis, stable-alias.js) passano
|
|
58
|
+
// l'entryPath gia' risolto. Contratto vecchio intatto: senza entryPath si
|
|
59
|
+
// deriva da repoRoot come prima.
|
|
60
|
+
function serviceEntryPath(ctx) {
|
|
61
|
+
return ctx.entryPath || path.join(ctx.repoRoot, 'bin', 'nexuscrew.js');
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
function generateService(platform, ctx) {
|
|
57
65
|
if (platform === 'linux') return generateLinux(ctx);
|
|
58
66
|
if (platform === 'mac') return generateMac(ctx);
|
|
@@ -70,7 +78,7 @@ function generateLinux(ctx) {
|
|
|
70
78
|
assertSystemdSafe('home', ctx.home);
|
|
71
79
|
const runtime = escapeSystemdPath(ctx.home);
|
|
72
80
|
const node = escapeSystemdExec(ctx.nodeBin);
|
|
73
|
-
const repoBin = escapeSystemdExec(
|
|
81
|
+
const repoBin = escapeSystemdExec(serviceEntryPath(ctx));
|
|
74
82
|
const nodeDir = escapeSystemdPath(path.dirname(ctx.nodeBin));
|
|
75
83
|
return `# NexusCrew service (systemd --user, loopback, solo tunnel SSH/VPN)
|
|
76
84
|
[Unit]
|
|
@@ -96,7 +104,7 @@ WantedBy=default.target
|
|
|
96
104
|
|
|
97
105
|
function generateMac(ctx) {
|
|
98
106
|
const nodeXml = escapeXml(ctx.nodeBin);
|
|
99
|
-
const repoBinXml = escapeXml(
|
|
107
|
+
const repoBinXml = escapeXml(serviceEntryPath(ctx));
|
|
100
108
|
// HOME remains stable when init or repair atomically replaces the runtime
|
|
101
109
|
// directory; launchd must not retain an unlinked runtime-directory cwd.
|
|
102
110
|
const runtimeXml = escapeXml(ctx.home);
|
|
@@ -146,7 +154,7 @@ function generateMac(ctx) {
|
|
|
146
154
|
|
|
147
155
|
function generateTermux(ctx) {
|
|
148
156
|
const nodeQ = shellQuote(ctx.nodeBin);
|
|
149
|
-
const repoBinQ = shellQuote(
|
|
157
|
+
const repoBinQ = shellQuote(serviceEntryPath(ctx));
|
|
150
158
|
return `#!/data/data/com.termux/files/usr/bin/sh
|
|
151
159
|
# NexusCrew boot (Termux) - loopback, localhost del telefono
|
|
152
160
|
export PATH=/data/data/com.termux/files/usr/bin:$PATH
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// R23 — alias stabili per i path del servizio di boot.
|
|
3
|
+
//
|
|
4
|
+
// Il companion di boot scrive nodeBin e entryPath LETTERALMENTE nel plist
|
|
5
|
+
// launchd (ProgramArguments[0]) e nell'unit systemd (ExecStart=). Quei path
|
|
6
|
+
// possono essere VERSIONATI:
|
|
7
|
+
// node -> /opt/homebrew/Cellar/node/<ver>/bin/node (Homebrew)
|
|
8
|
+
// ~/.nvm/versions/node/<ver>/bin/node (nvm)
|
|
9
|
+
// entry -> ~/.nvm/versions/node/<ver>/lib/node_modules/<pkg>/bin/nexuscrew.js
|
|
10
|
+
// Homebrew rompe all'upgrade (sostituisce il Cellar), nvm rompe a
|
|
11
|
+
// `nvm uninstall` della vecchia versione: il servizio punta a un file che non
|
|
12
|
+
// esiste piu' e la cella non parte, con un sintomo confuso (il node c'e', il
|
|
13
|
+
// file no — o viceversa).
|
|
14
|
+
//
|
|
15
|
+
// La cura, per CIASCUNO dei due path: fra candidati noti, cercare un alias
|
|
16
|
+
// stabile che risolva (realpath) allo STESSO IDENTICO FILE che sta girando.
|
|
17
|
+
// Se c'e', scrivere quello; se no, scrivere il path attuale e DICHIARARLO:
|
|
18
|
+
// un avviso alla scrittura e' meglio di un path che scade in silenzio.
|
|
19
|
+
//
|
|
20
|
+
// Tre vincoli non negoziabili (briefing R23):
|
|
21
|
+
// 1. La verifica «e' lo stesso file» si fa alla SCRITTURA del servizio, MAI a
|
|
22
|
+
// ogni avvio: dopo l'upgrade il symlink DEVE puntare a un node diverso —
|
|
23
|
+
// e' l'effetto voluto. Un controllo di identita' permanente si
|
|
24
|
+
// autoannullerebbe.
|
|
25
|
+
// 2. Su nvm NON esiste un alias stabile (~/.nvm/current non c'e',
|
|
26
|
+
// ~/.nvm/alias/default e' un file di testo, `command -v node` risolve al
|
|
27
|
+
// path versionato): qui il risolutore non trova nulla e il ramo che conta
|
|
28
|
+
// e' quello che lo dichiara. Non inventare un alias nvm.
|
|
29
|
+
// 3. Il PATH del plist che include /opt/homebrew/bin NON e' una protezione:
|
|
30
|
+
// ProgramArguments[0] e' assoluto. Non contarci.
|
|
31
|
+
|
|
32
|
+
const fs = require('node:fs');
|
|
33
|
+
const path = require('node:path');
|
|
34
|
+
|
|
35
|
+
// Prefissi stabili noti (ordinati per probabilita'), senza $PREFIX che viene
|
|
36
|
+
// aggiunto solo se presente (Termux).
|
|
37
|
+
const STABLE_ROOTS = ['/opt/homebrew', '/usr/local', '/usr'];
|
|
38
|
+
|
|
39
|
+
function rootsWithPrefix(env = process.env) {
|
|
40
|
+
const roots = [...STABLE_ROOTS];
|
|
41
|
+
if (env && env.PREFIX) roots.push(env.PREFIX);
|
|
42
|
+
return roots;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Candidati stabili per il BINARIO node. Almeno tre per costruzione —
|
|
46
|
+
// requisito del briefing: con uno solo «prova tutti i candidati» e «prova il
|
|
47
|
+
// primo» coincidono.
|
|
48
|
+
function nodeAliasCandidates(env = process.env) {
|
|
49
|
+
const c = rootsWithPrefix(env).map((r) => path.join(r, 'bin', 'node'));
|
|
50
|
+
return [...new Set(c)];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Candidati stabili per l'ENTRY del pacchetto. Il suffisso dopo node_modules/
|
|
54
|
+
// (es. '@mmmbuto/nexuscrew/bin/nexuscrew.js') e' DERIVATO dal path che sta
|
|
55
|
+
// girando, non inventato: se il path non attraversa un node_modules (checkout
|
|
56
|
+
// di sviluppo) non ci sono candidati e il risolutore dichiarera' il path
|
|
57
|
+
// attuale — un checkout di sviluppo non scade, ma l'avviso e' il prezzo
|
|
58
|
+
// dell'onestà.
|
|
59
|
+
function entryAliasCandidates(entryPath, env = process.env) {
|
|
60
|
+
if (!entryPath || typeof entryPath !== 'string') return [];
|
|
61
|
+
const marker = `${path.sep}node_modules${path.sep}`;
|
|
62
|
+
const i = entryPath.lastIndexOf(marker);
|
|
63
|
+
if (i < 0) return [];
|
|
64
|
+
const rel = entryPath.slice(i + marker.length);
|
|
65
|
+
if (!rel) return [];
|
|
66
|
+
const c = rootsWithPrefix(env).map((r) => path.join(r, 'lib', 'node_modules', rel));
|
|
67
|
+
return [...new Set(c)];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Risolutore: { path, alias, warning }.
|
|
71
|
+
// alias — il candidato scelto (realpath === realpath del path attuale),
|
|
72
|
+
// oppure null se nessuno risolve allo stesso file;
|
|
73
|
+
// path — l'alias se trovato, altrimenti il path attuale intatto;
|
|
74
|
+
// warning — null se trovato, altrimenti la dichiarazione scritta.
|
|
75
|
+
// realpathImpl e' iniettabile per i test deterministici; di default
|
|
76
|
+
// fs.realpathSync. Un candidato assente si salta (realpath lancia): l'assenza
|
|
77
|
+
// non e' un errore, e' solo un candidato in meno.
|
|
78
|
+
function resolveStableAlias(currentPath, candidates, opts = {}) {
|
|
79
|
+
const realpathImpl = typeof opts.realpath === 'function' ? opts.realpath : fs.realpathSync;
|
|
80
|
+
let currentReal = null;
|
|
81
|
+
try { currentReal = realpathImpl(currentPath); } catch (_) { currentReal = null; }
|
|
82
|
+
for (const cand of Array.isArray(candidates) ? candidates : []) {
|
|
83
|
+
let candReal;
|
|
84
|
+
try { candReal = realpathImpl(cand); } catch (_) { continue; } // assente: avanti
|
|
85
|
+
if (currentReal !== null && candReal === currentReal) {
|
|
86
|
+
return { path: cand, alias: cand, warning: null };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
path: currentPath,
|
|
91
|
+
alias: null,
|
|
92
|
+
warning: `nessun alias stabile per ${currentPath}: il servizio punta al path attuale; se l'installazione cambia (upgrade/uninstall di node) il path scade — rilancia l'installazione per rigenerare il servizio`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Composizione per il servizio di boot: risolve ENTRAMBI i path (node ed
|
|
97
|
+
// entry) e raccoglie le dichiarazioni. E' il punto che il flusso di
|
|
98
|
+
// installazione chiama alla SCRITTURA del servizio (vincolo 1: mai a ogni
|
|
99
|
+
// avvio). `env` e `realpath` iniettabili per test.
|
|
100
|
+
function resolveBootPaths({ nodeBin, entryPath, env = process.env, realpath } = {}) {
|
|
101
|
+
const opts = {};
|
|
102
|
+
if (typeof realpath === 'function') opts.realpath = realpath;
|
|
103
|
+
const node = resolveStableAlias(nodeBin, nodeAliasCandidates(env), opts);
|
|
104
|
+
const entry = resolveStableAlias(entryPath, entryAliasCandidates(entryPath, env), opts);
|
|
105
|
+
return {
|
|
106
|
+
nodeBin: node.path,
|
|
107
|
+
entryPath: entry.path,
|
|
108
|
+
warnings: [node.warning, entry.warning].filter((w) => w !== null),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
nodeAliasCandidates,
|
|
114
|
+
entryAliasCandidates,
|
|
115
|
+
resolveStableAlias,
|
|
116
|
+
resolveBootPaths,
|
|
117
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Un SOLO formatter al confine UI/log per gli esiti del resolver della
|
|
3
|
+
// pubblica (job 5, strutturale). Prima le frasi si componevano in basso —
|
|
4
|
+
// nel supervisor e nella route — dove non si sa abbastanza per scriverle:
|
|
5
|
+
// meta' della 0.9.5 e' nata cosi' (promesse false, cause enumerate «a caso»,
|
|
6
|
+
// silenzi). Ora il resolver produce DATI enumerati (resolvePublicKey in
|
|
7
|
+
// tunnel.js) e QUESTO punto unico li trasforma in frasi. Un test per ogni
|
|
8
|
+
// causa.
|
|
9
|
+
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const {
|
|
12
|
+
PUBKEY_DERIVED, PUBKEY_NO_IDENTITY, PUBKEY_ACTUAL_KEY_UNKNOWN,
|
|
13
|
+
PUBKEY_TOOL_UNAVAILABLE, PUBKEY_ENCRYPTED_OR_UNREADABLE,
|
|
14
|
+
} = require('./tunnel.js');
|
|
15
|
+
|
|
16
|
+
// La frase della CAUSA per ogni esito — il «perche'» che chi guarda deve
|
|
17
|
+
// leggere. `resolution` e' il dato del risolutore; `identityFile` il path
|
|
18
|
+
// dichiarato, per nominare l'oggetto giusto. Non inventare mai una causa che
|
|
19
|
+
// il dato non porta: l'ignosciuto si dice sconosciuto, non si indovina.
|
|
20
|
+
function pubkeyCauseText(resolution, { identityFile } = {}) {
|
|
21
|
+
const nome = identityFile ? path.basename(identityFile) : null;
|
|
22
|
+
switch (resolution && resolution.outcome) {
|
|
23
|
+
case PUBKEY_DERIVED:
|
|
24
|
+
// Nessuna causa da dichiarare: la riga c'e'.
|
|
25
|
+
return null;
|
|
26
|
+
case PUBKEY_NO_IDENTITY:
|
|
27
|
+
return `la chiave dichiarata${nome ? ` (${nome})` : ''} non esiste dove dovrebbe`;
|
|
28
|
+
case PUBKEY_ACTUAL_KEY_UNKNOWN:
|
|
29
|
+
return 'nessun -i: ssh usa le chiavi di default, un agent o la config, e da qui non si sa quale';
|
|
30
|
+
case PUBKEY_TOOL_UNAVAILABLE:
|
|
31
|
+
return 'ssh-keygen non e\' disponibile su questa macchina: la pubblica non si puo\' derivare';
|
|
32
|
+
case PUBKEY_ENCRYPTED_OR_UNREADABLE:
|
|
33
|
+
return `non riesco a derivare la pubblica da ${nome || 'quella chiave'}: e' cifrata o illeggibile`;
|
|
34
|
+
default:
|
|
35
|
+
// Al confine, l'ignosciuto si NOMINA: un silenzio qui sarebbe lo stesso
|
|
36
|
+
// difetto che questo modulo chiude.
|
|
37
|
+
return `esito non riconosciuto: ${resolution && resolution.outcome}`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// L'AZIONE suggerita per ogni esito — il «cosa fare» accanto al perche'.
|
|
42
|
+
// Aspettare non serve dove bisogna concedere; riprovare non serve dove la
|
|
43
|
+
// condizione dipende da una decisione altrove.
|
|
44
|
+
function pubkeyActionText(resolution) {
|
|
45
|
+
switch (resolution && resolution.outcome) {
|
|
46
|
+
case PUBKEY_DERIVED:
|
|
47
|
+
return 'sostituisci la riga in ~/.ssh/authorized_keys del nodo con questa';
|
|
48
|
+
case PUBKEY_NO_IDENTITY:
|
|
49
|
+
return 'verifica il path della chiave dichiarata per questo nodo';
|
|
50
|
+
case PUBKEY_ACTUAL_KEY_UNKNOWN:
|
|
51
|
+
return 'individua la chiave che ssh usa davvero e modifica A MANO la sua riga';
|
|
52
|
+
case PUBKEY_TOOL_UNAVAILABLE:
|
|
53
|
+
return 'installa ssh-keygen (openssh-client), poi riprova';
|
|
54
|
+
case PUBKEY_ENCRYPTED_OR_UNREADABLE:
|
|
55
|
+
return 'usa una chiave dedicata non cifrata per questo nodo, o modifica A MANO la riga';
|
|
56
|
+
default:
|
|
57
|
+
return 'verifica la configurazione del nodo';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// La nota del pairing quando la riga c'e': il testo che accompagnava la
|
|
62
|
+
// risposta, ora composto qui (l'unico punto) e non nella route.
|
|
63
|
+
function pubkeyPairingNote(resolution, { panelPort } = {}) {
|
|
64
|
+
if (!resolution || resolution.outcome !== PUBKEY_DERIVED) return null;
|
|
65
|
+
return 'il peer ha un pannello sulla propria porta ' + panelPort
|
|
66
|
+
+ ': SOSTITUISCI la riga già installata in ~/.ssh/authorized_keys del NODO con questa (due destinazioni), altrimenti il canale del pannello sarà rifiutato. È la riga della chiave DICHIARATA per questo nodo (-i), non un\'eventuale chiave "jump" dello stesso nodo. Se ssh sceglie un\'altra identità (agent o config), la riga giusta è quella della chiave che usa davvero.';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { pubkeyCauseText, pubkeyActionText, pubkeyPairingNote };
|
|
@@ -178,28 +178,24 @@ function probeForwardChannels({ ports, graceMs = CHANNEL_GRACE_MS, connect = net
|
|
|
178
178
|
// A MANO della riga esistente, mostrando solo le destinazioni da aggiungere.
|
|
179
179
|
function refusalDetails({ remoteDestinations, identityFile } = {}) {
|
|
180
180
|
const dests = (remoteDestinations || []).join('",permitopen="');
|
|
181
|
-
|
|
181
|
+
// Esito enumerato (resolver) + frase composta al confine UI/log — il punto
|
|
182
|
+
// unico pubkey-format. Prima qui si sceglieva fra cause che non si potevano
|
|
183
|
+
// distinguere e le si enumerava «a caso»; ora il dato distingue, e il
|
|
184
|
+
// testo dice la causa ESATTA: mai inventarne una che il dato non porta.
|
|
185
|
+
const resolution = require('./tunnel.js').resolvePublicKey(identityFile);
|
|
186
|
+
const { pubkeyCauseText, pubkeyActionText } = require('./pubkey-format.js');
|
|
182
187
|
const premessa = 'canale rifiutato dal NODO remoto: la chiave in ~/.ssh/authorized_keys non autorizza queste destinazioni.';
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// dichiarata affatto. Con `-i`, la derivazione dalla privata puo' fallire
|
|
187
|
-
// per piu' motivi (privata assente o illeggibile, protetta da passphrase,
|
|
188
|
-
// `ssh-keygen` non disponibile) e da qui non si distinguono: si elencano,
|
|
189
|
-
// invece di sceglierne uno a caso. Dire «la parte pubblica non e'
|
|
190
|
-
// leggibile» a chi ha una chiave cifrata col suo `.pub` accanto e' falso,
|
|
191
|
-
// e manda a cercare il problema dove non e'.
|
|
192
|
-
const causa = identityFile
|
|
193
|
-
? `non riesco a derivare la chiave pubblica da ${path.basename(identityFile)}`
|
|
194
|
-
+ ' (privata assente o illeggibile, protetta da passphrase, oppure ssh-keygen non disponibile)'
|
|
195
|
-
: 'nessun -i: ssh usa le chiavi di default, un agent o la config, e da qui non si sa quale';
|
|
188
|
+
if (resolution.outcome !== require('./tunnel.js').PUBKEY_DERIVED) {
|
|
189
|
+
const causa = pubkeyCauseText(resolution, { identityFile });
|
|
190
|
+
const azione = pubkeyActionText(resolution);
|
|
196
191
|
return {
|
|
197
|
-
hint: `${premessa} NON posso identificare la chiave da correggere — ${causa}`
|
|
198
|
-
+
|
|
192
|
+
hint: `${premessa} NON posso identificare la chiave da correggere — ${causa}. ${azione}`
|
|
193
|
+
+ `: aggiungi queste destinazioni: permitopen="${dests}"`,
|
|
199
194
|
authorizedKeys: '',
|
|
195
|
+
outcome: resolution.outcome,
|
|
200
196
|
};
|
|
201
197
|
}
|
|
202
|
-
const riga = `restrict,port-forwarding,permitopen="${dests}",command="/bin/false" ${
|
|
198
|
+
const riga = `restrict,port-forwarding,permitopen="${dests}",command="/bin/false" ${resolution.line}`;
|
|
203
199
|
// NON diciamo «quella con cui il supervisor si autentica»: `-i` DICHIARA
|
|
204
200
|
// un'identita', non la impone — senza `IdentitiesOnly=yes` OpenSSH puo'
|
|
205
201
|
// comunque usare l'agent o la config, e il canale essere autenticato da
|
|
@@ -209,6 +205,7 @@ function refusalDetails({ remoteDestinations, identityFile } = {}) {
|
|
|
209
205
|
return {
|
|
210
206
|
hint: `${premessa} Riga da usare (SOSTITUISCI quella esistente, non aggiungerne una seconda)${quale}: ${riga}`,
|
|
211
207
|
authorizedKeys: riga,
|
|
208
|
+
outcome: resolution.outcome,
|
|
212
209
|
};
|
|
213
210
|
}
|
|
214
211
|
|
package/lib/nodes/tunnel.js
CHANGED
|
@@ -732,8 +732,37 @@ function authorizedKeysLine({ remotePort, panelRemotePort, pub } = {}) {
|
|
|
732
732
|
// Torna null se la privata non e' leggibile, e' protetta da passphrase (in
|
|
733
733
|
// batch non si puo' sbloccare) o `ssh-keygen` non c'e': senza poterla derivare
|
|
734
734
|
// non si compone nessuna riga.
|
|
735
|
-
|
|
736
|
-
|
|
735
|
+
// Esiti ENUMERATI della derivazione della pubblica (difetto strutturale
|
|
736
|
+
// registrato dall'auditor: `null | stringa` comprimeva stati DIVERSI, e il
|
|
737
|
+
// livello basso componeva frasi su fatti che non conosce — meta' della 0.9.5
|
|
738
|
+
// e' nata li'). Gli esiti sono DATI, senza testo per l'utente: le frasi si
|
|
739
|
+
// compongono al confine UI/log, mai qui. Si aggancia a P4: una chiave cifrata
|
|
740
|
+
// dava `null`, indistinguibile da «file illeggibile», e il pairing ometteva
|
|
741
|
+
// la riga in silenzio.
|
|
742
|
+
//
|
|
743
|
+
// derived — pubblica derivata; `line` e' la riga
|
|
744
|
+
// no-identity — identityFile dichiarata ma il file NON esiste
|
|
745
|
+
// actual-key-unknown — identityFile non dichiarata: la chiave che ssh
|
|
746
|
+
// usera' DAVVERO (default, agent, config) non e'
|
|
747
|
+
// sapibile da qui; nessun altro canale ripara
|
|
748
|
+
// tool-unavailable — ssh-keygen assente sulla macchina
|
|
749
|
+
// encrypted-or-unreadable — ssh-keygen non riesce sul file (cifrata,
|
|
750
|
+
// permessi, formato) o l'output non e' una riga
|
|
751
|
+
// pubblica valida
|
|
752
|
+
const PUBKEY_DERIVED = 'derived';
|
|
753
|
+
const PUBKEY_NO_IDENTITY = 'no-identity';
|
|
754
|
+
const PUBKEY_ACTUAL_KEY_UNKNOWN = 'actual-key-unknown';
|
|
755
|
+
const PUBKEY_TOOL_UNAVAILABLE = 'tool-unavailable';
|
|
756
|
+
const PUBKEY_ENCRYPTED_OR_UNREADABLE = 'encrypted-or-unreadable';
|
|
757
|
+
|
|
758
|
+
function resolvePublicKey(identityFile, impl = {}) {
|
|
759
|
+
if (!identityFile || typeof identityFile !== 'string' || identityFile.trim() === '') {
|
|
760
|
+
return { outcome: PUBKEY_ACTUAL_KEY_UNKNOWN };
|
|
761
|
+
}
|
|
762
|
+
const existsImpl = impl.existsSync || require('node:fs').existsSync;
|
|
763
|
+
if (!existsImpl(identityFile)) {
|
|
764
|
+
return { outcome: PUBKEY_NO_IDENTITY, path: identityFile };
|
|
765
|
+
}
|
|
737
766
|
const exec = impl.execImpl || require('node:child_process').execFileSync;
|
|
738
767
|
// Timeout iniettabile (test): il default 5000 ms e' la RETE, non il
|
|
739
768
|
// meccanismo — il meccanismo e' l'ambiente controllato qui sotto.
|
|
@@ -757,7 +786,7 @@ function readPublicKey(identityFile, impl = {}) {
|
|
|
757
786
|
// chiave cifrata (misurato: 0.11 s contro gli 8 s dell'attesa su tty) e
|
|
758
787
|
// non puo' lasciare helper. Un path inesistente vince su /bin/false: su
|
|
759
788
|
// Termux i binari stanno sotto $PREFIX, e un path assoluto sbagliato e'
|
|
760
|
-
// una scommessa.
|
|
789
|
+
// una scommessa; e non `setsid`, che su macOS non esiste.
|
|
761
790
|
// LIMITE DA SAPERE: SSH_ASKPASS_REQUIRE esiste da OpenSSH 8.4. Le versioni
|
|
762
791
|
// piu' vecchie lo ignorano e tornano al tty: li' il timeout qui sopra
|
|
763
792
|
// resta come rete — il degrado e' «lento», non «appeso».
|
|
@@ -770,14 +799,32 @@ function readPublicKey(identityFile, impl = {}) {
|
|
|
770
799
|
SSH_ASKPASS: path.join(process.env.HOME || os.homedir(), '.nexuscrew', 'askpass-inesistente'),
|
|
771
800
|
},
|
|
772
801
|
});
|
|
773
|
-
} catch (
|
|
802
|
+
} catch (e) {
|
|
803
|
+
// ENOENT dello SPAWN = il binario ssh-keygen non esiste: un'altra causa
|
|
804
|
+
// rispetto al file che non si deriva, e chi compone il messaggio deve
|
|
805
|
+
// poterle distinguere (prima erano lo stesso null).
|
|
806
|
+
if (e && e.code === 'ENOENT') return { outcome: PUBKEY_TOOL_UNAVAILABLE };
|
|
807
|
+
return { outcome: PUBKEY_ENCRYPTED_OR_UNREADABLE };
|
|
808
|
+
}
|
|
774
809
|
const righe = String(out || '').split('\n').filter((r) => r.trim() !== '');
|
|
775
|
-
if (righe.length !== 1) return
|
|
810
|
+
if (righe.length !== 1) return { outcome: PUBKEY_ENCRYPTED_OR_UNREADABLE };
|
|
776
811
|
const riga = righe[0].trim();
|
|
777
|
-
if (/[\u0000-\u0008\u000b-\u001f\u007f]/.test(riga))
|
|
812
|
+
if (riga.length === 0 || /[\u0000-\u0008\u000b-\u001f\u007f]/.test(riga)) {
|
|
813
|
+
return { outcome: PUBKEY_ENCRYPTED_OR_UNREADABLE };
|
|
814
|
+
}
|
|
778
815
|
const campi = riga.split(/[ \t]+/);
|
|
779
|
-
if (campi.length < 2 || !/^[A-Za-z0-9+/]+={0,2}$/.test(campi[1]))
|
|
780
|
-
|
|
816
|
+
if (campi.length < 2 || !/^[A-Za-z0-9+/]+={0,2}$/.test(campi[1])) {
|
|
817
|
+
return { outcome: PUBKEY_ENCRYPTED_OR_UNREADABLE };
|
|
818
|
+
}
|
|
819
|
+
return { outcome: PUBKEY_DERIVED, line: riga };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Contratto storico (supervisor e chiamanti esistenti): null | stringa.
|
|
823
|
+
// Sopra resolvePublicKey per gli esiti enumerati; questo resta per chi
|
|
824
|
+
// compone ancora sul contratto vecchio — il confine UI/log migrera'.
|
|
825
|
+
function readPublicKey(identityFile, impl = {}) {
|
|
826
|
+
const r = resolvePublicKey(identityFile, impl);
|
|
827
|
+
return r.outcome === PUBKEY_DERIVED ? r.line : null;
|
|
781
828
|
}
|
|
782
829
|
|
|
783
830
|
// La riga per UN nodo, dal nodo. Sta qui e non nella route perche' la stessa
|
|
@@ -796,14 +843,21 @@ function readPublicKey(identityFile, impl = {}) {
|
|
|
796
843
|
// provisionare un'identita' per peer — lavoro suo, non un ramo di questa
|
|
797
844
|
// funzione.
|
|
798
845
|
function authorizedKeysForNode(nodo, panelRemotePort) {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
846
|
+
// Esito enumerato, non piu' null|stringa: chi risponde (pairing) porta il
|
|
847
|
+
// DATO fino al confine, e li' — non qui — si compone cio' che si dice.
|
|
848
|
+
const r = resolvePublicKey(nodo && nodo.identityFile);
|
|
849
|
+
if (r.outcome !== PUBKEY_DERIVED) return { outcome: r.outcome, line: null };
|
|
850
|
+
return {
|
|
851
|
+
outcome: r.outcome,
|
|
852
|
+
line: authorizedKeysLine({ remotePort: nodo.remotePort, panelRemotePort, pub: r.line }),
|
|
853
|
+
};
|
|
802
854
|
}
|
|
803
855
|
|
|
804
856
|
module.exports = {
|
|
805
857
|
SSH_BASE_OPTS,
|
|
806
|
-
authorizedKeysLine, readPublicKey, authorizedKeysForNode,
|
|
858
|
+
authorizedKeysLine, readPublicKey, resolvePublicKey, authorizedKeysForNode,
|
|
859
|
+
PUBKEY_DERIVED, PUBKEY_NO_IDENTITY, PUBKEY_ACTUAL_KEY_UNKNOWN,
|
|
860
|
+
PUBKEY_TOOL_UNAVAILABLE, PUBKEY_ENCRYPTED_OR_UNREADABLE,
|
|
807
861
|
buildForwardArgs, buildReverseArgs, backoffDelay,
|
|
808
862
|
tunnelDir, tunnelPidPath, tunnelLogPath, tunnelStatePath, readTunnelState,
|
|
809
863
|
prepareTunnelDir, openTunnelLog,
|
|
@@ -423,11 +423,17 @@ function createPairHandler(deps) {
|
|
|
423
423
|
}
|
|
424
424
|
send(res, 200, {
|
|
425
425
|
paired: true, name: b.name, instanceId: joined.instanceId, transport: 'auto', health: { status: health.status },
|
|
426
|
-
...(authorizedKeysPayload ? {
|
|
427
|
-
authorizedKeys: authorizedKeysPayload,
|
|
428
|
-
|
|
429
|
-
|
|
426
|
+
...(authorizedKeysPayload && authorizedKeysPayload.outcome === 'derived' ? {
|
|
427
|
+
authorizedKeys: authorizedKeysPayload.line,
|
|
428
|
+
// La nota si compone al confine UI/log (pubkey-format), non qui.
|
|
429
|
+
authorizedKeysNote: require('../nodes/pubkey-format.js')
|
|
430
|
+
.pubkeyPairingNote(authorizedKeysPayload, { panelPort: peerPanelPort }),
|
|
430
431
|
} : {}),
|
|
432
|
+
// Esito enumerato come DATO (resolver a cinque esiti): prima una
|
|
433
|
+
// chiave cifrata era indistinguibile da «file illeggibile» e la riga
|
|
434
|
+
// spariva in silenzio. Il confine UI/log compone le frasi su questo;
|
|
435
|
+
// qui si porta il fatto, non il testo.
|
|
436
|
+
...(authorizedKeysPayload ? { authorizedKeysOutcome: authorizedKeysPayload.outcome } : {}),
|
|
431
437
|
});
|
|
432
438
|
} catch (e) {
|
|
433
439
|
await rollback();
|
package/lib/settings/routes.js
CHANGED
|
@@ -57,6 +57,7 @@ const {
|
|
|
57
57
|
} = require('../proxy/federation.js');
|
|
58
58
|
const { rotateToken } = require('../auth/token.js');
|
|
59
59
|
const { generateService, installService, installPath: svcInstallPath } = require('../cli/service.js');
|
|
60
|
+
const { resolveBootPaths } = require('../cli/stable-alias.js');
|
|
60
61
|
const { detectPlatform, nodeBin, repoRoot, uid } = require('../cli/platform.js');
|
|
61
62
|
const { isServiceRunning, readRoles, bootState } = require('../cli/commands.js');
|
|
62
63
|
const { configJsonPath } = require('../config.js');
|
|
@@ -993,6 +994,15 @@ function settingsRoutes(deps = {}) {
|
|
|
993
994
|
uid: seams.uid || uid(),
|
|
994
995
|
installPath: seams.serviceInstallPath,
|
|
995
996
|
};
|
|
997
|
+
// 1-bis (R23): come in init — il path del boot si risolve alla
|
|
998
|
+
// scrittura e cio' che non ha alias stabile torna DICHIARATO. Le
|
|
999
|
+
// warnings sono DATI nella risposta, non testo composto qui.
|
|
1000
|
+
const svcBoot = resolveBootPaths({
|
|
1001
|
+
nodeBin: ctx.nodeBin,
|
|
1002
|
+
entryPath: path.join(ctx.repoRoot, 'bin', 'nexuscrew.js'),
|
|
1003
|
+
});
|
|
1004
|
+
ctx.nodeBin = svcBoot.nodeBin;
|
|
1005
|
+
ctx.entryPath = svcBoot.entryPath;
|
|
996
1006
|
const content = generateService(platform, ctx);
|
|
997
1007
|
const out = installService(platform, content, ctx, { activate: false });
|
|
998
1008
|
send(res, 200, {
|
|
@@ -1000,6 +1010,7 @@ function settingsRoutes(deps = {}) {
|
|
|
1000
1010
|
target: out.target,
|
|
1001
1011
|
note: 'unit rigenerata; nessun restart automatico — riavvia il service per applicarla',
|
|
1002
1012
|
skippedActivation: out.skippedActivation,
|
|
1013
|
+
warnings: svcBoot.warnings,
|
|
1003
1014
|
});
|
|
1004
1015
|
} catch (e) { send(res, 500, { error: String(e.message || e) }); }
|
|
1005
1016
|
});
|