@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.
@@ -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(os.hostname() || '').trim();
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: defaultDeviceName(),
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) --------------------
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.8.22",
3
+ "version": "0.8.24",
4
4
  "description": "Faithful browser tmux client — attach to live sessions over a real PTY, localhost-only, mobile-easy",
5
5
  "main": "lib/server.js",
6
6
  "bin": {