aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node')
-rw-r--r--packages/meshbay-node/src/meshbay_node/cli/setup.py2
-rw-r--r--packages/meshbay-node/src/meshbay_node/cli/status.py2
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py10
-rw-r--r--packages/meshbay-node/src/meshbay_node/hub_client.py48
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: