aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/transport-pins.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport-pins.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport-pins.js86
1 files changed, 86 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport-pins.js b/packages/meshbay-hub/src/meshbay_hub/static/transport-pins.js
new file mode 100644
index 0000000..c0699dc
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport-pins.js
@@ -0,0 +1,86 @@
+// 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;