From 06062d2e8352a205fa634970d260ab0f5de97f04 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 18 Aug 2026 12:54:29 +0200 Subject: fix(client): three defects a real desktop found in ten minutes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three came from the operator running the application on Ubuntu GNOME. None would have been found by anything already in the suite. **A second copy of the hub address.** `keyderive.js` carried `const HUB = '' // same origin` — true of a page the hub served, false of one loaded from a package, where the origin is `app://meshbay` and `/v1/users/register` resolves against the application's own protocol handler. **Sign-up and sign-in, the first two things anybody does, failed with "Not found."** The seam was changed in `app.js` and in the signalling call and this was missed: the same shape as the duplicate `MNP_VERSION` in `protocol.py`, a second copy of a constant that is harmless until the context changes. `test_hub_address_seam.py` refuses any file that decides where the hub is, and any `fetch('/v1/…')` relative to the page origin. **A window handler reading a variable another path reassigns.** Changing the hub closes one window and opens another; `closed` arrives *after* the replacement is assigned, so the outgoing window nulled the reference to the incoming one and its `ready-to-show` crashed on it — a modal "A JavaScript error occurred in the main process". Every handler now belongs to the window it was created with. The CDP test wrote `config.json` in advance, so it never took the one path that creates a second window; it does now, starting from an empty user-data dir. **A first run that could not be undone.** The hub address was accepted on anything URL-shaped and there was no way to change it afterwards — the prompt only appears when none is set, so a typo meant editing JSON by hand. `https` typed at a hub speaking `http` produced `TypeError: fetch failed`, which names nothing. Now: the address is probed before being written, failures say which URL and why ("does not speak https. If this hub is on your own machine, it is probably http"), Settings can change it, and Electron's "Error invoking remote method" wrapper is stripped from what a person reads. Verified on the operator's desktop: **safeStorage really uses the GNOME keyring** — Settings reports `gnome-libsecret`, and `secrets.bin` is written 0600 with Chromium's `v11` prefix, the marker for keyring-backed encryption (the fixed-key fallback writes `v10`). Headless, the same code reports `unavailable` and refuses to store rather than downgrading in silence, which is now explained in Settings instead of shown as a bare word. Unrelated but found while testing: `test_locales.py` assigned to `globalThis.navigator`, which is read-only from Node 22. The client's build already requires Node 22+, so the first CI machine configured for it would have failed these tests for no visible reason. 809 tests pass on Node 18 and Node 24. Co-Authored-By: Claude Opus 5 --- docs/desktop-client-v1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/desktop-client-v1.md b/docs/desktop-client-v1.md index fedfc8d..cde8453 100644 --- a/docs/desktop-client-v1.md +++ b/docs/desktop-client-v1.md @@ -1057,7 +1057,7 @@ build existing. |---|---|---| | 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 | ✅ **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 | +| D3 | ✅ **DONE, verified on a real desktop** (Ubuntu 24.04 GNOME, 2026-08-18). Settings reports `gnome-libsecret`, and `secrets.bin` is written 0600 with Chromium's **`v11`** prefix — the marker for keyring-backed encryption; the fixed-key fallback writes `v10`. The key name does not appear in clear. On a headless session the same code reports `unavailable` and **refuses to store**, which is the honest outcome and is now explained in Settings rather than left as a bare word | | D4 | ✅ **DONE 2026-08-18, verified against a hub running this code** — first-run hub prompt (no default, on purpose), passphrase sign-in registers this device, later launches sign in with the device key and no passphrase. **The renderer never holds that key**: it is generated, stored and used entirely in the main process, which signs on request — the same rule as the save dialog, because the renderer is the part that parses hostile input. Measured: register 201 → passphrase login 200 → device register 201 → **device sign-in 200 with a real session** → a stranger's key 401. **Not verified:** safeStorage actually persisting the key, which needs a desktop with a keyring — this session has none, and the application correctly *refuses* rather than storing unprotected | 1 | | D5 | Node management panel over the Stage-B ops, root selection included | 2 | | D6 | First-run wizard — detect, enable the unit, link, group, `gek-init`, pair (§7.4) | 2 | @@ -1088,7 +1088,7 @@ Deletions enabled once native is the recommended client are unchanged from | O2 | LAN enrolment door | One endpoint, bounded window, one-time code, closes permanently on success. Small but it executes before any authentication | | O3 | `device_policy {allow_bundle: false}` | The mechanism that actually closes C4 (§5.1). Needs to be signed by a pinned key, never settable by the hub | | O4 | Node-admin panel isolation | Node-supplied strings (filenames, hub-originated usernames) rendered in a process holding the user's keys. H2 was exactly this. Separate window or partition at minimum | -| O5 | `MESHBAY_UNLOCK_KEY` in `node.env` | Plaintext in the user's home. The client could move it to the OS keychain for the desktop persona | +| O5 | `MESHBAY_UNLOCK_KEY` in `node.env` | Still open for the **node**. For the **client**, the OS keychain path is proven: `safeStorage` on a real GNOME desktop uses the keyring (`v11`), and refuses rather than downgrading where there is none | | O6 | Electron version floor | X25519 and Ed25519 in WebCrypto must be verified on the pinned version, not assumed | | ~~O7~~ | Several directories in one group | **Decided 2026-08-17** — named roots, unique names, union root. See §6.7 | | O8 | Minimum client version in `GET /v1/hub/version` | Needed before the first public package (§2.6). Trivial now, awkward once clients are in the wild | -- cgit v1.2.3