diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-18 11:00:32 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-18 11:00:32 +0200 |
| commit | 7c46b4e7dc2893974a37d6a701123a95803fcb95 (patch) | |
| tree | a919618ce1c105e36c1fc85f7e492fbcb3265779 /packages/meshbay-client/src/preload.js | |
| parent | 68bfe56a19aeb4c16f8e185fc85d8eee61aef78f (diff) | |
| download | meshbay-7c46b4e7dc2893974a37d6a701123a95803fcb95.tar.gz | |
feat(client): hybrid sign-in — passphrase once, then this device's key
D4. The passphrase stays the account's credential and its only recovery path;
what changes is that it is not asked for on every launch.
**The renderer never holds the device key.** It is generated, stored and used
entirely in the main process, which signs `meshbay:user_auth:<username>:<ts>` on
request. Same rule as the save dialog, for the same reason: the renderer is the
part of this application that parses decrypted content from nodes, which is
attacker-controlled input. And this key is *not* a per-node identity key — those
are generated per node and never leave that relationship, so nothing here
correlates a person across operators.
First run asks which hub, with no default. A client that picks its own hub is a
client that can be pointed at one, and the address is the whole of what this
application trusts a hub for — the interface comes from the package.
Verified against a hub running this code, not against the deployed one:
register 201 → passphrase login 200 → device register 201 → **device sign-in 200
with a real session** → `/v1/users/me` 200 → a stranger's key 401. The
signature was also checked directly against the hub's own Python verifier before
any of that.
Inside the running application, over the debugging protocol: the bridge reaches
the main process, the renderer calls the hub **through it** (200 — the CORS fix
working end to end), and a call to a host that is not the configured hub is
refused.
**Not verified:** safeStorage persisting the key. This session has no secret
service, and standing one up in xvfb did not succeed. The application behaves
correctly there — it *refuses* rather than storing unprotected, and now says so
in Settings, which is a real case rather than a hypothetical one since it is
exactly what a headless or minimal desktop looks like.
Worth remembering for next time: meshbay.org runs whatever was last deployed. It
answered 405 on the Stage-C endpoints and reported MNP 0.2 while the tree had
0.3, so a local `uvicorn meshbay_hub.app:create_app --factory` on SQLite is what
tests hub changes. Nothing was deployed to production for this.
799 tests pass; e2e.py passes end to end.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-client/src/preload.js')
| -rw-r--r-- | packages/meshbay-client/src/preload.js | 10 |
1 files changed, 10 insertions, 0 deletions
diff --git a/packages/meshbay-client/src/preload.js b/packages/meshbay-client/src/preload.js index e3e240f..b649ae3 100644 --- a/packages/meshbay-client/src/preload.js +++ b/packages/meshbay-client/src/preload.js @@ -46,6 +46,16 @@ contextBridge.exposeInMainWorld('meshbay', { // which CORS refuses and which is not a credential anyway. fetch: (url, init) => ipcRenderer.invoke('hub:fetch', url, init), + // The device's hub key. Generated, held and used entirely in the main + // process: the interface asks for a signature and never sees a key, because + // it is the part of this application that parses hostile input. + device: { + ensure: () => ipcRenderer.invoke('device:ensure'), + publicKey: () => ipcRenderer.invoke('device:public'), + sign: (username) => ipcRenderer.invoke('device:sign', username), + forget: () => ipcRenderer.invoke('device:forget'), + }, + secrets: { get: (name) => ipcRenderer.invoke('secrets:get', name), set: (name, value) => ipcRenderer.invoke('secrets:set', name, value), |