summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/hub_client.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-10 03:07:56 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-10 03:07:56 +0200
commit4b3e8c3b8b9d10c8ac333dd8db614a7569052472 (patch)
tree8828d7d549adc8bb21d8a7f6533c8e9db9c298b9 /packages/meshbay-node/src/meshbay_node/hub_client.py
parent8ccbe262ecf4a8f7545cbf1e9f1cc5a485acae67 (diff)
downloadmeshbay-4b3e8c3b8b9d10c8ac333dd8db614a7569052472.tar.gz
feat: Phase 7 — Node v2 (multi-group, Sender Keys, 0-RTT, chat, denylist)
Implements all 8 milestones (7.0-7.7): - 7.0: JWT carries `groups` claim; node verifies group membership at MNP handshake (QUIC + TCP+TLS). Resolves security review C2. - 7.1: QUIC 0-RTT session resumption via stored session tickets (17-21ms reconnect vs 47ms cold). - 7.2: Hub→node WebSocket signaling for NAT punch coordination (`client_incoming`/`punch_ready`) + jti denylist push. Denylist class blocks revoked users/jtis at handshake. - 7.3: Multi-group daemon — one QUIC port serves N groups with per-group GEK, shared_root, and index routing. - 7.4: HLS streaming via QUIC (STREAM_SEGMENT message type, ffmpeg segment extraction). - 7.5: Sender Keys protocol for group chat (Signal Groups approach). Each member has own sending chain key, HKDF chain ratchet, AES-256-GCM encryption, Ed25519 signing. Resolves security review C1. - 7.6: Chat store (SQLite via aiosqlite), CHAT_MESSAGE MNP wire type with peer broadcast, web UI with WebSocket push. - 7.7: Argon2id calibration CLI. First security review included (first-review.md). 109 tests, demo-v3 validated against meshbay.org production hub. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/hub_client.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/hub_client.py54
1 files changed, 54 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/hub_client.py b/packages/meshbay-node/src/meshbay_node/hub_client.py
index d91b945..74851c1 100644
--- a/packages/meshbay-node/src/meshbay_node/hub_client.py
+++ b/packages/meshbay-node/src/meshbay_node/hub_client.py
@@ -19,6 +19,7 @@ import logging
import time
from dataclasses import dataclass, field
from pathlib import Path
+from typing import Any, Callable
import httpx
import jwt
@@ -243,6 +244,59 @@ class HubClient:
r.raise_for_status()
return r.json()
+ # ── Persistent WebSocket (signaling + revocations) ──────────────────────
+
+ async def maintain_ws(
+ self,
+ on_incoming: Any = None,
+ on_revocation: Any = None,
+ ) -> None:
+ """
+ Maintain a persistent WebSocket connection to the hub.
+ Receives NAT punch requests and revocation tokens.
+ Runs until cancelled.
+ """
+ import websockets
+
+ if self._session is None:
+ raise RuntimeError("Not logged in")
+
+ hub_url = self._session.hub_url.replace("https://", "wss://").replace("http://", "ws://")
+ ws_url = f"{hub_url}/v1/nodes/ws"
+
+ while True:
+ try:
+ async with websockets.connect(ws_url) as ws:
+ await ws.send(json.dumps({
+ "type": "auth",
+ "token": self._session.access_token,
+ }))
+ auth_resp = json.loads(await ws.recv())
+ if auth_resp.get("type") != "auth_ok":
+ log.error("WS auth failed: %s", auth_resp)
+ return
+
+ log.info("Hub WS connected")
+
+ async for raw in ws:
+ msg = json.loads(raw)
+ mtype = msg.get("type")
+
+ if mtype == "client_incoming" and on_incoming:
+ await on_incoming(msg["peer_ip"], msg["peer_port"])
+ await ws.send(json.dumps({"type": "punch_ready"}))
+
+ elif mtype == "revocation" and on_revocation:
+ on_revocation(msg.get("token", ""))
+
+ elif mtype == "pong":
+ pass
+
+ except Exception as e:
+ log.warning("Hub WS disconnected: %s — reconnecting in 5s", e)
+ import asyncio
+ await asyncio.sleep(5)
+
# ── Convenience: full startup sequence ───────────────────────────────────
async def startup(self, endpoint_hint: str | None = None) -> HubSession: