diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/hub_client.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/hub_client.py | 48 |
1 files changed, 40 insertions, 8 deletions
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: |