diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node')
4 files changed, 51 insertions, 11 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/cli/setup.py b/packages/meshbay-node/src/meshbay_node/cli/setup.py index 48a55ef..44224fb 100644 --- a/packages/meshbay-node/src/meshbay_node/cli/setup.py +++ b/packages/meshbay-node/src/meshbay_node/cli/setup.py @@ -105,7 +105,7 @@ def init(args) -> None: print(f"Node key: {keys.pk_ed25519_b64}") print() print("Next steps:") - print(f" 1. Link this node key on {hub_url} → Settings → Link Node") + print(f" 1. Link this node key on {hub_url} → Profile → Linked nodes") if sys.platform == "win32": print(" 2. meshbay-node autostart install (run at each sign-in)") print(" — or just: meshbay-node (start it now, this session)") diff --git a/packages/meshbay-node/src/meshbay_node/cli/status.py b/packages/meshbay-node/src/meshbay_node/cli/status.py index ebc6c77..51e4c87 100644 --- a/packages/meshbay-node/src/meshbay_node/cli/status.py +++ b/packages/meshbay-node/src/meshbay_node/cli/status.py @@ -47,7 +47,7 @@ def status(args) -> None: "node_key_link": ( "Link node key", f"Copy the node key above and paste it in " - f"Settings → Link Node on {cfg.hub.url}"), + f"Profile → Linked nodes on {cfg.hub.url}"), "group_add": ( "Add a group", "meshbay-node group add <name> --dir /path/to/files"), diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py index 557b523..46b4ae1 100644 --- a/packages/meshbay-node/src/meshbay_node/daemon.py +++ b/packages/meshbay-node/src/meshbay_node/daemon.py @@ -645,8 +645,15 @@ class NodeDaemon(EnrichmentMixin): log.warning("Invalid revocation token: %s", e) async def on_connected(): + # Back from an unlink: the key was linked again and the hub let + # this node in, so it is serving once more. + if self._state.get("status") == "waiting_for_node_key": + self._state["status"] = "running" await self._sync_blocklist(hub) + def on_unlinked(): + self._state["status"] = "waiting_for_node_key" + ws_task = asyncio.create_task(hub.maintain_ws( on_incoming=on_incoming, on_revocation=on_revocation, @@ -654,6 +661,7 @@ class NodeDaemon(EnrichmentMixin): group_ids=lambda: list((self._state.get("groups_ctx") or {}).keys()), on_connected=on_connected, on_blocklist=self._on_blocklist_update, + on_unlinked=on_unlinked, )) self._tasks.append(ws_task) log.info("Hub WS task started") @@ -1043,7 +1051,7 @@ class NodeDaemon(EnrichmentMixin): self._state["status"] = "waiting_for_node_key" log.warning( "Node key not linked. Get it from `meshbay-node " - "status` and paste it in Settings > Link Node on %s " + "status` and paste it in Profile > Linked nodes on %s " "(the desktop client links it automatically). " "Retrying in %ds...", self._config.hub.url, self.LINK_WAIT_S, diff --git a/packages/meshbay-node/src/meshbay_node/hub_client.py b/packages/meshbay-node/src/meshbay_node/hub_client.py index 54f87f0..020903d 100644 --- a/packages/meshbay-node/src/meshbay_node/hub_client.py +++ b/packages/meshbay-node/src/meshbay_node/hub_client.py @@ -230,12 +230,21 @@ class HubClient: return r.json().get("email", "") async def unlink_node_key(self) -> None: - """Clear pk_node_ed25519 on the hub (best-effort before reset).""" + """Unlink this node's key from its account; the account's other nodes keep theirs.""" if self._session is None: return - await self.ensure_fresh_token() - r = await self._http.delete("/v1/users/me/node_key", - headers=self._session.auth_headers) + # A fresh token, not merely an unexpired one: the hub lets a node unlink + # the key its token names, and a token issued before that claim existed + # names none. A key already unlinked has nothing left to let go of. + try: + await self.login() + except httpx.HTTPStatusError as e: + if e.response.status_code == 401 and "No node key" in e.response.text: + return + raise + r = await self._http.request("DELETE", "/v1/users/me/node_key", + json={"pk_node_ed25519": self._keys.pk_ed25519_b64}, + headers=self._session.auth_headers) r.raise_for_status() # ── User pubkey lookup ──────────────────────────────────────────────────── @@ -336,6 +345,7 @@ class HubClient: group_ids: list[str] | None = None, # static list or callable returning one on_connected: Any = None, on_blocklist: Any = None, + on_unlinked: Any = None, ) -> None: """ Maintain a persistent WebSocket connection to the hub. @@ -385,12 +395,18 @@ class HubClient: auth_resp = json.loads( await asyncio.wait_for(ws.recv(), timeout=15)) if auth_resp.get("type") != "auth_ok": - # Not fatal: the token may simply have expired while we - # were disconnected. Refresh on the next pass rather than - # ending the task, which used to strand the node for good. + # Not fatal: the token may have expired while we were + # disconnected, or this node's key was unlinked from the + # account (which ends its tokens at once). Sign in again + # rather than wait for the token to near its expiry: a key + # linked again is then back within seconds, and one still + # unlinked says so through `on_unlinked`. log.warning("WS auth refused: %s — retrying in 5s", auth_resp) await asyncio.sleep(5) - await self.ensure_fresh_token() + if "no longer linked" in str(auth_resp.get("detail", "")): + await self._sign_in_again(on_unlinked) + else: + await self.ensure_fresh_token() continue self._ws = ws @@ -447,6 +463,22 @@ class HubClient: finally: self._ws = None + async def _sign_in_again(self, on_unlinked: Any) -> None: + """Sign in afresh; tell `on_unlinked` when the hub no longer knows this key.""" + try: + await self.login() + except httpx.HTTPStatusError as e: + if e.response.status_code == 401 and "No node key" in e.response.text: + log.warning("This node's key is no longer linked to %r — waiting " + "for it to be linked again", self._config.username) + if on_unlinked: + on_unlinked() + # Ten seconds a round in all: the hub allows 10 node sign-ins a + # minute, and a node waiting for its link must stay under it. + await asyncio.sleep(5) + return + raise + async def _answer_offer(self, ws, on_webrtc_offer, msg: dict) -> None: """Negotiate one WebRTC offer and return the answer, off the read loop.""" try: |