From 4cce50f09a73739387d5058a6f8183ebac65ae2c Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 12 Sep 2026 09:47:07 +0200 Subject: fix: an empty group claim is a claim on nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A node that hosts no groups sends no `group_ids` on its hub socket, and the hub resolved the claim with `set(claimed_groups or authorized)` — so "I host nothing" arrived as "I host every group this account belongs to", other members' included. Such a node can serve none of them: it holds no GEK, and its own handshake refuses them with "Group not hosted on this node". `/v1/groups/{id}/nodes` answers in registration order and `_node_groups` is in-memory, so which node a client was sent to depended on who reconnected first after a hub restart. GroupPage took `nodes[0]` with no fallback. On 2026-09-11 a hub deploy at 20:14 reshuffled the registry, a second member's unconfigured node won the race, and a group stopped opening for everyone in it with its only real host online throughout. Any member could take one of their groups down, by accident, by leaving an empty node running. Four changes, because no one of them is sufficient: - the hub never widens an absent claim, and `update_groups` goes through the same ceiling as registration — it assigned its list verbatim, so the bound that makes C2 hold at authentication was one message wide - the node states the empty set rather than omitting the field - the refusal carries `not_hosted`, so a client can tell "try the next node" from "you, here, must do something first" - GroupPage walks the list instead of indexing into it The three lines involved date from 13, 20 and 23 August and each is defensible alone. The defect is in the seam, which is where the last two also were: a falsy empty collection must never mean "unspecified". Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01T4YmK41VsEURWFdop4EEeT --- packages/meshbay-common/src/meshbay_common/handshake.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'packages/meshbay-common/src') diff --git a/packages/meshbay-common/src/meshbay_common/handshake.py b/packages/meshbay-common/src/meshbay_common/handshake.py index 65b4e85..4d2cac2 100644 --- a/packages/meshbay-common/src/meshbay_common/handshake.py +++ b/packages/meshbay-common/src/meshbay_common/handshake.py @@ -278,7 +278,11 @@ def authorize_token( raise HandshakeError("Not a member of this group", code="not_a_member") if hosted_groups is not None and group_id not in hosted_groups: - raise HandshakeError("Group not hosted on this node") + # Coded, because a client handed several nodes for one group has to tell + # "this node cannot serve it, try the next one" apart from "you, in this + # browser, must do something first". Uncoded it was neither, and a node + # wrongly registered for a group took that group down for everyone. + raise HandshakeError("Group not hosted on this node", code="not_hosted") return AuthorizedPeer( user_id=user_id, -- cgit v1.2.3