summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_desktop_shell.py
Commit message (Collapse)AuthorAgeFilesLines
* feat(client): system tray on Windows, and a clearer tray iconChristophe Besson2026-09-051-0/+32
| | | | | | | | | | | | | | | The tray's Start/Stop already drove nodeService.status/stop/restart, which had full win32 branches for both startup modes from the Node page work -- so enabling it on Windows is widening two platform gates (the `tray` capability in preload.js, the window:minimize-to-tray handler in main.js), not new logic. Replaced the tray icon: the previous white chevron-in-a-box read as an envelope at tray size. New icon is a small "M" drawn as mesh nodes and edges, echoing the app icon's own motif, in the brand blue instead of plain white so it stays legible on both light and dark taskbars/panels. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* chore(client): build against the latest Electron, and stop defining the ↵Christophe Besson2026-09-031-0/+44
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | package twice Chromium CVEs are fixed in Electron releases, and a client built against an old one ships those holes to every user. That is a certain harm; a build that breaks on a new Electron is a repairable one. build-client.sh now bumps to the latest on every build, prints the comparison, and lets the build fail if it cannot cope — the failure is the signal to fix, not a reason to stay behind. It writes package.json and the lockfile on purpose: the new pin is meant to be committed. A registry it cannot reach is a warning, not a failure. Exercised by pinning back to 42.9.2 and building: "==> Electron 42.9.2 -> 44.1.1", exit 0, electron=44.1.1 in the packaged output. Note npm audit would have said nothing about any of this — Chromium CVEs fixed in Electron do not reliably reach the npm advisory database. Separately, package.json declared linux.target [deb, rpm] with its own deb/rpm depends, so `npm run dist` built a second package under the same name. The two had drifted: /opt/MeshBay/meshbay-client against /opt/meshbay-client/meshbay, and Depends: python3-meshbay-common naming none of the Electron runtime libraries the real DEBIAN/control lists — it would have installed cleanly and then refused to start. Nothing in the tree referenced `npm run dist`, which is why the drift was free to happen. That config is gone, "dist" delegates to build-client.sh, and test_desktop_shell.py refuses its return. `--dir` was re-run with no linux block at all: exit 0, same binary build-client.sh consumes. It needs appId, productName and files, nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AbwJDbNTkiRUh7HTWEoyss
* fix(client): allow reCAPTCHA in the Electron CSPChristophe Besson2026-09-011-4/+38
| | | | | | | | | | | | | | | | | | | | | | | | | Needed for the paired meshbay-hub commit that makes the registration captcha unconditional (M1): the desktop client renders the same RegisterPage widget the browser does, which needs its script, its challenge iframe and its assets to load. script-src, the new frame-src, and img-src now allow exactly https://www.google.com and https://www.gstatic.com, and nothing else external — the hub's own origin is still absent from script-src, so T3 (nothing the hub returns is executed) is unaffected. This is a one-time source change: it ships identical in every build via `files: ["src/**"]` in electron-builder's config, with no build step, packaging step, or installer action for anyone to perform, and no setting for an end user to touch. test_desktop_shell.py updated to pin the exception precisely: the reCAPTCHA hosts are the *only* external origins allowed anywhere in the policy, and a bare `https:` scheme is still refused in script-src. Third security review, finding M1 (Option A, desktop half). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pG75yGK3NthNfyjH74omG
* fix: update test_desktop_shell.py for build/ → scripts/ renameChristophe Besson2026-08-221-2/+2
| | | | | | | The test reads index.html and sync-ui.js to verify security properties. Update the paths to match the directory rename from the prior commit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(client): a film can go full-screen, and automatic saving is automaticChristophe Besson2026-08-181-4/+61
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | **Full-screen was denied, and the denial was invisible.** The permission handler was written from a true sentence — nothing here needs a camera, a microphone or a location — and implemented as `callback(false)` for everything. Chromium's own video controls ask for the `fullscreen` permission, so a film could not be watched full-screen. What made it hard to find, and what the test now pins: **a denied `fullscreen` does not reject.** `requestFullscreen()` returns a promise that never settles. No exception, no console message, nothing in the renderer that mentions a permission — the button just does nothing. Measured rather than reasoned: the probe reported `NEVER SETTLED` while the main process, instrumented for one run, logged `PERMISSION ASKED: fullscreen`. After the fix the same probe reports `granted` with `document.fullscreenElement` set. The handler now enumerates what is *granted* — `fullscreen`, and nothing else — so a camera, a microphone, a location, notifications and MIDI are still refused and whatever Chromium adds next arrives refused rather than quietly allowed. `Permissions.query` takes the other handler, so both now answer from the one list instead of eventually disagreeing. The old test asserted `callback(false)`, which is to say it locked in the bug. It is replaced by three: what must stay denied, that `fullscreen` is granted, and that both handlers read the same list. **"Save automatically" opened a dialog.** The automatic path required a folder to have been chosen first, and on a new profile nobody has chosen one — so the very first download fell through to Save As, which is the one thing the setting promises not to do. A browser does not make you pick a folder before it will save a file; the system Downloads folder is the answer when there is no other. Verified on a fresh profile with a home of its own: no dialog, 1024 bytes on disk, destination reported as the default (`/home/…/Téléchargements` on this machine, via the localized XDG directory). A folder that *was* chosen and has since gone still asks. Silently redirecting those files is worse than a dialog: someone who picked an external drive wants to be told it is not there, not to find the film in their home directory a week later. Settings shows the effective destination either way, and offers "forget" only for a folder somebody actually chose. 813 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(client): the desktop client runs, and running it corrected three thingsChristophe Besson2026-08-181-12/+37
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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>
* feat(client): the platform seam, and an Electron shell that has never been runChristophe Besson2026-08-181-0/+218
Stage D, and the honest half of it. D1 — the seam (done, and verified) ---------------------------------- `static/platform.js`. `HUB` becomes `platform.hubBase()` and the transport is built with the same base, so one address has one source. In a browser it returns '' and every path stays relative to the origin that served the page — the acceptance criterion for this split was "the browser SPA behaves identically", and it does. `platform.js` joins `_ASSETS`, or a change to it would not move the content hash and a cached browser would never ask for it. D2 — the shell (written, never launched) ----------------------------------------- **There is no npm on this machine. Electron was never installed and `packages/meshbay-client/` has not been run once.** That is stated here rather than discovered later. What is there: a main process serving the packaged interface over a privileged `app://` scheme (`secure` and `standard` are not cosmetic — without them the service worker refuses to register and streamed downloads break silently), a preload exposing an enumerated bridge that never passes a filesystem path, a window with `sandbox`, `contextIsolation` and no node integration, navigation away from the package refused, and a CSP where the hub is reachable over connect-src and is not a script source. The hub address arrives as a process argument because `platform.hubBase()` runs before anything can await. `test_desktop_shell.py` pins each of those by reading the source — the treatment `test_downloads.py` already gives the three browser save paths. It catches a property being removed and proves nothing about the application running. Two were checked by breaking them. The interface is *copied* into the package by `build/sync-ui.js` from the hub's static directory, and `ui/` is gitignored: a silent fork is the only real way to end up maintaining the interface twice. D3 — partial ------------ The bridge, and the part worth having now: safeStorage's backend is reported rather than assumed. On Linux it falls back to a fixed key when no keyring is running, silently — someone who believes the OS is holding their keys is told when it is not. The native key lifecycle belongs with D4 and needs a running application to mean anything. D8 — partial, and a real defect found -------------------------------------- `meshbay-node.spec` installed the SYSTEM template — the one carrying `User=%i` — into `%{_userunitdir}`. A user unit already runs as its owner and cannot carry `User=`; systemd refuses the file, so the packaged unit could never have started. Nothing noticed because nobody had built and installed the RPM. Two units now: the template to `%{_unitdir}`, and a new `meshbay-node-user.service` that a person enables themselves without a password — which is what lets the desktop client install a node without asking for one. It carries ExecReload, so `meshbay-node reload` does not have to stop a service somebody is streaming from, and documents the drop-in for a drive outside the home, RequiresMountsFor included. 798 tests pass; e2e.py still passes end to end. Nothing here was built or launched: no npm, no rpmbuild. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>