From 1d169141f3a5542efda2f7fdb0bb88308c06191b Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 23 Sep 2026 18:16:02 +0200 Subject: feat(node): member invite --link and member cancel in the CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI makes both halves itself — the node's code, then the hub's ticket bound to the address — and prints the link; a refused ticket takes the code back, and cancel takes back both. The CLI never asks the hub to mail. Co-Authored-By: Claude Opus 5.5 --- .../meshbay-node/src/meshbay_node/hub_client.py | 42 ++++++++++++++++++++++ 1 file changed, 42 insertions(+) (limited to 'packages/meshbay-node/src/meshbay_node/hub_client.py') diff --git a/packages/meshbay-node/src/meshbay_node/hub_client.py b/packages/meshbay-node/src/meshbay_node/hub_client.py index 58bf657..06d3503 100644 --- a/packages/meshbay-node/src/meshbay_node/hub_client.py +++ b/packages/meshbay-node/src/meshbay_node/hub_client.py @@ -85,6 +85,11 @@ class HubClient: async def __aenter__(self): return self + @property + def hub_url(self) -> str: + """The hub's address as this node was configured with it.""" + return self._config.hub_url + async def __aexit__(self, *_): await self.close() @@ -258,6 +263,43 @@ class HubClient: r.raise_for_status() return r.json() + async def create_invite_link(self, group_id: str, email: str, expires_at: str, + node_invite_id: str) -> dict: + """ + The hub's half of an invitation link: a ticket bound to `email`. + + Never with `send_email`: the hub refuses mail to a node token, and the + CLI mails nothing — the operator sends the link (docs/USERGUIDE.md §7). + """ + if self._session is None: + raise RuntimeError("Not logged in") + await self.ensure_fresh_token() + r = await self._http.post( + f"/v1/groups/{group_id}/invite-links", + json={"email": email, "expires_at": expires_at, + "node_invite_id": node_invite_id}, + headers=self._session.auth_headers, + ) + r.raise_for_status() + return r.json() + + async def list_invite_links(self, group_id: str) -> list[dict]: + if self._session is None: + raise RuntimeError("Not logged in") + await self.ensure_fresh_token() + r = await self._http.get(f"/v1/groups/{group_id}/invite-links", + headers=self._session.auth_headers) + r.raise_for_status() + return r.json().get("links", []) + + async def delete_invite_link(self, group_id: str, link_id: str) -> None: + if self._session is None: + raise RuntimeError("Not logged in") + await self.ensure_fresh_token() + r = await self._http.delete(f"/v1/groups/{group_id}/invite-links/{link_id}", + headers=self._session.auth_headers) + r.raise_for_status() + # ── Persistent WebSocket (signaling + revocations) ────────────────────── async def send_ws(self, data: str) -> None: -- cgit v1.2.3