@mmmbuto/nexuscrew 0.9.65 → 0.9.66
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 +10 -0
- package/docs/NODES.md +44 -0
- package/docs/SECURITY.md +40 -0
- package/frontend/dist/assets/{index-DkYRC1vV.js → index-DhZOap1-.js} +1 -1
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +31 -0
- package/lib/nodes/access-presets.js +27 -7
- package/lib/nodes/commands.js +63 -4
- package/lib/nodes/store.js +197 -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 +43 -4
- package/lib/server.js +28 -5
- package/lib/settings/routes.js +6 -0
- package/package.json +1 -1
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// dies by construction. Transport failures back off with jitter 5→60 s; a
|
|
12
12
|
// stream silent for 60 s is aborted and resynced.
|
|
13
13
|
|
|
14
|
+
const askCell = require('./ask-cell.js');
|
|
15
|
+
|
|
14
16
|
const BACKOFF_MIN_MS = 5000;
|
|
15
17
|
const BACKOFF_MAX_MS = 60000;
|
|
16
18
|
const IDLE_TIMEOUT_MS = 60000;
|
|
@@ -259,6 +261,28 @@ function createEventFeedClient(opts = {}) {
|
|
|
259
261
|
}
|
|
260
262
|
}
|
|
261
263
|
|
|
264
|
+
// An ask of a snapshot, attributed to its owner, with the cell the OWNER
|
|
265
|
+
// resolved for it. `cellId` on the wire becomes `ownerCellId` here (validated by
|
|
266
|
+
// the schema check) and any `ownerCellId`/`originCell` the wire carried is
|
|
267
|
+
// dropped: those two names are ours, set only by authenticated ingress. When the
|
|
268
|
+
// snapshot has no cell for the ask, the cell the view already held for the SAME
|
|
269
|
+
// generation (same owner timestamp and content) is kept; a new generation never
|
|
270
|
+
// inherits it.
|
|
271
|
+
function withOwnerCell(wire, ownerId, knownCells) {
|
|
272
|
+
const { cellId, ownerCellId: _forged, originCell: _forgedOrigin, ...ask } = wire;
|
|
273
|
+
const out = { ...ask, ownerId };
|
|
274
|
+
if (askCell.isCellId(cellId)) return { ...out, ownerCellId: cellId };
|
|
275
|
+
const hit = knownCells.get(out.ownerAskId || out.id);
|
|
276
|
+
if (hit) {
|
|
277
|
+
const { askFingerprint } = require('./asks.js');
|
|
278
|
+
const priorTs = Object.hasOwn(hit.prior, 'ownerAskTs') ? hit.prior.ownerAskTs : hit.prior.ts;
|
|
279
|
+
const nextTs = Object.hasOwn(out, 'ownerAskTs') ? out.ownerAskTs : out.ts;
|
|
280
|
+
if (Number.isSafeInteger(priorTs) && priorTs > 0 && priorTs === nextTs
|
|
281
|
+
&& askFingerprint(hit.prior) === askFingerprint(out)) return { ...out, ownerCellId: hit.cellId };
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
262
286
|
// Apply a fetched snapshot ONLY if it belongs to the current generation:
|
|
263
287
|
// an obsolete fetch must never overwrite a newer view (the snapshot
|
|
264
288
|
// WINS over the previous view of the same owner).
|
|
@@ -268,10 +292,18 @@ function createEventFeedClient(opts = {}) {
|
|
|
268
292
|
view.cursor = snap.cursor || null;
|
|
269
293
|
// A snapshot taken before the owner processed a dismiss still carries the
|
|
270
294
|
// ask: the tombstone is what keeps it from coming back.
|
|
295
|
+
// What this view already knows about the cell of each ask, taken BEFORE the
|
|
296
|
+
// view is replaced: an owner that does not publish the cell in its snapshot
|
|
297
|
+
// (older release) must not make a card forget what its live frame said.
|
|
298
|
+
const knownCells = new Map();
|
|
299
|
+
for (const prior of [...view.asks, ...(view.alertAsks ? view.alertAsks.values() : [])]) {
|
|
300
|
+
const resolved = askCell.resolveAskCell({ ownerCellId: prior.ownerCellId, originCell: prior.originCell });
|
|
301
|
+
if (resolved.cellId) knownCells.set(prior.ownerAskId || prior.id, { prior, cellId: resolved.cellId });
|
|
302
|
+
}
|
|
271
303
|
view.asks = Array.isArray(snap.asks)
|
|
272
304
|
? snap.asks
|
|
273
305
|
.filter((a) => a && !askIsClosed(ownerId, a.ownerAskId || a.id, snap.viewEpoch, a) && !isAskDismissed(ownerId, a))
|
|
274
|
-
.map((a) => (
|
|
306
|
+
.map((a) => withOwnerCell(a, ownerId, knownCells))
|
|
275
307
|
: [];
|
|
276
308
|
view.alertAsks = new Map(); // fresh snapshot replaces all live-only alert sources
|
|
277
309
|
// A snapshot taken before the owner processed the dismissal still carries
|
|
@@ -405,7 +437,10 @@ function createEventFeedClient(opts = {}) {
|
|
|
405
437
|
if (historicalAsk && typeof opts.onAuthoritativeAsks === 'function') opts.onAuthoritativeAsks(envelope.ownerId, [historicalAsk]);
|
|
406
438
|
} else if (f.type === 'ask') {
|
|
407
439
|
const ask = { id: f.askId, question: f.question, options: f.options, session: f.session, ts: f.ts,
|
|
408
|
-
ownerAskTs: Number.isSafeInteger(f.askTs) && f.askTs > 0 ? f.askTs : null, ownerId: envelope.ownerId
|
|
440
|
+
ownerAskTs: Number.isSafeInteger(f.askTs) && f.askTs > 0 ? f.askTs : null, ownerId: envelope.ownerId,
|
|
441
|
+
// The envelope of an ask frame is scoped to the cell the owner resolved;
|
|
442
|
+
// it is the owner's statement, carried on the authenticated stream.
|
|
443
|
+
...(envelope.scope === 'cell' && askCell.isCellId(envelope.cellId) ? { ownerCellId: envelope.cellId } : {}) };
|
|
409
444
|
if (!rememberLiveAsk(envelope.ownerId, ask)) return false;
|
|
410
445
|
if (typeof opts.onAuthoritativeAsks === 'function') opts.onAuthoritativeAsks(envelope.ownerId, [ask]);
|
|
411
446
|
opts.eventsHub.broadcast({ type: 'ask', ownerId: envelope.ownerId, eventId: envelope.eventId,
|
|
@@ -712,6 +747,9 @@ function createEventFeedClient(opts = {}) {
|
|
|
712
747
|
&& (a.question === undefined || typeof a.question === 'string')
|
|
713
748
|
&& (a.options === undefined || Array.isArray(a.options))
|
|
714
749
|
&& (a.session === undefined || typeof a.session === 'string')
|
|
750
|
+
// The cell the OWNER resolved for the ask: absent (an older owner, or a
|
|
751
|
+
// session the owner could not resolve) or a well-formed cell id, never anything else.
|
|
752
|
+
&& (a.cellId === undefined || askCell.isCellId(a.cellId))
|
|
715
753
|
&& (a.ts === undefined || typeof a.ts === 'number');
|
|
716
754
|
if (!snap.asks.every(askFieldsOk)) return schema('asks');
|
|
717
755
|
return { ok: true };
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
const express = require('express');
|
|
15
15
|
const nodesStore = require('../nodes/store.js');
|
|
16
16
|
const eventFeedAcl = require('./event-feed-acl.js');
|
|
17
|
+
const askCell = require('./ask-cell.js');
|
|
17
18
|
|
|
18
19
|
const SNAPSHOT_MAX_BYTES = 3 * 1024 * 1024;
|
|
19
20
|
const SNAPSHOT_MAX_ASKS = 100;
|
|
@@ -241,8 +242,13 @@ function createEventFeedRoutes(deps) {
|
|
|
241
242
|
// guardia e' esplicita sui campi di provenienza, non dedotta dalla
|
|
242
243
|
// visibilita' della cella.
|
|
243
244
|
.filter((a) => !a.originNode && !(a.ownerId && self && a.ownerId !== self))
|
|
244
|
-
|
|
245
|
-
|
|
245
|
+
// The cell is resolved ONCE, by this owner, through its own definitions, and
|
|
246
|
+
// it travels with the ask: a session with a custom tmux name does not decode
|
|
247
|
+
// to a cell on the receiving side, and the receiver must not guess it. Only
|
|
248
|
+
// a well-formed id is published; an unresolved one is simply absent.
|
|
249
|
+
.map((a) => ({ id: a.id, question: a.question, options: a.options, session: a.session, ts: a.ts, cell: cellForSession(a.session) }))
|
|
250
|
+
.filter((a) => peer.allows({ scope: 'cell', cellId: a.cell }))
|
|
251
|
+
.map(({ cell, ...ask }) => (askCell.isCellId(cell) ? { ...ask, cellId: cell } : ask));
|
|
246
252
|
askSourceCount = visible.length;
|
|
247
253
|
asks = visible.slice(0, SNAPSHOT_MAX_ASKS);
|
|
248
254
|
} catch (e) {
|
package/lib/proxy/federation.js
CHANGED
|
@@ -113,8 +113,31 @@ function peerAllows(peer, otherId) {
|
|
|
113
113
|
return Array.isArray(peer.selected) && peer.selected.includes(otherId);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// A host-only peer (publishRemote, schema 4) is a REMOTE node this installation
|
|
117
|
+
// publishes to an explicit audience of its own admin nodes. The boundary is
|
|
118
|
+
// one-way and does not depend on the peer's own claims or grants:
|
|
119
|
+
// audience -> peer : yes, while the reader is in the audience AND still holds
|
|
120
|
+
// the operator grant (re-read on every call);
|
|
121
|
+
// peer -> anyone : never (not another peer, not a topology, not a route);
|
|
122
|
+
// everyone else : no, whatever `visibility` or `shared` say.
|
|
123
|
+
function isHostOnlyPeer(peer) {
|
|
124
|
+
return !!peer && peer.publishRemote === true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function audienceAllows(reader, hostOnly) {
|
|
128
|
+
return Array.isArray(hostOnly.publishRemoteAudience)
|
|
129
|
+
&& typeof reader.nodeId === 'string'
|
|
130
|
+
&& hostOnly.publishRemoteAudience.includes(reader.nodeId)
|
|
131
|
+
&& !isHostOnlyPeer(reader)
|
|
132
|
+
&& allowedByClass('operator', reader);
|
|
133
|
+
}
|
|
134
|
+
|
|
116
135
|
function canTransit(ingress, egress) {
|
|
117
136
|
if (!ingress || !egress || ingress.name === egress.name) return !ingress;
|
|
137
|
+
// The ingress is the AUTHENTICATED peer record (looked up by its token), never
|
|
138
|
+
// something read from the query, a header or the visited chain.
|
|
139
|
+
if (isHostOnlyPeer(ingress)) return false;
|
|
140
|
+
if (isHostOnlyPeer(egress)) return audienceAllows(ingress, egress);
|
|
118
141
|
// `shared` is the explicit publication gate. Visibility remains the hub ACL,
|
|
119
142
|
// but it cannot make a private peer routable on its own.
|
|
120
143
|
return egress.shared === true
|
|
@@ -274,6 +297,9 @@ function liveHostAllowedFor(ingress) {
|
|
|
274
297
|
// WebSocket upgrade. A resource whose class is unknown, or whose grants the
|
|
275
298
|
// owner has not conceded, is refused before anything is dispatched.
|
|
276
299
|
function classGateCheck(resource, method, ingress) {
|
|
300
|
+
// A host-only peer reaches nothing of ours, whatever grants its record carries
|
|
301
|
+
// or gains: the zeroed vector is the first lock, this is the second.
|
|
302
|
+
if (isHostOnlyPeer(ingress)) return { ok: false, status: 403, reason: 'host-only-peer' };
|
|
277
303
|
const cls = classifyResource(resource, method);
|
|
278
304
|
if (!cls) return { ok: false, status: 404, reason: 'resource-not-classified' };
|
|
279
305
|
if (!allowedByClass(cls, ingress)) return { ok: false, status: 403, reason: `grant-required:${cls}` };
|
|
@@ -1209,7 +1235,9 @@ async function collectTopologyDetailed({
|
|
|
1209
1235
|
instanceId: n.nodeId, name: n.name, route: [n.name], direct: true,
|
|
1210
1236
|
label: store.validLabel(n.label) ? n.label.trim() : '',
|
|
1211
1237
|
});
|
|
1212
|
-
|
|
1238
|
+
// A host-only peer is published as a LEAF: whatever it says about its own
|
|
1239
|
+
// children is never asked for, so it cannot announce nodes to our readers.
|
|
1240
|
+
if (ttl <= 1 || !n.token || isHostOnlyPeer(n)) continue;
|
|
1213
1241
|
probes.push({ n, pending: fetchPeerTopology({ node: n, ttl, seen, fetchImpl, timeoutMs: childBudget }) });
|
|
1214
1242
|
}
|
|
1215
1243
|
const results = await Promise.all(probes.map(async ({ n, pending }) => {
|
|
@@ -1286,7 +1314,9 @@ async function collectLocalTopology({
|
|
|
1286
1314
|
const live = await collectTopologyDetailed({ nodesPath, fetchImpl, timeoutMs, childSnapshots: false });
|
|
1287
1315
|
const st = store.loadStore(nodesPath);
|
|
1288
1316
|
const directNames = new Set(((st && st.nodes) || [])
|
|
1289
|
-
|
|
1317
|
+
// A host-only peer is a leaf: nothing cached "behind" it survives (entries
|
|
1318
|
+
// written before the mode was turned on included).
|
|
1319
|
+
.filter((n) => (n.direction !== 'inbound' || n.shared === true) && !isHostOnlyPeer(n))
|
|
1290
1320
|
.map((n) => n.name));
|
|
1291
1321
|
const authoritative = new Set(live.authoritative);
|
|
1292
1322
|
const liveIds = new Set(live.nodes.map((n) => n.instanceId));
|
|
@@ -1362,6 +1392,15 @@ function peerRouter({ nodesPath, localPort, localCredential, fetchImpl, readonly
|
|
|
1362
1392
|
if (!peer) return res.status(401).json({ error: 'unauthorized peer' });
|
|
1363
1393
|
req.peer = peer; next();
|
|
1364
1394
|
});
|
|
1395
|
+
// A host-only peer (publish-remote) reaches NOTHING of ours over the protocol:
|
|
1396
|
+
// the only thing it may do is the authenticated health probe. Share, topology,
|
|
1397
|
+
// reverse-pool and routes are all refused here, before their handlers run.
|
|
1398
|
+
r.use((req, res, next) => {
|
|
1399
|
+
if (isHostOnlyPeer(req.peer) && !(req.method === 'GET' && req.path === '/health')) {
|
|
1400
|
+
return res.status(403).json({ error: 'non concesso a questo nodo', reason: 'host-only-peer' });
|
|
1401
|
+
}
|
|
1402
|
+
return next();
|
|
1403
|
+
});
|
|
1365
1404
|
// Health federato: il peer autenticato (acceptToken matchato sopra) ottiene un
|
|
1366
1405
|
// 200 esplicito con instanceId/version. Serve da target di probeHealth() lato
|
|
1367
1406
|
// Initiator: distingue transport (porta aperta) da auth (200 vs 401) da
|
|
@@ -2072,9 +2111,9 @@ function acceptWsThenClose(socket, req, code, reason) {
|
|
|
2072
2111
|
|
|
2073
2112
|
|
|
2074
2113
|
module.exports = {
|
|
2075
|
-
isPanelResource, panelAllowedFor, isLiveHostResource, liveHostAllowedFor, routeHandler,
|
|
2114
|
+
isPanelResource, panelAllowedFor, isLiveHostResource, liveHostAllowedFor, classGateCheck, routeHandler,
|
|
2076
2115
|
MAX_HOPS, ROUTE_DELIMITER, TOPOLOGY_PEER_TIMEOUT_MS, SHARE_NOT_READY_CODE, classifyShareFailure, activeReversePort,
|
|
2077
|
-
peerFromToken, peerAllows, canTransit, parseRoute, knownResource, allowedResource, allowedQuery, readonlyBlocksFederated,
|
|
2116
|
+
peerFromToken, peerAllows, canTransit, isHostOnlyPeer, parseRoute, knownResource, allowedResource, allowedQuery, readonlyBlocksFederated,
|
|
2078
2117
|
collectTopology, collectTopologyDetailed, collectLocalTopology, peerRouter, localRouter, forwardUpgrade, createHubAttachStore,
|
|
2079
2118
|
probeHealth, waitForHealthyPeer, canListenLoopback, preflightHubReverse, notifyHubShare, reconcilePeerShare, runShareRevokeBoot, probeReverseOwner,
|
|
2080
2119
|
hubPoolRequest, verifyHubPoolSlot, reserveHubPoolSlot, commitHubPoolSlot, settleHubPoolSlot, abortHubPoolSlot, getHubPoolStatus,
|
package/lib/server.js
CHANGED
|
@@ -58,6 +58,7 @@ const { createCellScopeGuard } = require('./cells/scope-guard.js');
|
|
|
58
58
|
const { createCellScope } = require('./cells/scope.js');
|
|
59
59
|
const { cellIdFromTmuxSession } = require('./fleet/definitions.js');
|
|
60
60
|
const { createOwnerAskScope } = require('./notify/owner-ask-scope.js');
|
|
61
|
+
const askCell = require('./notify/ask-cell.js');
|
|
61
62
|
// Budget a tre finestre, riusato dalle notifiche federate: e' gia' scritto per
|
|
62
63
|
// limitare il danno di un nodo che mente sulla propria cella (il tetto per
|
|
63
64
|
// target si calcola sul NODO, che la catena visited verifica).
|
|
@@ -1519,15 +1520,32 @@ function createServer(opts = {}) {
|
|
|
1519
1520
|
// Guards and alias mutation belong only to the selected generation.
|
|
1520
1521
|
const alias = storedAlias && ask && asksStore.sameImportedGeneration(storedAlias, ask) ? storedAlias : null;
|
|
1521
1522
|
const grants = peer && accessPresets.grantsOf(peer);
|
|
1522
|
-
|
|
1523
|
-
|
|
1523
|
+
// The cell comes from what the OWNER resolved (feed or attested origin) and,
|
|
1524
|
+
// only for canonical names, from the session; see lib/notify/ask-cell.js. A
|
|
1525
|
+
// body field is never consulted: the dismissal request carries no cell.
|
|
1526
|
+
// The selected ask keeps its OWN cell. The other source (alias <-> feed card)
|
|
1527
|
+
// lends its cell only when it is bound to the same generation by an explicit,
|
|
1528
|
+
// equal owner timestamp and the same content: a generation that is unknown
|
|
1529
|
+
// on either side is not "the same" for a cell (sameImportedGeneration stays
|
|
1530
|
+
// permissive about it, because guards and alias mutation want that). If both
|
|
1531
|
+
// sources are authenticated and disagree there is no cell at all.
|
|
1532
|
+
const boundToSelected = item => item === ask || (tsOf(item) !== null && tsOf(item) === tsOf(ask)
|
|
1533
|
+
&& asksStore.askFingerprint(item) === asksStore.askFingerprint(ask));
|
|
1534
|
+
const sameAsk = ask && [storedAlias, feedAsk].filter(item => item && boundToSelected(item));
|
|
1535
|
+
const cellId = ask ? askCell.resolveAskCellFrom(sameAsk).cellId : previous && previous.cellId;
|
|
1536
|
+
const grantsAllowFeed = !!(grants && grants.configured && grants.grants.eventsAccess === true);
|
|
1537
|
+
const visible = !!((ask || previous) && cellId && grantsAllowFeed
|
|
1524
1538
|
&& eventFeedAcl.visibilityAllows(grants.grants, cellId));
|
|
1539
|
+
// The ask is known and this node may read the owner's feed, but no
|
|
1540
|
+
// authenticated source tells which cell it belongs to (a card persisted by an
|
|
1541
|
+
// older release, an owner that does not publish the cell): said, not guessed.
|
|
1542
|
+
const cellUnresolved = !!(ask && !cellId && grantsAllowFeed);
|
|
1525
1543
|
const blocked = !!((ask && ask.answered) || (alias && (asksStore.isAnswering(alias.id) || askReceipts.isBlocked(alias.id)))
|
|
1526
1544
|
|| askRelay.state().some(attempt => attempt.ownerId === ownerId && attempt.askId === askId && ['sent', 'uncertain'].includes(attempt.state)));
|
|
1527
1545
|
const slot = peer && peer.reversePool && peer.reversePool.slots && peer.reversePool.slots[peer.reversePool.activeSlot];
|
|
1528
1546
|
const unavailable = !!(peer && peer.direction === 'inbound' && (!peer.shared || !slot || !Number.isInteger(slot.port)))
|
|
1529
1547
|
|| askRelay.channelState(ownerId).unavailable;
|
|
1530
|
-
return { peer, alias, previous, ask, grants, cellId, visible, blocked: blocked || ambiguous, unavailable, ambiguous };
|
|
1548
|
+
return { peer, alias, previous, ask, grants, cellId, visible, cellUnresolved, blocked: blocked || ambiguous, unavailable, ambiguous };
|
|
1531
1549
|
}
|
|
1532
1550
|
api.post('/asks-relay', express.json({ limit: '8kb' }), async (req, res) => {
|
|
1533
1551
|
try {
|
|
@@ -1541,8 +1559,13 @@ function createServer(opts = {}) {
|
|
|
1541
1559
|
}
|
|
1542
1560
|
try { if (!asksStore.health().readable) return res.status(503).json({ error: 'dismissal store unavailable' }); }
|
|
1543
1561
|
catch (_) { return res.status(503).json({ error: 'dismissal store unavailable' }); }
|
|
1544
|
-
const { peer, alias, previous, ask, grants, cellId, visible, unavailable, ambiguous } = localDismissalEligibility(b.ownerId, b.askId);
|
|
1545
|
-
if (!visible)
|
|
1562
|
+
const { peer, alias, previous, ask, grants, cellId, visible, cellUnresolved, unavailable, ambiguous } = localDismissalEligibility(b.ownerId, b.askId);
|
|
1563
|
+
if (!visible) {
|
|
1564
|
+
if (cellUnresolved) {
|
|
1565
|
+
return res.status(409).json({ error: 'the cell of this ask is not known from an authenticated owner source; refresh the owner view', reason: 'cell-unresolved' });
|
|
1566
|
+
}
|
|
1567
|
+
return res.status(404).json({ error: 'unknown or invisible ask' });
|
|
1568
|
+
}
|
|
1546
1569
|
if (ambiguous) return res.status(409).json({ error: 'ask generation is ambiguous; refresh the owner view', reason: 'generation-ambiguous' });
|
|
1547
1570
|
if (ask && ask.answered) return res.status(409).json({ error: 'ask already answered', reason: 'answered' });
|
|
1548
1571
|
if ((alias && (asksStore.isAnswering(alias.id) || askReceipts.isBlocked(alias.id)))
|
package/lib/settings/routes.js
CHANGED
|
@@ -710,6 +710,12 @@ function settingsRoutes(deps = {}) {
|
|
|
710
710
|
if (!node) return send(res, 404, { error: `nodo sconosciuto "${name}"` });
|
|
711
711
|
if (node.direction !== 'outbound') return send(res, 409, { error: 'Share si attiva sul dispositivo che possiede la connessione SSH' });
|
|
712
712
|
if (!node.token || !node.nodeId) return send(res, 409, { error: 'nodo non associato: ripeti il pairing' });
|
|
713
|
+
// A host-only peer (publish-remote) is published ONE WAY to our own admin
|
|
714
|
+
// nodes; Share would publish THIS node on it. Refuse before any SSH restart
|
|
715
|
+
// or reverse negotiation is attempted (turning Share off stays possible).
|
|
716
|
+
if (node.publishRemote === true && body.shared) {
|
|
717
|
+
return send(res, 409, { error: 'publish-remote attivo su questo peer: Share resta spento (spegni prima publish-remote)', reason: 'publish-remote-active' });
|
|
718
|
+
}
|
|
713
719
|
if (body.shared && !nodesStore.isPort(node.reversePort)) {
|
|
714
720
|
return send(res, 409, { error: 'canale share non negoziato: ripeti il pairing' });
|
|
715
721
|
}
|
package/package.json
CHANGED