@mmmbuto/nexuscrew 0.9.28 → 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.
- package/CHANGELOG.md +16 -0
- package/docs/LIVE_PROMPT.md +1 -1
- package/docs/NOTIFICATIONS.md +37 -0
- package/docs/OPERATIONS.md +20 -0
- package/frontend/dist/assets/index-BbcjoZS0.js +167 -0
- package/frontend/dist/assets/index-DqCfZFGi.css +32 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/sw.js +91 -10
- package/frontend/dist/version.json +1 -1
- package/lib/audio/groups.js +1 -1
- package/lib/auth/token.js +4 -4
- package/lib/cli/commands.js +70 -15
- package/lib/cli/doctor.js +5 -5
- package/lib/cli/fleet-service.js +7 -7
- package/lib/cli/init.js +55 -31
- package/lib/cli/pidfile.js +1 -1
- package/lib/cli/service.js +124 -8
- package/lib/cli/stable-alias.js +4 -4
- package/lib/cli/url.js +1 -1
- package/lib/config.js +36 -6
- package/lib/files/routes.js +1 -1
- package/lib/fleet/boot.js +2 -2
- package/lib/fleet/builtin.js +15 -15
- package/lib/fleet/causes.js +1 -1
- package/lib/fleet/cell-exec.js +23 -23
- package/lib/fleet/cell-hold.js +6 -6
- package/lib/fleet/cell-lease-server.js +106 -106
- package/lib/fleet/cell-lease.js +21 -21
- package/lib/fleet/definitions.js +10 -10
- package/lib/fleet/identity-authority.js +1 -1
- package/lib/fleet/launch-broker.js +13 -13
- package/lib/fleet/launch.js +15 -15
- package/lib/fleet/lease-client.js +23 -23
- package/lib/fleet/lease-routes.js +3 -3
- package/lib/fleet/lease-verifier.js +22 -22
- package/lib/fleet/managed.js +35 -35
- package/lib/fleet/prompt-delivery.js +13 -13
- package/lib/fleet/routes.js +15 -10
- package/lib/fleet/runtime.js +24 -24
- package/lib/live-host/bridge.js +53 -43
- package/lib/live-host/federation.js +97 -0
- package/lib/live-host/routes.js +166 -8
- package/lib/live-host/store.js +44 -6
- package/lib/mcp/server.js +14 -14
- package/lib/mcp/tools.js +5 -5
- package/lib/nodes/access-presets.js +258 -0
- package/lib/nodes/commands.js +198 -4
- package/lib/nodes/store.js +152 -9
- package/lib/nodes/tunnel.js +7 -7
- package/lib/notify/ask-answer-service.js +184 -0
- package/lib/notify/ask-receipts.js +197 -0
- package/lib/notify/ask-relay.js +152 -0
- package/lib/notify/asks.js +28 -3
- package/lib/notify/event-feed-acl.js +95 -0
- package/lib/notify/event-feed-asks-routes.js +144 -0
- package/lib/notify/event-feed-client.js +434 -0
- package/lib/notify/event-feed-history.js +115 -0
- package/lib/notify/event-feed-producers.js +175 -0
- package/lib/notify/event-feed-routes.js +254 -0
- package/lib/notify/event-feed.js +328 -0
- package/lib/notify/events.js +1 -1
- package/lib/notify/notifier.js +3 -3
- package/lib/notify/push-relay.js +240 -0
- package/lib/notify/push.js +8 -8
- package/lib/notify/routes.js +62 -23
- package/lib/proxy/federation.js +132 -12
- package/lib/proxy/node-proxy.js +13 -13
- package/lib/proxy/panel-auth.js +4 -4
- package/lib/proxy/panel-proxy.js +2 -2
- package/lib/proxy/resource-acl.js +145 -0
- package/lib/server.js +396 -40
- package/lib/settings/pairing-coordinator.js +1 -1
- package/lib/settings/routes.js +45 -32
- package/lib/update/manager.js +1 -1
- package/lib/update/runner.js +1 -1
- package/lib/voice/transcribe.js +1 -1
- package/lib/ws/bridge.js +3 -3
- package/package.json +1 -1
- package/skills/live/SKILL.md +12 -0
- package/frontend/dist/assets/index-CWra6B79.js +0 -167
- package/frontend/dist/assets/index-D6CIfCjv.css +0 -32
package/lib/settings/routes.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
// lib/settings/routes.js — Settings API
|
|
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
|
-
// -
|
|
8
|
-
//
|
|
9
|
-
// lib/fleet/routes.js, 403
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
39
|
-
//
|
|
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
|
|
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([
|
|
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
|
-
//
|
|
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
|
-
//
|
|
363
|
-
// WS/API
|
|
364
|
-
//
|
|
365
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
//
|
|
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
|
|
775
|
-
// false -> revoke
|
|
776
|
-
//
|
|
777
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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({
|
package/lib/update/manager.js
CHANGED
|
@@ -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
|
-
//
|
|
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)
|
package/lib/update/runner.js
CHANGED
|
@@ -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
|
|
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
|
package/lib/voice/transcribe.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
246
|
-
//
|
|
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
|
-
//
|
|
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
package/skills/live/SKILL.md
CHANGED
|
@@ -33,6 +33,18 @@ read from the roster and the designated cell's lease state
|
|
|
33
33
|
(`live`/`grace`/`expired`/`none`/`unavailable`) — a designated-but-not-eligible
|
|
34
34
|
host is a distinct, readable state, not an error to explain away.
|
|
35
35
|
|
|
36
|
+
## The rule is per node — and the cell may live on another node
|
|
37
|
+
|
|
38
|
+
One host cell **per node** is the rule, and it keeps holding when the designated
|
|
39
|
+
cell belongs to a peer: the node serving the page keeps the pointer locally
|
|
40
|
+
(`ownerId` plus the owner's own `revision`, and `remote: true` on `GET`) and
|
|
41
|
+
forwards the bridge resolution to the owner instead of resolving it locally.
|
|
42
|
+
The caller still chooses nothing — the designation is the choice, and it can now
|
|
43
|
+
point off-node. An unreachable owner, or a peer that was never granted
|
|
44
|
+
`liveHostAccess`, is a **named** outcome (`live-host-owner-unreachable`,
|
|
45
|
+
`live-host-not-granted`), never a silent fallback to a local cell. `clear`
|
|
46
|
+
removes the pointer on both sides.
|
|
47
|
+
|
|
36
48
|
## Command the node that owns the cell, not the one serving the page
|
|
37
49
|
|
|
38
50
|
This is the point of the feature: **the request must reach the node whose
|