aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/meshbay-draft-v6.md105
1 files changed, 103 insertions, 2 deletions
diff --git a/docs/meshbay-draft-v6.md b/docs/meshbay-draft-v6.md
index 72cfdbf..a5fb8d3 100644
--- a/docs/meshbay-draft-v6.md
+++ b/docs/meshbay-draft-v6.md
@@ -10,6 +10,11 @@
> devices are admitted, how authorship is established, and which shell the native client
> uses. It also records one rule v5 assumed without writing down.
>
+> **Amended 2026-08-28** with three changes that post-date that discussion: the hub gains a
+> runtime instance-policy store (public groups can be switched off hub-wide), group names
+> become unique per owner account (`name@owner`), and chat gains link previews. See §1
+> rows 11–13 and §§2.8–2.10. Wire versions at that point: MNP 0.12, MHP 0.1; packages 0.8.0.
+>
> The v5 convention is carried forward and is not negotiable: **a claim in this document
> must name the adversary it holds against.** A property that holds against a passive hub
> and not an active one is written that way.
@@ -45,6 +50,9 @@
| 8 | Authorship | Chat senders are **cryptographically authenticated to each other**; an upload has a **provable owner** who may delete it, as the operator may. v5's node-asserted attribution is replaced | operator decision, §2.4b |
| 9 | Node authority | The operator may **close uploading to everyone but themselves**, per group. Signed MNP op, stored on the node, enforced by the node — the hidden button is a courtesy, the refusal is the control | §2.1b |
| 10 | Client | A group's UI is a **set of pluggable applications** (Chat, Files today), not one monolithic page. Which are shown is a per-group, operator-signed setting on the same pattern as change 9 | §2.7 |
+| 11 | Hub role | The hub gains a **runtime instance-policy store** (`hub_settings`). First policy: an admin switches **public groups off** hub-wide, enforced server-side on every hub-mediated path. `suspend` vs `revoke` on a group are now written down as the distinct things they are | §2.8 |
+| 12 | Group registry | A group name is **unique per owner account**, not globally; the group's identity is still its UUID. Listed everywhere as `name@owner` | §2.9 |
+| 13 | Enrichment | **Chat link previews** — the node unfurls a pasted URL into an OpenGraph card. A new instance of the §2.7 "node on demand, asking device caches, nothing durable" rule; new SSRF surface, gated. MNP 0.12 | §2.10 |
---
@@ -210,8 +218,8 @@ And the rule v5 assumed without writing:
> **Group-related server state lives on the node.** Files, indexes, members' devices,
> pending device requests, invitations, chat, per-root availability, and anything a future
> feature wants to keep about a group — all on the node. The hub holds accounts, the group
-> registry and membership, signaling, notifications and the moderation surface, and
-> nothing else about content.
+> registry and membership, signaling, notifications, the moderation surface, and
+> instance policy (`hub_settings`, §2.8) — and nothing else about content.
Verified for the multi-root change: `SwarmSource` carries `content_hash`, `node_id` and
`endpoint` — **no paths, no filenames** — and private groups register nothing (H7). The
@@ -261,6 +269,99 @@ application — is `docs/apps.md`, on the same basis `docs/desktop-client-v1.md`
desktop client's detail: this document states what changed and what holds, not how to build
on it.
+### 2.8 §6.1 Hub role — instance policy, and public groups can be switched off
+
+New (2026-08-28). The hub had no runtime-configurable setting: everything was `hub.toml`
+(read once, restart to change) or code. It now has `hub_settings`, a key/value table an
+admin edits from the panel. The first entry is `allow_public_groups`.
+
+**This does not contradict change 5 / §2.5.** `hub_settings` is *instance policy, not group
+content*: it says how this hub behaves, and holds nothing about any group's files, index,
+membership or keys. "Group-related server state lives on the node" is untouched.
+
+**What "off" does** — server-side, on every path the hub mediates, read live:
+
+- `create_group` refuses `visibility=public` — staff included; the way back is to
+ re-enable it, not to slip past
+- the public directory (`GET /v1/groups`) returns nothing, local **and** federated
+- open-joining a public group is refused
+- `group_online_nodes` hands a non-member no node to connect to
+- `webrtc_offer` drops its "the node hosts an open-join group, admit anyone" fallback
+- the MHP directory export advertises nothing to peers
+
+Flipping it back restores every path. **Existing members of a group that predates the
+switch keep their membership row and their access** — this is a directory-and-brokering
+control, not a purge.
+
+**What it is not.** The node still reads `visibility` and `join_policy` from its own
+`node.toml`, never the hub (`invite-pairing-v1.md`). A node whose operator set
+`join_policy = "open"` still TOFU-pins and serves whoever reaches it directly over MNP.
+The switch removes the hub-provided ways to *find and reach* such a node; it is **not a
+remote kill**. For a hard stop on one group, `POST /v1/admin/revoke` is the tool.
+
+**`suspend` vs `revoke`, made explicit** — the two moderation verbs on a group were never
+written down as distinct, and they are:
+
+| | `suspend` (`PATCH /v1/admin/groups/{id}`) | `revoke` (`POST /v1/admin/revoke`) |
+|---|---|---|
+| Hub DB | `status = "suspended"` | `status = "revoked"` |
+| Node | nothing | signed revocation broadcast → denylist + live sessions dropped, **persisted across a node restart** |
+| Reversible from the panel | yes | no |
+
+The client shows the real state now (`Group is revoked`, not a blanket `Group is
+suspended`).
+
+### 2.9 §6.1 Hub role — group names unique per owner, shown as `name@owner`
+
+New (2026-08-28). "The name is unique" was an unenforced expectation — the `groups` table
+had only a non-unique index. Two changes:
+
+- A functional unique index `uq_groups_owner_name (admin_id, lower(name))`. Uniqueness is
+ **per owner account**, case-insensitively, trimmed — two different owners may each have a
+ `photos`. `create_group` returns 409 on a clash, with an `IntegrityError` backstop for
+ the race. The migration pre-flights and **aborts listing the offending pairs** rather
+ than renaming anyone's group.
+- **The group's identity is still its UUID.** `#/group/<uuid>`, `_node_groups`,
+ `node.toml [[groups]] id`, membership — all unchanged. `name@owner` is a *label plus a
+ create-time check*, not an addressing scheme.
+
+`owner_username` (derived from `admin_id`, nothing stored anew) rides `/v1/groups/mine`,
+`GET /v1/groups` (local rows), the join response and `/v1/admin/groups`. The SPA renders it
+under the name, smaller and grey, prefixed `@`. The handle is **hub-local**:
+`foofoo@grenet` on two federated hubs are different groups, and a federated row shows
+`@<source_hub>`, not an account.
+
+Design record and the decisions behind it: `~/next/groupnames.md` (kept out of the repo).
+
+### 2.10 Chat link previews
+
+New (2026-08-28). A URL pasted in a group's chat unfurls into an OpenGraph card — title,
+description, site name, image.
+
+**Whose request it is: the node's**, never the browser's or the hub's. The browser cannot
+(a strict `img-src`/`connect-src` and CORS), and a direct fetch would leak every reader's
+IP to the linked host on each render. The hub must not touch group content (§2.5). The node
+already fetches third-party metadata for the Videos and Music apps, over the same
+authorised path.
+
+This is a new instance of the §2.7 / draft-v5 §5.2 rule: **the node produces enrichment on
+demand and the asking device caches; neither node nor hub keeps durable derived state.**
+The card text lives in a bounded in-memory TTL cache on the node; the OG image rides the
+existing `media_cache` thumb store (blake3-keyed) and the browser fetches it over the
+normal `file_req` path, exactly like a poster. Nothing reaches the hub — this is node↔
+browser P2P.
+
+**New adversary surface: SSRF.** The URL is a *member's* choice and it triggers an outbound
+HTTP request from the operator's machine. `linkpreview.safe_url` is the gate — http(s)
+only, no credentials, and every resolved address must be globally routable (loopback,
+private, link-local, multicast, reserved and cloud-metadata ranges refused); redirects are
+followed by hand so each hop is re-checked. Residual, documented in the module: a name that
+resolves clean at check time and internal at connect time (rebinding), closed properly by
+pinning the checked IP — deferred.
+
+**MNP 0.12** — `link_preview_req` / `link_preview_resp`, additive: an older node logs
+"unknown type" and the client shows the bare link.
+
---
## 3. Filesystem portability as a security property