diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 12 | ||||
| -rw-r--r-- | docs/MESHBAY_NODE_PROTOCOL.md | 10 |
2 files changed, 20 insertions, 2 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 39a24da..f125825 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -1519,7 +1519,17 @@ account has and when each last signed in. Registration on the node socket requires a **node-scoped token**, verifies the node record against the token subject, and **derives group claims from the database**: a node may narrow the set to what it hosts but cannot widen it, and cannot displace a -live registration (**C2**). +live registration (**C2**). Narrowing goes all the way down: **an empty +claim is a claim on nothing**, never on everything. Reading it as "all of this +account's groups" made an unconfigured node a registered source for groups it could +not serve — including other members' — and since `/v1/groups/{id}/nodes` answers in +registration order, one such node reaching the hub first made a group unopenable for +everyone in it (2026-09-11). The ceiling applies to **every** message that changes the +set, not only to the registration: a node that may narrow on connecting and widen on +reload has no ceiling. + +A client must therefore treat that list as candidates rather than a ranking, and try +the next node on a `not_hosted` refusal (`MESHBAY_NODE_PROTOCOL.md` §6.3). The node authenticates to the hub with an Ed25519 signature over a domain-separated timestamped message — **no password and no auth key on a node** — and receives a diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md index f7e8c48..81605c6 100644 --- a/docs/MESHBAY_NODE_PROTOCOL.md +++ b/docs/MESHBAY_NODE_PROTOCOL.md @@ -466,7 +466,7 @@ absent. `verify_proof` compares with `hmac.compare_digest`. | `group_id` non-empty | `group_id is required` | an absent group means no membership check to make; there is no default group, and a node's first group is not one | | not on the denylist for `user_id`, `jti` **or** `group_id` | `Token revoked` | all three targets, and persisted to disk: a revocation that a restart forgets is not one | | `group_id ∈ token.groups` | `Not a member of this group`, code `not_a_member` | the membership check itself — a token is proof of an account, never of a group | -| `group_id ∈ node.hosted_groups` | `Group not hosted on this node` | | +| `group_id ∈ node.hosted_groups` | `Group not hosted on this node`, code `not_hosted` | the hub may hand a client several nodes for one group, and only some of them host it | `AuthorizedPeer` carries `user_id`, `group_id`, `username`, `jti` — and deliberately **no user public key**. A key arriving in a token would be a key the hub chose, and the @@ -478,6 +478,14 @@ node's roster. The hub certifies accounts, not keys. group (`groups` is baked in at login and the hub pushes no updates), so the client refreshes once and retries on that code rather than telling a member they are not one. +`not_hosted` is the client's signal to try the **next** node the hub offered for the +group rather than to report a failure. `/v1/groups/{id}/nodes` returns every node +registered for the group, in hub registration order, and that order is not a ranking: +a node listed first is not necessarily one that holds the group's files. Refusing +without a code made this indistinguishable from a refusal the reader has to act on, +and a client that stopped at the first node let one wrongly registered peer make a +group unopenable for all of its members (2026-09-11). + ### 6.4 Channel binding | Transport | Anchor | Construction | |