From 6f68bb6c1970e26de1f66c073ea5d7fecd21840d Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 15 Oct 2026 00:56:04 +0200 Subject: feat(cli): invitations, join, decline, leave, devices, browser access join accepts an invitation or redeems a link pasted as sent, then has the node admit a key pair made here, with the invitation's code (a link's code goes only to the node it names; one for a node that is away is kept for the next connection). decline, leave and devices revoke ask first, and refuse without --yes where there is no terminal. devices approve countersigns the one waiting request a code matches, none when several do. browser-access is the application's setting, kept per machine and never enabled from the hub's mirror: on leaves a copy sealed under M on each node, off withdraws it, unset touches nothing. A key pair made for a first contact is now kept. Co-Authored-By: Claude Opus 5.5 --- .../meshbay-cli/src/meshbay_cli/client/account.py | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) (limited to 'packages/meshbay-cli/src/meshbay_cli/client/account.py') diff --git a/packages/meshbay-cli/src/meshbay_cli/client/account.py b/packages/meshbay-cli/src/meshbay_cli/client/account.py index d620f6d..9bb85ce 100644 --- a/packages/meshbay-cli/src/meshbay_cli/client/account.py +++ b/packages/meshbay-cli/src/meshbay_cli/client/account.py @@ -163,11 +163,14 @@ class Account: # A node's key does not depend on who signed in. "pins": previous.get("pins", {}) if previous.get("hub") == self.hub.url else {}, } + if same_account and previous.get("browser_access"): + state["browser_access"] = previous["browser_access"] secrets = { "access_token": token, "access_exp": time.time() + int(r.get("expires_in", 0)), "refresh_token": r["refresh_token"], "master": b64(master), + "pepper_version": int(r.get("bundle_pepper_version") or 1), "legacy": b64(a), "identities": self.secrets.get("identities", {}) if same_account else {}, } @@ -306,6 +309,38 @@ class Account: secrets.setdefault("identities", {})[node_pk] = p["keys"] await self.update(move) + # An invitation's code waiting for its node: kept when the node was not + # reachable at `join`, sent by the next connection to it, dropped once used + # or refused. A link's code names its node; an invitation's does not. + + def join_code(self, group_id: str, node_pk: str) -> str | None: + c = self.secrets.get("join_codes", {}).get(group_id) + if not c or (c.get("node_pk") and c["node_pk"] != node_pk): + return None + return c["code"] + + async def keep_join_code(self, group_id: str, code: str, node_pk: str | None) -> None: + def put(state: dict, secrets: dict) -> None: + secrets.setdefault("join_codes", {})[group_id] = {"code": code, + "node_pk": node_pk} + await self.update(put) + + async def drop_join_code(self, group_id: str) -> None: + def drop(state: dict, secrets: dict) -> None: + secrets.get("join_codes", {}).pop(group_id, None) + if group_id in self.secrets.get("join_codes", {}): + await self.update(drop) + + @property + def browser_access(self) -> str | None: + """This machine's setting: "on", "off", or None (unset — leave the node + as it is). Set only by `browser-access`; never from the hub.""" + return self.state.get("browser_access") + + @property + def pepper_version(self) -> int: + return int(self.secrets.get("pepper_version") or 1) + def pin(self, node_id: str) -> str | None: return self.state.get("pins", {}).get(node_id) -- cgit v1.2.3