@mmmbuto/nexuscrew 0.9.65 → 0.9.67
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 +24 -0
- package/docs/NODES.md +55 -0
- package/docs/SECURITY.md +40 -0
- package/frontend/dist/assets/{index-DkYRC1vV.js → index-D__76yWR.js} +42 -42
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/audio/dispatch.js +14 -1
- package/lib/audio/routes.js +2 -2
- package/lib/cli/commands.js +31 -0
- package/lib/live-host/federation.js +10 -0
- package/lib/live-host/routes.js +12 -2
- package/lib/nodes/access-presets.js +27 -7
- package/lib/nodes/commands.js +63 -4
- package/lib/nodes/reverse-slot-listeners.js +4 -1
- package/lib/nodes/store.js +205 -12
- package/lib/notify/ask-cell.js +53 -0
- package/lib/notify/event-feed-client.js +40 -2
- package/lib/notify/event-feed-routes.js +8 -2
- package/lib/proxy/federation.js +308 -17
- package/lib/proxy/node-proxy.js +86 -3
- package/lib/server.js +126 -12
- package/lib/settings/routes.js +6 -0
- package/package.json +1 -1
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-D__76yWR.js"></script>
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-Dwd37Z_R.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.9.
|
|
1
|
+
{"version":"0.9.67"}
|
package/lib/audio/dispatch.js
CHANGED
|
@@ -30,6 +30,10 @@ function createDispatcher(opts = {}) {
|
|
|
30
30
|
// stesso percorso e rispondono `delivered`. Il default resta quello audio,
|
|
31
31
|
// cosi' nessun chiamante esistente cambia comportamento.
|
|
32
32
|
const statuses = opts.statuses instanceof Set ? opts.statuses : PER_ENDPOINT_STATUS;
|
|
33
|
+
// Policy for requests that arrived FEDERATED and terminate here: re-read on the
|
|
34
|
+
// resolution used for the I/O, just before the request leaves. Local callers are
|
|
35
|
+
// never subject to it.
|
|
36
|
+
const federatedGuard = typeof opts.federatedGuard === 'function' ? opts.federatedGuard : null;
|
|
33
37
|
|
|
34
38
|
function isLocal(target) {
|
|
35
39
|
const self = localNodeId();
|
|
@@ -143,10 +147,19 @@ function createDispatcher(opts = {}) {
|
|
|
143
147
|
resolveRoute,
|
|
144
148
|
probeCapability,
|
|
145
149
|
// dispatch(): unico ingresso per un target remoto.
|
|
146
|
-
async dispatch({ resource, target, origin, payload }) {
|
|
150
|
+
async dispatch({ resource, target, origin, payload, federated = false }) {
|
|
151
|
+
// What the policy says when the request begins counts too: a publication that ends while the topology is
|
|
152
|
+
// awaited does not turn the request into an allowed one.
|
|
153
|
+
let deniedBefore = false;
|
|
154
|
+
if (federated && federatedGuard) { try { deniedBefore = federatedGuard({ target, route: [] }) !== false; } catch (_) { deniedBefore = true; } }
|
|
147
155
|
const resolved = await resolveRoute(target);
|
|
148
156
|
if (resolved.error === 'invalid-target') return { status: 'refused', reason: 'invalid-target' };
|
|
149
157
|
if (resolved.error) return { status: 'unreachable', reason: resolved.error };
|
|
158
|
+
if (federated && federatedGuard) {
|
|
159
|
+
let denied = true;
|
|
160
|
+
try { denied = federatedGuard({ target, route: resolved.route, peer: resolved.peer }) !== false; } catch (_) { denied = true; }
|
|
161
|
+
if (denied || deniedBefore) return { status: 'refused', reason: 'host-only-target' };
|
|
162
|
+
}
|
|
150
163
|
return forward(resource, resolved.route, {
|
|
151
164
|
...payload,
|
|
152
165
|
target,
|
package/lib/audio/routes.js
CHANGED
|
@@ -251,7 +251,7 @@ function audioRoutes(deps = {}) {
|
|
|
251
251
|
origin, target, status: 'accepted', utteranceId, attested: trust === 'federated',
|
|
252
252
|
});
|
|
253
253
|
const out = await dispatcher.dispatch({
|
|
254
|
-
resource: '/audio/speak', target, origin,
|
|
254
|
+
resource: '/audio/speak', target, origin, federated: trust === 'federated',
|
|
255
255
|
payload: {
|
|
256
256
|
text: parsed.text, lang: parsed.lang, voice: parsed.voice, urgency: parsed.urgency,
|
|
257
257
|
utteranceId: local.utteranceId,
|
|
@@ -285,7 +285,7 @@ function audioRoutes(deps = {}) {
|
|
|
285
285
|
const { target } = parsed;
|
|
286
286
|
if (dispatcher && isValidInstance(target) && !dispatcher.isLocal(target)) {
|
|
287
287
|
const out = await dispatcher.dispatch({
|
|
288
|
-
resource: '/audio/stop', target, origin,
|
|
288
|
+
resource: '/audio/stop', target, origin, federated: resolved.trust === 'federated',
|
|
289
289
|
payload: { ...(parsed.utteranceId ? { utteranceId: parsed.utteranceId } : {}) },
|
|
290
290
|
});
|
|
291
291
|
return send(res, out.status === 'accepted' ? 200 : out.status === 'unreachable' ? 503 : 422, out);
|
package/lib/cli/commands.js
CHANGED
|
@@ -85,6 +85,8 @@ Usage:
|
|
|
85
85
|
nexuscrew nodes cells <name|nodeId> all|none|Cella1,Cella2
|
|
86
86
|
[--selected NODE_ID,...]
|
|
87
87
|
nexuscrew nodes share <name|nodeId> on|off [--json]
|
|
88
|
+
nexuscrew nodes publish-remote <name|nodeId> on --audience <instanceId,...> [--json]
|
|
89
|
+
nexuscrew nodes publish-remote <name|nodeId> off|status [--json]
|
|
88
90
|
nexuscrew nodes panel <name|nodeId> on|off
|
|
89
91
|
nexuscrew nodes live-host <name|nodeId> on|off
|
|
90
92
|
nexuscrew nodes edit <name|nodeId> --access-role admin|user|nexushost
|
|
@@ -118,6 +120,7 @@ const CLI_VALUE_FLAGS = new Set([
|
|
|
118
120
|
'name', 'local-name', 'local-label', 'identity-file', 'port', 'dir', 'events-receive',
|
|
119
121
|
'access-role', 'access-revision',
|
|
120
122
|
'events-access', 'node-events-access', 'ask-reply-access', 'files-read-access', 'peer-operator-access',
|
|
123
|
+
'audience',
|
|
121
124
|
]);
|
|
122
125
|
|
|
123
126
|
// valueFlags (Set|array opzionale): flag che consumano il token successivo nella
|
|
@@ -1367,6 +1370,34 @@ async function dispatchNodes(rest, flags, opts = {}) {
|
|
|
1367
1370
|
return { code: 0 };
|
|
1368
1371
|
} catch (error) { log(`nodes share: ${error.message}`); return { code: 1 }; }
|
|
1369
1372
|
}
|
|
1373
|
+
// Host-only publication of a remote peer, one way, to an explicit audience of
|
|
1374
|
+
// our own admin nodes. NOT Share: it never calls the share path.
|
|
1375
|
+
if (sub === 'publish-remote') {
|
|
1376
|
+
const arg = rest[3];
|
|
1377
|
+
if (arg !== undefined && arg !== 'on' && arg !== 'off' && arg !== 'status') {
|
|
1378
|
+
log('nodes publish-remote: uso `nodes publish-remote <nodo> on --audience <id,id>|off|status`');
|
|
1379
|
+
return { code: 1 };
|
|
1380
|
+
}
|
|
1381
|
+
if (arg === 'on') {
|
|
1382
|
+
if (typeof flags.audience !== 'string' || !flags.audience.trim()) {
|
|
1383
|
+
log('nodes publish-remote: `on` richiede --audience <instanceId,instanceId,...> (gli id dei nodi admin che vedranno il peer)');
|
|
1384
|
+
return { code: 1 };
|
|
1385
|
+
}
|
|
1386
|
+
} else if (flags.audience !== undefined) {
|
|
1387
|
+
log('nodes publish-remote: --audience si usa solo con `on`');
|
|
1388
|
+
return { code: 1 };
|
|
1389
|
+
}
|
|
1390
|
+
const audience = arg === 'on' ? String(flags.audience).split(',').map((x) => x.trim()).filter(Boolean) : undefined;
|
|
1391
|
+
const expectedRevision = flags['access-revision'] === undefined ? undefined : Number(flags['access-revision']);
|
|
1392
|
+
if (expectedRevision !== undefined && !Number.isInteger(expectedRevision)) {
|
|
1393
|
+
log('nodes publish-remote: --access-revision vuole un intero (la revision letta da `nodes access`)');
|
|
1394
|
+
return { code: 1 };
|
|
1395
|
+
}
|
|
1396
|
+
return { code: nodesCmds.nodesPublishRemote({
|
|
1397
|
+
...opts, log, ref, json: flags.json === true, audience, expectedRevision,
|
|
1398
|
+
enabled: arg === 'on' ? true : (arg === 'off' ? false : undefined),
|
|
1399
|
+
}).code };
|
|
1400
|
+
}
|
|
1370
1401
|
if (sub === 'invite') {
|
|
1371
1402
|
const body = {
|
|
1372
1403
|
...(flags.ssh !== undefined ? { ssh: flags.ssh } : {}),
|
|
@@ -30,6 +30,9 @@ function createLiveHostFederation({
|
|
|
30
30
|
localToken = () => '',
|
|
31
31
|
fetchImpl = fetch,
|
|
32
32
|
timeoutMs = FED_TIMEOUT_MS,
|
|
33
|
+
// Refuses an owner that is (or routes through) a host-only peer. Read on the
|
|
34
|
+
// resolution used for the I/O, just before the request leaves.
|
|
35
|
+
ownerGuard = null,
|
|
33
36
|
} = {}) {
|
|
34
37
|
async function resolveRoute(target) {
|
|
35
38
|
if (!INSTANCE_ID_RE.test(String(target || ''))) return { error: 'invalid-target' };
|
|
@@ -48,11 +51,18 @@ function createLiveHostFederation({
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
async function forward(target, resource, method, body) {
|
|
54
|
+
let deniedBefore = false;
|
|
55
|
+
if (typeof ownerGuard === 'function') { try { deniedBefore = ownerGuard({ target, route: [] }) !== false; } catch (_) { deniedBefore = true; } }
|
|
51
56
|
const resolved = await resolveRoute(target);
|
|
52
57
|
if (resolved.error) return { ok: false, reason: resolved.error === 'invalid-target' ? 'invalid-target' : 'live-host-owner-unreachable' };
|
|
53
58
|
const port = localPort();
|
|
54
59
|
const token = localToken();
|
|
55
60
|
if (!port || !token) return { ok: false, reason: 'live-host-owner-unreachable' };
|
|
61
|
+
if (typeof ownerGuard === 'function') {
|
|
62
|
+
let denied = true;
|
|
63
|
+
try { denied = ownerGuard({ target, route: resolved.route }) !== false; } catch (_) { denied = true; }
|
|
64
|
+
if (denied || deniedBefore) return { ok: false, reason: 'live-host-owner-unreachable' };
|
|
65
|
+
}
|
|
56
66
|
const url = `http://127.0.0.1:${port}/api/route/${resolved.route.join('/')}/_${resource}`;
|
|
57
67
|
let res;
|
|
58
68
|
try {
|
package/lib/live-host/routes.js
CHANGED
|
@@ -95,7 +95,10 @@ function liveHostRoutes({ fleetP, store, readonly = () => false, now = () => Dat
|
|
|
95
95
|
// verifica (prova di hop + catena `visited` costruita dal server), e il budget
|
|
96
96
|
// del ponte si applica per NODO mittente ATTESTATO — stessa regola e stessi
|
|
97
97
|
// limiti dell'audio (lib/audio/rate-limit.js).
|
|
98
|
-
federation = null, originResolver = null, federatedRate = null
|
|
98
|
+
federation = null, originResolver = null, federatedRate = null,
|
|
99
|
+
// (req, ownerId) => true when this reader must not learn that the designation
|
|
100
|
+
// points at a host-only peer.
|
|
101
|
+
ownerHiddenFor = null }) {
|
|
99
102
|
const r = express.Router();
|
|
100
103
|
|
|
101
104
|
// Una designazione con `ownerId` e' REMOTA: il puntamento vive qui,
|
|
@@ -124,11 +127,18 @@ function liveHostRoutes({ fleetP, store, readonly = () => false, now = () => Dat
|
|
|
124
127
|
// hostCell e revision vengono dallo store (preservato); eligible e' la verita'
|
|
125
128
|
// COMPOSTA roster+lease (si veda hostLeaseState sopra); host.lease espone lo
|
|
126
129
|
// stato del lease della cella designata, distinto, perche' chi legge distingue.
|
|
127
|
-
r.get('/', async (
|
|
130
|
+
r.get('/', async (req, res) => {
|
|
128
131
|
try {
|
|
129
132
|
const snap = store.snapshot();
|
|
133
|
+
if (snap.hostCell != null && snap.ownerId && ownerHiddenFor && ownerHiddenFor(req, snap.ownerId)) {
|
|
134
|
+
return res.json({ hostCell: null, revision: snap.revision, eligible: false, threadStatus: 'absent', host: { lease: null }, at: now() });
|
|
135
|
+
}
|
|
130
136
|
if (snap.hostCell != null && snap.ownerId) {
|
|
131
137
|
const remote = await remoteOwnerState(snap);
|
|
138
|
+
// the reader is judged again after the wait: a publication that changed meanwhile must not name the owner
|
|
139
|
+
if (ownerHiddenFor && ownerHiddenFor(req, snap.ownerId)) {
|
|
140
|
+
return res.json({ hostCell: null, revision: snap.revision, eligible: false, threadStatus: 'absent', host: { lease: null }, at: now() });
|
|
141
|
+
}
|
|
132
142
|
return res.json({
|
|
133
143
|
hostCell: snap.hostCell, revision: snap.revision, ownerId: snap.ownerId, remote: true,
|
|
134
144
|
eligible: remote.state ? remote.state.eligible : false,
|
|
@@ -225,15 +225,34 @@ function grantsOf(peer) {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
228
|
+
// The vector the resource gate APPLIES to a record: the stored booleans and the
|
|
229
|
+
// cell scope, read exactly the way resource-acl.allowedByClass reads them — no
|
|
230
|
+
// `accessConfigured` marker, no coherence check. This is what a VIEW must show.
|
|
231
|
+
// `grantsOf` is different on purpose: it answers "was this vector set
|
|
232
|
+
// deliberately?" and feeds decisions that demand a configured record (the event
|
|
233
|
+
// stream, the ASK target check); it must keep denying for a legacy record.
|
|
234
|
+
// An absent scope is `all`: that is what the parser stores and what the gate
|
|
235
|
+
// sees (it only closes the cell-scoped classes on an explicit `none`).
|
|
236
|
+
function effectiveGrants(peer) {
|
|
237
|
+
const p = isPlainObject(peer) ? peer : {};
|
|
238
|
+
const grants = { cellVisibility: CELL_VISIBILITY.includes(p.cellVisibility) ? p.cellVisibility : 'all' };
|
|
239
|
+
if (grants.cellVisibility === 'selected') grants.cells = Array.isArray(p.cells) ? [...p.cells] : [];
|
|
240
|
+
for (const key of BOOLEAN_GRANTS) grants[key] = p[key] === true;
|
|
241
|
+
return grants;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Read view of ONE peer for listing endpoints and the settings UI: the derived
|
|
245
|
+
// label and the configured marker (PROVENANCE: how the vector was set) and the
|
|
246
|
+
// EFFECTIVE grant vector (what the gate applies, see effectiveGrants). The
|
|
247
|
+
// vector is flat and always complete, so a caller never has to guess defaults,
|
|
248
|
+
// and no token or secret is ever included: grants are decisions, not
|
|
249
|
+
// credentials. A legacy record with a grant and no marker reads "unconfigured"
|
|
250
|
+
// AND shows the grant on, because both are true.
|
|
233
251
|
function accessView(peer) {
|
|
234
252
|
const view = grantsOf(peer);
|
|
235
|
-
const
|
|
236
|
-
|
|
253
|
+
const effective = effectiveGrants(peer);
|
|
254
|
+
const access = { cellVisibility: effective.cellVisibility };
|
|
255
|
+
for (const key of BOOLEAN_GRANTS) access[key] = effective[key];
|
|
237
256
|
return { accessLabel: view.label, accessConfigured: view.configured, access };
|
|
238
257
|
}
|
|
239
258
|
|
|
@@ -252,6 +271,7 @@ module.exports = {
|
|
|
252
271
|
accessView,
|
|
253
272
|
applyPreset,
|
|
254
273
|
deriveLabel,
|
|
274
|
+
effectiveGrants,
|
|
255
275
|
grantsOf,
|
|
256
276
|
isCompleteAdminVector,
|
|
257
277
|
isConfigured,
|
package/lib/nodes/commands.js
CHANGED
|
@@ -336,9 +336,22 @@ function nodesInspect(opts) {
|
|
|
336
336
|
// inferred from defaults.
|
|
337
337
|
if (peer.kind === 'direct') {
|
|
338
338
|
const view = accessPresets.grantsOf(peer);
|
|
339
|
+
// The label says how the vector was set; the vector is what the resource
|
|
340
|
+
// gate applies, so it is shown as stored even for a legacy record.
|
|
341
|
+
const effective = accessPresets.effectiveGrants(peer);
|
|
339
342
|
log(`accesso: ${view.label}`);
|
|
340
|
-
log(`grant: ${
|
|
341
|
-
log(`scope: celle ${
|
|
343
|
+
log(`grant: ${ACCESS_VIEW_KEYS.map((key) => `${key}=${effective[key] ? 'on' : 'off'}`).join(' ')}`);
|
|
344
|
+
log(`scope: celle ${effective.cellVisibility}`);
|
|
345
|
+
if (!view.configured) {
|
|
346
|
+
log('nota: record non configurato: i grant mostrati sono quelli che il gate delle risorse applica; il feed eventi richiede un vettore configurato');
|
|
347
|
+
}
|
|
348
|
+
if (peer.publishRemote === true) {
|
|
349
|
+
const names = (peer.publishRemoteAudience || []).map((id) => {
|
|
350
|
+
const reader = out.nodes.find((n) => n.nodeId === id);
|
|
351
|
+
return reader ? `${reader.name} (${id})` : id;
|
|
352
|
+
});
|
|
353
|
+
log(`pubblicato: solo-host, audience: ${names.join(', ')}`);
|
|
354
|
+
}
|
|
342
355
|
for (const line of feedLinesFor(opts.feedDiagnostics, peer.nodeId)) log(line);
|
|
343
356
|
}
|
|
344
357
|
log(`azioni: ${Object.keys(peer.actions || {}).filter((key) => peer.actions[key]).join(', ')}`);
|
|
@@ -351,6 +364,8 @@ function nodesInspect(opts) {
|
|
|
351
364
|
// validated write. The label (admin/user/nexushost/custom) is derived from the
|
|
352
365
|
// grants; nothing here stores or reads a role.
|
|
353
366
|
const ACCESS_GRANT_KEYS = ['eventsAccess', 'nodeEventsAccess', 'askReplyAccess', 'filesReadAccess', 'peerOperatorAccess'];
|
|
367
|
+
// What the views print: the five grant flags of the edit commands plus the two the panel and live-host classes read.
|
|
368
|
+
const ACCESS_VIEW_KEYS = [...ACCESS_GRANT_KEYS, 'panelAccess', 'liveHostAccess'];
|
|
354
369
|
const ACCESS_EDIT_KEYS = new Set(['accessRole', ...ACCESS_GRANT_KEYS]);
|
|
355
370
|
|
|
356
371
|
const ACCESS_VECTOR_FIELDS = ['cellVisibility', 'cells', 'panelAccess', 'liveHostAccess'];
|
|
@@ -545,6 +560,50 @@ function nodesEdit(opts) {
|
|
|
545
560
|
return { code: 0, name: node.name, node: edited, restarted: wasUp };
|
|
546
561
|
}
|
|
547
562
|
|
|
563
|
+
// --- nodes publish-remote ---------------------------------------------------
|
|
564
|
+
// Host-only publication of a REMOTE peer: `nodes publish-remote <peer> on
|
|
565
|
+
// --audience <instanceId,...>` / `off`. Separate from Share on purpose: it
|
|
566
|
+
// never calls the share path, never restarts the tunnel and never touches the
|
|
567
|
+
// reverse pool. It edits the store (one validated, compare-and-set write, after
|
|
568
|
+
// a full backup on `on`); the running server reads the store per request.
|
|
569
|
+
function nodesPublishRemote(opts) {
|
|
570
|
+
const log = opts.log || console.log;
|
|
571
|
+
const { nodesPath } = resolveNodePaths(opts);
|
|
572
|
+
let st;
|
|
573
|
+
try { st = store.loadStoreStrict(nodesPath); }
|
|
574
|
+
catch (e) { log(`nodes publish-remote: ${e.message}`); return { code: 1, reason: 'store invalido', status: e.status || 500, errorCode: e.code || null }; }
|
|
575
|
+
const node = resolveStoredNode(st, opts.ref || opts.name);
|
|
576
|
+
if (!node) { log(`nodes publish-remote: nodo sconosciuto "${opts.ref || opts.name || ''}"`); return { code: 1, reason: 'unknown-node' }; }
|
|
577
|
+
if (opts.enabled === undefined) {
|
|
578
|
+
// Read-only status.
|
|
579
|
+
const state = node.publishRemote === true
|
|
580
|
+
? { name: node.name, enabled: true, audience: [...node.publishRemoteAudience] }
|
|
581
|
+
: { name: node.name, enabled: false, audience: [] };
|
|
582
|
+
if (opts.json) log(JSON.stringify(state, null, 2));
|
|
583
|
+
else log(`nodes publish-remote [${node.name}]: ${state.enabled ? `on · audience ${state.audience.join(', ')}` : 'off'}`);
|
|
584
|
+
return { code: 0, ...state };
|
|
585
|
+
}
|
|
586
|
+
if (isReadonly(opts)) { log('nodes publish-remote: READONLY, mutazione bloccata'); return { code: 1, reason: 'readonly' }; }
|
|
587
|
+
let res;
|
|
588
|
+
try {
|
|
589
|
+
res = store.setPublishRemoteCas({
|
|
590
|
+
filePath: nodesPath, name: node.name, enabled: opts.enabled === true,
|
|
591
|
+
audience: opts.audience, expectedRevision: opts.expectedRevision,
|
|
592
|
+
});
|
|
593
|
+
} catch (e) { log(`nodes publish-remote: ${e.message}`); return { code: 1, reason: 'refused', error: e.message }; }
|
|
594
|
+
if (!res.ok) {
|
|
595
|
+
log(`nodes publish-remote: revision delle concessioni superata (attuale ${res.revision}) — rileggi e riprova`);
|
|
596
|
+
return { code: 1, reason: 'revision-conflict', revision: res.revision };
|
|
597
|
+
}
|
|
598
|
+
if (opts.json) log(JSON.stringify(res, null, 2));
|
|
599
|
+
else if (res.enabled) {
|
|
600
|
+
log(`nodes publish-remote [${res.name}]: on · audience ${res.audience.join(', ')}`);
|
|
601
|
+
log(' grant verso il peer azzerati (celle none, eventi/file/ask/operatore/pannello/live host off, eventsReceive off); store a schema 4');
|
|
602
|
+
if (res.backup) log(` backup dello store: ${res.backup.path} (sha256 ${res.backup.sha256})`);
|
|
603
|
+
} else log(`nodes publish-remote [${res.name}]: off${res.unchanged ? ' (era gia\' spento)' : ' · i grant restano azzerati'}`);
|
|
604
|
+
return { code: 0, ...res };
|
|
605
|
+
}
|
|
606
|
+
|
|
548
607
|
// --- nodes access: read-only inventory --------------------------------------
|
|
549
608
|
// Shows what every peer may currently do on this node, with the revision of
|
|
550
609
|
// the policy. Peers whose grants were never configured are listed as such:
|
|
@@ -566,7 +625,7 @@ function nodesAccess(opts = {}) {
|
|
|
566
625
|
if (view.peers.length === 0) log('nessun peer diretto');
|
|
567
626
|
for (const peer of view.peers) {
|
|
568
627
|
log(`${peer.name}${peer.nodeId ? ` (${peer.nodeId})` : ''}: ${peer.label}`);
|
|
569
|
-
log(` ${
|
|
628
|
+
log(` ${ACCESS_VIEW_KEYS.map((key) => `${key}=${peer.grants[key] ? 'on' : 'off'}`).join(' ')} · celle ${peer.grants.cellVisibility}`);
|
|
570
629
|
}
|
|
571
630
|
if (legacy.length > 0) log(`da configurare esplicitamente (${legacy.length}): ${legacy.map((p) => p.name).join(', ')}`);
|
|
572
631
|
return { code: 0, ...view, unconfigured: legacy.map((p) => p.name) };
|
|
@@ -880,7 +939,7 @@ function nodesSetToken(opts) {
|
|
|
880
939
|
module.exports = {
|
|
881
940
|
fetchFeedDiagnostics, feedLinesFor,
|
|
882
941
|
|
|
883
|
-
nodesAccess, ACCESS_GRANT_KEYS,
|
|
942
|
+
nodesAccess, nodesPublishRemote, ACCESS_GRANT_KEYS,
|
|
884
943
|
nodesAdd, nodesList, nodesInspect, nodesEdit, nodesRemove, nodesTest, nodesTestAll,
|
|
885
944
|
nodesUp, nodesDown, nodesRestart, nodesSetToken,
|
|
886
945
|
// helper esposti per test/riuso
|
|
@@ -83,7 +83,10 @@ function createReverseSlotListeners({ app, createServerImpl = http.createServer,
|
|
|
83
83
|
return true;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
// A request that arrived on one of these listeners comes from a peer's channel,
|
|
87
|
+
// never from this machine's own browser: callers use it to tell the two apart.
|
|
88
|
+
const isSlotPort = (localPort) => listeners.has(localPort);
|
|
89
|
+
return { open, closePort, closeAll, respond, isSlotPort, size: () => listeners.size };
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
module.exports = { createReverseSlotListeners };
|