// Which node this browser has met under which key (trust on first use), and // the version range a node declares at the handshake. // ── Node identity pinning (11.5.8) ─────────────────────────────────────────── const NODE_PIN_PREFIX = 'mb_nodepin_'; /** * The node's half of the version range, from `handshake_challenge`. * * Mirrors meshbay_common/handshake.py::check_version(). A node that declares no * range at all is a node that predates negotiation — that is every 0.x node, and * none of them can serve a sealed index or a sealed ack — so it is refused here * rather than left to fail later as a message that will not open. */ function _checkNodeVersion(reply) { const parse = (v) => { const m = /^(\d+)\.(\d+)$/.exec(String(v || '')); return m ? [Number(m[1]), Number(m[2])] : null; }; const cmp = (a, b) => (a[0] - b[0]) || (a[1] - b[1]); const fail = (reason, message) => { const e = new Error(message); e.reason = reason; throw e; }; const theirs = parse(reply.v); if (!theirs) { fail('node_version_unreadable', 'The node did not declare a readable protocol version.'); } // No declared minimum means "only what I speak" — the correct reading of a // node from before this field existed. const theirMin = parse(reply.v_min) || theirs; if (cmp(theirs, parse(MNP_V_MIN)) < 0) { fail('node_too_old', 'This node is running an older MeshBay than this page needs. ' + 'Its operator has to update it.'); } if (cmp(theirMin, parse(MNP_V)) > 0) { fail('client_too_old', 'This page is older than the node it is talking to. ' + 'Reload to pick up the current version.'); } } function _checkNodePin(nodeId, nodePk) { if (!nodeId || !nodePk) return; const key = NODE_PIN_PREFIX + nodeId; let pinned = null; try { pinned = localStorage.getItem(key); } catch { return; } if (pinned === null) { try { localStorage.setItem(key, nodePk); } catch {} return; } if (pinned !== nodePk) { throw new Error( 'This node\'s identity key has changed. That is expected only if its ' + 'operator reinstalled the node — otherwise someone may be impersonating ' + 'it. Verify with the operator out of band, then clear the pin in ' + 'Settings to accept the new key.'); } } /** Forget a pinned node identity — the deliberate escape hatch for a legitimate rotation. */ function clearNodePin(nodeId) { try { if (nodeId) localStorage.removeItem(NODE_PIN_PREFIX + nodeId); else { for (const k of Object.keys(localStorage)) if (k.startsWith(NODE_PIN_PREFIX)) localStorage.removeItem(k); } } catch {} } function pinnedNodeCount() { try { return Object.keys(localStorage).filter(k => k.startsWith(NODE_PIN_PREFIX)).length; } catch { return 0; } } MeshBayTransport.clearNodePin = clearNodePin; MeshBayTransport.pinnedNodeCount = pinnedNodeCount;