@mmmbuto/nexuscrew 0.9.16 → 0.9.17
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 +132 -3
- package/docs/ALIBABA_TOKEN_PLAN.md +70 -0
- package/docs/AUDIO_SHARE.md +119 -0
- package/docs/CELL_PANEL.md +156 -0
- package/docs/CONFIGURATION.md +122 -0
- package/docs/FLEET.md +222 -0
- package/docs/INSTALLATION.md +128 -0
- package/docs/LIVE_PROMPT.md +110 -0
- package/docs/MCP.md +151 -0
- package/docs/NODES.md +214 -0
- package/docs/NOTIFICATIONS.md +101 -0
- package/docs/OPERATIONS.md +114 -0
- package/docs/README.md +55 -0
- package/docs/SECURITY.md +185 -0
- package/docs/VL_MICRO_NODES.md +107 -0
- package/docs/img/fleet-deck-desktop.png +0 -0
- package/docs/img/session-mobile.png +0 -0
- package/docs/live-prompt-templates/LIVE_PROMPT.en.md +67 -0
- package/docs/live-prompt-templates/LIVE_PROMPT.es.md +70 -0
- package/docs/live-prompt-templates/LIVE_PROMPT.it.md +67 -0
- package/frontend/dist/assets/{index-CwsGpQwK.js → index-jBYBPXhB.js} +1 -1
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/auth/middleware.js +1 -1
- package/lib/cells/scope-guard.js +2 -2
- package/lib/cli/commands.js +7 -7
- package/lib/cli/fleet-service.js +2 -2
- package/lib/cli/pidfile.js +15 -15
- package/lib/cli/service.js +2 -2
- package/lib/cli/stable-alias.js +1 -1
- package/lib/files/routes.js +2 -2
- package/lib/fleet/builtin.js +6 -6
- package/lib/fleet/cell-exec.js +4 -4
- package/lib/fleet/cell-lease-server.js +17 -17
- package/lib/fleet/definitions.js +5 -5
- package/lib/fleet/launch-broker.js +3 -3
- package/lib/fleet/lease-client.js +1 -1
- package/lib/fleet/managed.js +11 -11
- package/lib/fleet/prompt-delivery.js +1 -1
- package/lib/fleet/runtime.js +3 -3
- package/lib/live-host/bridge.js +3 -3
- package/lib/live-host/routes.js +1 -1
- package/lib/mcp/server.js +7 -8
- package/lib/mcp/tools.js +1 -1
- package/lib/nodes/commands.js +2 -2
- package/lib/nodes/health.js +1 -1
- package/lib/nodes/identity.js +10 -13
- package/lib/nodes/store.js +2 -2
- package/lib/nodes/tunnel-supervisor.js +7 -7
- package/lib/nodes/tunnel.js +10 -11
- package/lib/notify/asks.js +4 -4
- package/lib/notify/persist.js +1 -1
- package/lib/notify/push.js +4 -4
- package/lib/notify/routes.js +12 -12
- package/lib/proxy/node-proxy.js +4 -4
- package/lib/proxy/panel-proxy.js +2 -2
- package/lib/pty/attach.js +1 -1
- package/lib/server.js +3 -3
- package/lib/settings/pairing-coordinator.js +1 -1
- package/lib/settings/routes.js +4 -4
- package/lib/tmux/lifecycle.js +4 -4
- package/lib/tmux/preview.js +1 -1
- package/lib/update/runner.js +9 -9
- package/lib/vl-nodes/broker.js +1 -1
- package/lib/ws/bridge.js +1 -1
- package/package.json +20 -2
- package/frontend/index.html +0 -20
package/lib/fleet/managed.js
CHANGED
|
@@ -103,7 +103,7 @@ function declaredFor(extraModels, profileId, model) {
|
|
|
103
103
|
// uno la dimentichi perche' un modello dichiarato smetta di essere valido
|
|
104
104
|
// proprio nel punto che conta — l'avvio.
|
|
105
105
|
//
|
|
106
|
-
//
|
|
106
|
+
// da revisione: Map<engine, Map<id, model>> — porta il descrittore intero, non solo
|
|
107
107
|
// l'id (stesso motivo del commento gemello in definitions.js: extraModels e'
|
|
108
108
|
// costruita due volte, una dentro parseDefinitions per il parsing, una qui per
|
|
109
109
|
// il runtime che rilegge le definizioni gia' salvate; le due DEVONO restare
|
|
@@ -767,7 +767,7 @@ function binaryCandidates(client, home) {
|
|
|
767
767
|
// (non possiamo dichiararlo "trovato"); ma chi costruisce il messaggio
|
|
768
768
|
// (describeManaged) puo' ora distinguere "client non trovato" da "non ho potuto
|
|
769
769
|
// verificare un candidato" — il discriminante e' CHI ha fallito, non che ci sia
|
|
770
|
-
// stata un'eccezione. Stesso principio gia' applicato in checkTermuxExec
|
|
770
|
+
// stata un'eccezione. Stesso principio gia' applicato in checkTermuxExec.
|
|
771
771
|
function findBinary(client, home, out) {
|
|
772
772
|
const blocked = Array.isArray(out && out.blocked) ? out.blocked : null;
|
|
773
773
|
for (const candidate of binaryCandidates(client, home)) {
|
|
@@ -1061,7 +1061,7 @@ function describeManaged(spec, cfg = {}) {
|
|
|
1061
1061
|
const extraModels = cfg.extraModels || null;
|
|
1062
1062
|
const normalized = normalizeManagedSpec(spec, { extraModels });
|
|
1063
1063
|
if (!normalized) {
|
|
1064
|
-
//
|
|
1064
|
+
// da revisione: il rifiuto resta (i descrittori NON appartengono al profilo di una
|
|
1065
1065
|
// cella: vengono dalla definizione dell'ENGINE, in `d.models` — due
|
|
1066
1066
|
// soggetti diversi, mescolarli renderebbe ambiguo chi dichiara cosa). Ma
|
|
1067
1067
|
// se la causa e' proprio questa, il messaggio generico "invalid managed
|
|
@@ -1238,7 +1238,7 @@ function sharedMcpArgs(home) {
|
|
|
1238
1238
|
// indicizzato sul percorso assoluto — ed e' reale: su questa installazione nove
|
|
1239
1239
|
// progetti ne hanno, fra cui la directory di lavoro delle celle. Dimenticarlo
|
|
1240
1240
|
// significava che un elenco parziale non negava server che la sessione carica
|
|
1241
|
-
// davvero, e l'operatore avrebbe creduto di averli esclusi. Trovato
|
|
1241
|
+
// davvero, e l'operatore avrebbe creduto di averli esclusi. Trovato da una revisione.
|
|
1242
1242
|
//
|
|
1243
1243
|
// RESTANO FUORI, e non sono enumerabili da qui: plugin, connettori claude.ai e
|
|
1244
1244
|
// la configurazione gestita di sistema. Per quelli l'unica esclusione certa e'
|
|
@@ -1356,7 +1356,7 @@ function ensurePrivateClaudeConfig(home, profileId, label, penguinMode) {
|
|
|
1356
1356
|
return configDir;
|
|
1357
1357
|
}
|
|
1358
1358
|
|
|
1359
|
-
//
|
|
1359
|
+
// da revisione: il file .ts generato NON e' il consumatore — Pi lo E'. Un test che
|
|
1360
1360
|
// legge solo il file resta verde su un'estensione che Pi rifiuta a runtime.
|
|
1361
1361
|
// CONTRATTO REALE che Pi (@earendil-works/pi-coding-agent 0.80.10) impone a
|
|
1362
1362
|
// ogni modello di `pi.registerProvider(id, {models: [...]})` — fonte:
|
|
@@ -1382,13 +1382,13 @@ function toPiModelConfig(m) {
|
|
|
1382
1382
|
};
|
|
1383
1383
|
}
|
|
1384
1384
|
|
|
1385
|
-
//
|
|
1385
|
+
// da revisione: `declaredModels` (opzionale) sono i descrittori dichiarati per l'ENGINE
|
|
1386
1386
|
// (via declaredModelsFor), per il ramo Pi CUSTOM — stesso motivo/stessa fonte
|
|
1387
1387
|
// di customCatalogFor, mai letti da `spec.models`. Il ramo Pi NON-custom
|
|
1388
1388
|
// (profile.piExtension) continua a passare `models` dentro l'oggetto spec-like
|
|
1389
1389
|
// come sempre: e' un catalogo STATICO cablato nel codice (es. alibaba-token-
|
|
1390
1390
|
// plan), GIA' nella forma completa che Pi si aspetta — un caso diverso, non
|
|
1391
|
-
// toccato dal difetto
|
|
1391
|
+
// toccato dal difetto da revisione. declaredModels, quando presente, ha priorita', ed e'
|
|
1392
1392
|
// sempre passato per toPiModelConfig (mai i descrittori grezzi cosi' come sono).
|
|
1393
1393
|
function writePiProviderExtension(spec, home, declaredModels) {
|
|
1394
1394
|
const dir = path.join(home, '.nexuscrew', 'pi-providers');
|
|
@@ -1524,7 +1524,7 @@ function vlSystemAppendGate(binary, cfg) {
|
|
|
1524
1524
|
return { ok: true };
|
|
1525
1525
|
}
|
|
1526
1526
|
|
|
1527
|
-
//
|
|
1527
|
+
// (fix definitivo — una revisione del pacchetto aveva bocciato la prima versione:
|
|
1528
1528
|
// il test costruiva `spec.models` a mano, e in produzione quel campo non
|
|
1529
1529
|
// esiste mai). `declaredModels` NON viene da `spec`: i descrittori sono
|
|
1530
1530
|
// proprieta' della definizione dell'ENGINE (`d.models` del documento), non del
|
|
@@ -1614,14 +1614,14 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1614
1614
|
// non basta: la normalizzazione che decide se la cella PARTE e' questa, e
|
|
1615
1615
|
// senza l'elenco dichiarato un modello legittimo diventa «invalid managed
|
|
1616
1616
|
// profile» — cioe' una cella che non si avvia per una ragione invisibile.
|
|
1617
|
-
// E' il difetto che
|
|
1617
|
+
// E' il difetto che la revisione ha ripreso due volte: la vista diceva di si',
|
|
1618
1618
|
// il boot diceva di no, e nessun test guardava il boot.
|
|
1619
1619
|
const spec = normalizeManagedSpec(engine.managed, { extraModels: cfg.extraModels || null });
|
|
1620
1620
|
const info = describeManaged(spec, cfg);
|
|
1621
1621
|
if (!spec || !info.configured) return { ok: false, reason: info.reason, info };
|
|
1622
1622
|
const home = cfg.home || require('node:os').homedir();
|
|
1623
1623
|
const profile = profileFor(spec.client, spec.provider, spec.credentialProfile || '');
|
|
1624
|
-
//
|
|
1624
|
+
// da revisione: i descrittori dichiarati per QUESTO profilo (client.provider — la
|
|
1625
1625
|
// stessa chiave che declaredFor usa dentro normalizeManagedSpec per
|
|
1626
1626
|
// validare gli id). Vengono dalla definizione dell'ENGINE (extraModels),
|
|
1627
1627
|
// MAI da spec: e' il ponte che customCatalogFor/writePiProviderExtension
|
|
@@ -1845,7 +1845,7 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
1845
1845
|
} else if (spec.provider === 'custom') {
|
|
1846
1846
|
env[spec.envKey] = cred.value;
|
|
1847
1847
|
args.push(...codexProviderArgs(spec.providerId, spec.displayName, spec.baseUrl, spec.envKey));
|
|
1848
|
-
//
|
|
1848
|
+
// da revisione: onora `models` (engine definition, già validato) come gli altri
|
|
1849
1849
|
// provider — deriva model_catalog_json e model_context_window, così
|
|
1850
1850
|
// Codex-VL non ricade sul fallback 272K. Senza `models` -> null (no regressione).
|
|
1851
1851
|
const customMeta = customCatalogFor(spec, model, declaredModels, home);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// managed conservano il prompt su argv (finding separato) e i custom send-keys
|
|
5
5
|
// conservano injectPrompt legacy senza Enter.
|
|
6
6
|
//
|
|
7
|
-
// Contratto (design
|
|
7
|
+
// Contratto (design della revisione 0.8.47):
|
|
8
8
|
// - Readiness REALE prima della consegna: il pane viene classificato sul SOLO
|
|
9
9
|
// viewport visibile (capture-pane -p, mai scrollback -S: su generation>0 lo
|
|
10
10
|
// scrollback puo' contenere vecchi prompt e marker stale). Auth, consenso
|
package/lib/fleet/runtime.js
CHANGED
|
@@ -80,7 +80,7 @@ function createBuiltinRuntime(ctx) {
|
|
|
80
80
|
// solo — il ponte Live, che la chiede esplicitamente e in-processo — quindi
|
|
81
81
|
// e' lui a doverla chiedere, non tutti gli altri a doversene ricordare.
|
|
82
82
|
// Il backup vieta gia' le cwd assolute per la stessa ragione: sono specifiche
|
|
83
|
-
// del dispositivo. Rilievo di
|
|
83
|
+
// del dispositivo. Rilievo di una revisione indipendente.
|
|
84
84
|
async function cellStatus({ includeCwd = false } = {}) {
|
|
85
85
|
if (Date.now() - cache.at > STATUS_TTL_MS) {
|
|
86
86
|
reloadDefs(); // pick-up di edit esterne/file
|
|
@@ -100,7 +100,7 @@ function createBuiltinRuntime(ctx) {
|
|
|
100
100
|
const effectivePolicy = ['pi', 'shell'].includes(engineDef?.managed?.client)
|
|
101
101
|
? 'standard'
|
|
102
102
|
: (remembered || engineDefault || '');
|
|
103
|
-
//
|
|
103
|
+
// da revisione: panelUrl per-cella vince su quello precompilato dall'engine (es.
|
|
104
104
|
// desktop.local), che a sua volta e' il default. Stessa forma di
|
|
105
105
|
// engineDef gia' usata sopra per la permission policy: un valore
|
|
106
106
|
// presente qui e' gia' passato da validPanelUrl a monte (parseEngine/
|
|
@@ -222,7 +222,7 @@ function createBuiltinRuntime(ctx) {
|
|
|
222
222
|
// broker-backed supervisor: credentials never enter tmux state/argv and a
|
|
223
223
|
// client that exits after readiness is restarted with bounded backoff.
|
|
224
224
|
// '-P -F #{pane_id}': tmux stampa il pane id della sessione appena creata,
|
|
225
|
-
// cosi' l'iniezione del prompt bersaglia ESATTAMENTE quel pane (
|
|
225
|
+
// cosi' l'iniezione del prompt bersaglia ESATTAMENTE quel pane (da revisione impl
|
|
226
226
|
// #5: elimina la race di riuso del nome sessione tra waitAlive e paste).
|
|
227
227
|
const readyMs = cfg.launchReadyMs != null ? cfg.launchReadyMs : 500;
|
|
228
228
|
const child = composeClientInvocation(launchEngine, cell);
|
package/lib/live-host/bridge.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// emendata da rev5 MC2). La riga che decide è in codex-rs
|
|
23
23
|
// core/src/config/mod.rs: `developer_instructions.or(cfg.developer_
|
|
24
24
|
// instructions)` — l'override Some scarta il valore di config. R2
|
|
25
|
-
// (
|
|
25
|
+
// (verso corretto dopo revisione pre-release): l'identità della
|
|
26
26
|
// cella designata viaggia SEMPRE come intestazione anteposta al campo,
|
|
27
27
|
// anche senza prompt. Il campo NON è additivo: una cella senza
|
|
28
28
|
// LIVE_PROMPT.md, che prima non passava nulla e riceveva le developer
|
|
@@ -227,7 +227,7 @@ function readCellPrompt(filesRoot, tmuxSession) {
|
|
|
227
227
|
// il ponte: il nome esatto della sessione, che il server MCP accetta da
|
|
228
228
|
// NEXUSCREW_MCP_SESSION via stdio. Il valore NON si mette in systemd
|
|
229
229
|
// Environment= (una identità statica condivisa = impersonare una cella
|
|
230
|
-
// fissa, demolito in
|
|
230
|
+
// fissa, demolito in revisione v1): lo dice l'intestazione, per-cella.
|
|
231
231
|
//
|
|
232
232
|
// Il fatto, niente di più: quale cella (id Fleet) e, se il roster la dichiara,
|
|
233
233
|
// la sessione tmux esatta — quella con cui la voce raggiunge i canonici della
|
|
@@ -497,7 +497,7 @@ function createLiveBridge({
|
|
|
497
497
|
// Tre condizioni diverse, tre nomi: la designazione dichiarata non
|
|
498
498
|
// eleggibile dall'hub, la cella che non esiste piu' nel roster, e la cella
|
|
499
499
|
// che c'e' ma e' spenta. Un nome solo mandava a guardare l'hub anche quando
|
|
500
|
-
// il problema era una sessione chiusa. Rilievo di
|
|
500
|
+
// il problema era una sessione chiusa. Rilievo di una revisione indipendente.
|
|
501
501
|
if (snap.eligible !== true) return none('host-ineligible');
|
|
502
502
|
|
|
503
503
|
let cell;
|
package/lib/live-host/routes.js
CHANGED
|
@@ -39,7 +39,7 @@ function isActive(cell) {
|
|
|
39
39
|
return !!(cell && cell.active === true && cell.tmux !== false);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// --- Seam lease↔designazione (
|
|
42
|
+
// --- Seam lease↔designazione (decisione presa in revisione: grace = false) -----
|
|
43
43
|
//
|
|
44
44
|
// L'idoneita' dell'host designato non e' piu' solo «sessione tmux viva»: con
|
|
45
45
|
// remain-on-exit la sessione sopravvive alla morte del supervisore, e la
|
package/lib/mcp/server.js
CHANGED
|
@@ -162,7 +162,7 @@ function resolveIdentity({ env, tmuxBin, execFileImpl }) {
|
|
|
162
162
|
return settle(missing);
|
|
163
163
|
}
|
|
164
164
|
const name = String(stdout || '').trim();
|
|
165
|
-
// tmux 3.4, misura
|
|
165
|
+
// tmux 3.4, misura della revisione (probe doppia): pane morto con -t ->
|
|
166
166
|
// rc=0 e stdout VUOTO. Il vuoto è il segnale dello stantio.
|
|
167
167
|
if (!name) return settle(stalePane);
|
|
168
168
|
if (isValidSession(name)) {
|
|
@@ -288,12 +288,11 @@ function createMcpServer(opts = {}) {
|
|
|
288
288
|
return j;
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
// NC-R. Aggiornare NexusCrew NON
|
|
292
|
-
//
|
|
293
|
-
// riavvio della cella.
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
// messo un giro intero a capirlo.
|
|
291
|
+
// NC-R. Aggiornare NexusCrew NON cambia il codice gia' caricato dai bridge
|
|
292
|
+
// MCP delle celle attive: ogni processo resta sulla versione con cui e' stato
|
|
293
|
+
// avviato fino al riavvio della cella. Dopo una correzione installata, un
|
|
294
|
+
// bridge ancora attivo puo' quindi restituire l'errore precedente; prima di
|
|
295
|
+
// valutare la correzione vanno confrontate le versioni di hub e bridge.
|
|
297
296
|
//
|
|
298
297
|
// Il momento in cui serve saperlo e' esattamente quello in cui qualcosa
|
|
299
298
|
// fallisce, quindi la verifica sta SOLO sul ramo d'errore: a regime non costa
|
|
@@ -367,7 +366,7 @@ function createMcpServer(opts = {}) {
|
|
|
367
366
|
return replyError(null, INVALID_REQUEST, 'richiesta non valida (atteso oggetto JSON-RPC)');
|
|
368
367
|
}
|
|
369
368
|
const id = (typeof msg.id === 'string' || typeof msg.id === 'number') ? msg.id : undefined;
|
|
370
|
-
//
|
|
369
|
+
// da revisione: SOLO JSON-RPC 2.0 — versione assente/errata -> -32600 anche
|
|
371
370
|
// per i messaggi senza id (il no-op vale solo per notification 2.0 valide).
|
|
372
371
|
if (msg.jsonrpc !== '2.0') {
|
|
373
372
|
return replyError(id !== undefined ? id : null, INVALID_REQUEST, 'jsonrpc "2.0" richiesto');
|
package/lib/mcp/tools.js
CHANGED
|
@@ -345,7 +345,7 @@ const TOOLS = [
|
|
|
345
345
|
// Restituirlo cosi' evita di far leggere un successo dove c'e' un rifiuto.
|
|
346
346
|
// Nessun conteggio: il dispatcher non lo propaga, e un ramo che non puo'
|
|
347
347
|
// mai essere vero e' codice morto travestito da informazione
|
|
348
|
-
// (rilievo
|
|
348
|
+
// (rilievo di una revisione indipendente). Se un giorno servira' il dettaglio,
|
|
349
349
|
// va fatto propagare da forward(), non dedotto qui.
|
|
350
350
|
if (target) return { status: j.status, ...(j.reason ? { reason: j.reason } : {}) };
|
|
351
351
|
return { delivered: j.delivered };
|
package/lib/nodes/commands.js
CHANGED
|
@@ -204,7 +204,7 @@ 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
|
-
//
|
|
207
|
+
// da revisione: permitopen con le destinazioni EXPLICITE dei -L. Qui si conosce solo
|
|
208
208
|
// la porta nexus: la porta pannello del peer la annuncia il JOIN, non
|
|
209
209
|
// esiste ancora — quando apparirà, la riga completa (due destinazioni) la
|
|
210
210
|
// emette il pairing. Mai un permesso generico: è ciò che impedisce a questa
|
|
@@ -392,7 +392,7 @@ function nodesRemove(opts) {
|
|
|
392
392
|
let next;
|
|
393
393
|
try { next = store.removeNode(st, name); }
|
|
394
394
|
catch (e) { log(`nodes remove: ${e.message}`); return { code: 1 }; }
|
|
395
|
-
// Ferma un eventuale forward tunnel ATTIVO prima di togliere la config (
|
|
395
|
+
// Ferma un eventuale forward tunnel ATTIVO prima di togliere la config (da revisione):
|
|
396
396
|
// senza questo il ssh resterebbe orfano — porta locale aperta verso un nodo che non
|
|
397
397
|
// e' piu' in config, irraggiungibile e senza piu' uno stop pulito via CLI/API.
|
|
398
398
|
let stopped = false;
|
package/lib/nodes/health.js
CHANGED
|
@@ -51,7 +51,7 @@ function applyProbeHysteresis(key, probed, previous, now, threshold) {
|
|
|
51
51
|
// now is simply down — reporting 'unknown' would keep a declared-dead peer
|
|
52
52
|
// out of the down state forever (one probe per process, never reaching the
|
|
53
53
|
// threshold unless someone keeps probing). The hysteresis only exists to
|
|
54
|
-
// protect a measured up state from jitter, per
|
|
54
|
+
// protect a measured up state from jitter, per una revisione precedente.
|
|
55
55
|
return { ...probed, consecutiveFailures: failures };
|
|
56
56
|
}
|
|
57
57
|
|
package/lib/nodes/identity.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// secondo scrittore — la proprieta' regge per assenza, non per controllo, e la
|
|
29
29
|
// differenza va detta invece che lasciata credere.
|
|
30
30
|
//
|
|
31
|
-
// Rilievo
|
|
31
|
+
// Rilievo della revisione indipendente: una funzione provata e senza chiamanti e' il
|
|
32
32
|
// segnale piu' forte che una garanzia esista, ed e' il piu' facile da dare per
|
|
33
33
|
// sbaglio.
|
|
34
34
|
const fs = require('node:fs');
|
|
@@ -79,14 +79,11 @@ function readKeyFile(p) {
|
|
|
79
79
|
+ 'la chiave privata e\' leggibile oltre il proprietario, va rigenerata a mano');
|
|
80
80
|
}
|
|
81
81
|
// UN FILE CHE ESISTE MA NON E' USABILE FA RUMORE, NON SI SOSTITUISCE.
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// con una chiave che non corrisponde. Meglio fermarsi e farlo sapere.
|
|
88
|
-
// Rilievo dell'audit indipendente; risolto nel verso opposto a quello
|
|
89
|
-
// proposto, perche' la coerenza va cercata sul ramo sicuro.
|
|
82
|
+
// Uno schema errato e un JSON corrotto sono due forme dello stesso stato non
|
|
83
|
+
// usabile: entrambi devono fare rumore e nessuno deve autorizzare una nuova
|
|
84
|
+
// chiave. Rigenerare cambia l'identita' del nodo e lascia ogni peer gia'
|
|
85
|
+
// associato con una chiave che non corrisponde. Il ramo sicuro si ferma e
|
|
86
|
+
// rende esplicita la causa senza sostituire il file.
|
|
90
87
|
let parsed;
|
|
91
88
|
try {
|
|
92
89
|
parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
@@ -136,10 +133,10 @@ function writeKeyFile(p, privateKeyPem) {
|
|
|
136
133
|
// ensureNodeKey: idempotente. La prima chiamata genera, le successive leggono.
|
|
137
134
|
// `created` distingue i due casi per chi vuole registrarlo una volta sola.
|
|
138
135
|
// `afterWriteSeam` e' un seam di prova, come `sessionExistsSeam` e `fleetSeam`
|
|
139
|
-
// altrove: viene chiamato subito dopo la scrittura e
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
136
|
+
// altrove: viene chiamato subito dopo la scrittura e rende deterministico il
|
|
137
|
+
// punto della corsa. Senza il seam, il test puo' osservare un file gia'
|
|
138
|
+
// esistente e non esercitare mai il percorso di CREAZIONE; il controllo
|
|
139
|
+
// negativo deve quindi fissare esplicitamente quel punto.
|
|
143
140
|
function ensureNodeKey({ home, keyPath, afterWriteSeam } = {}) {
|
|
144
141
|
const p = keyPath || keyPathFor(home);
|
|
145
142
|
const existing = readKeyFile(p);
|
package/lib/nodes/store.js
CHANGED
|
@@ -80,7 +80,7 @@ function isAbsPath(p) {
|
|
|
80
80
|
// token remoto: segreto opaco, single-line, cap. Vuoto -> assente (non salvato).
|
|
81
81
|
// Charset ristretto a header-safe (VCHAR + spazio/tab): il token viene iniettato
|
|
82
82
|
// in `Authorization: Bearer <t>` verso l'upstream; un char fuori range farebbe
|
|
83
|
-
// lanciare setHeader in modo sincrono (ERR_INVALID_CHAR). (hardening
|
|
83
|
+
// lanciare setHeader in modo sincrono (ERR_INVALID_CHAR). (hardening).
|
|
84
84
|
function validToken(t) {
|
|
85
85
|
return typeof t === 'string' && t.length > 0 && t.length <= MAX_TOKEN_LEN
|
|
86
86
|
&& /^[\x20-\x7e\t]+$/.test(t);
|
|
@@ -589,7 +589,7 @@ function initStore(p) {
|
|
|
589
589
|
}
|
|
590
590
|
|
|
591
591
|
// Compatibilita' API interna: il vecchio nome non inizializza piu' a runtime.
|
|
592
|
-
// Tenerlo strict impedisce che un call-site dimenticato reintroduca
|
|
592
|
+
// Tenerlo strict impedisce che un call-site dimenticato reintroduca quel difetto.
|
|
593
593
|
function loadOrInitStore(p) { return loadStoreStrict(p); }
|
|
594
594
|
|
|
595
595
|
// --- Mutazioni pure (ritornano un nuovo store; il caller lo scrive) ---------
|
|
@@ -21,7 +21,7 @@ const ownershipGraceMs = Number.isFinite(ownershipGraceRaw) && ownershipGraceRaw
|
|
|
21
21
|
const reverseFailureMaxRaw = Number(process.env.NEXUSCREW_TUNNEL_REVERSE_FAILURE_MAX || 8);
|
|
22
22
|
const reverseFailureMax = Number.isInteger(reverseFailureMaxRaw) && reverseFailureMaxRaw >= 1
|
|
23
23
|
? Math.min(reverseFailureMaxRaw, 32) : 8;
|
|
24
|
-
//
|
|
24
|
+
// da revisione — contratto per la sonda del canale -L quando non conclude MAI:
|
|
25
25
|
// continuare a sondare ogni 250ms e' giusto per la finestra transitoria (il
|
|
26
26
|
// servizio remoto non e' ancora su dopo un restart/aggiornamento: la prossima
|
|
27
27
|
// sonda e' una connessione FRESCA, si qualifica da sola appena il servizio
|
|
@@ -35,7 +35,7 @@ const reverseFailureMax = Number.isInteger(reverseFailureMaxRaw) && reverseFailu
|
|
|
35
35
|
// quella soglia il canale entra in degraded, ESATTAMENTE come il fallimento
|
|
36
36
|
// del forward inverso — stesso stato, stessa auto-guarigione a cadenza fissa,
|
|
37
37
|
// mai un "pronto" dichiarato senza averlo verificato (sarebbe la stessa bugia
|
|
38
|
-
// che
|
|
38
|
+
// che una revisione ha tolto, solo con una bandiera "non verificato" appesa sopra).
|
|
39
39
|
const channelProbeMaxRaw = Number(process.env.NEXUSCREW_TUNNEL_CHANNEL_PROBE_MAX || 60);
|
|
40
40
|
const channelProbeMax = Number.isInteger(channelProbeMaxRaw) && channelProbeMaxRaw >= 1
|
|
41
41
|
? Math.min(channelProbeMaxRaw, 240) : 60;
|
|
@@ -112,7 +112,7 @@ function clearForwardProbe() {
|
|
|
112
112
|
// forces OpenSSH to establish the real forward channel. Only that event may
|
|
113
113
|
// advertise transport-ready or reset retry backoff.
|
|
114
114
|
//
|
|
115
|
-
//
|
|
115
|
+
// da revisione: connect NON è prova di canale. OpenSSH accetta la TCP sul listener
|
|
116
116
|
// locale SUBITO e chiede il canale al server DOPO: se il server lo nega
|
|
117
117
|
// (permitopen senza quella destinazione) il socket viene CHIUSO nel giro di
|
|
118
118
|
// millisecondi — bind locale riuscito, canale morto. Una finestra di grazia
|
|
@@ -159,7 +159,7 @@ function probeForwardChannels({ ports, graceMs = CHANNEL_GRACE_MS, connect = net
|
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
//
|
|
162
|
+
// da revisione punto 3: chi ha installato la chiave quando il pannello non esisteva è
|
|
163
163
|
// rotto e NON lo sapeva. Il prodotto lo DICE, con la riga da sostituire: le
|
|
164
164
|
// destinazioni arrivano dagli `-L`, la pubblica si DERIVA dalla privata
|
|
165
165
|
// indicata da `-i`. Quando non si riesce a derivarla, dice comunque COSA
|
|
@@ -359,7 +359,7 @@ function enterDegraded(diagnosis) {
|
|
|
359
359
|
retryTimer = setTimeout(run, steadyRetryMs);
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
//
|
|
362
|
+
// secondo difetto della stessa famiglia (da revisione interna):
|
|
363
363
|
// enterDegraded per il canale -L riusava la macchina del forward inverso
|
|
364
364
|
// senza la sua precondizione implicita. Nel reverse failure `child` e' GIA'
|
|
365
365
|
// null quando enterDegraded gira (handleFailure lo azzera PRIMA, perche' e'
|
|
@@ -369,7 +369,7 @@ function enterDegraded(diagnosis) {
|
|
|
369
369
|
// generazione. run() faceva `child = spawn(...)` incondizionatamente: la
|
|
370
370
|
// vecchia generazione, ancora viva, restava senza piu' nessuna variabile che
|
|
371
371
|
// la referenzi — irraggiungibile da stop(), orfana, titolare dei suoi bind
|
|
372
|
-
// per sempre. Misurato
|
|
372
|
+
// per sempre. Misurato in revisione: due fake-ssh vivi dopo un degraded,
|
|
373
373
|
// SIGTERM al supervisor ne ferma solo l'ultimo.
|
|
374
374
|
//
|
|
375
375
|
// La correzione non e' locale (un kill dentro enterDegraded): e' che
|
|
@@ -525,6 +525,6 @@ function acquireGeneration() {
|
|
|
525
525
|
}
|
|
526
526
|
if (require.main === module) acquireGeneration();
|
|
527
527
|
|
|
528
|
-
// Esportate per prova diretta (
|
|
528
|
+
// Esportate per prova diretta (da revisione): il main resta argv/env-driven e NON parte
|
|
529
529
|
// al require.
|
|
530
530
|
module.exports = { probeForwardChannels, refusalHint, refusalDetails, CHANNEL_GRACE_MS };
|
package/lib/nodes/tunnel.js
CHANGED
|
@@ -480,7 +480,7 @@ function supervisorExited(pid, timeoutMs = 2500, impl = {}) {
|
|
|
480
480
|
// Pre-flight sincrono: l'unico modo di "surfaccare" un binario ssh assente come
|
|
481
481
|
// failure ESPLICITA nel valore di ritorno (lo spawn emette 'error' asincrono, non
|
|
482
482
|
// catturabile sync). spawnSyncImpl iniettabile per test deterministici.
|
|
483
|
-
// ssh -V esiste su ogni platform supportata; ENOENT = binario mancante (
|
|
483
|
+
// ssh -V esiste su ogni platform supportata; ENOENT = binario mancante (da revisione).
|
|
484
484
|
function sshBinaryAvailable(sshBin, spawnSyncImpl) {
|
|
485
485
|
try {
|
|
486
486
|
const r = (spawnSyncImpl || spawnSync)(sshBin, ['-V'], { encoding: 'utf8' });
|
|
@@ -493,7 +493,7 @@ function sshBinaryAvailable(sshBin, spawnSyncImpl) {
|
|
|
493
493
|
// nel logfile del tunnel; pidfile con cmd verificabile (kill anti PID-reuse).
|
|
494
494
|
// spawnImpl/spawnSyncImpl iniettabili per i test (mai lancia ssh vero in suite).
|
|
495
495
|
//
|
|
496
|
-
// INVARIANTI (
|
|
496
|
+
// INVARIANTI (da revisione):
|
|
497
497
|
// - MAI crashare: spawn throw, error async, pid mancante -> failure esplicita
|
|
498
498
|
// {started:false, reason}, con cleanup di pidfile/process/log fd di nostra proprieta'.
|
|
499
499
|
// - NESSUN leak del log fd aperto internamente: il padre chiude la SUA copia dopo
|
|
@@ -558,7 +558,7 @@ function startTunnel(opts) {
|
|
|
558
558
|
};
|
|
559
559
|
|
|
560
560
|
// Pre-flight: binario ssh mancante -> failure esplicita, nessuno spawn, nessun
|
|
561
|
-
// crash (
|
|
561
|
+
// crash (da revisione: prima restituiva started:true e poi l'event 'error' killava Node).
|
|
562
562
|
if (!sshBinaryAvailable(sshBin, spawnSyncImpl)) {
|
|
563
563
|
closeOwnedFd();
|
|
564
564
|
return { started: false, reason: 'ssh binary not found', sshBin };
|
|
@@ -630,7 +630,7 @@ function startTunnel(opts) {
|
|
|
630
630
|
if (Number.isInteger(logFd)) {
|
|
631
631
|
try { fs.writeSync(logFd, `[nexuscrew] supervisor requested transport=${path.basename(sshBin)}\n`); } catch (_) {}
|
|
632
632
|
}
|
|
633
|
-
closeOwnedFd(); // copia del padre: il figlio ha la sua dup; nessun leak (
|
|
633
|
+
closeOwnedFd(); // copia del padre: il figlio ha la sua dup; nessun leak (da revisione)
|
|
634
634
|
return { started: true, pid, logPath, transport: sshBin };
|
|
635
635
|
}
|
|
636
636
|
|
|
@@ -694,7 +694,7 @@ function readSshVersion(spawnSyncImpl) {
|
|
|
694
694
|
} catch (_) { return null; }
|
|
695
695
|
}
|
|
696
696
|
|
|
697
|
-
//
|
|
697
|
+
// da revisione: la riga authorized_keys con le destinazioni EXPLICITE dei -L. Una sola
|
|
698
698
|
// porta quando il pannello non è (ancora) noto: nodesAdd non può conoscerla,
|
|
699
699
|
// la annuncia il peer nel join — e allora la riga la emette CHI ha
|
|
700
700
|
// l'informazione (pairing, supervisor). Due permitopen distinti, MAI un
|
|
@@ -715,11 +715,10 @@ function authorizedKeysLine({ remotePort, panelRemotePort, pub } = {}) {
|
|
|
715
715
|
|
|
716
716
|
// LA PUBBLICA SI DERIVA DALLA PRIVATA, non si legge dal file accanto.
|
|
717
717
|
//
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
// chiave valida — non che sia LA chiave di `A`. Se `A.pub` e' stale, ripristinato
|
|
718
|
+
// Validare il nome dell'algoritmo, la struttura del blob o il risultato di
|
|
719
|
+
// `ssh-keygen -l` su `A.pub` prova soltanto che il file contiene UNA chiave
|
|
720
|
+
// valida — non che sia LA chiave derivata dalla privata di `A`. Se `A.pub` e'
|
|
721
|
+
// stale, ripristinato
|
|
723
722
|
// da un backup o sostituito, si pubblica la chiave sbagliata: l'utente
|
|
724
723
|
// sostituisce la riga di A con quella di B, e al reconnect successivo A perde
|
|
725
724
|
// l'accesso. Il prodotto avrebbe causato il guasto che prometteva di riparare.
|
|
@@ -733,7 +732,7 @@ function authorizedKeysLine({ remotePort, panelRemotePort, pub } = {}) {
|
|
|
733
732
|
// batch non si puo' sbloccare) o `ssh-keygen` non c'e': senza poterla derivare
|
|
734
733
|
// non si compone nessuna riga.
|
|
735
734
|
// Esiti ENUMERATI della derivazione della pubblica (difetto strutturale
|
|
736
|
-
// registrato
|
|
735
|
+
// registrato in revisione: `null | stringa` comprimeva stati DIVERSI, e il
|
|
737
736
|
// livello basso componeva frasi su fatti che non conosce — meta' della 0.9.5
|
|
738
737
|
// e' nata li'). Gli esiti sono DATI, senza testo per l'utente: le frasi si
|
|
739
738
|
// compongono al confine UI/log, mai qui. Si aggancia a P4: una chiave cifrata
|
package/lib/notify/asks.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
// Stato in-memory + persistenza <dir>/asks.json 0600 (sopravvive al restart:
|
|
4
4
|
// un ask aperto resta risponibile). Lo store e' PURO stato: il paste tmux e la
|
|
5
5
|
// notify li orchestra la route (lib/notify/routes.js), che qui fa il ciclo
|
|
6
|
-
// claim -> (paste) -> commit/release (
|
|
6
|
+
// claim -> (paste) -> commit/release (da revisione: una sola answer puo' vincere).
|
|
7
7
|
const path = require('node:path');
|
|
8
8
|
const crypto = require('node:crypto');
|
|
9
9
|
const { readJsonSafe, atomicWriteJson } = require('./persist.js');
|
|
10
10
|
|
|
11
11
|
const ASKS_FILE = 'asks.json';
|
|
12
|
-
//
|
|
12
|
+
// da revisione: MAX_OPEN e' un cap DURO sugli ask aperti — al cap il nuovo ask viene
|
|
13
13
|
// RIFIUTATO (reason 'cap'), mai droppato uno aperto. MAX_KEEP pota solo gli
|
|
14
14
|
// answered piu' vecchi dal file.
|
|
15
15
|
const MAX_OPEN = 100;
|
|
@@ -72,7 +72,7 @@ function createAsksStore(opts = {}) {
|
|
|
72
72
|
function create({ question, options, session }) {
|
|
73
73
|
const v = validate({ question, options });
|
|
74
74
|
if (!v.ok) return { ok: false, reason: 'invalid', error: v.error };
|
|
75
|
-
//
|
|
75
|
+
// da revisione: cap duro sugli aperti — rifiuto esplicito, MAI drop di ask aperti.
|
|
76
76
|
if (openCount() >= MAX_OPEN) {
|
|
77
77
|
return {
|
|
78
78
|
ok: false,
|
|
@@ -103,7 +103,7 @@ function createAsksStore(opts = {}) {
|
|
|
103
103
|
.map((a) => ({ ...a, ...(a.options ? { options: a.options.slice() } : {}) }));
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
// --- ciclo answer
|
|
106
|
+
// --- ciclo answer (da revisione): claim atomico open -> answering ---------------
|
|
107
107
|
// Node e' single-threaded ma il paste e' un await: due answer concorrenti
|
|
108
108
|
// superavano entrambe il check `answered` prima che una marcasse. Il claim
|
|
109
109
|
// sincrono (nessun await tra check e set) fa vincere UNA sola richiesta; le
|
package/lib/notify/persist.js
CHANGED
|
@@ -6,7 +6,7 @@ const fs = require('node:fs');
|
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const crypto = require('node:crypto');
|
|
8
8
|
|
|
9
|
-
// Legge un JSON (oggetto) da file segreto. Fail-closed (
|
|
9
|
+
// Legge un JSON (oggetto) da file segreto. Fail-closed (da revisione): open
|
|
10
10
|
// O_NOFOLLOW + fstat sullo STESSO fd (niente race lstat->open), e un file
|
|
11
11
|
// preesistente con symlink, tipo non-regolare, owner inatteso o permessi di
|
|
12
12
|
// gruppo/altri viene RIFIUTATO con errore chiaro — mai riparato in silenzio
|
package/lib/notify/push.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Il sender e' iniettabile (webpushImpl) cosi' i test non toccano MAI la rete;
|
|
8
8
|
// la chiave PRIVATA non esce da questo modulo.
|
|
9
9
|
//
|
|
10
|
-
//
|
|
10
|
+
// da revisione (threat model SSRF): sendNotification fa una richiesta server-side.
|
|
11
11
|
// Ogni endpoint e' https-only, viene risolto sia al subscribe sia immediatamente
|
|
12
12
|
// prima del send, e la richiesta usa un https.Agent con lookup PINNATO agli IP
|
|
13
13
|
// verificati. Cosi' un secondo lookup/DNS rebinding non puo' cambiare destinazione.
|
|
@@ -168,7 +168,7 @@ function createPushService(opts = {}) {
|
|
|
168
168
|
if (!opts.dir) throw new Error('createPushService: dir richiesta');
|
|
169
169
|
const vapidPath = path.join(opts.dir, VAPID_FILE);
|
|
170
170
|
const subsPath = path.join(opts.dir, SUBS_FILE);
|
|
171
|
-
//
|
|
171
|
+
// da revisione: closure iniettata dal server — in READONLY questo modulo non scrive MAI
|
|
172
172
|
// (niente generazione VAPID, niente cleanup subscription).
|
|
173
173
|
const readonly = typeof opts.readonly === 'function' ? opts.readonly : () => false;
|
|
174
174
|
const lookupImpl = opts.lookupImpl || dns.promises.lookup;
|
|
@@ -191,7 +191,7 @@ function createPushService(opts = {}) {
|
|
|
191
191
|
vapid = { publicKey: cur.publicKey, privateKey: cur.privateKey };
|
|
192
192
|
return vapid;
|
|
193
193
|
}
|
|
194
|
-
//
|
|
194
|
+
// da revisione: la generazione e' una SCRITTURA — vietata in READONLY (503 esplicito,
|
|
195
195
|
// niente vapid.json fantasma).
|
|
196
196
|
if (readonly()) {
|
|
197
197
|
const e = new Error('READONLY: chiavi VAPID assenti e non generabili (riavvia senza READONLY per il primo setup push)');
|
|
@@ -241,7 +241,7 @@ function createPushService(opts = {}) {
|
|
|
241
241
|
function count() { return readSubs().length; }
|
|
242
242
|
|
|
243
243
|
// Invia il payload a tutte le subscription. 404/410 (endpoint morto) -> la
|
|
244
|
-
// subscription si rimuove, MA NON in READONLY (
|
|
244
|
+
// subscription si rimuove, MA NON in READONLY (da revisione: riscrivere push.json e' una
|
|
245
245
|
// scrittura persistente; la notify viene comunque consegnata alle vive).
|
|
246
246
|
// Nessuna subscription -> {sent:0} senza generare VAPID (resta lazy davvero).
|
|
247
247
|
async function sendToAll(payload) {
|
package/lib/notify/routes.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
// Route del MCP bridge (design §2): notify, web-push, asks. Montate dentro il
|
|
3
3
|
// router /api gia' dietro requireToken (server.js) — qui restano solo:
|
|
4
|
-
// - READONLY come FLOOR (
|
|
4
|
+
// - READONLY come FLOOR (da revisione): sono gated 403 TUTTI i mutanti — answer
|
|
5
5
|
// (scrittura PTY via paste), push subscribe/unsubscribe (push.json) e la
|
|
6
6
|
// CREAZIONE di ask (persiste asks.json e genererebbe domande non
|
|
7
7
|
// risponibili dallo stesso server). L'UNICA eccezione dichiarata e' la
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
// push senza persistenza; anche il cleanup delle subscription morte e'
|
|
10
10
|
// sospeso in READONLY, vedi lib/notify/push.js). Le GET restano lettura
|
|
11
11
|
// pura: /push/vapid in READONLY non genera chiavi (503 se assenti).
|
|
12
|
-
// - rate-limit (
|
|
12
|
+
// - rate-limit (da revisione): il campo `session` e' dichiarato dal chiamante e
|
|
13
13
|
// NON e' un confine di sicurezza — il limite che conta e' GLOBALE per
|
|
14
14
|
// principal/token (un Bearer = un'installazione); il bucket per-sessione
|
|
15
15
|
// resta come fairness tra celle oneste. Stessa coppia di limiti sulla
|
|
16
|
-
// creazione ask (
|
|
16
|
+
// creazione ask (da revisione).
|
|
17
17
|
// - validazione input strict fail-closed (schema chiuso per ogni body).
|
|
18
18
|
// Il paste della risposta riusa ESATTAMENTE pasteToSession (bracketed literal,
|
|
19
19
|
// niente Invio, control char rifiutati): qui si sanifica il testo PRIMA.
|
|
@@ -43,7 +43,7 @@ const MAX_BODY = 2000;
|
|
|
43
43
|
const MAX_ANSWER = 3900;
|
|
44
44
|
const MAX_REPLY_LABEL = 48;
|
|
45
45
|
|
|
46
|
-
// Sliding window in-memory per chiave. La mappa ha un cap duro (
|
|
46
|
+
// Sliding window in-memory per chiave. La mappa ha un cap duro (da revisione): entry
|
|
47
47
|
// scadute potate a ogni giro, poi evizione LRU deterministica (ordine di
|
|
48
48
|
// iterazione della Map = ordine di ultimo uso, re-insert ad ogni allow).
|
|
49
49
|
// NB: l'evizione azzera il conteggio del bucket evitto — per questo il cap
|
|
@@ -68,7 +68,7 @@ function createRateLimiter({ max = RATE_MAX, windowMs = RATE_WINDOW_MS, maxBucke
|
|
|
68
68
|
return { allow, size: () => hits.size };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// Coppia di limiti
|
|
71
|
+
// Coppia di limiti (da revisione): globale per token (confine di sicurezza) + per
|
|
72
72
|
// sessione (fairness). Una richiesta oltre-limite consuma comunque il budget
|
|
73
73
|
// globale: anche lo spam rifiutato e' attivita' del principal.
|
|
74
74
|
function createSenderLimiter(rateCfg = {}) {
|
|
@@ -189,7 +189,7 @@ function notifyRoutes({
|
|
|
189
189
|
// R31-A3: lo status e' DERIVATO dai conteggi, non dichiarato a parte.
|
|
190
190
|
// `emit` e' best-effort — push fallito → 0, `ui` conta i write SSE
|
|
191
191
|
// riusciti — e il dispatcher propaga SOLO l'etichetta (i conteggi
|
|
192
|
-
// muoiono in forward(), rilievo
|
|
192
|
+
// muoiono in forward(), rilievo da revisione): per la cella mittente e'
|
|
193
193
|
// tutta l'informazione. Non puo' affermare una consegna che i conteggi
|
|
194
194
|
// smentiscono: zero canali raggiunti → 'no-delivery'.
|
|
195
195
|
const status = delivered.ui + delivered.push > 0 ? 'delivered' : 'no-delivery';
|
|
@@ -222,7 +222,7 @@ function notifyRoutes({
|
|
|
222
222
|
|
|
223
223
|
// --- web-push --------------------------------------------------------------
|
|
224
224
|
// GET lettura pura; in READONLY push.vapidPublicKey() NON genera chiavi e
|
|
225
|
-
// segnala 503 (e.status) se assenti —
|
|
225
|
+
// segnala 503 (e.status) se assenti — da revisione.
|
|
226
226
|
r.get('/push/vapid', (_req, res) => {
|
|
227
227
|
try { res.json({ publicKey: push.vapidPublicKey() }); }
|
|
228
228
|
catch (e) { res.status(e.status || 500).json({ error: String(e.message || e) }); }
|
|
@@ -232,7 +232,7 @@ function notifyRoutes({
|
|
|
232
232
|
try {
|
|
233
233
|
const sub = req.body && req.body.subscription;
|
|
234
234
|
const out = await push.subscribe(sub);
|
|
235
|
-
//
|
|
235
|
+
// da revisione: cap sul numero di subscription -> 429 (quota), input invalido -> 400.
|
|
236
236
|
if (!out.ok) return res.status(out.reason === 'cap' ? 429 : 400).json({ error: out.error });
|
|
237
237
|
res.json({ subscribed: true, count: out.count });
|
|
238
238
|
} catch (e) { res.status(500).json({ error: String(e.message || e) }); }
|
|
@@ -247,8 +247,8 @@ function notifyRoutes({
|
|
|
247
247
|
});
|
|
248
248
|
|
|
249
249
|
// --- asks ------------------------------------------------------------------
|
|
250
|
-
//
|
|
251
|
-
// che lo stesso server vieterebbe di rispondere.
|
|
250
|
+
// da revisione: gated READONLY (mutGate) — crea stato durevole (asks.json) e domande
|
|
251
|
+
// che lo stesso server vieterebbe di rispondere. da revisione: rate-limit creazione
|
|
252
252
|
// (globale per token + per sessione) + cap duro dello store -> 429.
|
|
253
253
|
r.post('/asks', mutGate, json, async (req, res) => {
|
|
254
254
|
try {
|
|
@@ -294,7 +294,7 @@ function notifyRoutes({
|
|
|
294
294
|
});
|
|
295
295
|
|
|
296
296
|
// Dismiss (scarta domanda): NON cancella la riga, la marca `dismissed` (lo
|
|
297
|
-
// storico serve). Stesso mutGate degli altri mutanti (
|
|
297
|
+
// storico serve). Stesso mutGate degli altri mutanti (da revisione: scrittura durevole).
|
|
298
298
|
// Idempotente; 404 se id inesistente; 409 se answering (claim attivo: non si
|
|
299
299
|
// scarta una risposta in corso). Emette il frame per le UI aperte come fa
|
|
300
300
|
// POST /asks con emitRaw: la card sparisce senza aspettare il poll.
|
|
@@ -312,7 +312,7 @@ function notifyRoutes({
|
|
|
312
312
|
} catch (e) { res.status(500).json({ error: String(e.message || e) }); }
|
|
313
313
|
});
|
|
314
314
|
|
|
315
|
-
// Answer: READONLY floor (il paste e' una scrittura PTY).
|
|
315
|
+
// Answer: READONLY floor (il paste e' una scrittura PTY). da revisione: il
|
|
316
316
|
// ciclo e' claim atomico (open -> answering, sincrono, PRIMA dell'await del
|
|
317
317
|
// paste) -> paste -> commit su successo / release su fallimento. Una sola
|
|
318
318
|
// richiesta concorrente vince; le altre vedono 409. Paste fallito -> 502 e
|