From 50ebb4f2e620dad8e1fbca8307b97c5e10e7e6c0 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 18 Aug 2026 02:14:39 +0200 Subject: docs: settle the desktop client, and draft v6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A design discussion on 2026-08-17 settled Phase 13 and, in doing so, changed four things the spec states. v6 restates only those; v5 stays authoritative for everything it does not touch, per the convention v5 itself used with v4. What changed: * The native shell is **Electron**, not pywebview — structural decision 18 reversed. The SPA depends on Chromium-class APIs (WebRTC, WebCrypto X25519/Ed25519, MSE, Service Workers), so keeping Chromium keeps transport.js, crypto.js, keyderive.js, downloads.js and sw.js *as the client*. A system webview meant reimplementing ~2500-3000 lines. The old "69 % reused" figure was measured against an app.js of ~2600 lines; it is 4586. * A group's content is **several named roots**, not one directory, because the planned video and audio libraries will not live in one folder on one disk. * **Device linking**: one person may hold several devices on a node, admitted by a key the node already pinned and bound by a one-time code the new device generates. Without it a native client is refused where a browser is not, and an account created natively could never be opened in a browser. * **Authorship is authenticated, not asserted** — chat senders sign, uploads have a provable owner, and delete authorization moves from the uploading key to the account. And one rule v5 assumed without writing down: **group-related server state lives on the node.** Verified for multi-root — SwarmSource carries hashes and endpoints, no paths. Also here: the Caddy configuration, which was a snippet in the roadmap that would have broken the SPA (it predates /a// asset versioning and would have 404ed /sw.js, silently killing streamed downloads on Firefox and Safari); and downloads.html, which becomes a security page once a release key exists. Phase 15 was re-read against device linking and is wrong as written: a sender key must be per **device**, never per person, or two devices sharing a chain produce key and nonce reuse — C1 again, one level down. senderkeys.py already fails this silently. Co-Authored-By: Claude Opus 5 --- devel-phases-next.md | 414 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 302 insertions(+), 112 deletions(-) (limited to 'devel-phases-next.md') diff --git a/devel-phases-next.md b/devel-phases-next.md index db9267d..338beb2 100644 --- a/devel-phases-next.md +++ b/devel-phases-next.md @@ -1,7 +1,9 @@ # MeshBay — Next Implementation Phases > Base: Phases 1–12 complete (except 10.9 → Phase 18). Web SPA + admin panel + self-service UI + MSE video streaming live on meshbay.org. Node daemon is production-ready (WebRTC, WS, chat, HTTP, index push, swarm all wired). -> Architecture reference: **docs/meshbay-draft-v5.md** (v4 superseded 2026-08-13) +> Architecture reference: **docs/meshbay-draft-v6.md** (2026-08-17; v5 remains +> authoritative for everything v6 does not restate, v4 superseded 2026-08-13). +> Desktop client, roots, device linking: **docs/desktop-client-v1.md** > First security review: first-review.md (2026-08-10) > **Second security review: second-review.md (2026-08-13) — 6 critical, 7 high findings.** > @@ -300,27 +302,28 @@ and requests to `/app/` fall through to the hub. ### Caddy integration -Recommended Caddyfile snippet for meshbay.org: -``` -meshbay.org { - root * /path/to/meshbay/site - try_files {path} {path}.html - file_server - - handle /v1/* { - reverse_proxy localhost:8000 - } - handle /app* { - reverse_proxy localhost:8000 - } - handle /style.css { - reverse_proxy localhost:8000 - } - handle /*.js { - reverse_proxy localhost:8000 - } -} -``` +**The real configuration lives at `packaging/caddy/meshbay.org.Caddyfile`** (added +2026-08-17). Use it, not the snippet this section used to carry. + +The snippet that was here served `site/` from the root with `try_files` and proxied +`/v1/*`, `/app*`, `/style.css` and `/*.js` to the hub. It predates asset versioning and +**would have broken the SPA**: the module graph is served under `/a//`, which +`handle /*.js` does not match, and neither does `/locales/*.js`. Worse, `/sw.js` would +have 404ed — the service worker has to stay at the root or its scope stops covering the +pages it intercepts downloads for, which breaks streamed downloads on Firefox and Safari +without any visible error. + +The rule is inverted: an **allowlist** of site paths served statically, everything else +proxied to the hub. The hub mounts its whole static directory at `/` (`app.py`), so it +owns the root namespace by default and the site takes only what it names. + +**Consequence to be aware of:** with the site overlay in front, `/` is the landing page, +so `webapp.py`'s `GET /` (which returns the SPA shell) is unreachable on meshbay.org. That +route stays — a **generic** hub with no site overlay should serve the application at its +root. The overlay is meshbay.org-specific by design. + +`site/` is **not** pushed by the hub deploy procedure; it syncs separately to +`/srv/meshbay/site`. ### Hub mirror (design only — implementation deferred) @@ -785,17 +788,31 @@ metadata minimization, residual schema cleanup. --- -## Phase 13 — Native desktop client (pywebview + aiortc) +## Phase 13 — Native desktop client (Electron + optional Python sidecar) -> **Status (2026-08-13): 13.1 active, 13.2–13.11 DEFERRED to after Phase 15**, pending -> decision D2 in `tmp-decisions.md` (browser extension vs native client vs both). +> **Reworked 2026-08-17 by operator decision. `docs/desktop-client-v1.md` is +> authoritative for this phase** — shell, device linking, account creation, node +> management, packaging and the open items. The milestone table below is the summary; +> the design and its rationale are there. +> +> **The shell changed: pywebview → Electron.** Structural decision 18 is reversed. The +> reason is measured, not aesthetic: the SPA depends on Chromium-class APIs (WebRTC, +> WebCrypto X25519/Ed25519, MSE, Service Workers, File System Access), so keeping +> Chromium keeps `transport.js`, `crypto.js`, `keyderive.js`, `downloads.js` and `sw.js` +> unchanged, while a system webview meant reimplementing ~2500–3000 lines plus a loopback +> media server and native dialogs. The old "69 % reused" figure was measured against an +> `app.js` of ~2600 lines; it is **4586** as of 2026-08-17, and `app.js` now holds 2 direct +> `crypto.subtle` calls — the three-globals seam had already leaked. > -> **13.1 (platform adapter split) proceeds regardless** — it is pure refactoring whose -> acceptance criterion is "the browser SPA behaves identically", and it is the prerequisite -> for every option under D2. +> **One blocking addition: device linking (13.3b).** Identity keys are per node, so a +> native client holding its own keys is refused by `webrtc_server.py:886-894` where a +> browser is not. Without linking, an account created natively could never be opened in a +> browser without an operator code per node — the "native must not prevent web use" +> objective fails outright. See `docs/desktop-client-v1.md` §4. -**Objective:** ship a desktop application with durable key storage, hub-independent -`group://` access, and a better media path than the browser allows. +**Objective:** ship a desktop application whose UI is not served by the hub, with durable +local key storage, at feature parity with the web for a standard user, that does not +prevent web use, and that can manage locally installed nodes. > ⚠️ **Do not justify this phase as "the fix for T3".** An earlier draft of > `second-review.md` claimed a native client makes code integrity independent of the hub. @@ -803,31 +820,37 @@ metadata minimization, residual schema cleanup. > operator holds relocates the trust rather than removing it. What native actually changes is > **detectability** — an attack must ship as an artifact that can be hashed and compared > instead of a one-off HTTP response — and that value is realised only by **18.7 reproducible -> builds** plus published hashes. Native also *costs* the browser sandbox, hands you patch -> velocity for WebKitGTK and every bundled dependency, and adds the loopback media server, -> the IPC bridge and the updater as new attack surface. +> builds** plus published hashes. +> +> **Cost line revised 2026-08-17.** Two of the three costs recorded here were pywebview's, +> not native's. Electron with `sandbox` and `contextIsolation` **keeps** the Chromium +> renderer sandbox — the strongest available — so "native costs the browser sandbox" is +> false for this shell, and the D2 table in `tmp-decisions.md` is wrong on that row. The +> loopback media server is deleted (13.6). What remains, and is real: **we own Chromium's +> patch cadence**, the renderer parses attacker-controlled content from nodes, npm enters +> the supply chain, and the updater is new surface. > > The security-per-effort ranking is: **11.5 ≫ 12 ≫ 14 (CLI) ≫ 13.** This phase is justified -> on product grounds. It permanently closes **C4** as a side effect, but C4 can also be closed -> in a browser by not storing keypair bundles remotely at all. +> on product grounds. It closes **C4** for native devices, but **not for accounts that also +> use a browser** — see `docs/desktop-client-v1.md` §5.1. Reproducible builds are unusually +> tractable here: the UI has no bundler or minifier, and Electron's own binaries have +> published upstream hashes. -### Why this is cheap +### Why this is cheap (re-measured 2026-08-17) -The SPA never touches a browser crypto or network primitive directly: `app.js` contains -**0** occurrences of `crypto.subtle` and **0** of `RTCPeerConnection`. All crypto and -transport go through three injected globals (`window.MeshBayCrypto`, `MeshBayKeys`, -`MeshBayTransport` — 16 call sites) and all hub I/O through one function (`hubFetch`, 30 -call sites). That is the seam. +| Asset | Lines | Under Electron | Under a system webview | +|---|---|---|---| +| `style.css`, `i18n.js`, `vendor/`, `zipstream.js`, `transfers.js` | ~4200 | reuse | reuse | +| `app.js` | 4586 | reuse, minus the hub base URL | reuse, minus storage glue + MSE player | +| `transport.js`, `crypto.js`, `keyderive.js` | 1907 | **reuse** | delete and reimplement | +| `downloads.js`, `sw.js` | 363 | **reuse** | delete | -| Asset | Lines | Native | -|---|---|---| -| `style.css`, `i18n.js`, `vendor/htm-preact.js` | 1708 | **reuse as-is** | -| `app.js` — components, routing, theme, admin | ~2050 | **reuse as-is** | -| `app.js` — storage glue, `hubFetch`, download/upload callbacks, MSE `VideoPlayer` | ~550 | rewrite | -| `transport.js`, `crypto.js`, `keyderive.js` | 1145 | **delete** | +`HUB = ''` (`app.js:12`) becoming a configurable absolute base URL is the one structural +change. That is what 13.1 exists for, and under Electron it is nearly all of it. -≈ **69 % reused unchanged**, and the 31 % that is not is largely code `second-review.md` -says to delete anyway (WebCrypto AES variant, PBKDF2 password split, keypair bundles). +The old plan also budgeted a loopback media server (WebKitGTK MSE is unreliable), a SQLite +index cache (IndexedDB is restricted under `file://`) and native file dialogs. Chromium +provides all three, so those milestones are deleted rather than rewritten. ### Non-negotiable @@ -835,30 +858,48 @@ says to delete anyway (WebCrypto AES variant, PBKDF2 password split, keypair bun `https://meshbay.org/app/` is a browser with a different icon and fixes nothing. The hub is used for the API only, and the bundle is covered by 13.9 signing. +Shell hardening is part of that and is not optional: `contextIsolation` and `sandbox` on, +`nodeIntegration` off, a custom `app://` protocol rather than `file://`, navigation to +non-local targets refused, and a strict in-package CSP that **keeps `wasm-unsafe-eval`** — +the bundle KDF is Argon2id in WebAssembly, and a policy forbidding it locks every user out +of their keys. + ### Milestones -| # | Component | Description | -|---|---|---| -| 13.1 | Platform adapter split | Extract `platform-web.js` (WebRTC/WebCrypto/fetch — today's behaviour) and `platform-native.js` (pywebview bridge). `app.js` imports neither directly. **Acceptance: the browser SPA is byte-for-byte functional after the split** — this lands first, on its own, with no native code | -| 13.2 | pywebview shell + Python bridge | `meshbay-client` package; `window.pywebview.api.*` implements the same surface as the three globals; single-instance, tray, window state | -| 13.3 | Local keystore + Ed25519 client auth — **the real close for C4**, now partially mitigated by Argon2id (draft-v5 §7.1) | Reuse `keystore.py` (Argon2id 256 MB, OS keychain later). Client authenticates like the daemon does: signed timestamp, `POST /v1/users/auth`. **No password on the wire, no `auth_key`/`bundle_key`, no keypair bundle anywhere** → closes **C4** permanently | -| 13.4 | aiortc client transport | `RTCPeerConnection` + `createDataChannel` + `createOffer` in Python; ICE/STUN via `aioice` — the traversal path validated on 2 ISPs. Calls the unified handshake from 11.5.4. QUIC (`quic_client.py`) retained as opt-in for LAN / port-forwarded / hub-less `group://` | -| 13.5 | Local index cache | SQLite in the client profile dir, replacing IndexedDB (also restricted under `file://` in some WebViews) | -| 13.6 | Loopback media server | Python decrypts and serves with HTTP Range; `