summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/hub_client.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/hub_client.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/hub_client.py48
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: