aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-13 20:28:52 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-13 20:28:52 +0200
commit729d28030746bb8b3688f85833c228acd304be61 (patch)
tree6145f5af95497bc232a8b234b29e55ca21994a71 /docs
parent9da3cfc5d31bc4e1b9c4ea70f56e07b9aed8bb85 (diff)
downloadmeshbay-729d28030746bb8b3688f85833c228acd304be61.tar.gz
fix(node): chat is bounded in size and in rate
A chat message is the plainest member-supplied write there is: the node stores it in `chat.db`, where nothing expires it — retention is a manual command (§6.6) — relays it to every other connected member, and has the hub write a notification for every member of the group. Nothing bounded any of it. The only ceiling was the DataChannel frame, 64 MB once the handshake is done, so one member in a loop filled the operator's disk and saturated everyone else's connection, and the node's answer to each message was `ack`. Uploads, the other member-supplied write, have carried a filename allowlist, strict chunk ordering, a no-overwrite rule and a 4 GB cap since C5a — because somebody asked what one member costs the others on that path. Nobody had asked it on this one. Two bounds, for the two halves of the question: **64 KB of ciphertext** for what one message may cost, and **60 a minute per account per group** for how often one member may impose it. Both are checked before anything is stored or relayed; a refusal names itself and is audited, so "why is my disk full" has an answer. The rate is keyed by account, not by connection: a second tab does not make anyone type faster, and keying on the session would hand a script one budget per socket it opens. No node-wide ceiling beside it, deliberately. The link-preview limiter has one because a preview spends the *node's* egress and its third-party quota, which is one shared thing; a chat message spends the sender's own group, and a node-wide ceiling would let a busy group silence a quiet one — this same defect one level up. The last test in the new file is that property: a member at their limit has not spent anybody else's. Two things stay open on purpose and are named rather than quietly done: retention still keeps everything, because a default that deletes people's history is not a review's call; and the composer still offers to send an oversized message, so this is §6.4's pattern with only the node half built. §6.6 gains the rule, §13.5b the label — AV20, with AV21–AV23 registering the three fixes this week that closed the same kind of gap elsewhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UMxEQadpzPkYLFf5CYKhpW
Diffstat (limited to 'docs')
-rw-r--r--docs/MESHBAY_DESIGN.md26
1 files changed, 26 insertions, 0 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md
index d1630f0..4147fe7 100644
--- a/docs/MESHBAY_DESIGN.md
+++ b/docs/MESHBAY_DESIGN.md
@@ -1425,6 +1425,28 @@ chat opens at the newest page. A forwards pager is not what a chat opens with.
epoch with no messages is harmless; an epoch key deleted while messages still need
it is an unreadable archive.
+**A message is bounded in size and in rate, like every other member-supplied
+write.** Sending one costs the operator a row that nothing expires, every other
+connected member a relayed copy, and every member of the group a notification —
+so the two bounds answer the two halves: **64 KB of ciphertext** for what one
+message may cost, and **60 a minute per account per group** for how often one
+member may impose it. Both are checked before anything is stored or relayed, and
+a refusal names itself (`chat_too_large`, `chat_rate_limited`) and is audited.
+
+The numbers are meant to be invisible. The sealed payload is the text, a thread
+id, a display name and a timestamp — an attachment is a file on a root and
+travels as a reference (§4.5) — so 64 KB is some sixty thousand characters, and
+sixty a minute is far above a person typing. The rate is keyed by **account**,
+not by connection: a second tab does not make anyone type faster, and keying on
+the session would hand a script one budget per socket it opens.
+
+**There is deliberately no node-wide chat ceiling** beside the per-account one,
+and the contrast with link previews is the reason. A preview spends the *node's*
+egress and its third-party quota, which is one shared thing and deserves a
+shared bound; a chat message spends the sender's own group. A node-wide ceiling
+would let a busy group silence a quiet one — the same defect this bound closes,
+one level up.
+
### 6.7 Operator surface
Two personas need different tools, and the headless one is the normal deployment:
@@ -2574,6 +2596,10 @@ had already been asked.
| **AV18** | **The hub runs on exactly one worker, and says so at startup.** `_connected_nodes`, `_node_groups`, `_webrtc_answers` and the relay registry are per-process: a second worker makes a node intermittently unreachable for half its members, which is a symptom that describes something else entirely |
| **AV14** | **MHP binds its audience, and the hub reads its own identity at call time.** A token is minted for one peer and accepted by that peer only. `federation.py` bound `_hub_id` and `_hub_sk_pem` at import, which is before `load_hub_keypair` runs, so it signed with `None` and called itself `meshbay.org` whatever the instance was named — and the verifier named no audience for the `aud` the issuer sets, which PyJWT refuses outright. MHP could not complete one authenticated request between two hubs |
| **AV15** | **A hash is checked for shape before it is a key lookup**, on the unauthenticated blocklist endpoints a node consults |
+| **AV20** | **Chat is bounded in size and in rate, like every other member-supplied write** (§6.6). A message is a row on the operator's disk that nothing expires, a relayed copy for every connected member and a notification for every member of the group; the only ceiling was the frame size. Uploads had carried four protections and a cap since C5a because somebody asked what one member costs the others on that path, and nobody had asked it on this one |
+| **AV21** | **A lease is what the node granted, not what the client called it** (§5.5). `tr` was read as a boolean, so any non-empty string skipped the leaseless ceiling and every cap behind it, and a queued transfer was held back only by the honesty of the client waiting in the queue |
+| **AV22** | **The node's own controls take no authority from a hub token** (§6.7). `node_status`, `node_settings_set`, `roster_read`, `denylist_read`, `denylist_clear` and `node_reload` were gated on the account id in the JWT, which is the hub's to choose — NS4 and M3 with the check written the other way round. The gate is a proved operator device, which a hub holding no user keys cannot produce |
+| **AV23** | **An upload's owner is recorded when the upload ends and applied when the entry is created**, which are different moments (§5.4). Written against the index at the end of the upload it matched nothing, every time, and left every uploaded file owned by nobody — so no member could delete what they had sent |
### 13.6 Chat design findings