@mmmbuto/nexuscrew 0.8.22 → 0.8.24
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/README.md +11 -3
- package/frontend/dist/assets/index-CU9OOtL-.js +91 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +40 -4
- package/lib/cli/doctor.js +65 -11
- package/lib/cli/fleet-service.js +3 -0
- package/lib/cli/init.js +30 -6
- package/lib/config.js +7 -0
- package/lib/fleet/builtin.js +13 -1
- package/lib/fleet/provider.js +1 -1
- package/lib/fleet/routes.js +1 -0
- package/lib/fleet/runtime.js +5 -2
- package/lib/nodes/store.js +40 -1
- package/lib/server.js +9 -3
- package/lib/settings/pairing-coordinator.js +22 -1
- package/lib/settings/public-peering-routes.js +11 -2
- package/lib/settings/routes.js +14 -5
- package/lib/tmux/lifecycle.js +4 -2
- package/lib/tmux/shared-server.js +60 -0
- package/package.json +1 -1
- package/frontend/dist/assets/index-ClJP2j6k.js +0 -91
package/lib/settings/routes.js
CHANGED
|
@@ -71,8 +71,8 @@ const EDIT_KEYS = new Set(['label', 'ssh', 'sshPort', 'autostart', 'visibility',
|
|
|
71
71
|
// NON usa ciecamente hostname: se l'hostname e' vuoto o 'localhost' (tipico di
|
|
72
72
|
// chiavi di test / host dietro tunnel) propone un fallback neutro. L'utente puo'
|
|
73
73
|
// sempre sovrascriverlo: questo e' solo il valore iniziale del campo.
|
|
74
|
-
function defaultDeviceName() {
|
|
75
|
-
const h = String(
|
|
74
|
+
function defaultDeviceName(hostname = os.hostname()) {
|
|
75
|
+
const h = String(hostname || '').trim();
|
|
76
76
|
if (!h || /^localhost$/i.test(h)) return 'NexusCrew';
|
|
77
77
|
return h.slice(0, nodesStore.LABEL_MAX);
|
|
78
78
|
}
|
|
@@ -143,6 +143,7 @@ function readConfigFile(p) {
|
|
|
143
143
|
function settingsRoutes(deps = {}) {
|
|
144
144
|
const cfg = deps.cfg || {};
|
|
145
145
|
const seams = cfg.settingsSeams || {};
|
|
146
|
+
const localHostname = typeof seams.hostname === 'function' ? seams.hostname : os.hostname;
|
|
146
147
|
// tokenStore/closeSessions iniettati da server.js per la semantica di invalidazione
|
|
147
148
|
// live (audit F7 / §4b(3)). Assenti nei test unitari puri su routes -> la rotazione
|
|
148
149
|
// scrive solo il file (come prima), senza reload in-memory.
|
|
@@ -217,6 +218,10 @@ function settingsRoutes(deps = {}) {
|
|
|
217
218
|
boot: bootState({ platform, execImpl: seams.execImpl, uid: seams.uid, home }).enabled,
|
|
218
219
|
};
|
|
219
220
|
const updateStatus = updater && typeof updater.status === 'function' ? updater.status() : null;
|
|
221
|
+
const localStore = nodesStore.loadStore(nodesPath);
|
|
222
|
+
const localNodeId = localStore && nodesStore.NODE_ID_RE.test(localStore.nodeId)
|
|
223
|
+
? localStore.nodeId : null;
|
|
224
|
+
const deviceName = defaultDeviceName(localHostname());
|
|
220
225
|
const out = {
|
|
221
226
|
roles: readRoles(configPath),
|
|
222
227
|
firstRun,
|
|
@@ -227,7 +232,11 @@ function settingsRoutes(deps = {}) {
|
|
|
227
232
|
autoUpdate: updateStatus ? updateStatus.enabled : fileCfg.autoUpdate !== false,
|
|
228
233
|
// Nome dispositivo proposto per i form di pairing (etichetta umana, non
|
|
229
234
|
// lo slug). La UI lo precompila e lascia editing libero.
|
|
230
|
-
deviceName
|
|
235
|
+
deviceName,
|
|
236
|
+
nodeId: localNodeId,
|
|
237
|
+
localName: localNodeId
|
|
238
|
+
? nodesStore.deriveNodeHandle(deviceName, localHostname(), localNodeId)
|
|
239
|
+
: null,
|
|
231
240
|
};
|
|
232
241
|
if (updateStatus) out.update = updateStatus;
|
|
233
242
|
send(res, 200, out);
|
|
@@ -385,8 +394,8 @@ function settingsRoutes(deps = {}) {
|
|
|
385
394
|
// prima di paired:true. Qui resta solo la registrazione dietro mutGate;
|
|
386
395
|
// token/credenziali MAI nel payload (redazione nel handler estratto).
|
|
387
396
|
r.post('/nodes/pair', mutGate, createPairHandler({
|
|
388
|
-
send, validName, defaultDeviceName,
|
|
389
|
-
nodesPath, configPath, home, seams, runtimePort,
|
|
397
|
+
send, validName, defaultDeviceName: () => defaultDeviceName(localHostname()),
|
|
398
|
+
localHostname, nodesPath, configPath, home, seams, runtimePort,
|
|
390
399
|
}));
|
|
391
400
|
|
|
392
401
|
// --- POST /nodes — nodes add (riusa nodesCmds.nodesAdd) --------------------
|
package/lib/tmux/lifecycle.js
CHANGED
|
@@ -51,7 +51,8 @@ function buildCreateArgs(name, realCwd, preset, extraPresets) {
|
|
|
51
51
|
|
|
52
52
|
function httpError(status, msg) { const e = new Error(msg); e.status = status; return e; }
|
|
53
53
|
|
|
54
|
-
function createSession(tmuxBin, { name, cwd, preset }, { home, presets } = {}) {
|
|
54
|
+
async function createSession(tmuxBin, { name, cwd, preset }, { home, presets, ensureProtection } = {}) {
|
|
55
|
+
if (typeof ensureProtection === 'function') await ensureProtection();
|
|
55
56
|
return new Promise((resolve, reject) => {
|
|
56
57
|
if (!validSessionName(name)) return reject(httpError(400, 'nome sessione non valido'));
|
|
57
58
|
// Il namespace cloud-* e' delle celle fleet (audit finale #1): una generica
|
|
@@ -71,7 +72,8 @@ function createSession(tmuxBin, { name, cwd, preset }, { home, presets } = {}) {
|
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
function killSession(tmuxBin, name) {
|
|
75
|
+
async function killSession(tmuxBin, name, { ensureProtection } = {}) {
|
|
76
|
+
if (typeof ensureProtection === 'function') await ensureProtection();
|
|
75
77
|
return new Promise((resolve, reject) => {
|
|
76
78
|
execFile(tmuxBin, ['kill-session', '-t', `=${name}`], (err, _o, stderr) => {
|
|
77
79
|
if (err) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const { execFile } = require('node:child_process');
|
|
5
|
+
const { minimalRuntimeEnv } = require('../runtime/env.js');
|
|
6
|
+
|
|
7
|
+
// Operational guard for NexusCrew's shared tmux server. tmux command aliases
|
|
8
|
+
// are not a security boundary against another process running as the same UID,
|
|
9
|
+
// but they neutralize accidental `tmux kill-server` calls while exit-empty=off
|
|
10
|
+
// keeps the server alive when the last managed session is intentionally stopped.
|
|
11
|
+
const KILL_SERVER_ALIAS_INDEX = 'command-alias[100]';
|
|
12
|
+
const KILL_SERVER_ALIAS = 'kill-server=display-message "DENIED: kill-server disabled by NexusCrew"';
|
|
13
|
+
|
|
14
|
+
function protectionArgs() {
|
|
15
|
+
return [
|
|
16
|
+
'start-server',
|
|
17
|
+
';', 'set-option', '-s', 'exit-empty', 'off',
|
|
18
|
+
';', 'set-option', '-s', KILL_SERVER_ALIAS_INDEX, KILL_SERVER_ALIAS,
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function protectSharedTmuxServer(tmuxBin = 'tmux', opts = {}) {
|
|
23
|
+
if (opts.enabled === false) return Promise.resolve({ ok: true, protected: false, reason: 'disabled' });
|
|
24
|
+
const execFileImpl = opts.execFileImpl || execFile;
|
|
25
|
+
const env = opts.env || minimalRuntimeEnv(process.env, { home: opts.home || os.homedir() });
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
try {
|
|
28
|
+
execFileImpl(tmuxBin, protectionArgs(), { env, timeout: opts.timeoutMs || 5000 }, (err, _stdout, stderr) => {
|
|
29
|
+
if (err) {
|
|
30
|
+
return resolve({
|
|
31
|
+
ok: false,
|
|
32
|
+
protected: false,
|
|
33
|
+
reason: String(stderr || err.message || 'tmux protection failed').trim(),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
resolve({ ok: true, protected: true, reason: 'kill-server guarded; exit-empty off' });
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {
|
|
39
|
+
resolve({ ok: false, protected: false, reason: String(error && error.message || error) });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function requireSharedTmuxProtection(tmuxBin, opts = {}) {
|
|
45
|
+
const result = await protectSharedTmuxServer(tmuxBin, opts);
|
|
46
|
+
if (!result.ok) {
|
|
47
|
+
const error = new Error(`tmux shared-server protection failed: ${result.reason || 'unknown error'}`);
|
|
48
|
+
error.status = 500;
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
KILL_SERVER_ALIAS_INDEX,
|
|
56
|
+
KILL_SERVER_ALIAS,
|
|
57
|
+
protectionArgs,
|
|
58
|
+
protectSharedTmuxServer,
|
|
59
|
+
requireSharedTmuxProtection,
|
|
60
|
+
};
|