@mmmbuto/nexuscrew 0.9.3 → 0.9.5
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 +100 -0
- package/frontend/dist/assets/index-BUbgwJsU.js +93 -0
- package/frontend/dist/assets/index-BzmGsXBy.css +32 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +11 -3
- package/lib/cli/pidfile.js +412 -13
- package/lib/cli/runtime-lifecycle.js +42 -1
- package/lib/nodes/commands.js +8 -2
- package/lib/nodes/health.js +1 -0
- package/lib/nodes/peer-transitions.js +87 -0
- package/lib/nodes/tunnel-supervisor.js +274 -30
- package/lib/nodes/tunnel.js +112 -0
- package/lib/proxy/panel-auth.js +45 -5
- package/lib/server.js +10 -0
- package/lib/settings/pairing-coordinator.js +28 -1
- package/lib/update/runner.js +40 -32
- package/package.json +1 -1
- package/frontend/dist/assets/index-0vuhL1YP.css +0 -32
- package/frontend/dist/assets/index-uRh_hSop.js +0 -93
package/lib/cli/pidfile.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
// Pidfile con verified kill: metadata {pid, cmd, startTs}; kill verifica cmd+pid
|
|
3
|
-
// prima di signalare (no
|
|
3
|
+
// prima di signalare (no broad match by name). Il controllo pid+nascita riduce
|
|
4
|
+
// il PID reuse a una finestra residua fra verifica e segnale — non lo elimina;
|
|
5
|
+
// dettaglio e motivo nel commento sopra il kill in killPidfile. [R1]
|
|
4
6
|
// Primario su Termux (serve --pidfile); opzionale --manual su linux/mac.
|
|
5
7
|
const fs = require('node:fs');
|
|
6
8
|
const os = require('node:os');
|
|
@@ -26,33 +28,265 @@ function currentUid() {
|
|
|
26
28
|
// `/proc/<pid>/stat` field 22 is the kernel start tick. Unlike a PID or an
|
|
27
29
|
// argv it cannot be recreated by a later process. macOS has no /proc, so a
|
|
28
30
|
// conservative `ps lstart` fallback still combines with UID, argv and runId.
|
|
29
|
-
|
|
31
|
+
//
|
|
32
|
+
// probeProcessStart espone anche la CAUSA del fallimento (non solo il suo
|
|
33
|
+
// esito): serve a writePidfile per distinguere, alla creazione, «questa
|
|
34
|
+
// piattaforma non sa attestare le nascite» (fatto stabile) da «il tentativo
|
|
35
|
+
// e' fallito su QUESTO pid per un motivo che non sappiamo escludere
|
|
36
|
+
// transitorio» (non un fatto sulla piattaforma). Guardando solo se il
|
|
37
|
+
// risultato e' null le due cose sono indistinguibili — ed e' esattamente
|
|
38
|
+
// l'errore di categoria che questa funzione evita: unsupported si dichiara
|
|
39
|
+
// SOLO quando ENTRAMBI i meccanismi (proc e ps) sono strutturalmente
|
|
40
|
+
// impossibili qui — /proc assente o negato con EACCES/EPERM (una policy di
|
|
41
|
+
// visibilita' di sistema, non un fatto su un pid), E il binario `ps` stesso
|
|
42
|
+
// non si trova (ENOENT sullo spawn, non un suo rifiuto degli argomenti, che
|
|
43
|
+
// potrebbe voler dire tante cose diverse e non le distinguiamo in modo
|
|
44
|
+
// affidabile). Ogni altro fallimento resta indeterminate: onesto, non
|
|
45
|
+
// generoso.
|
|
46
|
+
function probeProcessStart(pid) {
|
|
47
|
+
let procStructurallyBlocked = false;
|
|
30
48
|
try {
|
|
31
49
|
const raw = fs.readFileSync(`/proc/${pid}/stat`, 'utf8').trim();
|
|
32
50
|
const match = raw.match(/^\d+\s+\([^)]*\)\s+(.+)$/);
|
|
33
51
|
const fields = match && match[1].trim().split(/\s+/);
|
|
34
52
|
const ticks = fields && fields[19]; // field 22, after state=field 3
|
|
35
|
-
if (/^\d+$/.test(String(ticks || ''))) return `linux:${ticks}
|
|
36
|
-
} catch (
|
|
53
|
+
if (/^\d+$/.test(String(ticks || ''))) return { value: `linux:${ticks}`, cause: null };
|
|
54
|
+
} catch (e) {
|
|
55
|
+
const denied = e && (e.code === 'EACCES' || e.code === 'EPERM');
|
|
56
|
+
const procMissing = e && e.code === 'ENOENT' && !fs.existsSync('/proc');
|
|
57
|
+
procStructurallyBlocked = Boolean(denied || procMissing);
|
|
58
|
+
}
|
|
59
|
+
let psMissing = false;
|
|
37
60
|
try {
|
|
38
61
|
const text = execFileSync('ps', ['-p', String(pid), '-o', 'lstart='], { encoding: 'utf8' }).trim();
|
|
39
|
-
|
|
40
|
-
} catch (
|
|
62
|
+
if (text) return { value: `ps:${text}`, cause: null };
|
|
63
|
+
} catch (e) {
|
|
64
|
+
psMissing = Boolean(e && e.code === 'ENOENT');
|
|
65
|
+
}
|
|
66
|
+
const unsupported = procStructurallyBlocked && psMissing;
|
|
67
|
+
return { value: null, cause: unsupported ? 'unsupported' : 'indeterminate' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readProcessStart(pid) {
|
|
71
|
+
return probeProcessStart(pid).value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Campi che SOLO writePidfile puo' scrivere: mai dal chiamante via `extra`.
|
|
75
|
+
// Misurato (Dev, 2026-08-17): con lo spread di extra per ultimo,
|
|
76
|
+
// writePidfile(f, pid, cmd, {processStart:'FINTO', attestation:'unsupported'})
|
|
77
|
+
// vinceva sul valore vero calcolato da probeProcessStart — un chiamante
|
|
78
|
+
// poteva scrivere un'attestazione INVENTATA. safeExtra la filtra qui, e i
|
|
79
|
+
// campi veri sono comunque scritti DOPO nello spread finale: due barriere,
|
|
80
|
+
// non una, perche' un bug nel filtro da solo basterebbe a riaprire il buco.
|
|
81
|
+
const RESERVED_META_FIELDS = new Set(['pid', 'cmd', 'startTs', 'uid', 'processStart', 'attestation']);
|
|
82
|
+
|
|
83
|
+
// Il marker di schema vive ACCANTO al pidfile (stessa directory, es.
|
|
84
|
+
// ~/.nexuscrew/), MAI dentro di esso: il pidfile e' il file sospetto, non e'
|
|
85
|
+
// lui a poter certificare se stesso. La sua presenza e' un evento ONE-WAY per
|
|
86
|
+
// installazione: "codice v2 ha scritto qui almeno una volta", non
|
|
87
|
+
// "l'attestazione e' riuscita" — writePidfile lo crea al primo successo,
|
|
88
|
+
// qualunque sia l'esito della sonda su quello specifico pid (se lo legassi
|
|
89
|
+
// al successo dell'attestazione, su una piattaforma davvero unsupported il
|
|
90
|
+
// marker non arriverebbe MAI e il bypass resterebbe aperto per sempre —
|
|
91
|
+
// proprio dove serve di piu' che si chiuda). Da quel momento in poi
|
|
92
|
+
// (killPidfile, sotto) un pidfile senza nessun campo di attestazione smette
|
|
93
|
+
// di essere spiegabile come "legittimo, pre-migrazione": diventa sospetto
|
|
94
|
+
// (downgrade, restore da backup, corruzione) e si rifiuta.
|
|
95
|
+
//
|
|
96
|
+
// TRE CONFINI DICHIARATI, non dedotti — chi indaga un incidente fra sei mesi
|
|
97
|
+
// deve trovarli qui, non scoprirli:
|
|
98
|
+
// 1. Un restore che riporta indietro l'INTERA directory (~/.nexuscrew/)
|
|
99
|
+
// riporta indietro anche il marker: la finestra di bypass si RIAPRE. Non
|
|
100
|
+
// e' un difetto di questa scelta — non esiste un posto migliore: accanto
|
|
101
|
+
// al binario e' read-only, e l'update lo sovrascrive comunque.
|
|
102
|
+
// 2. Con NEXUSCREW_PIDFILE puntato a una directory nuova il marker non c'e',
|
|
103
|
+
// quindi il bypass e' concesso li'. Nei test e' voluto (isolamento senza
|
|
104
|
+
// un parametro home in piu' da cablare); in produzione vuol dire che chi
|
|
105
|
+
// controlla quella variabile d'ambiente controlla anche la finestra — non
|
|
106
|
+
// un confine di sicurezza reale (chi setta l'ambiente del processo ha gia'
|
|
107
|
+
// vinto), ma va detto: il marker non e' una garanzia piu' forte di questa.
|
|
108
|
+
// 3. R-pidfile-5: claimSchemaMarker (sotto) crea il marker al KILL, non solo
|
|
109
|
+
// al write — quindi un pidfile che sta per essere RIMOSSO puo' lasciare
|
|
110
|
+
// dietro un marker in una directory che magari non ospitera' mai piu' un
|
|
111
|
+
// runtime. E' INNOCUO: il marker non ha altro effetto che rendere PIU'
|
|
112
|
+
// caute le decisioni FUTURE su quella stessa directory (fail-closed per
|
|
113
|
+
// meta senza attestazione, come se la migrazione fosse gia' avvenuta li'
|
|
114
|
+
// — che e' vero: e' avvenuta, anche se il pidfile che l'ha innescata non
|
|
115
|
+
// c'e' piu'). Il verso del residuo e' sempre quello sicuro: mai una
|
|
116
|
+
// concessione in piu', al massimo un rifiuto in un caso limite di riuso
|
|
117
|
+
// di un percorso abbandonato — coerente col confine 1 sopra.
|
|
118
|
+
function schemaMarkerPath(pidfilePath) {
|
|
119
|
+
return path.join(path.dirname(pidfilePath), '.pidfile-schema-v2');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// R-pidfile-4 (2026-08-17, audit su develop@437d29f): misurato dall'auditor
|
|
123
|
+
// sul frozen — se .pidfile-schema-v2 esiste come DIRECTORY (o symlink, o
|
|
124
|
+
// qualunque cosa non sia un file regolare), il vecchio hasSchemaMarker
|
|
125
|
+
// tornava `false` per costruzione (isFile() falso), e ensureSchemaMarker
|
|
126
|
+
// ingoiava OGNI errore della creazione (EEXIST incluso) senza distinguere.
|
|
127
|
+
// Risultato: il marker non nasceva MAI, ogni writePidfile successiva
|
|
128
|
+
// ripeteva lo stesso fallimento silenzioso, e "nessun marker" veniva letto
|
|
129
|
+
// come "pre-migrazione" — concedendo ambiguous-compat PER SEMPRE, non un
|
|
130
|
+
// giro in piu'. L'errore di fondo: la scrittura e' best-effort per buoni
|
|
131
|
+
// motivi (non deve mai far fallire writePidfile), ma un fallimento della
|
|
132
|
+
// scrittura si traduceva in "stato assente", cioe' nella risposta
|
|
133
|
+
// PERMISSIVA. Un meccanismo che non riesce a determinare il proprio stato
|
|
134
|
+
// non deve concedere: deve chiudere. L'incertezza va nella direzione
|
|
135
|
+
// sicura, sempre — lo stesso principio di isAlive/isAttributable in questo
|
|
136
|
+
// file, applicato qui al marker invece che al pid.
|
|
137
|
+
//
|
|
138
|
+
// La decisione al KILL non LEGGE se il marker esiste: PROVA A CREARLO adesso
|
|
139
|
+
// (claimSchemaMarker). Leggerlo soltanto lasciava fuori il caso in cui la
|
|
140
|
+
// scrittura era fallita senza lasciare traccia sul percorso — disco pieno,
|
|
141
|
+
// sola lettura, permesso transitorio: il percorso resta libero, la lettura
|
|
142
|
+
// dice 'assente', e si concedeva per sempre. Una prova fatta ORA ha una
|
|
143
|
+
// risposta verificabile; un evento passato che nessuno ha osservato no.
|
|
144
|
+
// 'created' — non c'era e la creazione riesce adesso: pre-migrazione
|
|
145
|
+
// vera -> ambiguous-compat concesso UNA volta, e da qui
|
|
146
|
+
// in poi il marker esiste per davvero.
|
|
147
|
+
// 'present' — EEXIST su file regolare (mai symlink: lstat, non stat)
|
|
148
|
+
// -> post-migrazione, fail-closed.
|
|
149
|
+
// 'undeterminable' — EEXIST su un tipo non atteso, oppure la creazione
|
|
150
|
+
// fallisce ADESSO per qualunque motivo (ostacolo,
|
|
151
|
+
// ENOSPC, EROFS, permessi, race) -> fail-closed, con il
|
|
152
|
+
// motivo che nomina la causa. Il fallimento della
|
|
153
|
+
// scrittura non e' un effetto collaterale da temere: e'
|
|
154
|
+
// il segnale che produce la risposta sicura.
|
|
155
|
+
// `checkSchemaMarker` resta come lettura pura per diagnostica e test, ma non
|
|
156
|
+
// guida piu' nessuna decisione di sicurezza.
|
|
157
|
+
//
|
|
158
|
+
// COSTO DICHIARATO: su un'installazione nuova con un ostacolo gia' presente
|
|
159
|
+
// su quel percorso, la PRIMA fermata dopo l'aggiornamento fallisce invece di
|
|
160
|
+
// essere concessa (nessun modo di distinguere "ostacolo innocente" da
|
|
161
|
+
// "ostacolo messo li' apposta" — e non e' compito di questa funzione
|
|
162
|
+
// indovinarlo). E' il verso giusto: un errore che dice cosa succede e' meglio
|
|
163
|
+
// di un bypass che non lo dice — ma va scritto qui, non scoperto da chi
|
|
164
|
+
// indaga un incidente.
|
|
165
|
+
function checkSchemaMarker(pidfilePath) {
|
|
166
|
+
const markerPath = schemaMarkerPath(pidfilePath);
|
|
167
|
+
let st;
|
|
168
|
+
try {
|
|
169
|
+
st = fs.lstatSync(markerPath);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
if (e && e.code === 'ENOENT') return { state: 'absent' };
|
|
172
|
+
return { state: 'undeterminable', reason: `marker path not readable (${(e && e.code) || 'unknown error'})` };
|
|
173
|
+
}
|
|
174
|
+
if (st.isSymbolicLink()) return { state: 'undeterminable', reason: 'marker path is a symlink, not a regular file' };
|
|
175
|
+
if (!st.isFile()) return { state: 'undeterminable', reason: 'marker path occupied by a non-regular-file' };
|
|
176
|
+
return { state: 'present' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Wrapper booleano SOLO per lettura passiva (test, diagnostica esterna) —
|
|
180
|
+
// non guida piu' la decisione di sicurezza: vedi claimSchemaMarker sotto,
|
|
181
|
+
// che e' cosa killPidfile usa davvero. SOLO 'present' e' true.
|
|
182
|
+
// 'undeterminable' NON e' 'absent' — tornerebbe a leggere l'incertezza come
|
|
183
|
+
// permissiva, l'errore del giro precedente.
|
|
184
|
+
function hasSchemaMarker(pidfilePath) {
|
|
185
|
+
return checkSchemaMarker(pidfilePath).state === 'present';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Best-effort per costruzione: EEXIST (un altro processo ha appena scritto
|
|
189
|
+
// lo stesso marker, avvii concorrenti allo start) e' il caso NORMALE di un
|
|
190
|
+
// evento one-way, non un errore. Qualunque ALTRO fallimento (un ostacolo al
|
|
191
|
+
// path, permessi, disco pieno) non deve MAI far fallire writePidfile — il
|
|
192
|
+
// pidfile e' gia' stato scritto quando arriviamo qui, ed e' lui
|
|
193
|
+
// l'informazione che conta.
|
|
194
|
+
//
|
|
195
|
+
// R-pidfile-5 (2026-08-17, audit su develop@fa8bd90): questa funzione NON
|
|
196
|
+
// garantisce piu' nulla sul futuro — prima il commento qui diceva "il
|
|
197
|
+
// prossimo killPidfile lo scoprira' da solo", ed era FALSO: se questa
|
|
198
|
+
// scrittura fallisce con ENOSPC/EROFS/quota/permesso transitorio, non resta
|
|
199
|
+
// NESSUNA traccia sul filesystem (il path resta ENOENT, esattamente come se
|
|
200
|
+
// non si fosse mai tentato) — una garanzia dichiarata che il codice non
|
|
201
|
+
// aveva. La chiusura vera non dipende da questa funzione: killPidfile (vedi
|
|
202
|
+
// claimSchemaMarker) non si fida di un evento passato non osservabile,
|
|
203
|
+
// riprova la creazione ADESSO, nell'istante in cui la decisione conta. Se
|
|
204
|
+
// funziona ancora qui (il caso comune: nessun ostacolo, disco con spazio) e'
|
|
205
|
+
// solo un'ottimizzazione — anticipa la migrazione al primo write invece di
|
|
206
|
+
// aspettare il primo kill su un meta ambiguo — non la fonte della garanzia.
|
|
207
|
+
function ensureSchemaMarker(pidfilePath) {
|
|
208
|
+
try {
|
|
209
|
+
fs.writeFileSync(schemaMarkerPath(pidfilePath), `${JSON.stringify({ since: Date.now() })}\n`,
|
|
210
|
+
{ flag: 'wx', mode: 0o600 });
|
|
211
|
+
} catch (e) {
|
|
212
|
+
if (e && e.code === 'EEXIST') return; // normale: gia' scritto da un altro avvio
|
|
213
|
+
// Qualunque altro fallimento: mai propagare. Non promettiamo che qualcun
|
|
214
|
+
// altro se ne accorgera' — vedi claimSchemaMarker per la vera garanzia.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// R-pidfile-5: LA decisione di sicurezza. checkSchemaMarker (sopra) guarda
|
|
219
|
+
// un EVENTO PASSATO — "il marker e' stato scritto" — che puo' fallire in
|
|
220
|
+
// silenzio senza lasciare traccia (vedi ensureSchemaMarker): il percorso
|
|
221
|
+
// resta ENOENT, indistinguibile da "mai tentato". claimSchemaMarker sposta
|
|
222
|
+
// la domanda da "e' successo in passato?" a "posso ADESSO?" — verificabile
|
|
223
|
+
// nell'istante in cui la decisione conta, non dedotta da una scrittura che
|
|
224
|
+
// potrebbe essere fallita senza lasciare traccia:
|
|
225
|
+
// 'created' — non c'era, la creazione ORA riesce: il percorso era
|
|
226
|
+
// davvero libero, non solo "sembrava" (perche' una
|
|
227
|
+
// scrittura precedente era fallita in silenzio).
|
|
228
|
+
// Pre-migrazione per davvero — e ORA il marker esiste
|
|
229
|
+
// per davvero: i prossimi kill lo vedranno 'present'.
|
|
230
|
+
// 'present' — EEXIST su un file regolare: gia' migrato (invariato).
|
|
231
|
+
// 'undeterminable' — EEXIST su un simlink/non-file, O la creazione
|
|
232
|
+
// fallisce ADESSO per qualunque altro motivo (ENOSPC,
|
|
233
|
+
// EROFS, EACCES/EPERM, EISDIR, una race): non sappiamo
|
|
234
|
+
// perche' in generale, ma sappiamo che ORA non si puo'
|
|
235
|
+
// scrivere qui — ed e' esattamente la domanda che
|
|
236
|
+
// conta, verificata nel momento giusto invece che
|
|
237
|
+
// dedotta da un momento passato che potrebbe mentire.
|
|
238
|
+
//
|
|
239
|
+
// killPidfile diventa, in questo solo ramo, una funzione che SCRIVE (crea un
|
|
240
|
+
// file nuovo), non solo rimuove: valutato deliberatamente, non per svista.
|
|
241
|
+
// Ogni chiamante che puo' leggere un pidfile in questa directory ha gia',
|
|
242
|
+
// nella prassi di questo prodotto, permesso di scrittura sulla STESSA
|
|
243
|
+
// directory (e' li' che pidfile.js scrive di suo — ~/.nexuscrew/ o
|
|
244
|
+
// l'override NEXUSCREW_PIDFILE): non attraversa un confine di permessi che
|
|
245
|
+
// non fosse gia' presupposto per arrivare a chiamare killPidfile affatto.
|
|
246
|
+
// Se la scrittura genuinamente non e' permessa o e' pericolosa li' (EROFS,
|
|
247
|
+
// policy), il ramo 'undeterminable' rifiuta — la scrittura fallita e'
|
|
248
|
+
// proprio il segnale che produce la risposta sicura, non un effetto
|
|
249
|
+
// collaterale da temere.
|
|
250
|
+
function claimSchemaMarker(pidfilePath) {
|
|
251
|
+
const markerPath = schemaMarkerPath(pidfilePath);
|
|
252
|
+
try {
|
|
253
|
+
fs.writeFileSync(markerPath, `${JSON.stringify({ since: Date.now() })}\n`, { flag: 'wx', mode: 0o600 });
|
|
254
|
+
return { state: 'created' };
|
|
255
|
+
} catch (e) {
|
|
256
|
+
if (e && e.code === 'EEXIST') {
|
|
257
|
+
let st;
|
|
258
|
+
try { st = fs.lstatSync(markerPath); }
|
|
259
|
+
catch (e2) { return { state: 'undeterminable', reason: `marker path not readable after EEXIST (${(e2 && e2.code) || 'unknown error'})` }; }
|
|
260
|
+
if (st.isSymbolicLink()) return { state: 'undeterminable', reason: 'marker path is a symlink, not a regular file' };
|
|
261
|
+
if (!st.isFile()) return { state: 'undeterminable', reason: 'marker path occupied by a non-regular-file' };
|
|
262
|
+
return { state: 'present' };
|
|
263
|
+
}
|
|
264
|
+
return { state: 'undeterminable', reason: `marker not creatable now (${(e && e.code) || 'unknown error'})` };
|
|
265
|
+
}
|
|
41
266
|
}
|
|
42
267
|
|
|
43
268
|
// Exclusive create (wx): fallisce se il pidfile esiste già (no overwrite silenzioso).
|
|
269
|
+
// La classificazione (attested/unsupported/indeterminate) si decide QUI, alla
|
|
270
|
+
// creazione, non al kill: e' un fatto su questo tentativo di attestazione,
|
|
271
|
+
// non qualcosa da ricalcolare piu' tardi con lo stesso rischio di confondere
|
|
272
|
+
// "non so attestare" con "non ci sono riuscito adesso" (vedi killPidfile).
|
|
44
273
|
function writePidfile(p, pid, cmd, extra = {}) {
|
|
45
274
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
46
|
-
const
|
|
47
|
-
const
|
|
275
|
+
const rawExtra = extra && typeof extra === 'object' && !Array.isArray(extra) ? extra : {};
|
|
276
|
+
const safeExtra = {};
|
|
277
|
+
for (const key of Object.keys(rawExtra)) {
|
|
278
|
+
if (!RESERVED_META_FIELDS.has(key)) safeExtra[key] = rawExtra[key];
|
|
279
|
+
}
|
|
280
|
+
const { value: processStart, cause } = probeProcessStart(pid);
|
|
48
281
|
const uid = currentUid();
|
|
49
282
|
const meta = JSON.stringify({
|
|
283
|
+
...safeExtra,
|
|
50
284
|
pid, cmd: cmd || '', startTs: Date.now(),
|
|
51
285
|
...(uid === null ? {} : { uid }),
|
|
52
|
-
...(processStart ? { processStart } : {}),
|
|
53
|
-
...safeExtra,
|
|
286
|
+
...(processStart ? { processStart } : { attestation: cause }),
|
|
54
287
|
});
|
|
55
288
|
fs.writeFileSync(p, meta + '\n', { flag: 'wx', mode: 0o600 });
|
|
289
|
+
ensureSchemaMarker(p);
|
|
56
290
|
}
|
|
57
291
|
|
|
58
292
|
// La rimozione NON e' un unlink nudo. Il pidfile e' la prova che un processo
|
|
@@ -112,12 +346,26 @@ function cmdMatches(savedCmd, liveCmd) {
|
|
|
112
346
|
return liveCmd.includes(savedCmd) || savedCmd.includes(liveCmd);
|
|
113
347
|
}
|
|
114
348
|
|
|
115
|
-
// true se il pid appartiene a questo UID
|
|
349
|
+
// true se il pid appartiene a questo UID, la NASCITA è ancora quella attestata
|
|
350
|
+
// dal pidfile e il cmd matcha (o non è verificabile).
|
|
116
351
|
// EPERM is deliberately false: NexusCrew must neither adopt nor signal a
|
|
117
352
|
// process owned by another Android/Linux user.
|
|
353
|
+
// La nascita (processStart, già scritta da writePidfile) decide PRIMA del cmd:
|
|
354
|
+
// cmdMatches è per inclusioni, quindi un comando più lungo del salvato matcha
|
|
355
|
+
// — due supervisor dello stesso tunnel, due serve, il restart di ieri. Se il
|
|
356
|
+
// numero è nato in un istante diverso da quello attestato, il proprietario è
|
|
357
|
+
// morto e il numero è di un altro: il pidfile è stale anche se il cmd «quadra».
|
|
358
|
+
// Non calcolabile (meta senza processStart: pidfile vecchio o /proc nascosto;
|
|
359
|
+
// lettura che ora non riesce: macOS senza ps) → nel dubbio vale il cmd, come
|
|
360
|
+
// sempre: mai dichiarare morto un vivo. Stessa identità e stessa asimmetria
|
|
361
|
+
// del lock delle definizioni (pid + nascita; il falso-morto non esiste).
|
|
118
362
|
function isAlive(meta, impl = {}) {
|
|
119
363
|
if (!meta || !Number.isFinite(meta.pid)) return false;
|
|
120
364
|
if (pidOwnership(meta.pid, impl.killImpl || process.kill) !== 'owned') return false;
|
|
365
|
+
if (meta.processStart) {
|
|
366
|
+
const liveStart = (impl.readProcessStartImpl || readProcessStart)(meta.pid);
|
|
367
|
+
if (liveStart && liveStart !== meta.processStart) return false; // riassegnato
|
|
368
|
+
}
|
|
121
369
|
if (meta.cmd) {
|
|
122
370
|
const live = (impl.readCmdlineImpl || readCmdline)(meta.pid);
|
|
123
371
|
if (live) return cmdMatches(meta.cmd, live);
|
|
@@ -171,13 +419,163 @@ function killPidfile(p, signal = 'SIGTERM', impl = {}) {
|
|
|
171
419
|
return { killed: false, reason: 'pid reuse (cmd mismatch)', liveCmd: live };
|
|
172
420
|
}
|
|
173
421
|
}
|
|
422
|
+
// PID reuse che il cmd NON vede: due processi con lo stesso comando (o un
|
|
423
|
+
// comando che lo contiene). La nascita li distingue — ma solo AL MOMENTO DI
|
|
424
|
+
// QUESTO CONTROLLO. Se la nascita letta ORA non corrisponde a quella
|
|
425
|
+
// attestata, il proprietario è già morto e il numero è di un altro: qui la
|
|
426
|
+
// garanzia regge, il pidfile è stale e si toglie senza segnalare.
|
|
427
|
+
//
|
|
428
|
+
// Quello che questa verifica NON copre: fra la lettura di liveStart qui e
|
|
429
|
+
// killImpl() poco sotto non c'è atomicità. Se il proprietario muore in
|
|
430
|
+
// quell'istante e il kernel riassegna il pid prima del kill, il segnale
|
|
431
|
+
// parte verso il sostituto — identità confermata un momento fa, non nel
|
|
432
|
+
// momento in cui conta. Questa finestra è intrinseca al modello pid+segnale
|
|
433
|
+
// di POSIX (kill(2) non lega il segnale all'identità appena controllata) e
|
|
434
|
+
// non si chiude qui: la verifica sopra riduce l'esposizione da "sempre" a
|
|
435
|
+
// "l'istante fra questo controllo e il kill", non la elimina. La cura vera
|
|
436
|
+
// è pidfd_open + pidfd_send_signal (il segnale è legato a un file
|
|
437
|
+
// descriptor, non a un numero riassegnabile); Node non la espone senza un
|
|
438
|
+
// addon nativo, e un addon per un prodotto che gira anche su Termux e
|
|
439
|
+
// macOS costa più di quanto renda. Una rilettura dopo il kill non chiude
|
|
440
|
+
// la finestra — è best-effort, non rilevazione certa: il riusato può
|
|
441
|
+
// essere già uscito, e la rilettura corre a sua volta la stessa corsa. Non
|
|
442
|
+
// aggiungerne una come se fosse una guardia.
|
|
443
|
+
//
|
|
444
|
+
// Prima di questo controllo (nascita, aggiunta dopo cmd) il segnale
|
|
445
|
+
// partiva sul solo cmd, senza verifica di identità, per QUALUNQUE pidfile
|
|
446
|
+
// senza processStart — vecchio o nuovo, piattaforma capace o no: era
|
|
447
|
+
// esattamente il buco piu' largo trovato dall'audit (2026-08-17), perche'
|
|
448
|
+
// "nessuna nascita nel meta" copriva sia "questa macchina non sa attestare"
|
|
449
|
+
// sia "l'attestazione e' fallita alla creazione per un motivo che non
|
|
450
|
+
// conosciamo" con lo STESSO comportamento permissivo. Il ramo sotto separa
|
|
451
|
+
// i due casi guardando cosa writePidfile ha DICHIARATO al momento in cui
|
|
452
|
+
// contava (probeProcessStart, sopra), non ricalcolando una sonda adesso —
|
|
453
|
+
// rifarla qui confonderebbe di nuovo "non so attestare" con "non ci sono
|
|
454
|
+
// riuscito in QUESTO istante", l'errore di categoria che l'audit ha
|
|
455
|
+
// contestato nel primo disegno di questo fix.
|
|
456
|
+
let unverifiedBirth = false;
|
|
457
|
+
let schemaMarker = null; // calcolato pigro: solo se serve davvero (vedi sotto)
|
|
458
|
+
if (meta.processStart) {
|
|
459
|
+
const liveStart = (impl.readProcessStartImpl || readProcessStart)(meta.pid);
|
|
460
|
+
if (liveStart && liveStart !== meta.processStart) {
|
|
461
|
+
removePidfile(p, { allowLive: true });
|
|
462
|
+
return { killed: false, reason: 'pid reuse (start mismatch)' };
|
|
463
|
+
}
|
|
464
|
+
// Nascita ATTESTATA ma non rileggibile ora (/proc nascosto, ps assente,
|
|
465
|
+
// permessi): l'identita' non e' verificabile, e questa e' un'operazione
|
|
466
|
+
// DISTRUTTIVA. Il cmd da solo non discrimina — matcha per inclusioni, e
|
|
467
|
+
// due processi con lo stesso comando si somigliano. Si rinuncia: un
|
|
468
|
+
// segnale mancato costa un pidfile stale, un segnale sbagliato uccide il
|
|
469
|
+
// processo di qualcun altro. Il pidfile NON si tocca: non sappiamo se sia
|
|
470
|
+
// stale.
|
|
471
|
+
if (!liveStart) {
|
|
472
|
+
return { killed: false, reason: 'start unverifiable (refusing to signal)' };
|
|
473
|
+
}
|
|
474
|
+
} else if (meta.attestation === 'unsupported') {
|
|
475
|
+
// DICHIARATO alla creazione: ne' /proc ne' `ps` esistevano affatto su
|
|
476
|
+
// questa macchina (probeProcessStart, causa strutturale — non un pid
|
|
477
|
+
// specifico che non rispondeva). Nessun pidfile su questa macchina potra'
|
|
478
|
+
// MAI avere una nascita: qui e' dove un fail-closed spegnerebbe il
|
|
479
|
+
// prodotto proprio dove serve (Termux, primario — vedi testa del file).
|
|
480
|
+
// Si ricade sul criterio storico pid+cmd, ma DICHIARATO nel risultato
|
|
481
|
+
// (unverifiedBirth sotto), non in silenzio.
|
|
482
|
+
//
|
|
483
|
+
// ONESTO NON VUOL DIRE SICURO: su una piattaforma davvero unsupported la
|
|
484
|
+
// proprieta' "mai segnalare un PID solo ereditato" NON E' GARANTITA — un
|
|
485
|
+
// pid morto qui e riassegnato a un processo nostro con cmd compatibile
|
|
486
|
+
// riceve comunque il segnale, perche' senza nascita non c'e' modo di
|
|
487
|
+
// vedere il riassegno. Dichiarare il degrado lo rende TRACCIABILE, non
|
|
488
|
+
// lo chiude. Se un giorno serve la garanzia anche qui, la via non e' un
|
|
489
|
+
// altro probe su PID/cmd (la stessa prova debole con un altro nome): serve
|
|
490
|
+
// una prova ALTERNATIVA del proprietario — per esempio un runId verificato
|
|
491
|
+
// contro un endpoint locale autenticato. Non e' implementata: e' un lavoro
|
|
492
|
+
// a se', con un suo giro di audit (registrato qui, non eseguito).
|
|
493
|
+
unverifiedBirth = 'unsupported';
|
|
494
|
+
} else if (meta.attestation === 'indeterminate') {
|
|
495
|
+
// Il codice NUOVO (questo, o una versione successiva a questo fix) ha
|
|
496
|
+
// PROVATO ad attestare alla creazione e non ci e' riuscito per un motivo
|
|
497
|
+
// che non sa classificare come proprieta' della piattaforma (vedi
|
|
498
|
+
// probeProcessStart sopra) — sa che avrebbe potuto, su questo pid,
|
|
499
|
+
// adesso, e non ce l'ha fatta. Trattarlo come "la piattaforma non sa
|
|
500
|
+
// attestare" sarebbe l'errore di categoria contestato dall'audit:
|
|
501
|
+
// un'assenza non e' una proprieta' stabile solo perche' e' comoda da
|
|
502
|
+
// leggere cosi'. Si rifiuta: il cmd da solo matcha per inclusioni, e non
|
|
503
|
+
// basta a chi non sa nemmeno perche' la nascita manca QUI. Il pidfile
|
|
504
|
+
// NON si tocca, per lo stesso motivo del ramo attestato-ma-illeggibile
|
|
505
|
+
// sopra: non sappiamo se sia stale.
|
|
506
|
+
return { killed: false, reason: 'unattested pidfile (attestation indeterminate: a fresh attempt failed): refusing to signal a pid identified only by cmd' };
|
|
507
|
+
} else {
|
|
508
|
+
// NE' processStart NE' attestation nel meta: non "il codice nuovo ha
|
|
509
|
+
// provato e fallito" (quello e' il ramo indeterminate sopra) — e' un
|
|
510
|
+
// pidfile scritto da un codice che l'attestazione non la conosceva
|
|
511
|
+
// affatto, O un file scritto fuori da writePidfile del tutto (la stessa
|
|
512
|
+
// forma di meta la puo' produrre anche un chiamante non nostro): "nessun
|
|
513
|
+
// campo" da solo non prova la provenienza.
|
|
514
|
+
//
|
|
515
|
+
// R-pidfile-5: la domanda che decide non e' piu' "il marker e' gia'
|
|
516
|
+
// stato scritto?" (un evento passato che puo' fallire in silenzio —
|
|
517
|
+
// vedi ensureSchemaMarker/claimSchemaMarker sopra) ma "posso scriverlo
|
|
518
|
+
// ADESSO?" — claimSchemaMarker prova a crearlo in questo esatto istante:
|
|
519
|
+
// 'created' — non c'era davvero: QUESTA installazione non ha
|
|
520
|
+
// ancora mai completato una scrittura v2. Pre-
|
|
521
|
+
// migrazione per davvero, non solo "sembrava" —
|
|
522
|
+
// compatibilita' ambigua concessa UNA volta, e ORA
|
|
523
|
+
// il marker esiste per davvero: il prossimo kill
|
|
524
|
+
// su questa directory lo vedra' 'present'.
|
|
525
|
+
// 'present' — codice v2 ha gia' scritto qui (o un kill
|
|
526
|
+
// precedente l'ha appena creato): non e' piu'
|
|
527
|
+
// spiegabile come pre-migrazione — sospetto
|
|
528
|
+
// (downgrade, restore parziale del solo pidfile,
|
|
529
|
+
// corruzione). Si rifiuta.
|
|
530
|
+
// 'undeterminable' — la creazione fallisce ADESSO (ostacolo strutturale
|
|
531
|
+
// — directory, symlink — o ENOSPC/EROFS/permesso
|
|
532
|
+
// transitorio/una race): non sappiamo perche' in
|
|
533
|
+
// generale, ma sappiamo che ORA non si puo'
|
|
534
|
+
// scrivere qui, verificato nell'istante in cui
|
|
535
|
+
// conta, non dedotto da uno passato. Si rifiuta.
|
|
536
|
+
//
|
|
537
|
+
// MISURATO (Dev, 2026-08-17): trattare OGNI meta senza attestazione come
|
|
538
|
+
// indeterminate rompe l'aggiornamento automatico per ogni nodo il cui
|
|
539
|
+
// runtime e' ancora precedente a quando processStart e' nato (8fe514f,
|
|
540
|
+
// v0.9.0) — npm install sovrascrive pidfile.js PRIMA che il runner lo
|
|
541
|
+
// richieda, il runner legge il pidfile VECCHIO del runtime in esecuzione,
|
|
542
|
+
// killPidfile rifiuta, stopPortableRuntime torna killed:false,
|
|
543
|
+
// restartRuntime lancia, l'update muore (lib/update/runner.js). Il nodo
|
|
544
|
+
// resta indietro finche' qualcuno non interviene a mano — su un telefono
|
|
545
|
+
// Termux spesso vuol dire mai. Il rischio del ramo pid+cmd (concesso solo
|
|
546
|
+
// su 'created') e' quello che il prodotto ha GIA' in produzione oggi, ne'
|
|
547
|
+
// piu' ne' meno: non lo peggioriamo. Il PID reuse che questo ramo non
|
|
548
|
+
// vede richiede una coincidenza (numero riassegnato E cmd compatibile);
|
|
549
|
+
// il blocco dell'aggiornamento sarebbe invece CERTO per ogni nodo in
|
|
550
|
+
// questo stato, ogni volta. Fra un rischio raro gia' presente e un
|
|
551
|
+
// guasto sicuro introdotto da noi, si sceglie di non introdurre il
|
|
552
|
+
// guasto — decisione di prodotto, non mia.
|
|
553
|
+
//
|
|
554
|
+
// Alternative valutate e scartate: far degradare il runner invece di
|
|
555
|
+
// morire (il difetto gia' visto sul campo che l'ingresso unico di
|
|
556
|
+
// riavvio doveva chiudere); un ramo "legacy" permanente basato sulla sola
|
|
557
|
+
// FORMA del meta (lasciava aperti per sempre restore da backup,
|
|
558
|
+
// corruzione e downgrade — l'auditor l'ha contestato); un marker
|
|
559
|
+
// controllato ma mai riprovato al kill (questo giro — R-pidfile-5:
|
|
560
|
+
// l'auditor ha dimostrato che una scrittura fallita in silenzio riapre
|
|
561
|
+
// lo stesso buco senza lasciare traccia).
|
|
562
|
+
schemaMarker = (impl.claimSchemaMarkerImpl || claimSchemaMarker)(p);
|
|
563
|
+
if (schemaMarker.state === 'created') {
|
|
564
|
+
unverifiedBirth = 'ambiguous-compat';
|
|
565
|
+
} else {
|
|
566
|
+
const causa = schemaMarker.state === 'present' ? 'schema migration' : `marker state undeterminable: ${schemaMarker.reason}`;
|
|
567
|
+
return { killed: false, reason: `unattested pidfile after ${causa}: refusing to signal a pid identified only by cmd` };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
174
570
|
try {
|
|
571
|
+
// Qui si chiude la verifica e si entra nella finestra residua dichiarata
|
|
572
|
+
// sopra: nessuna riverifica fra questo punto e il segnale, per scelta.
|
|
175
573
|
killImpl(meta.pid, signal);
|
|
176
574
|
// allowLive: il segnale e' partito verso il pid VERIFICATO del file (cmd
|
|
177
575
|
// matchato sopra): la rimozione e' giusta anche se il processo non e' ancora
|
|
178
576
|
// sparito da /proc quando unlink gira.
|
|
179
577
|
removePidfile(p, { allowLive: true });
|
|
180
|
-
return { killed: true, pid: meta.pid };
|
|
578
|
+
return { killed: true, pid: meta.pid, ...(unverifiedBirth ? { unverifiedBirth } : {}) };
|
|
181
579
|
} catch (e) {
|
|
182
580
|
return { killed: false, reason: e.message };
|
|
183
581
|
}
|
|
@@ -185,6 +583,7 @@ function killPidfile(p, signal = 'SIGTERM', impl = {}) {
|
|
|
185
583
|
|
|
186
584
|
module.exports = {
|
|
187
585
|
defaultPidfilePath, readPidfile, writePidfile, removePidfile,
|
|
188
|
-
currentUid, readProcessStart, pidOwnership, pidExists, readCmdline,
|
|
586
|
+
currentUid, readProcessStart, probeProcessStart, pidOwnership, pidExists, readCmdline,
|
|
189
587
|
isAlive, isAttributable, cleanStale, killPidfile,
|
|
588
|
+
schemaMarkerPath, hasSchemaMarker, checkSchemaMarker, claimSchemaMarker,
|
|
190
589
|
};
|
|
@@ -103,6 +103,31 @@ function stopManagedTunnels(opts = {}) {
|
|
|
103
103
|
return stopped;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// IL PASSO «I TUNNEL SI FERMANO PRIMA», una volta sola. Ogni percorso di
|
|
107
|
+
// riavvio — commands.restart (sincrona, mano), stop() e il ramo portatile
|
|
108
|
+
// dell'aggiornamento automatico — attraversa QUESTO ingresso: è il posto
|
|
109
|
+
// unico dove un domani si aggiunge un passo al riavvio, invece di scriverlo
|
|
110
|
+
// in una copia e dimenticarlo nell'altra (la divergenza trovata tre volte,
|
|
111
|
+
// una per guasto).
|
|
112
|
+
//
|
|
113
|
+
// Non lancia MAI: il passo è intrinsecamente best-effort (un tunnel che non
|
|
114
|
+
// si ferma non deve lasciare il riavvio a metà in modo diverso a seconda del
|
|
115
|
+
// chiamante) — ma non tace: l'esito torna a chi chiama, e la POLICY (abort
|
|
116
|
+
// fail-closed per il riavvio gestito, proseguire per l'aggiornamento il cui
|
|
117
|
+
// compito principale è aggiornare) resta a chi la possiede.
|
|
118
|
+
function fermaTunnelPrimaDiRiavviare(opts = {}) {
|
|
119
|
+
const log = opts.log || (() => {});
|
|
120
|
+
try {
|
|
121
|
+
const stopped = (opts.stopTunnelsImpl || stopManagedTunnels)(opts);
|
|
122
|
+
return { stopped, error: null };
|
|
123
|
+
} catch (e) {
|
|
124
|
+
const reason = String(e && e.message ? e.message : e);
|
|
125
|
+
// Tacerlo lo renderebbe irrintracciabile la prossima volta.
|
|
126
|
+
log(`stop tunnel non riuscito — ${reason}`);
|
|
127
|
+
return { stopped: [], error: reason };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
106
131
|
function refusePairedPortRelocation(opts, from, to) {
|
|
107
132
|
if (Number(from) === Number(to)) return;
|
|
108
133
|
const { configDir } = urlmod.resolvePaths(opts);
|
|
@@ -183,8 +208,19 @@ function waitForPidExit(pid, timeoutMs = 2000) {
|
|
|
183
208
|
|
|
184
209
|
function stopPortableRuntime(opts = {}) {
|
|
185
210
|
const home = opts.home || require('node:os').homedir();
|
|
211
|
+
const log = opts.log || console.log;
|
|
186
212
|
const pidPath = pidf.defaultPidfilePath(home);
|
|
187
213
|
const result = pidf.killPidfile(pidPath);
|
|
214
|
+
// killPidfile puo' fermarsi su un'identita' non completamente verificata
|
|
215
|
+
// (unverifiedBirth: 'unsupported' | 'ambiguous-compat') e tornare comunque
|
|
216
|
+
// killed:true — il valore di ritorno da solo non basta: runner.js guarda
|
|
217
|
+
// solo `.killed` quando e' true e ignora il resto, quindi senza questa riga
|
|
218
|
+
// nessun operatore vedrebbe MAI che uno stop e' passato per il ramo
|
|
219
|
+
// permissivo. Qui, non nel chiamante: e' l'unico posto attraversato sia dal
|
|
220
|
+
// percorso manuale (commands.js stop/restart) sia dall'update automatico.
|
|
221
|
+
if (result.killed && result.unverifiedBirth) {
|
|
222
|
+
log(`stop portatile: pid ${result.pid} fermato con identita' NON completamente verificata (${result.unverifiedBirth}) — confermata solo per cmd, non per nascita`);
|
|
223
|
+
}
|
|
188
224
|
if (result.killed && !waitForPidExit(result.pid, opts.stopWaitMs || 2000)) {
|
|
189
225
|
return { ...result, killed: false, reason: `pid ${result.pid} did not exit after SIGTERM` };
|
|
190
226
|
}
|
|
@@ -240,7 +276,11 @@ function stop(opts = {}) {
|
|
|
240
276
|
|
|
241
277
|
// Detached tunnel supervisors are outside both server owners. A real stop
|
|
242
278
|
// closes them once; the next start restores only autostart:true links.
|
|
243
|
-
|
|
279
|
+
// Attraverso lo STESSO ingresso del riavvio (fermaTunnelPrimaDiRiavviare):
|
|
280
|
+
// prima un fallimento qui sfuggiva come eccezione non gestita al chiamante
|
|
281
|
+
// sincrono; ora è esito del stop, come ogni altro errore qui sopra.
|
|
282
|
+
const tunnels = fermaTunnelPrimaDiRiavviare(opts);
|
|
283
|
+
if (tunnels.error) errors.push(`stop tunnel: ${tunnels.error}`);
|
|
244
284
|
if (platform === 'termux') {
|
|
245
285
|
try { execImpl('termux-wake-lock-release', [], { stdio: 'ignore' }); } catch (_) {}
|
|
246
286
|
}
|
|
@@ -286,6 +326,7 @@ function unlinkRegular(target) {
|
|
|
286
326
|
module.exports = {
|
|
287
327
|
start,
|
|
288
328
|
stopManagedTunnels,
|
|
329
|
+
fermaTunnelPrimaDiRiavviare,
|
|
289
330
|
refusePairedPortRelocation,
|
|
290
331
|
managedRuntimeState,
|
|
291
332
|
portableRuntimeState,
|
package/lib/nodes/commands.js
CHANGED
|
@@ -204,8 +204,14 @@ function nodesAdd(opts) {
|
|
|
204
204
|
|
|
205
205
|
log(`nodes add: nodo "${name}" aggiunto (ssh ${ssh}${sshPort ? `:${sshPort}` : ''}, nexus remoto ${remotePort} -> locale ${localPort})`);
|
|
206
206
|
log('Incolla nel ~/.ssh/authorized_keys del NODO (lato forward, chiave dedicata):');
|
|
207
|
-
// permitopen
|
|
208
|
-
|
|
207
|
+
// R19: permitopen con le destinazioni EXPLICITE dei -L. Qui si conosce solo
|
|
208
|
+
// la porta nexus: la porta pannello del peer la annuncia il JOIN, non
|
|
209
|
+
// esiste ancora — quando apparirà, la riga completa (due destinazioni) la
|
|
210
|
+
// emette il pairing. Mai un permesso generico: è ciò che impedisce a questa
|
|
211
|
+
// chiave di aprire canali arbitrari. command=/bin/false + restrict.
|
|
212
|
+
const panelRemotePort = opts.panelRemotePort !== undefined ? Number(opts.panelRemotePort) : undefined;
|
|
213
|
+
const riga = tunnel.authorizedKeysLine({ remotePort, panelRemotePort, pub });
|
|
214
|
+
if (riga) log(riga);
|
|
209
215
|
return { code: 0, name, sshPort, localPort, remotePort, transport: entry.transport };
|
|
210
216
|
}
|
|
211
217
|
|
package/lib/nodes/health.js
CHANGED
|
@@ -102,6 +102,7 @@ async function nodeHealth({ node, home, fetchImpl, now = Date.now(), force = fal
|
|
|
102
102
|
transport: 'down', auth: 'unknown', reachability: 'unknown', status: 'down',
|
|
103
103
|
detail: diagnostic.detail, code: diagnostic.code, stage: diagnostic.stage,
|
|
104
104
|
...(diagnostic.hint ? { hint: diagnostic.hint } : {}),
|
|
105
|
+
...(diagnostic.authorizedKeys ? { authorizedKeys: diagnostic.authorizedKeys } : {}),
|
|
105
106
|
transportEngine: ts.transport || 'ssh', managed: ts.managed !== false, at: now,
|
|
106
107
|
};
|
|
107
108
|
} else if (!node.token) {
|