""" A hub that keeps the rules the CLI depends on, in memory. Its refresh tokens **rotate, and a spent one presented again revokes the whole family**, as the real hub does. A lax stub would pass a client that renews twice with the same token — which is exactly the failure two runs at once produce. """ import asyncio import base64 import secrets from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from meshbay_cli.client.hub import HubError from meshbay_cli.client.keys import auth_key USERNAME = "alice-test" PASSPHRASE = "a passphrase for the fake hub" USER_ID = "0b4f6f0e-5d7e-4e8a-9d2b-6a1c1b9e2f11" PEPPER = base64.b64encode(b"\x07" * 32).decode() class FakeHub: def __init__(self, url: str = "https://hub.test"): self.url = url self.ak = auth_key(USERNAME, PASSPHRASE) self.refresh_tokens: dict[str, dict] = {} # token → {family, spent} self.revoked_families: set[str] = set() self.access: set[str] = set() self.devices: dict[str, str] = {} # id → pk self.refresh_calls = 0 self.reuse_detected = False self.refresh_delay = 0.0 self.groups = [{"id": "g1", "name": "Family", "owner_username": USERNAME, "node_online": True, "visibility": "private", "is_admin": True}] self.pending: list[dict] = [] self.links: dict[str, dict] = {} self.prefs: dict[str, str] = {} def _session(self, family: str | None = None) -> dict: family = family or secrets.token_hex(4) rt, at = secrets.token_hex(8), secrets.token_hex(8) self.refresh_tokens[rt] = {"family": family, "spent": False} self.access.add(at) return {"access_token": at, "refresh_token": rt, "expires_in": 14400} def _auth(self, token: str) -> None: if token not in self.access: raise HubError(401, "Invalid token") async def aclose(self) -> None: pass async def check_version(self) -> None: return None async def login(self, username, ak, known_browser): from meshbay_cli.client.hub import _sign_in_refusal if username != USERNAME or ak != self.ak: raise _sign_in_refusal(HubError(401, "Invalid credentials")) return {**self._session(), "bundle_pepper": PEPPER, "bundle_pepper_version": 1, "known_browser": "kb-1"} async def me(self, token): self._auth(token) return {"user_id": USER_ID, "username": USERNAME} async def register_device(self, token, pk, label, ak): self._auth(token) assert ak == self.ak, "registering a device takes the passphrase" for did, known in self.devices.items(): if known == pk: return {"id": did, "existing": True} did = f"dev-{len(self.devices) + 1}" self.devices[did] = pk return {"id": did, "existing": False} async def delete_device(self, token, device_id): self._auth(token) self.devices.pop(device_id) async def device_auth(self, username, sk): pk = base64.b64encode(sk.public_key().public_bytes_raw()).decode() if pk not in self.devices.values(): from meshbay_cli.errors import AUTH, CliError raise CliError(AUTH, "Sign-in refused") Ed25519PublicKey.from_public_bytes(sk.public_key().public_bytes_raw()) return {**self._session(), "bundle_pepper": PEPPER} async def refresh(self, rt): self.refresh_calls += 1 row = self.refresh_tokens.get(rt) if row is None or row["family"] in self.revoked_families: raise HubError(401, "Invalid refresh token") if row["spent"]: self.reuse_detected = True self.revoked_families.add(row["family"]) raise HubError(401, "Token reuse detected — family revoked") row["spent"] = True await asyncio.sleep(self.refresh_delay) return self._session(row["family"]) async def logout(self, rt): row = self.refresh_tokens.get(rt) if row: self.revoked_families.add(row["family"]) async def my_groups(self, token): self._auth(token) return self.groups # ── Joining and leaving ────────────────────────────────────────────────── # # `pending` holds invitations (group rows with `invited_by`); `links` maps a # ticket to the group it admits to, and is spent by the first redeem. def _group_row(self, g: dict) -> dict: return {k: v for k, v in g.items() if k not in ("invited_by", "created_at")} async def invitations(self, token): self._auth(token) return [{"group_id": g["id"], "name": g["name"], "owner_username": g["owner_username"], "invited_by": g["invited_by"], "created_at": "2026-10-14T09:30:00+00:00"} for g in self.pending] async def accept_invitation(self, token, group_id): self._auth(token) found = [g for g in self.pending if g["id"] == group_id] if not found: raise HubError(404, "No such invitation") self.pending.remove(found[0]) self.groups.append(self._group_row(found[0])) return {"status": "joined", "group_id": group_id} async def decline_invitation(self, token, group_id): self._auth(token) found = [g for g in self.pending if g["id"] == group_id] if not found: raise HubError(404, "No such invitation") self.pending.remove(found[0]) return {"status": "declined", "group_id": group_id} async def preview_link(self, token, ticket): self._auth(token) g = self.links.get(ticket) if g is None: raise HubError(404, "invite_not_valid") return {"group_id": g["id"], "group_name": g["name"], "inviter": "bob-test", "expires_at": "2026-10-21T00:00:00+00:00", "already_member": any(m["id"] == g["id"] for m in self.groups)} async def redeem_link(self, token, ticket): self._auth(token) g = self.links.pop(ticket, None) if g is None: raise HubError(404, "invite_not_valid") self.groups.append(self._group_row(g)) return {"group_id": g["id"], "group_name": g["name"]} async def leave_group(self, token, group_id): self._auth(token) g = next((g for g in self.groups if g["id"] == group_id), None) if g is None: raise HubError(404, "You are not a member of this group") if g.get("is_admin"): raise HubError(409, "You own this group, so you cannot leave it") self.groups.remove(g) return {"status": "left", "group_id": group_id} async def preferences(self, token): self._auth(token) return dict(self.prefs) async def set_preference(self, token, key, value): self._auth(token) self.prefs[key] = value return {"key": key, "value": value}