summaryrefslogtreecommitdiffstats
path: root/docs/desktop-client-v1.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-18 10:30:59 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-18 10:30:59 +0200
commit68bfe56a19aeb4c16f8e185fc85d8eee61aef78f (patch)
tree2a9ae0352ec09aca413448c9b2e5e9a638b36471 /docs/desktop-client-v1.md
parent30e855f55f1d920b25da0bdd8e538c249d3c0c26 (diff)
downloadmeshbay-68bfe56a19aeb4c16f8e185fc85d8eee61aef78f.tar.gz
fix(client): the desktop client runs, and running it corrected three things
Electron 42 / Chromium 148, launched under xvfb. The packaged interface mounts over `app://` with a secure context, `crypto.subtle` present, Argon2 WASM loaded, and no console errors. Three statements in the design were wrong, and only launching it found them. **A CSP in a `<meta>` tag silently drops `frame-ancestors`.** Chromium says so in the console. A policy carrying a directive that does nothing is worse than one without it, so the policy is sent as a header by the protocol handler — which is also the only thing serving the interface, so one source instead of two. **`secure: true` is not what makes the service worker register.** Chromium refuses a worker on a custom scheme whatever its privileges: "The URL protocol of the current origin ('app://meshbay') is not supported". The application has no service worker and needs none — it saves through a native dialog, which is the better of the two paths. `sw.js` stays in the package because the same files serve the browser, where it is one of only three ways to write a large file. What `secure: true` is actually for was measured at the same time: without it **the whole of `crypto.subtle` is undefined**. The first probe loaded a `data:` URL and every algorithm failed with TypeError, AES-GCM included — which is why the probe was rewritten before believing its answer. X25519 and Ed25519 are both present on Chromium 148, settling the version floor left open as O6. **The renderer cannot call the hub.** Its origin is `app://meshbay` and CORS refuses it. The hub has *no CORS middleware at all* — its API is reachable from no web origin whatever — and that is worth keeping. Widening it for `app://meshbay` would be worse than it looks: that origin is not a credential, since any Electron application can claim the same scheme and host name. So every hub call leaves from the main process, exactly as saving a file does, and it refuses any origin that is not the hub the user signed in to. `platform.apiFetch()` is `fetch` in a browser and the bridge in the application, so no caller has to know which it got. `transport.js` reaches it through a global because it is a classic script, not a module — the alternative was a second fetch path, which is how two callers of one hub start disagreeing about how to reach it. Verified from inside Electron: the main process gets 200 from /v1/hub/version, the renderer is refused by CORS, and **a script served by the hub is refused by the policy** — T3's mitigation demonstrated rather than asserted. Build note, written into the README because it will bite the next person: **Ubuntu 24.04's nodejs 18 cannot install Electron at all** — the download script `require()`s an ESM module, which Node gained in 22. Node 24 LTS, checksum-verified against nodejs.org, is what this was built with. package-lock.json is committed; builds use `npm ci`, not `npm install`. 799 tests pass, e2e.py still passes end to end. The session harness needed a platform stub: it lifts `hubFetch` out of app.js as text and runs it, so the adapter is now part of the environment it models. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'docs/desktop-client-v1.md')
-rw-r--r--docs/desktop-client-v1.md37
1 files changed, 36 insertions, 1 deletions
diff --git a/docs/desktop-client-v1.md b/docs/desktop-client-v1.md
index 6b68bbd..c4b5380 100644
--- a/docs/desktop-client-v1.md
+++ b/docs/desktop-client-v1.md
@@ -179,6 +179,41 @@ The single structural change to the SPA: `HUB = ''` (`app.js:12`) becomes a conf
absolute base URL. That is what the old 13.1 platform split existed to do, and under
Electron it is nearly all of it.
+### 3.1 What running it changed
+
+Three of the statements above were wrong, and only launching the application found them.
+
+**The CSP cannot live in a `<meta>` tag.** `frame-ancestors` is ignored there — Chromium
+says so in the console — so a policy carrying it has one directive that silently does
+nothing. It is sent as a header by the protocol handler instead, which is also the only
+thing that serves the interface, so there is one source rather than two.
+
+**`secure: true` is not what makes the service worker register.** Chromium refuses a
+worker on a custom scheme whatever its privileges: *"The URL protocol of the current
+origin ('app://meshbay') is not supported"*. The application therefore has **no service
+worker**, and does not need one — it saves through a native dialog, which is the better
+of the two paths anyway. `sw.js` stays in the package because the same files serve the
+browser, where it is one of only three ways to write a large file.
+
+What `secure: true` *is* for was measured at the same time: without it **the whole of
+`crypto.subtle` is undefined**. The first probe loaded a `data:` URL and every algorithm
+failed with `TypeError`, AES-GCM included. X25519 and Ed25519 are both present on
+Chromium 148, which settles the version floor that was open (O6).
+
+**The renderer cannot call the hub.** Its origin is `app://meshbay` and CORS refuses it —
+the hub has *no CORS middleware at all*, which is a posture worth keeping: its API is
+reachable from no web origin whatever. Widening it for `app://meshbay` would be worse
+than it looks, because that origin is not a credential: any Electron application on any
+machine can claim the same scheme and host name.
+
+So **every hub call leaves from the main process**, exactly as saving a file does. The
+renderer asks; the main process goes, and refuses any origin that is not the hub the user
+signed in to. `platform.apiFetch()` is `fetch` in a browser and the bridge in the
+application, so no caller has to know which one it got. Verified from inside Electron:
+the main process reaches `https://meshbay.org/v1/hub/version` (200), the renderer is
+refused by CORS, and **a script served by the hub is refused by the policy** — T3's
+mitigation demonstrated rather than asserted.
+
---
## 4. Device linking
@@ -1021,7 +1056,7 @@ build existing.
| # | Component | Prio |
|---|---|---|
| D1 | ✅ **DONE 2026-08-18** — `static/platform.js`; `HUB` is `platform.hubBase()` and the transport is built with the same base. Browser behaviour identical, which was the acceptance criterion | 1 |
-| D2 | ⚠️ **WRITTEN, NEVER RUN** — `packages/meshbay-client/`: main process, preload bridge, `app://` handler, CSP, single instance, window state. **There is no npm on the development machine, so Electron could not be installed and the application has never been launched.** Its security contract is pinned by `test_desktop_shell.py`, which reads the source the way `test_downloads.py` reads the save paths — weak evidence, and the only kind available without a packaged build | 1 |
+| D2 | ✅ **RUNS** (2026-08-18, Electron 42 / Chromium 148 under xvfb). The packaged interface mounts over `app://`, secure context, `crypto.subtle` present, Argon2 WASM loaded, zero console errors. Three things were learned by running it — see §3.1 |
| D3 | ◐ **PARTIAL** — the bridge (`secrets.get/set/clear/backend`) and the honest report of what the OS is actually doing: `unprotected_fallback` when safeStorage finds no keyring, surfaced in Settings rather than swallowed. The native key *lifecycle* belongs with D4 and needs a running application to mean anything | 1 |
| D4 | Hybrid registration and first sign-in (§5) | 1 |
| D5 | Node management panel over the Stage-B ops, root selection included | 2 |