@mmmbuto/nexuscrew 0.8.23 → 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 +7 -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 +16 -2
- package/lib/nodes/store.js +40 -1
- 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/package.json +1 -1
- package/frontend/dist/assets/index-C0QL3gEp.js +0 -91
package/frontend/dist/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="NexusCrew" />
|
|
12
12
|
<link rel="manifest" href="/manifest.json" />
|
|
13
13
|
<title>NexusCrew</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-CU9OOtL-.js"></script>
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-77r8nzQf.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.8.
|
|
1
|
+
{"version":"0.8.24"}
|
package/lib/cli/commands.js
CHANGED
|
@@ -31,6 +31,7 @@ const HELP = `NexusCrew — PWA for local and remote AI workers.
|
|
|
31
31
|
|
|
32
32
|
Usage:
|
|
33
33
|
nexuscrew start in background; show status and quick guide
|
|
34
|
+
nexuscrew init initialize missing stores idempotently
|
|
34
35
|
nexuscrew show start when needed and open the authenticated PWA
|
|
35
36
|
nexuscrew show token print the clickable authenticated URL
|
|
36
37
|
nexuscrew boot enable startup at boot (use: boot off|status)
|
|
@@ -66,6 +67,7 @@ Usage:
|
|
|
66
67
|
[--label TEXT] [--json]
|
|
67
68
|
printf '%s' "$PAIRING_URL" | nexuscrew nodes pair|join
|
|
68
69
|
[--ssh TARGET] [--name SLUG] [--label TEXT]
|
|
70
|
+
[--local-name SLUG] [--local-label TEXT]
|
|
69
71
|
nexuscrew nodes identity [--json]
|
|
70
72
|
|
|
71
73
|
Pairing links are read from stdin, never from argv. Mutations honor
|
|
@@ -79,7 +81,7 @@ Lifecycle semantics:
|
|
|
79
81
|
|
|
80
82
|
const CLI_VALUE_FLAGS = new Set([
|
|
81
83
|
'label', 'ssh', 'ssh-port', 'autostart', 'visibility', 'selected',
|
|
82
|
-
'name', 'local-label', 'identity-file',
|
|
84
|
+
'name', 'local-name', 'local-label', 'identity-file', 'port',
|
|
83
85
|
]);
|
|
84
86
|
|
|
85
87
|
// valueFlags (Set|array opzionale): flag che consumano il token successivo nella
|
|
@@ -846,6 +848,7 @@ async function dispatchNodes(rest, flags, opts = {}) {
|
|
|
846
848
|
...(flags.label !== undefined || decoded.label ? { label: flags.label || decoded.label } : {}),
|
|
847
849
|
...(flags['ssh-port'] !== undefined || decoded.sshPort ? { sshPort: Number(flags['ssh-port'] || decoded.sshPort) } : {}),
|
|
848
850
|
...(flags['local-label'] !== undefined ? { localLabel: flags['local-label'] } : {}),
|
|
851
|
+
...(flags['local-name'] !== undefined ? { localName: flags['local-name'] } : {}),
|
|
849
852
|
...(flags['identity-file'] !== undefined ? { identityFile: flags['identity-file'] } : {}),
|
|
850
853
|
};
|
|
851
854
|
if (!body.name || !body.ssh) {
|
|
@@ -886,7 +889,7 @@ function dispatch(argv, opts = {}) {
|
|
|
886
889
|
log(HELP);
|
|
887
890
|
return { code: 0 };
|
|
888
891
|
}
|
|
889
|
-
// Public surface: normal start, show, boot, doctor, help, version.
|
|
892
|
+
// Public surface: normal start, init, show, boot, doctor, help, version.
|
|
890
893
|
if (!cmd) {
|
|
891
894
|
if (Object.keys(flags).length) {
|
|
892
895
|
log(`unknown option: --${Object.keys(flags)[0]}\n\n${HELP}`);
|
|
@@ -904,6 +907,17 @@ function dispatch(argv, opts = {}) {
|
|
|
904
907
|
if (rest[1]) { log('usage: nexuscrew show [token]'); return { code: 1 }; }
|
|
905
908
|
return smartUp({ ...opts, forceOpen: true, log: opts.lifecycleLog || (() => {}) }).then(() => ({ code: 0 }));
|
|
906
909
|
}
|
|
910
|
+
if (cmd === 'init') {
|
|
911
|
+
const port = flags.port === undefined ? undefined : Number(flags.port);
|
|
912
|
+
if (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65535)) {
|
|
913
|
+
log('usage: nexuscrew init [--dry-run] [--port PORT]');
|
|
914
|
+
return { code: 1 };
|
|
915
|
+
}
|
|
916
|
+
(opts.runInitImpl || runInit)({
|
|
917
|
+
...opts, dryRun: flags['dry-run'] === true, port, log,
|
|
918
|
+
});
|
|
919
|
+
return { code: 0 };
|
|
920
|
+
}
|
|
907
921
|
if (cmd === 'boot') {
|
|
908
922
|
const sub = rest[1] || 'on';
|
|
909
923
|
if (sub === 'status') {
|
package/lib/nodes/store.js
CHANGED
|
@@ -520,6 +520,45 @@ function toSlug(input) {
|
|
|
520
520
|
return s || 'node';
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
+
// Handle tecnico locale proposto durante il pairing. L'identita' resta il
|
|
524
|
+
// nodeId; questo slug e' soltanto il segmento route leggibile visto dai peer.
|
|
525
|
+
// Il suffisso deriva dal nodeId stabile, quindi due Termux con hostname
|
|
526
|
+
// "localhost" ottengono handle diversi e ogni retry dello stesso device
|
|
527
|
+
// ripropone esattamente lo stesso valore (nessun random per tentativo).
|
|
528
|
+
function deriveNodeHandle(label, hostname, nodeId, existing = []) {
|
|
529
|
+
const id = String(nodeId || '').toLowerCase();
|
|
530
|
+
if (!NODE_ID_RE.test(id)) throw new Error('nodeId non valido per derivare il route handle');
|
|
531
|
+
const used = new Set(Array.isArray(existing) ? existing.filter((x) => typeof x === 'string') : []);
|
|
532
|
+
const readable = String(label || hostname || 'NexusCrew').replace(/([a-z0-9])([A-Z])/g, '$1 $2');
|
|
533
|
+
let base = toSlug(readable);
|
|
534
|
+
if (base === 'localhost' || base === 'node') base = 'nexuscrew';
|
|
535
|
+
|
|
536
|
+
// Se il chiamante ripropone un handle gia' suffissato (es. dopo un 409),
|
|
537
|
+
// evita di produrre asus-5bd6-5bd6 quando rigeneriamo un suffisso piu lungo.
|
|
538
|
+
for (const length of [32, 24, 16, 12, 8, 6, 4]) {
|
|
539
|
+
const suffix = `-${id.slice(0, length)}`;
|
|
540
|
+
if (base.endsWith(suffix) && base.length > suffix.length) {
|
|
541
|
+
base = base.slice(0, -suffix.length).replace(/-+$/g, '') || 'nexuscrew';
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const candidate = (suffix) => {
|
|
547
|
+
const room = Math.max(1, 32 - suffix.length - 1);
|
|
548
|
+
const prefix = base.slice(0, room).replace(/-+$/g, '') || 'n';
|
|
549
|
+
return `${prefix}-${suffix}`;
|
|
550
|
+
};
|
|
551
|
+
for (const length of [4, 6, 8, 12, 16, 24]) {
|
|
552
|
+
const value = candidate(id.slice(0, length));
|
|
553
|
+
if (!used.has(value)) return value;
|
|
554
|
+
}
|
|
555
|
+
for (let n = 2; n < 100; n += 1) {
|
|
556
|
+
const value = candidate(`${id.slice(0, 8)}-${n}`);
|
|
557
|
+
if (!used.has(value)) return value;
|
|
558
|
+
}
|
|
559
|
+
throw new Error('nessun route handle univoco disponibile per nodeId');
|
|
560
|
+
}
|
|
561
|
+
|
|
523
562
|
// suggestNodeName: slug univoco dato un input libero e l'elenco dei name gia'
|
|
524
563
|
// usati. Disambigua con suffisso -2/-3/... Rende la creazione di un nodo non
|
|
525
564
|
// fallibile per collisione di slug (l'utente scrive "Home Relay" e ottiene
|
|
@@ -547,7 +586,7 @@ module.exports = {
|
|
|
547
586
|
// migrazione
|
|
548
587
|
migrateLegacyNodes,
|
|
549
588
|
// label / slug
|
|
550
|
-
nodeLabel, validLabel, sanitizeLabel, toSlug, suggestNodeName, LABEL_MAX,
|
|
589
|
+
nodeLabel, validLabel, sanitizeLabel, toSlug, deriveNodeHandle, suggestNodeName, LABEL_MAX,
|
|
551
590
|
// costanti
|
|
552
591
|
SCHEMA_VERSION, LEGACY_SCHEMA_VERSION, MAX_NODES, MAX_TOKEN_LEN, NODE_NAME_RE, NODE_ID_RE,
|
|
553
592
|
};
|
|
@@ -43,6 +43,7 @@ function createPairHandler(deps) {
|
|
|
43
43
|
send, validName, defaultDeviceName,
|
|
44
44
|
nodesPath, configPath, home, seams, runtimePort,
|
|
45
45
|
} = deps;
|
|
46
|
+
const localHostname = typeof deps.localHostname === 'function' ? deps.localHostname : os.hostname;
|
|
46
47
|
|
|
47
48
|
return async (req, res) => {
|
|
48
49
|
const b = req.body || {};
|
|
@@ -60,6 +61,9 @@ function createPairHandler(deps) {
|
|
|
60
61
|
if (b.identityFile !== undefined && !nodesStore.isAbsPath(b.identityFile)) return fail(400, 'validation', 'bad-identity-file', 'identityFile non valido (path assoluto)', { retryable: true });
|
|
61
62
|
if (b.label !== undefined && !nodesStore.validLabel(b.label)) return fail(400, 'validation', 'bad-label', 'label non valida (max 64 char, niente a capo)', { retryable: true });
|
|
62
63
|
if (b.localLabel !== undefined && !nodesStore.validLabel(b.localLabel)) return fail(400, 'validation', 'bad-label', 'localLabel non valida (max 64 char, niente a capo)', { retryable: true });
|
|
64
|
+
if (b.localName !== undefined && (!validName(b.localName) || b.localName === 'localhost')) {
|
|
65
|
+
return fail(400, 'validation', 'bad-local-name', 'localName non valido (slug univoco a-z, 0-9, -, max 32; localhost non ammesso)', { retryable: true });
|
|
66
|
+
}
|
|
63
67
|
// label umana del peer come lo vedro' io (display); se assente usa lo slug.
|
|
64
68
|
const peerLabel = nodesStore.sanitizeLabel(b.label, b.name);
|
|
65
69
|
// etichetta umana con cui il peer vedra' questo dispositivo; default sensato.
|
|
@@ -118,6 +122,12 @@ function createPairHandler(deps) {
|
|
|
118
122
|
try {
|
|
119
123
|
// --- conflict: il nome non deve gia' esistere --------------------------
|
|
120
124
|
let st = nodesStore.loadStoreStrict(nodesPath);
|
|
125
|
+
// Backcompat per client 0.8.23 iniziali: se localName manca, derivarlo da
|
|
126
|
+
// label/hostname + suffisso nodeId stabile. Mai hostname puro o random per
|
|
127
|
+
// retry; il nodeId resta l'identita', questo e' soltanto il route handle.
|
|
128
|
+
const localName = b.localName || nodesStore.deriveNodeHandle(
|
|
129
|
+
localLabel, localHostname(), st.nodeId,
|
|
130
|
+
);
|
|
121
131
|
if (st.nodes.some((n) => n.name === b.name)) {
|
|
122
132
|
return fail(409, 'conflict', 'name-exists', `nodo "${b.name}" gia' presente`, {
|
|
123
133
|
retryable: true, hint: 'scegli un altro nome nelle opzioni avanzate e riprova',
|
|
@@ -206,7 +216,7 @@ function createPairHandler(deps) {
|
|
|
206
216
|
body: JSON.stringify({
|
|
207
217
|
invite: pair.invite,
|
|
208
218
|
instanceId: st.nodeId,
|
|
209
|
-
name:
|
|
219
|
+
name: localName,
|
|
210
220
|
label: localLabel,
|
|
211
221
|
port: runtimePort(),
|
|
212
222
|
acceptToken,
|
|
@@ -230,6 +240,17 @@ function createPairHandler(deps) {
|
|
|
230
240
|
hint: 'rigenera un nuovo link sul dispositivo che invita',
|
|
231
241
|
});
|
|
232
242
|
}
|
|
243
|
+
if (jr.status === 409 && joined.code === 'peer-name-conflict') {
|
|
244
|
+
const suggestedName = validName(joined.suggestedName) && joined.suggestedName !== 'localhost'
|
|
245
|
+
? joined.suggestedName
|
|
246
|
+
: nodesStore.deriveNodeHandle(localLabel, localHostname(), st.nodeId, [localName]);
|
|
247
|
+
return failRolledBack(409, 'conflict', 'peer-name-conflict',
|
|
248
|
+
joined.error || `nome peer gia' in uso: ${localName}`, {
|
|
249
|
+
retryable: true,
|
|
250
|
+
suggestedName,
|
|
251
|
+
hint: `usa l'handle locale proposto "${suggestedName}" e riprova: l'invito non e' stato consumato`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
233
254
|
if (!jr.ok) {
|
|
234
255
|
return failRolledBack(502, 'join', 'join-rejected', joined.error || `join rifiutato dal peer (HTTP ${jr.status})`, {
|
|
235
256
|
hint: 'rigenera un nuovo link e riprova',
|
|
@@ -75,11 +75,20 @@ function publicPeeringRoutes(deps = {}) {
|
|
|
75
75
|
if (st.nodeId === b.instanceId || st.nodes.some((n) => n.nodeId === b.instanceId)
|
|
76
76
|
|| pending.some((row) => row.instanceId === b.instanceId)) return res.status(409).json({ error: 'peer duplicato' });
|
|
77
77
|
const reservedNames = new Set(pending.map((row) => row.name).filter(validPeerName));
|
|
78
|
-
|
|
78
|
+
// `localhost` e' sintatticamente uno slug valido, ma su Termux non e'
|
|
79
|
+
// un handle di route: piu device espongono lo stesso hostname. Trattalo
|
|
80
|
+
// come conflitto risolvibile anche quando e' il primo, senza consumare
|
|
81
|
+
// l'invite; il client puo' riprovare col suffisso stabile proposto.
|
|
82
|
+
if (b.name === 'localhost' || nodesStore.getNode(st, b.name) || reservedNames.has(b.name)) {
|
|
83
|
+
const usedNames = [...st.nodes.map((node) => node.name), ...reservedNames];
|
|
84
|
+
const suggestedName = nodesStore.deriveNodeHandle(
|
|
85
|
+
b.label || b.name, b.name, b.instanceId, usedNames,
|
|
86
|
+
);
|
|
79
87
|
return res.status(409).json({
|
|
80
88
|
error: `nome peer gia' in uso: ${b.name}`,
|
|
81
89
|
code: 'peer-name-conflict',
|
|
82
|
-
|
|
90
|
+
suggestedName,
|
|
91
|
+
hint: `usa l'handle proposto "${suggestedName}" e riprova con lo stesso invito`,
|
|
83
92
|
});
|
|
84
93
|
}
|
|
85
94
|
const name = b.name;
|
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) --------------------
|