aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/MESHBAY_DESIGN.md12
-rw-r--r--docs/MESHBAY_NODE_PROTOCOL.md10
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 |