summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-18 12:54:29 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-18 12:54:29 +0200
commit06062d2e8352a205fa634970d260ab0f5de97f04 (patch)
tree0b10ee28d0de9d9af5fea0febfb404658177e1d5 /packages/meshbay-hub/src
parent7c46b4e7dc2893974a37d6a701123a95803fcb95 (diff)
downloadmeshbay-06062d2e8352a205fa634970d260ab0f5de97f04.tar.gz
fix(client): three defects a real desktop found in ten minutes
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 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js30
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/keyderive.js30
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/platform.js19
13 files changed, 113 insertions, 6 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 34f146a..4091ae7 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -675,7 +675,7 @@ function FirstRunPage({ onSet }) {
await window.meshbay.setHubBase(url.trim());
onSet();
} catch (err) {
- setError(err.message || String(err));
+ setError(platform.bridgeMessage(err));
setBusy(false);
}
};
@@ -3999,6 +3999,11 @@ function SettingsPage({ user, theme, onThemeChange, groups }) {
// headless session, a minimal desktop — and does it silently. Somebody who
// believes the OS is protecting their keys deserves to be told when it is not.
const [keyBackend, setKeyBackend] = useState('');
+ // Changing the hub after the first run. Without this a typo on the first
+ // screen was permanent: the prompt only appears when no hub is set, so a
+ // wrong one left editing a JSON file by hand as the only way out.
+ const [hubInput, setHubInput] = useState('');
+ const [hubError, setHubError] = useState('');
useEffect(() => {
if (!platform.secrets.available) return;
platform.secrets.backend().then(setKeyBackend).catch(() => {});
@@ -4105,6 +4110,29 @@ function SettingsPage({ user, theme, onThemeChange, groups }) {
</div>
`}
+ ${platform.isNative && html`
+ <div class="settings-section">
+ <h3 class="settings-heading">${t('settings.hub_heading')}</h3>
+ <div class="settings-row">
+ <span class="settings-label">${t('settings.hub_current')}</span>
+ <span class="settings-value">${platform.hubBase() || '—'}</span>
+ </div>
+ <p class="settings-hint">${t('settings.hub_hint')}</p>
+ <form onSubmit=${async (e) => {
+ e.preventDefault();
+ setHubError('');
+ try {
+ await window.meshbay.setHubBase(hubInput.trim());
+ } catch (err) { setHubError(platform.bridgeMessage(err)); }
+ }} style="display:flex;gap:8px">
+ <input type="text" placeholder=${platform.hubBase()}
+ value=${hubInput} onInput=${e => setHubInput(e.target.value)} />
+ <button class="admin-btn" type="submit">${t('settings.hub_change')}</button>
+ </form>
+ ${hubError && html`<p class="error-msg">${hubError}</p>`}
+ </div>
+ `}
+
${keyBackend && html`
<div class="settings-section">
<h3 class="settings-heading">${t('settings.keys_heading')}</h3>
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
index a27522d..ce38d35 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
@@ -21,7 +21,31 @@
*/
const PBKDF2_ITERATIONS = 600000; // OWASP 2023 recommendation for PBKDF2-SHA512
-const HUB = ''; // same origin
+/**
+ * Where the hub is, and how to reach it — resolved when a call is made, not
+ * when this file loads.
+ *
+ * This used to be `const HUB = ''`, "same origin", which is true of a page the
+ * hub served and false of one loaded from a package: there the origin is
+ * `app://meshbay`, so `/v1/users/register` resolved against it and the
+ * application's own protocol handler answered 404. Registration and sign-in —
+ * the first two things anybody does — failed with "Not found".
+ *
+ * This is a classic script, loaded before the module graph, so it cannot import
+ * the adapter. It reads the global the adapter publishes, at call time: by then
+ * `platform.js` has run, and in a browser both of these are exactly what they
+ * were before.
+ */
+function hubBase() {
+ const p = typeof window !== 'undefined' && window.MeshBayPlatform;
+ return p ? p.hubBase() : '';
+}
+
+function hubCall(path, init) {
+ const p = typeof window !== 'undefined' && window.MeshBayPlatform;
+ return p && p.apiFetch ? p.apiFetch(hubBase() + path, init)
+ : fetch(hubBase() + path, init);
+}
// ── Auth key derivation (password split) ──────────────────────────────────────
@@ -198,7 +222,7 @@ async function registerUser(username, email, password) {
// It also means the hub stores no user key to publish, which is what H3 read.
const authKey = await deriveAuthKey(password, username);
- const resp = await fetch(`${HUB}/v1/users/register`, {
+ const resp = await hubCall('/v1/users/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, email, auth_key: authKey }),
@@ -258,7 +282,7 @@ async function decryptBundleWithKey(bundleB64, aesKeyOrPair) {
async function loginAndRecover(username, password) {
const authKey = await deriveAuthKey(password, username);
- const resp = await fetch(`${HUB}/v1/users/login`, {
+ const resp = await hubCall('/v1/users/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, auth_key: authKey }),
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index 8b652b5..ff7bd8a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -204,6 +204,10 @@ export default {
'firstrun.note': 'There is no default on purpose: this application only trusts a hub for its API, never for the interface, which ships with the application itself.',
'device.this_device': 'This device',
'settings.keys_heading': 'Keys on this device',
+ 'settings.hub_heading': 'Hub',
+ 'settings.hub_current': 'Currently',
+ 'settings.hub_hint': 'Changing this signs you out and restarts the window. A hub on your own machine is usually http, not https.',
+ 'settings.hub_change': 'Change',
'settings.keys_where': 'Protected by',
'settings.keys_unprotected': 'No system keyring is running, so your keys are encrypted with a key that is not a secret. Anyone who can read this machine’s files can read them. Start a keyring, or treat this device as untrusted.',
'settings.keys_unavailable': 'This system offers no key storage at all, so this device cannot be remembered — you will be asked for your passphrase each time. That is the safe outcome: nothing was stored unprotected.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index 9018384..738a862 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -196,6 +196,10 @@ export default {
'firstrun.note': 'There is no default on purpose: this application only trusts a hub for its API, never for the interface, which ships with the application itself.',
'device.this_device': 'This device',
'settings.keys_heading': 'Keys on this device',
+ 'settings.hub_heading': 'Hub',
+ 'settings.hub_current': 'Currently',
+ 'settings.hub_hint': 'Changing this signs you out and restarts the window. A hub on your own machine is usually http, not https.',
+ 'settings.hub_change': 'Change',
'settings.keys_where': 'Protected by',
'settings.keys_unprotected': 'No system keyring is running, so your keys are encrypted with a key that is not a secret. Anyone who can read this machine’s files can read them. Start a keyring, or treat this device as untrusted.',
'settings.keys_unavailable': 'This system offers no key storage at all, so this device cannot be remembered — you will be asked for your passphrase each time. That is the safe outcome: nothing was stored unprotected.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index 32e5f8a..fb5cb0e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -200,6 +200,10 @@ export default {
'firstrun.note': 'There is no default on purpose: this application only trusts a hub for its API, never for the interface, which ships with the application itself.',
'device.this_device': 'This device',
'settings.keys_heading': 'Keys on this device',
+ 'settings.hub_heading': 'Hub',
+ 'settings.hub_current': 'Currently',
+ 'settings.hub_hint': 'Changing this signs you out and restarts the window. A hub on your own machine is usually http, not https.',
+ 'settings.hub_change': 'Change',
'settings.keys_where': 'Protected by',
'settings.keys_unprotected': 'No system keyring is running, so your keys are encrypted with a key that is not a secret. Anyone who can read this machine’s files can read them. Start a keyring, or treat this device as untrusted.',
'settings.keys_unavailable': 'This system offers no key storage at all, so this device cannot be remembered — you will be asked for your passphrase each time. That is the safe outcome: nothing was stored unprotected.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index 6517a7c..a849730 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -204,6 +204,10 @@ export default {
'firstrun.note': 'Il n’y a volontairement pas de valeur par défaut : cette application ne fait confiance à un hub que pour son API, jamais pour l’interface, qui est livrée avec l’application.',
'device.this_device': 'Cet appareil',
'settings.keys_heading': 'Clés sur cet appareil',
+ 'settings.hub_heading': 'Hub',
+ 'settings.hub_current': 'Actuellement',
+ 'settings.hub_hint': 'Changer cette adresse vous déconnecte et redémarre la fenêtre. Un hub sur votre propre machine est généralement en http, pas en https.',
+ 'settings.hub_change': 'Changer',
'settings.keys_where': 'Protégées par',
'settings.keys_unprotected': 'Aucun trousseau système ne fonctionne : vos clés sont chiffrées avec une clé qui n’est pas secrète. Quiconque peut lire les fichiers de cette machine peut les lire. Démarrez un trousseau, ou considérez cet appareil comme non fiable.',
'settings.keys_unavailable': 'Ce système n’offre aucun stockage de clés, donc cet appareil ne peut pas être mémorisé — votre phrase secrète vous sera redemandée à chaque fois. C’est le comportement sûr : rien n’a été stocké sans protection.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index 470801d..17ad3e0 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -203,6 +203,10 @@ export default {
'firstrun.note': 'There is no default on purpose: this application only trusts a hub for its API, never for the interface, which ships with the application itself.',
'device.this_device': 'This device',
'settings.keys_heading': 'Keys on this device',
+ 'settings.hub_heading': 'Hub',
+ 'settings.hub_current': 'Currently',
+ 'settings.hub_hint': 'Changing this signs you out and restarts the window. A hub on your own machine is usually http, not https.',
+ 'settings.hub_change': 'Change',
'settings.keys_where': 'Protected by',
'settings.keys_unprotected': 'No system keyring is running, so your keys are encrypted with a key that is not a secret. Anyone who can read this machine’s files can read them. Start a keyring, or treat this device as untrusted.',
'settings.keys_unavailable': 'This system offers no key storage at all, so this device cannot be remembered — you will be asked for your passphrase each time. That is the safe outcome: nothing was stored unprotected.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index 33bdc46..47221a6 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -198,6 +198,10 @@ export default {
'firstrun.note': 'There is no default on purpose: this application only trusts a hub for its API, never for the interface, which ships with the application itself.',
'device.this_device': 'This device',
'settings.keys_heading': 'Keys on this device',
+ 'settings.hub_heading': 'Hub',
+ 'settings.hub_current': 'Currently',
+ 'settings.hub_hint': 'Changing this signs you out and restarts the window. A hub on your own machine is usually http, not https.',
+ 'settings.hub_change': 'Change',
'settings.keys_where': 'Protected by',
'settings.keys_unprotected': 'No system keyring is running, so your keys are encrypted with a key that is not a secret. Anyone who can read this machine’s files can read them. Start a keyring, or treat this device as untrusted.',
'settings.keys_unavailable': 'This system offers no key storage at all, so this device cannot be remembered — you will be asked for your passphrase each time. That is the safe outcome: nothing was stored unprotected.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index 56a2c9f..617cdcd 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -204,6 +204,10 @@ export default {
'firstrun.note': 'There is no default on purpose: this application only trusts a hub for its API, never for the interface, which ships with the application itself.',
'device.this_device': 'This device',
'settings.keys_heading': 'Keys on this device',
+ 'settings.hub_heading': 'Hub',
+ 'settings.hub_current': 'Currently',
+ 'settings.hub_hint': 'Changing this signs you out and restarts the window. A hub on your own machine is usually http, not https.',
+ 'settings.hub_change': 'Change',
'settings.keys_where': 'Protected by',
'settings.keys_unprotected': 'No system keyring is running, so your keys are encrypted with a key that is not a secret. Anyone who can read this machine’s files can read them. Start a keyring, or treat this device as untrusted.',
'settings.keys_unavailable': 'This system offers no key storage at all, so this device cannot be remembered — you will be asked for your passphrase each time. That is the safe outcome: nothing was stored unprotected.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index 31e5c13..ddbd1a9 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -210,6 +210,10 @@ export default {
'firstrun.note': 'There is no default on purpose: this application only trusts a hub for its API, never for the interface, which ships with the application itself.',
'device.this_device': 'This device',
'settings.keys_heading': 'Keys on this device',
+ 'settings.hub_heading': 'Hub',
+ 'settings.hub_current': 'Currently',
+ 'settings.hub_hint': 'Changing this signs you out and restarts the window. A hub on your own machine is usually http, not https.',
+ 'settings.hub_change': 'Change',
'settings.keys_where': 'Protected by',
'settings.keys_unprotected': 'No system keyring is running, so your keys are encrypted with a key that is not a secret. Anyone who can read this machine’s files can read them. Start a keyring, or treat this device as untrusted.',
'settings.keys_unavailable': 'This system offers no key storage at all, so this device cannot be remembered — you will be asked for your passphrase each time. That is the safe outcome: nothing was stored unprotected.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index d6563ab..18c0921 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -202,6 +202,10 @@ export default {
'firstrun.note': 'There is no default on purpose: this application only trusts a hub for its API, never for the interface, which ships with the application itself.',
'device.this_device': 'This device',
'settings.keys_heading': 'Keys on this device',
+ 'settings.hub_heading': 'Hub',
+ 'settings.hub_current': 'Currently',
+ 'settings.hub_hint': 'Changing this signs you out and restarts the window. A hub on your own machine is usually http, not https.',
+ 'settings.hub_change': 'Change',
'settings.keys_where': 'Protected by',
'settings.keys_unprotected': 'No system keyring is running, so your keys are encrypted with a key that is not a secret. Anyone who can read this machine’s files can read them. Start a keyring, or treat this device as untrusted.',
'settings.keys_unavailable': 'This system offers no key storage at all, so this device cannot be remembered — you will be asked for your passphrase each time. That is the safe outcome: nothing was stored unprotected.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index a7bbb0e..d868335 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -189,6 +189,10 @@ export default {
'firstrun.note': 'There is no default on purpose: this application only trusts a hub for its API, never for the interface, which ships with the application itself.',
'device.this_device': 'This device',
'settings.keys_heading': 'Keys on this device',
+ 'settings.hub_heading': 'Hub',
+ 'settings.hub_current': 'Currently',
+ 'settings.hub_hint': 'Changing this signs you out and restarts the window. A hub on your own machine is usually http, not https.',
+ 'settings.hub_change': 'Change',
'settings.keys_where': 'Protected by',
'settings.keys_unprotected': 'No system keyring is running, so your keys are encrypted with a key that is not a secret. Anyone who can read this machine’s files can read them. Start a keyring, or treat this device as untrusted.',
'settings.keys_unavailable': 'This system offers no key storage at all, so this device cannot be remembered — you will be asked for your passphrase each time. That is the safe outcome: nothing was stored unprotected.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
index ae04e53..994f558 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
@@ -103,6 +103,20 @@ export const secrets = {
* keypair bundle on each node is what a second browser recovers — which is
* finding C4, and the reason the application exists.
*/
+/**
+ * The sentence out of a bridge error.
+ *
+ * Electron wraps anything thrown in the main process as
+ * "Error invoking remote method 'hub:set': Error: …", which puts plumbing in
+ * front of a message written for a person. The message is the part that was
+ * written for them.
+ */
+export function bridgeMessage(err) {
+ const raw = (err && err.message) || String(err);
+ const m = raw.match(/^Error invoking remote method '[^']*':\s*(?:\w*Error:\s*)?(.*)$/s);
+ return m ? m[1] : raw;
+}
+
export const device = {
available: Boolean(bridge && bridge.device),
async ensure() {
@@ -168,7 +182,7 @@ export async function nativeSave(suggestedName, size) {
}
export default { isNative, hubBase, capabilities, secrets, nativeSave,
- apiFetch, device };
+ apiFetch, device, bridgeMessage };
// Also a global, because `transport.js` is loaded as a classic script — it
// predates the module graph and exposes `MeshBayTransport` the same way. The
@@ -176,5 +190,6 @@ export default { isNative, hubBase, capabilities, secrets, nativeSave,
// hub end up disagreeing about how to reach it.
if (typeof window !== 'undefined') {
window.MeshBayPlatform = { isNative, hubBase, capabilities, secrets,
- nativeSave, apiFetch, device };
+ nativeSave, apiFetch, device,
+ bridgeMessage };
}