@mmmbuto/nexuscrew 0.9.29 → 0.9.30

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/docs/LIVE_PROMPT.md +1 -1
  3. package/docs/NOTIFICATIONS.md +37 -0
  4. package/frontend/dist/assets/index-BbcjoZS0.js +167 -0
  5. package/frontend/dist/assets/{index-CywSos2e.css → index-DqCfZFGi.css} +1 -1
  6. package/frontend/dist/index.html +2 -2
  7. package/frontend/dist/sw.js +91 -10
  8. package/frontend/dist/version.json +1 -1
  9. package/lib/audio/groups.js +1 -1
  10. package/lib/auth/token.js +4 -4
  11. package/lib/cli/commands.js +59 -12
  12. package/lib/cli/doctor.js +5 -5
  13. package/lib/cli/fleet-service.js +7 -7
  14. package/lib/cli/init.js +27 -27
  15. package/lib/cli/pidfile.js +1 -1
  16. package/lib/cli/service.js +6 -6
  17. package/lib/cli/stable-alias.js +4 -4
  18. package/lib/cli/url.js +1 -1
  19. package/lib/config.js +36 -6
  20. package/lib/files/routes.js +1 -1
  21. package/lib/fleet/boot.js +2 -2
  22. package/lib/fleet/builtin.js +15 -15
  23. package/lib/fleet/causes.js +1 -1
  24. package/lib/fleet/cell-exec.js +23 -23
  25. package/lib/fleet/cell-hold.js +6 -6
  26. package/lib/fleet/cell-lease-server.js +106 -106
  27. package/lib/fleet/cell-lease.js +21 -21
  28. package/lib/fleet/definitions.js +10 -10
  29. package/lib/fleet/identity-authority.js +1 -1
  30. package/lib/fleet/launch-broker.js +13 -13
  31. package/lib/fleet/launch.js +15 -15
  32. package/lib/fleet/lease-client.js +23 -23
  33. package/lib/fleet/lease-routes.js +3 -3
  34. package/lib/fleet/lease-verifier.js +22 -22
  35. package/lib/fleet/managed.js +35 -35
  36. package/lib/fleet/prompt-delivery.js +13 -13
  37. package/lib/fleet/routes.js +15 -10
  38. package/lib/fleet/runtime.js +24 -24
  39. package/lib/live-host/bridge.js +36 -36
  40. package/lib/live-host/routes.js +2 -2
  41. package/lib/live-host/store.js +4 -4
  42. package/lib/mcp/server.js +14 -14
  43. package/lib/mcp/tools.js +5 -5
  44. package/lib/nodes/access-presets.js +258 -0
  45. package/lib/nodes/commands.js +198 -4
  46. package/lib/nodes/store.js +152 -9
  47. package/lib/nodes/tunnel.js +7 -7
  48. package/lib/notify/ask-answer-service.js +184 -0
  49. package/lib/notify/ask-receipts.js +197 -0
  50. package/lib/notify/ask-relay.js +152 -0
  51. package/lib/notify/asks.js +28 -3
  52. package/lib/notify/event-feed-acl.js +95 -0
  53. package/lib/notify/event-feed-asks-routes.js +144 -0
  54. package/lib/notify/event-feed-client.js +434 -0
  55. package/lib/notify/event-feed-history.js +115 -0
  56. package/lib/notify/event-feed-producers.js +175 -0
  57. package/lib/notify/event-feed-routes.js +254 -0
  58. package/lib/notify/event-feed.js +328 -0
  59. package/lib/notify/events.js +1 -1
  60. package/lib/notify/notifier.js +3 -3
  61. package/lib/notify/push-relay.js +240 -0
  62. package/lib/notify/push.js +8 -8
  63. package/lib/notify/routes.js +62 -23
  64. package/lib/proxy/federation.js +74 -3
  65. package/lib/proxy/node-proxy.js +13 -13
  66. package/lib/proxy/panel-auth.js +4 -4
  67. package/lib/proxy/panel-proxy.js +2 -2
  68. package/lib/proxy/resource-acl.js +145 -0
  69. package/lib/server.js +372 -39
  70. package/lib/settings/pairing-coordinator.js +1 -1
  71. package/lib/settings/routes.js +45 -32
  72. package/lib/update/manager.js +1 -1
  73. package/lib/update/runner.js +1 -1
  74. package/lib/voice/transcribe.js +1 -1
  75. package/lib/ws/bridge.js +3 -3
  76. package/package.json +1 -1
  77. package/frontend/dist/assets/index-B_THfiL5.js +0 -167
@@ -1,21 +1,21 @@
1
1
  'use strict';
2
- // lib/settings/routes.js — Settings API per wizard/settings UI (design §4b(6), B2).
2
+ // lib/settings/routes.js — Settings API for the wizard/settings UI.
3
3
  //
4
4
  // Contratto duro:
5
5
  // - read-only: GET /api/settings (roles, firstRun, port, platform, service,
6
6
  // version).
7
- // - mutanti (LISTA CHIUSA §4b(6)), tutti dietro requireToken (montati sotto il
8
- // router /api gia' autenticato) + gate READONLY route-level (pattern
9
- // lib/fleet/routes.js, 403 esplicito):
7
+ // - mutators (CLOSED LIST), all behind requireToken (mounted under the
8
+ // already-authenticated /api router) + route-level READONLY gate
9
+ // (lib/fleet/routes.js pattern, explicit 403):
10
10
  // POST /config scrittura config.json ATOMICA (whitelist chiavi)
11
- // POST /token/rotate riusa rotateToken(); token MAI in risposta (§4b(3))
11
+ // POST /token/rotate reuses rotateToken(); token NEVER in the response
12
12
  // POST /nodes nodes add (riusa lib/nodes/commands.js)
13
13
  // DELETE /nodes/:name nodes remove
14
14
  // PATCH /nodes/:name/share publish/revoke the local node on its hub
15
15
  // POST /node-role retired compatibility endpoint (410)
16
16
  // POST /service/regenerate rigenera unit service (NO restart automatico)
17
17
  // - NON gate READONLY (lifecycle di PROCESSO, non mutazione di config — decisione
18
- // B0 documentata in lib/nodes/commands.js: READONLY blocca le mutazioni di
18
+ // Documentata in lib/nodes/commands.js: READONLY blocca le mutazioni di
19
19
  // config, non up/down/restart dei tunnel):
20
20
  // POST /nodes/:name/test non-mutante (POST per coerenza azione)
21
21
  // POST /nodes/:name/up|down|restart
@@ -28,15 +28,15 @@
28
28
  // 403 readonly, 404 nodo, 409 conflitto, 500 con messaggio). Niente stack trace.
29
29
  // - validazione input strict fail-closed: schema chiuso per ogni body, garbage
30
30
  // -> 400 con causa, mai guess.
31
- // - NIENTE reimplementazione della logica B0: i mutanti nodes incapsulano le
31
+ // NIENTE reimplementazione della logica: i mutanti nodes incapsulano le
32
32
  // funzioni CLI di lib/nodes/commands.js (log catturato per estrarre l'esito);
33
33
  // config.json scritto col pattern atomico
34
34
  // tmp+rename di lib/nodes/store.js.
35
35
  //
36
36
  // Nota lifecycle: i tunnel avviati da /nodes/:name/up sono spawn DETACHED+unref
37
37
  // con pidfile (lib/nodes/tunnel.js): non restano figli dell'handler HTTP; il
38
- // commento B0 "mai spawn dalla superficie web" e' superato per B2 da questo
39
- // contratto esplicito (§4b(6): "restart processi tunnel" e' mutazione ammessa).
38
+ // Commento "mai spawn dalla superficie web" e' superato per da questo
39
+ // explicit contract ("restart tunnel processes" is an allowed mutation).
40
40
  const fs = require('node:fs');
41
41
  const os = require('node:os');
42
42
  const path = require('node:path');
@@ -69,11 +69,17 @@ const { scrubError } = require('../update/core.js');
69
69
  const { createPairHandler } = require('./pairing-coordinator.js');
70
70
  const { publicPeeringRoutes } = require('./public-peering-routes.js');
71
71
 
72
- // Whitelist chiavi scrivibili di config.json via API (lista chiusa dal task B2).
72
+ // Whitelist chiavi scrivibili di config.json via API (lista chiusa dal task).
73
73
  const CONFIG_KEYS = new Set(['roles', 'port', 'wizardDone', 'autoUpdate', 'alternateScreen']);
74
74
  const ROLE_KEYS = new Set(['client', 'node']);
75
75
  const ADD_KEYS = new Set(['name', 'ssh', 'sshPort', 'remotePort', 'localPort', 'keyPath', 'label']);
76
- const EDIT_KEYS = new Set(['label', 'ssh', 'sshPort', 'autostart', 'visibility', 'selected', 'cellVisibility', 'cells']);
76
+ const EDIT_KEYS = new Set([
77
+ 'label', 'ssh', 'sshPort', 'autostart', 'visibility', 'selected', 'cellVisibility', 'cells',
78
+ // Federated access: `accessRole` applies a preset, the granular keys refine an
79
+ // explicitly set vector, and `accessRevision` makes the write a compare-and-set.
80
+ // A `role` key is NOT here: a declared role is refused instead of being stored.
81
+ 'accessRole', 'accessRevision', 'eventsAccess', 'nodeEventsAccess', 'askReplyAccess', 'filesReadAccess', 'peerOperatorAccess',
82
+ ]);
77
83
  const LOCAL_AUDIO_TEST_TEXT = 'NexusCrew audio test.';
78
84
 
79
85
  // Default sensato per il "nome dispositivo" proposto nei form (pairing/invite).
@@ -86,7 +92,7 @@ function defaultDeviceName(hostname = os.hostname()) {
86
92
  return h.slice(0, nodesStore.LABEL_MAX);
87
93
  }
88
94
 
89
- // Cintura §4b(3): rimozione ricorsiva di ogni chiave `token` da QUALUNQUE payload
95
+ // Belt: recursive removal of every `token` key from ANY response
90
96
  // di risposta (la vista redatta espone hasToken, mai il valore). Difesa in
91
97
  // profondita' oltre a redactStore: anche un bug a monte non fa uscire il segreto.
92
98
  function deepScrubTokens(v) {
@@ -204,9 +210,9 @@ function settingsRoutes(deps = {}) {
204
210
  });
205
211
 
206
212
  const validName = (name) => typeof name === 'string' && nodesStore.NODE_NAME_RE.test(name);
207
- const applyNodeEdit = (res, name, patch) => {
213
+ const applyNodeEdit = (res, name, patch, extra = {}) => {
208
214
  const cap = capture();
209
- const out = nodesCmds.nodesEdit({ ...cliOpts(cap), ref: name, patch });
215
+ const out = nodesCmds.nodesEdit({ ...cliOpts(cap), ref: name, patch, ...extra });
210
216
  if (out.code === 0) return send(res, 200, {
211
217
  saved: true, name: out.name, node: out.node, restarted: out.restarted === true,
212
218
  });
@@ -216,6 +222,9 @@ function settingsRoutes(deps = {}) {
216
222
  if (out.reason === 'store invalido') {
217
223
  return send(res, out.status || 503, { error: message, ...(out.errorCode ? { code: out.errorCode } : {}) });
218
224
  }
225
+ if (out.reason === 'revision-conflict') {
226
+ return send(res, 409, { error: message, revision: out.revision });
227
+ }
219
228
  const status = out.reason === 'write-or-restart-failed' ? 502 : 400;
220
229
  return send(res, status, { error: message });
221
230
  };
@@ -359,10 +368,10 @@ function settingsRoutes(deps = {}) {
359
368
  });
360
369
 
361
370
  // --- POST /token/rotate — atomico + reload live + chiusura WS; token MAI in risposta
362
- // Contratto §4b(3): "scrittura atomica del token file + chiusura delle sessioni
363
- // WS/API attive locali + reload credenziali proxy". Da revisione: prima questa route
364
- // scriveva solo il file, e il server teneva il VECCHIO token in memoria -> restava
365
- // accettato fino al restart manuale. Ora: (1) scrittura atomica, (2) reload live
371
+ // Contract: "atomic write of the token file + closing of the active local
372
+ // WS/API sessions + proxy credential reload". From review: before, this route
373
+ // only wrote the file, and the server kept the OLD token in memory -> it stayed
374
+ // accepted until a manual restart. Now: (1) atomic write, (2) live reload
366
375
  // dell'holder in memoria (requireToken/verify vedono il nuovo), (3) chiusura delle
367
376
  // sessioni WS long-lived (autenticate solo all'upgrade). Il nuovo token NON trapela
368
377
  // mai: rotateToken() lo ritorna ma il valore viene SCARTATO, e reload() non lo espone.
@@ -527,7 +536,11 @@ function settingsRoutes(deps = {}) {
527
536
  }
528
537
  const invalid = Object.keys(body).find((key) => !EDIT_KEYS.has(key));
529
538
  if (invalid) return send(res, 400, { error: `campo non modificabile: "${invalid}"` });
530
- return applyNodeEdit(res, name, body);
539
+ if (Object.hasOwn(body, 'accessRevision') && !Number.isInteger(body.accessRevision)) {
540
+ return send(res, 400, { error: 'accessRevision vuole un intero' });
541
+ }
542
+ const { accessRevision, ...patch } = body;
543
+ return applyNodeEdit(res, name, patch, { expectedRevision: accessRevision });
531
544
  });
532
545
 
533
546
  // --- POST /nodes/:name/test — non-mutante (POST per coerenza azione) -------
@@ -548,11 +561,11 @@ function settingsRoutes(deps = {}) {
548
561
  });
549
562
 
550
563
  // --- POST /nodes/:name/up|down|restart — lifecycle tunnel ------------------
551
- // GATE READONLY (da revisione, contract §4b(6): "restart processi tunnel" e' tra i
552
- // mutanti della lista chiusa e NEXUSCREW_READONLY blocca TUTTI i mutanti). Prima
553
- // questi endpoint erano esplicitamente NON gated ("decisione B0") — ma cio'
554
- // contradiceva il contratto duro §4b(6): up/down/restart mutano processi e vanno
555
- // bloccati in READONLY. /nodes/:name/test resta NON gated (probe diagnostica).
564
+ // READONLY gate (tunnel process restart is among the mutators of the
565
+ // closed list and NEXUSCREW_READONLY blocks ALL of them). Before, these
566
+ // Endpoints were explicitly NOT gated (" decision") — but that contradicted
567
+ // the hard contract: up/down/restart mutate processes and must be blocked
568
+ // in READONLY. /nodes/:name/test stays NOT gated (diagnostic probe).
556
569
  function lifecycleHandler(action) {
557
570
  return (req, res) => {
558
571
  try {
@@ -771,10 +784,10 @@ function settingsRoutes(deps = {}) {
771
784
  await notifyHub(true);
772
785
  return send(res, 200, { name, shared: true });
773
786
  }
774
- // OFF e' fail-safe e crash-durevole (design piano §3.2): persist desired
775
- // false -> revoke autenticata sul canale -L ancora vivo -> restart locale
776
- // senza -R. MAI restart-before-revoke: il -L e' il canale stesso della
777
- // revoca, e un crash prima dell'ACK non puo' mai ripubblicare da intent
787
+ // OFF is fail-safe and crash-durable: persist desired
788
+ // false -> authenticated revoke on the still-live -L channel -> local restart
789
+ // without -R. NEVER restart-before-revoke: the -L is the very channel of
790
+ // the revocation, and a crash before the ACK can never republish by intent.
778
791
  // stale (il file dice gia' false; il boot riparte -L only + reconcile).
779
792
  // 1) persist desired false (snapshot dei soli campi gia' validati).
780
793
  st = nodesStore.loadStoreStrict(nodesPath);
@@ -928,7 +941,7 @@ function settingsRoutes(deps = {}) {
928
941
  return send(res, 400, { error: 'body non valido: attesi targets e mode' });
929
942
  }
930
943
  try { return send(res, 200, { group: audioGroups.saveGroup(audioCfg, String(req.params.name || ''), body, home) }); }
931
- // R31-A4: il catch nudo raccontava QUALUNQUE errore come «gruppo non
944
+ // : il catch nudo raccontava QUALUNQUE errore come «gruppo non
932
945
  // valido» (400): un EACCES/ENOSPC sulla scrittura di audio-groups.json
933
946
  // finiva letto come colpa del nome. La validazione arriva dal modulo con
934
947
  // status 400 + code chiuso; tutto il resto — scrittura fallita compresa —
@@ -941,7 +954,7 @@ function settingsRoutes(deps = {}) {
941
954
  const removed = audioGroups.removeGroup(audioCfg, String(req.params.name || ''), home);
942
955
  return removed ? send(res, 200, { removed: true }) : send(res, 404, { error: 'gruppo audio non trovato' });
943
956
  } catch (e) {
944
- // R31-A4: come PUT sopra — vedi il commento lì.
957
+ // : come PUT sopra — vedi il commento lì.
945
958
  return send(res, e.status || 500, { error: scrubError(e), ...(e.code ? { code: e.code } : {}) });
946
959
  }
947
960
  });
@@ -987,7 +1000,7 @@ function settingsRoutes(deps = {}) {
987
1000
  // --- POST /service/regenerate — rigenera l'unit service --------------------
988
1001
  // Riusa generateService/installService (lib/cli/service.js: escaping per-platform,
989
1002
  // no-symlink, tmp+rename atomico). activate:false registra il piano di attivazione
990
- // SENZA eseguirlo: il contratto B2 vieta il restart automatico dall'API
1003
+ // SENZA eseguirlo: il contratto vieta il restart automatico dall'API
991
1004
  // (la UI avvisa di riavviare a mano).
992
1005
  r.post('/service/regenerate', mutGate, (_req, res) => {
993
1006
  try {
@@ -1003,7 +1016,7 @@ function settingsRoutes(deps = {}) {
1003
1016
  uid: seams.uid || uid(),
1004
1017
  installPath: seams.serviceInstallPath,
1005
1018
  };
1006
- // 1-bis (R23): come in init — il path del boot si risolve alla
1019
+ // 1-bis: come in init — il path del boot si risolve alla
1007
1020
  // scrittura e cio' che non ha alias stabile torna DICHIARATO. Le
1008
1021
  // warnings sono DATI nella risposta, non testo composto qui.
1009
1022
  const svcBoot = resolveBootPaths({
@@ -100,7 +100,7 @@ function createNpmUpdater(opts = {}) {
100
100
  supported, enabled, current: currentVersion,
101
101
  phase: state.phase || 'idle', latest: state.latest || '',
102
102
  available: state.available === true && isNewer(state.latest, currentVersion),
103
- // R31: `idle` di DEFAULT (stato mai scritto) e `idle` DOPO un check
103
+ // `idle` di DEFAULT (stato mai scritto) e `idle` DOPO un check
104
104
  // senza novita' coincidono — e la UI narrava «aggiornato» in entrambi.
105
105
  // `checked` deriva da lastCheckedAt, che viene scritto a OGNI check
106
106
  // (riuscito o fallito): chi legge distingue «mai controllato» (ignoranza)
@@ -19,7 +19,7 @@ const {
19
19
  // `nexuscrew init` — resta puntata a un binario che non esiste più, con exit
20
20
  // 78 EX_CONFIG e le celle boot:true mute (misurato sul campo 2026-08-18).
21
21
  //
22
- // Il criterio R23 resta intatto («alias stabile che realpath allo stesso
22
+ // Il criterio resta intatto («alias stabile che realpath allo stesso
23
23
  // file», verificato alla SCRITTURA, mai a ogni avvio) con una estensione
24
24
  // dichiarata per il caso del runner: il processo dell'update può girare con
25
25
  // un node già unlinkato su disco, e in quel caso si sceglie il primo percorso
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  // Voice STT proxy (graceful). voiceUrl null => non configurato: 503 pulito, non 502.
3
- // Web Speech nel browser resta indipendente da questo proxy (split model M5).
3
+ // Web Speech nel browser resta indipendente da questo proxy (split model).
4
4
  const fs = require('node:fs');
5
5
 
6
6
  function loadVoiceToken(cfg) {
package/lib/ws/bridge.js CHANGED
@@ -242,8 +242,8 @@ function bindWs(ws, deps) {
242
242
  // readonlyDefault del server e' un PAVIMENTO, non un default: se il server
243
243
  // e' READONLY nessun client puo' declassarlo (msg.readonly:false non deve
244
244
  // vincere). Il client puo' solo AGGIUNGERE restrizione (attach read-only
245
- // su un server read-write). Contratto design §4b(6): READONLY blocca anche
246
- // le scritture PTY. (fix da revisione finale: prima il ?? faceva vincere il client.)
245
+ // on a read-write server). Design contract: READONLY also blocks
246
+ // PTY writes. (final-review fix: before, the ?? let the client win.)
247
247
  readonly: defaults.readonlyDefault === true || !!msg.readonly,
248
248
  takeSize,
249
249
  cols: clamp(msg.cols, 20, 300, 80),
@@ -289,7 +289,7 @@ function bindWs(ws, deps) {
289
289
  if (msg.type === 'resize') record.pty.resize(clamp(msg.cols, 20, 300, 80), clamp(msg.rows, 5, 120, 24));
290
290
  // focus: il tile che prende il focus diventa size-owner (promote); perdendolo
291
291
  // torna ignore-size (demote). Cosi' N deck/tile sulla stessa sessione NON si
292
- // contendono la geometria: comanda solo chi ha il focus (§5b size policy).
292
+ // contend for geometry: only the focused tile commands it (size policy).
293
293
  else if (msg.type === 'focus') { if (msg.on) record.pty.promote(); else record.pty.demote(); }
294
294
  else if (msg.type === 'input' && !record.readonly) record.pty.write(typeof msg.data === 'string' ? msg.data : '');
295
295
  else if (msg.type === 'key' && !record.readonly) record.pty.write(typeof msg.seq === 'string' ? msg.seq.slice(0, 64) : '');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.9.29",
3
+ "version": "0.9.30",
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": {