aboutsummaryrefslogtreecommitdiffstats
path: root/docs/MESHBAY_DESIGN.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-19 01:49:26 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-19 01:49:56 +0200
commit765214c22e7956f9add1501c69e00c918f3e5f2b (patch)
tree73f86973b8633e23c490a570f3176bcab3e452b3 /docs/MESHBAY_DESIGN.md
parent3e7c7eba8e512df852da38a6b9ddd8467ec4d4e7 (diff)
downloadmeshbay-765214c22e7956f9add1501c69e00c918f3e5f2b.tar.gz
docs: correct the spec where it had drifted from the code
Found by writing the user guide against the CLI rather than against this document. §6.7 listed a `member device` verb nobody ever wrote; §9.9 still called several folders a Photos-only shape after app_directories gave every application a set; §6.8 said five settings and the Node page shows eight; §5.5 counted one transfer pool where there are three. The per-device revocation gap is real and stays open — §15.3 names it as the capability it is, not as a documentation error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'docs/MESHBAY_DESIGN.md')
-rw-r--r--docs/MESHBAY_DESIGN.md57
1 files changed, 43 insertions, 14 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md
index 551c0ed..d44de56 100644
--- a/docs/MESHBAY_DESIGN.md
+++ b/docs/MESHBAY_DESIGN.md
@@ -296,9 +296,10 @@ comes from the hash binding:
| Attempts per connection | 5, then a node-wide lockout |
| Filing a request | requires the account to have at least one pinned identity already |
-`member unpin <user>` removes **every** device. `device revoke` marks one rather
-than deleting the row, because a deleted row is a key the node would happily pin
-again.
+`member unpin <user>` removes **every** device of an account, and is the only
+form the CLI has (§6.7). Revoking **one** device is `revoke_device` over MNP,
+driven from a device the node has already pinned, and it *marks* the row rather
+than deleting it — a deleted row is a key the node would happily pin again.
**Which device is on a connection** is proved separately from which account. The
handshake proves the account; `device_hello` — signed over a transcript naming the
@@ -683,7 +684,7 @@ unique `(device, nonce)` — a replay is a validly signed copy, so nothing about
signature refuses it.
**Epochs.** A new epoch is opened when, and only when, the set of devices that may
-read *future* messages shrinks: `member revoke`, `member unpin`, `device revoke`,
+read *future* messages shrinks: `member revoke`, `member unpin`, `revoke_device`,
`gek_rotate`, or an explicit `chat rotate`. Epoch 1 is opened at group load — a
group with no epoch is a group nobody can speak in.
@@ -1012,7 +1013,7 @@ decisions:
| Bound | Default | Why it exists |
|---|---|---|
-| Node-wide concurrent transfers | 8 | operator's machine |
+| Node-wide concurrent transfers | 8 **per kind** | operator's machine. Downloads and uploads are **separate pools** (`max_concurrent_downloads`, `max_concurrent_uploads`, §6.8), because the two cost different things and one queue for both makes each cap mean nothing. Streaming is a third pool again (`max_concurrent_streams`): a member watching a film is not charged a download slot |
| Per account, per group | 2 | absent means this, not "unlimited" |
| Grant deadline | 30 s | a grant nobody takes up is a slot nobody can use |
| Idle timeout | 120 s | catches a peer that vanished without the connection noticing |
@@ -1513,26 +1514,36 @@ Two personas need different tools, and the headless one is the normal deployment
| Desktop | the desktop client's Node page |
| **Headless / SSH** | the CLI |
-**Every operation is reachable over SSH with no browser on the host.** `status`
+**Every operation is reachable over SSH with no browser on the host**, with one
+exception named in §15.3 rather than left to be discovered: **revoking a single
+device of a member is an interface action**, and the CLI's only lever is
+`member unpin`, which removes all of that account's devices at once. `status`
deliberately reads the keystore and config directly so it works while the daemon is
stopped — the state an operator is most often in, since the daemon will not stay up
before its key is linked or a group exists.
```
meshbay-node status
-meshbay-node group add <name> --dir <path> [--no-writable]
+meshbay-node group list|add|remove
meshbay-node root list|add|remove|set|eject|plug
meshbay-node gek init|rotate
meshbay-node operator pair
meshbay-node member list|invite|revoke|unpin
-meshbay-node member device list|revoke
-meshbay-node denylist show|clear
meshbay-node file list|rm
-meshbay-node chat status|prune|encrypt-history
+meshbay-node video rematch
+meshbay-node chat status|rotate|encrypt-history|prune
+meshbay-node transfers show|set|max-size|per-member
+meshbay-node denylist show|clear
meshbay-node stun list|add|remove|reset
-meshbay-node reload
+meshbay-node reload|restart-daemon
```
+**This block is a list of verbs that exist.** It carried
+`member device list|revoke` for as long as device linking has been built, and no
+such verb was ever written — `device` falls through to the username resolver and
+fails. A command list in a specification is read as a promise by whoever writes
+the next document from it, which is exactly how it was found (§15.3).
+
`member revoke`/`unpin` resolve a username against the roster and **refuse an
unknown one** rather than acting on nobody — a typo must not look like success.
Revocation tells the operator what it does *not* do: the ex-member stops receiving
@@ -1570,7 +1581,7 @@ a real capability reduction, not an oversight.
### 6.8 Node settings
-Five `[node] `settings affect what the node does rather than how it starts, and
+Eight `[node] `settings affect what the node does rather than how it starts, and
their value is invisible until something goes wrong — so they are surfaced on the
Node page:
@@ -1580,8 +1591,16 @@ Node page:
| `pair_ttl_hours` | 24 | how long an operator pairing code stays valid |
| `device_request_ttl_minutes` | 60 | how long a device request waits for approval. Comfort, not security: the code is bound to the keys by its hash |
| `max_concurrent_streams` | 8 | simultaneous video streams. One process per viewer, ~50 MB each; a slot is held for the length of a film, so this counts viewers |
+| `max_concurrent_downloads` | 8 | node-wide download leases (§5.5) |
+| `max_concurrent_uploads` | 8 | node-wide upload leases. A separate pool from downloads, because the two cost different things and one queue for both makes each cap meaningless |
+| `max_upload_gb` | 8 | the largest single upload, §6.4. Fractions are allowed, and it is read per chunk so a change reaches an upload already running |
| `transcode_incompatible_video` | true | whether browser-incompatible video is transcoded during streaming. Unlike remuxing this costs real CPU per viewer |
+`hardware_video_encode` (default true) governs whether that transcode may use the
+GPU. It is `node.toml` only and not on the page: the capability is established by
+encoding and reading the result back (`hwaccel.py`), so the setting means "use it
+if it works" and there is nothing for an operator to decide from a panel.
+
> **Turning transcoding off does not mean the same thing for every source.** A
> codec with an MSE codec string falls back to a copy and the viewer's own decoder
> decides; a codec with none has nothing to fall back to and the stream is refused,
@@ -2388,14 +2407,23 @@ it.
### 9.9 Photos
An album browser over the image files in the group's configured folders. It is
-**smaller** than Videos and Music, deliberately, in three ways:
+**smaller** than Videos and Music, deliberately, in two ways:
-- **Several root folders rather than one.**
- **One album-grid view, no mode toggle**, because there is nothing to fall back
from.
- **No third-party service at all** — there is nothing to match a photo *to*. It
already is what it is, per its own folder and filename.
+> **"Several root folders rather than one" used to be the third**, and is not a
+> difference any more. The generic `app_directories` op (§9.3) gave every
+> application a *set* of folders, so what Photos needed — a library scattered
+> across directories — is what Videos and Music get as well. The pane that
+> chooses them is the same shared widget in `multi` mode for all three; Chat's
+> is the only `single` one, because an attachment is written to exactly one
+> place. It is worth recording because the per-application shapes converging is
+> the whole point of §9.3, and this section had been left stating the
+> difference it removed.
+
An **album is a directory**, exactly as a season is a folder in Videos. Thumbnails
are node-side, orientation-corrected, and delivered through the same chunk path.
@@ -3084,6 +3112,7 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows.
| **QUIC** | Off by default, and **not at parity**: it serves the index and file chunks with no transfer lease, no leaseless ceiling and no root-availability check, does its file I/O on the event loop, and returns exception text to the peer (**L3**). No client speaks it. Either it comes to parity or it goes; until then §5.1's "chat is the only gap" is the one sentence here that overstates the code |
| **The relay registry** | **Closed in the code**: `relay.RELAYS_ENABLED` is False and every `/v1/relays` route answers 503, as federation does. Nothing in the tree calls them, node or client, and §11.1 measured two ISPs with no TURN relay needed. Kept code that nothing calls is what **L7** says not to keep; it stays only as the proof-of-possession design (**AV6**) until a node needs a relay or it is deleted |
| **Free-text third-party search** | `tmdb_search_req` takes a member's query and spends the operator's per-credential quota with no rate limit and no per-member bound, where link previews carry both. §6.5's standing rule — a bound and a named adversary in the same commit — was not applied here |
+| **Per-device revocation has no CLI** | A device is revoked over MNP (`roster.revoke_device`), from a device the node has already pinned. On a headless node the operator's only lever is `member unpin`, which removes **every** device of that account — so the per-device control the roster is built around is reachable from an interface and from nowhere else. §6.7 listed a `meshbay-node member device list\|revoke` verb that was never written, and that listing is how this was found: `USERGUIDE.md` was the first document written by reading the CLI rather than this specification, and the verb it copied out did not run |
| **Node announcements are not bounded** | One account may announce unlimited distinct node keys, each a row plus an IP-log row under a one-year retention. Proof of possession is checked (**M8**); the count is not |
| **Migrations run on SQLite only** | The chain reaches head and agrees with the models there (§12), which is not where it ships. **The exposure is one revision deep, not the whole chain**: every revision behind the first packaged release was development that no installation ever ran, so nothing replays them on PostgreSQL. What is unguarded is the *next* migration — a default, an index type or a constraint PostgreSQL refuses reaches a deploy without the suite saying so |