diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-18 09:42:34 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-18 09:42:34 +0200 |
| commit | 30e855f55f1d920b25da0bdd8e538c249d3c0c26 (patch) | |
| tree | 587dcad0beb8a120352613be8aa751a97040015c /packages/meshbay-hub/src/meshbay_hub/static/platform.js | |
| parent | 768e07046368819b8a8f15c8b21e5a8bbfcdf282 (diff) | |
| download | meshbay-30e855f55f1d920b25da0bdd8e538c249d3c0c26.tar.gz | |
feat(client): the platform seam, and an Electron shell that has never been run
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>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/platform.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/platform.js | 103 |
1 files changed, 103 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js new file mode 100644 index 0000000..0663a42 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js @@ -0,0 +1,103 @@ +/** + * What differs between running in a browser and running as an installed app. + * + * The interface is the same code either way — that is the whole reason Electron + * was chosen over a shell that replaces the engine (docs/desktop-client-v1.md + * §2). What genuinely differs is small and lives here: + * + * · **where the hub is.** Served from the hub, it is the current origin. Ship + * the interface in a package and it becomes a configured URL, because the + * page is loaded from disk and has no hub origin of its own. + * · **where a downloaded file goes**, and whether a native dialog picks it. + * · **what the app can do at all** — managing a local node, choosing folders + * on this machine. Features gated on these render nowhere in a browser + * rather than failing when clicked. + * + * The browser implementation below is exactly today's behaviour, so nothing + * changes for anyone until an app is installed. That is the acceptance + * criterion for this split: **the browser SPA behaves identically.** + * + * The native side arrives through `window.meshbay`, which the Electron preload + * exposes over a context bridge. Absent, everything falls back to the browser + * path — so this file is safe to load anywhere and there is no build flag. + */ + +const bridge = (typeof window !== 'undefined' && window.meshbay) || null; + +export const isNative = Boolean(bridge); + +/** + * The hub's base URL, prefixed to every API path. + * + * Empty string in a browser: the hub served this page, so a relative path goes + * to the right place and no configuration can be wrong. In the app it is + * whatever the user signed in against, and it is deliberately *not* guessed — + * a client that picks its own hub is a client that can be pointed at one. + */ +export function hubBase() { + return bridge ? (bridge.hubBase() || '') : ''; +} + +/** Native-only capabilities. A browser renders none of what these gate. */ +export const capabilities = { + // Install, configure and drive a node running on this machine. + nodeAdmin: Boolean(bridge && bridge.capabilities && bridge.capabilities.nodeAdmin), + // Choose directories on this machine to share. + localFolders: Boolean(bridge && bridge.capabilities && bridge.capabilities.localFolders), + // A real save dialog and a write that does not pass through the page. + nativeSave: Boolean(bridge && bridge.capabilities && bridge.capabilities.nativeSave), +}; + +/** + * Where the identity keys live. + * + * In a browser: exactly where they live today — IndexedDB and sessionStorage, + * with the keypair bundle on the node as the way a second browser recovers + * them, which is finding C4 and is the reason the app exists. + * + * In the app: the OS keychain, and no bundle is stored anywhere. That is what + * closes C4 for a native device — unconditionally for that device, and for the + * account only once it stops signing in from a browser too. + */ +export const secrets = { + available: Boolean(bridge && bridge.secrets), + async get(name) { + if (!bridge || !bridge.secrets) return null; + return bridge.secrets.get(name); + }, + async set(name, value) { + if (!bridge || !bridge.secrets) return false; + return bridge.secrets.set(name, value); + }, + async clear(name) { + if (!bridge || !bridge.secrets) return false; + return bridge.secrets.clear(name); + }, + /** + * Whether the OS is really protecting them. + * + * Electron's safeStorage falls back to a fixed key when no keyring is + * running — a headless session, a minimal desktop — and silently. A user who + * believes their keys are protected by the OS deserves to be told when they + * are not, so this is surfaced rather than swallowed. + */ + async backend() { + if (!bridge || !bridge.secrets) return 'browser'; + return bridge.secrets.backend(); + }, +}; + +/** + * Save a decrypted file to disk. + * + * Returns null when there is no native path, so the caller keeps today's + * behaviour — File System Access, a service worker stream, or a blob, decided + * in `downloads.js`. Adding a native writer must not remove the three that + * already work. + */ +export async function nativeSave(suggestedName, size) { + if (!bridge || !bridge.saveFile) return null; + return bridge.saveFile(suggestedName, size); +} + +export default { isNative, hubBase, capabilities, secrets, nativeSave }; |