diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-14 22:20:15 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-14 22:20:15 +0200 |
| commit | 422647e7898b8df67cd11f8fb060aec23dec84aa (patch) | |
| tree | 7ecca0e8df1bc8a7d26cf4b471bd44906dd9cfd1 /packages/meshbay-cli/tests/fake_hub.py | |
| parent | 28c88908b88c58cba4de08710f6bb268ede02d57 (diff) | |
| download | meshbay-422647e7898b8df67cd11f8fb060aec23dec84aa.tar.gz | |
feat(cli): meshbay-cli, a command-line client for members
Signs in to the hub, lists the account's groups and their folders, and
downloads and uploads files directly with each group's node, end-to-end
encrypted. With no argument it opens a two-pane interactive mode (Textual).
Session and device keys are kept encrypted under the system's key storage,
or a 0600 key file where there is none; node keys are pinned at first
contact; a new machine links as a device.
Packaged on its own on Linux (deb and rpm, on meshbay-common's venv, with a
man page generated from the parser) and frozen next to meshbay-node in the
full Windows installer, not the light one, with a self-test the build runs.
client/keys.py ports keyderive.js and is LGPL like it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-cli/tests/fake_hub.py')
| -rw-r--r-- | packages/meshbay-cli/tests/fake_hub.py | 108 |
1 files changed, 108 insertions, 0 deletions
diff --git a/packages/meshbay-cli/tests/fake_hub.py b/packages/meshbay-cli/tests/fake_hub.py new file mode 100644 index 0000000..ec1a0c0 --- /dev/null +++ b/packages/meshbay-cli/tests/fake_hub.py @@ -0,0 +1,108 @@ +""" +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}] + + 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 |