aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-30 17:13:40 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-30 17:13:40 +0200
commitb1ebcdeb9082457972c41a47e77494902335d262 (patch)
treeb19384b868197ecda88ce79765a0f60812dbc815 /packages/meshbay-hub
parent6d167392f6f8ede37e2794a68a3738f8ba03131d (diff)
downloadmeshbay-b1ebcdeb9082457972c41a47e77494902335d262.tar.gz
feat: browser access, decided in the desktop application
Off for an account made there: its identities stay on the device and nothing is left on nodes. Turned on from the Profile page behind a native confirmation; each node is settled when its group next opens. The hub keeps a mirror a browser reads to say why a group will not open; it grants nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/users.py5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js10
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/auth-page.js8
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-page.js14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/hub-client.js22
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/keyderive.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js8
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js8
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js8
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js8
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js8
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js8
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js8
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js8
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js8
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js8
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/profile-page.js56
-rw-r--r--packages/meshbay-hub/tests/test_bundle_pepper.py16
-rw-r--r--packages/meshbay-hub/tests/test_desktop_shell.py21
19 files changed, 231 insertions, 5 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py
index 88e6d9e..92b3d3d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/users.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py
@@ -1277,6 +1277,11 @@ ALLOWED_PREF_KEYS = frozenset([
# Whether the Members tab asks the hub to mail an invitation. Remembered
# because unticking it on every invitation is what nobody would keep doing.
"invite_email",
+ # "on" / "off": a mirror of what the desktop application holds for the
+ # account, written by it so a browser can say why it cannot open a group.
+ # It grants nothing — nodes never read it, and a browser session writing
+ # "on" leaves the application's own setting as it was.
+ "browser_access",
])
# `default_tab:<group_id>`, which is what the SPA writes (group-page.js). The
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index a12fea4..c101ec9 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -17,6 +17,7 @@ import {
_storeBundleKey, _loadBundleKey, _clearKeyDB,
loadAuth, saveAuth, setAuth, setAuthChangeListener, ensureFreshToken, hubFetch,
refreshAccessToken, logoutOnHub,
+ mirrorBrowserAccess,
} from './hub-client.js';
import { startIdleWatch, markActive } from './idle.js';
import { GroupPage } from './group-page.js';
@@ -1010,9 +1011,16 @@ function App() {
.then(data => setGroups(data.groups || []))
.catch(() => setGroups([]));
hubFetch('/v1/users/me/preferences', { token: user.token })
- .then(prefs => {
+ .then(async (prefs) => {
setUserPrefs(prefs || {});
if (prefs.notifications_disabled === 'true') setNotifDisabled(true);
+ // The desktop application keeps the hub's mirror of browser access
+ // current, at every start and sign-in.
+ const mirrored = await mirrorBrowserAccess(
+ user.token, user.userId, (prefs || {}).browser_access);
+ if (mirrored && mirrored !== (prefs || {}).browser_access) {
+ setUserPrefs((p) => ({ ...p, browser_access: mirrored }));
+ }
})
.catch(() => {});
refreshNodeKey();
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
index 8e67e20..d3d3bd7 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
@@ -409,9 +409,15 @@ export function RegisterPage() {
// username and every key derivation must fold in the same string.
// `captcha.token` rides along — the submit button is already disabled
// until it is set when a captcha is configured (see the form below).
- await window.MeshBayKeys.registerUser(
+ const reg = await window.MeshBayKeys.registerUser(
name, email, password, emailRecovery ? rk.mnemonic : null,
captcha.token);
+ // An account made in the desktop application starts without browser
+ // access: its identities stay on this device and nothing of them is
+ // left on nodes until the person turns it on, here, natively.
+ if (reg.userId && await platform.nativeKeys()) {
+ try { await platform.keys.createdHere(reg.userId); } catch { /* kept on */ }
+ }
setRecoveryMnemonic(rk.mnemonic);
session.recoveryKey =
await window.MeshBayKeys.deriveRecoveryKey(rk.mnemonic, name);
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
index d2259b9..4510848 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
@@ -9,6 +9,7 @@ import {
HUB, session, hubFetch, ensureFreshToken,
_loadBundleKey, _loadRecoveryKey, _storeBundleKey,
} from './hub-client.js';
+import * as platform from './platform.js';
import { APPS, visibleApps } from './apps.js';
import { GroupName } from './group-name.js';
import { useStickyBand } from './sticky.js';
@@ -202,6 +203,17 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// This browser holds a key the node does not know, for an account it does.
// Not the operator's problem: a device already paired here can admit it.
const [needsDevice, setNeedsDevice] = useState(false);
+ // In a browser, for an account whose identities the desktop application
+ // keeps to itself: said when this group cannot be opened here, so the way
+ // in is named — the application — instead of a code nobody can use. Starts
+ // as "native" so the application never flashes a notice about itself.
+ const [nativeKeys, setNativeKeys] = useState(true);
+ useEffect(() => {
+ let gone = false;
+ platform.nativeKeys().then((n) => { if (!gone) setNativeKeys(n); }, () => {});
+ return () => { gone = true; };
+ }, []);
+ const browserAccessOff = !nativeKeys && userPrefs && userPrefs.browser_access === 'off';
const [deviceCode, setDeviceCode] = useState('');
const [codeInput, setCodeInput] = useState('');
// This browser has never derived the passphrase-bundle key (fresh browser,
@@ -905,6 +917,8 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
</button>
`}
</div>
+ ${browserAccessOff && (error || needsDevice || needsCode || needsPass) && html`
+ <p class="settings-hint" style="margin-bottom:12px">${t('group.browser_access_off')}</p>`}
${error && html`<div class="error-msg" style="margin-bottom:12px">${error}${' '}
<button class="admin-btn" style="margin-left:8px;font-size:0.9em"
onClick=${() => setRetryKey(k => k + 1)}>${t('group.retry')}</button>
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js b/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js
index 18db1ba..8ad091f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js
@@ -382,8 +382,28 @@ async function hubFetch(path, { method = 'GET', body, token, _retried } = {}) {
// `openDB` and `IDB_PLAYLISTS` are exported so playlists.js reads and writes
// its own store without owning the database's version.
+/**
+ * Write to the hub what the desktop application holds for this account's
+ * browser access, so a browser can say why it cannot open a group. The
+ * application is the source; the hub's copy grants nothing. Returns the value
+ * written ('on' / 'off'), or null outside the application or on failure.
+ */
+async function mirrorBrowserAccess(token, userId, known) {
+ if (!await platform.nativeKeys()) return null;
+ try {
+ const want = await platform.keys.browserAccess(userId) ? 'on' : 'off';
+ if (known !== want) {
+ await hubFetch('/v1/users/me/preferences/browser_access',
+ { method: 'PUT', token, body: { value: want } });
+ }
+ return want;
+ } catch {
+ return null;
+ }
+}
+
export {
- HUB, navigate, session,
+ HUB, navigate, session, mirrorBrowserAccess,
openDB, IDB_PLAYLISTS,
purgeGroupIndexCache,
_storeBundleKey, _loadBundleKey, _storeRecoveryKey, _loadRecoveryKey, _clearKeyDB,
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
index d847aab..9f28b5c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
@@ -355,7 +355,9 @@ async function registerUser(username, email, password, recoveryMnemonic, captcha
});
if (!resp.ok) throw new Error(`Registration failed: ${await resp.text()}`);
- return { registered: true };
+ let userId = null;
+ try { userId = (await resp.json()).user_id || null; } catch { /* no body */ }
+ return { registered: true, userId };
}
/**
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 d34e9bf..cbc4798 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -1243,6 +1243,14 @@ export default {
'group.bundle_format_retired': 'Dieser Node speichert Ihre Identität in einem Format, das diese Version nicht mehr liest. Bitten Sie den Betreiber, „meshbay-node member unpin" für Ihr Konto auszuführen und Ihnen einen neuen Einladungscode zu senden.',
+ 'settings.browser_access': 'Browserzugang',
+ 'settings.browser_access_on_hint': 'Ein Browser kann Ihre Gruppen mit Ihrer Passphrase öffnen: Diese Anwendung legt Ihre Identität auf jedem Node ab, so versiegelt, dass nur Ihre Passphrase zusammen mit Ihrem Hub-Konto sie öffnen kann.',
+ 'settings.browser_access_off_hint': 'Ihre Identitäten bleiben auf diesem Gerät. Nichts davon liegt auf einem Node, und ein Browser kann Ihre Gruppen nicht allein mit Ihrer Passphrase öffnen. Ein Browser, den Sie in dieser Anwendung freigeben, erhält eigene Identitäten.',
+ 'settings.browser_access_turn_on': 'Browser erlauben',
+ 'settings.browser_access_turn_off': 'Identitäten nur auf diesem Gerät behalten',
+ 'settings.browser_access_next_open': 'Wird bei jeder Gruppe beim nächsten Öffnen angewendet.',
+ 'settings.browser_access_off_in_browser': 'Der Browserzugang ist für dieses Konto ausgeschaltet. Er wird in der MeshBay-Desktopanwendung eingeschaltet, die diesen Browser auch für sich selbst freigeben kann.',
+ 'group.browser_access_off': 'Der Browserzugang ist für dieses Konto ausgeschaltet. Schalten Sie ihn in der MeshBay-Desktopanwendung (Profil) ein oder geben Sie diesen Browser dort frei.',
// Worded by the desktop main process for its own dialogs (main.js).
'native.attach_confirm': 'Die Gruppe „{name}" auf diesem Computer hosten und den Ordner {path} mit ihren Mitgliedern teilen?',
'native.folder_confirm': 'Den Ordner {path} mit den Mitgliedern einer auf diesem Computer gehosteten Gruppe teilen? Er wurde nicht in der Ordnerauswahl gewählt.',
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 217a5d7..38616aa 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -1224,6 +1224,14 @@ export default {
'group.bundle_format_retired': 'This node holds your identity in a format this version no longer reads. Ask its operator to run “meshbay-node member unpin” for your account and send you a new invitation code.',
+ 'settings.browser_access': 'Browser access',
+ 'settings.browser_access_on_hint': 'A browser can open your groups with your passphrase: this application leaves your identity on each node, sealed so that only your passphrase together with your hub account can open it.',
+ 'settings.browser_access_off_hint': 'Your identities stay on this device. Nothing of them is left on any node, and a browser cannot open your groups with your passphrase alone. A browser you approve from this application gets identities of its own.',
+ 'settings.browser_access_turn_on': 'Allow browsers',
+ 'settings.browser_access_turn_off': 'Keep identities on this device only',
+ 'settings.browser_access_next_open': 'Applied to each group the next time it opens.',
+ 'settings.browser_access_off_in_browser': 'Browser access is off for this account. It is turned on in the MeshBay desktop application, which can also approve this browser for itself.',
+ 'group.browser_access_off': 'Browser access is off for this account. Turn it on in the MeshBay desktop application (Profile), or approve this browser from it.',
// Worded by the desktop main process for its own dialogs (main.js).
'native.attach_confirm': 'Host the group "{name}" on this computer and share the folder {path} with its members?',
'native.folder_confirm': 'Share the folder {path} with the members of a group hosted on this computer? It was not chosen in the folder picker.',
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 76c3884..e510d09 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -1237,6 +1237,14 @@ export default {
'group.bundle_format_retired': 'Este node guarda su identidad en un formato que esta versión ya no lee. Pida a su operador que ejecute «meshbay-node member unpin» para su cuenta y le envíe un nuevo código de invitación.',
+ 'settings.browser_access': 'Acceso desde el navegador',
+ 'settings.browser_access_on_hint': 'Un navegador puede abrir sus grupos con su frase de contraseña: esta aplicación deja su identidad en cada node, sellada de modo que solo su frase de contraseña, junto con su cuenta del hub, pueda abrirla.',
+ 'settings.browser_access_off_hint': 'Sus identidades se quedan en este dispositivo. No se deja nada de ellas en ningún node, y un navegador no puede abrir sus grupos solo con su frase de contraseña. Un navegador que apruebe desde esta aplicación recibe identidades propias.',
+ 'settings.browser_access_turn_on': 'Permitir navegadores',
+ 'settings.browser_access_turn_off': 'Guardar las identidades solo en este dispositivo',
+ 'settings.browser_access_next_open': 'Se aplica a cada grupo la próxima vez que se abra.',
+ 'settings.browser_access_off_in_browser': 'El acceso desde el navegador está desactivado para esta cuenta. Se activa en la aplicación de escritorio de MeshBay, que también puede aprobar este navegador por sí mismo.',
+ 'group.browser_access_off': 'El acceso desde el navegador está desactivado para esta cuenta. Actívelo en la aplicación de escritorio de MeshBay (Perfil) o apruebe este navegador desde ella.',
// Worded by the desktop main process for its own dialogs (main.js).
'native.attach_confirm': '¿Alojar el grupo «{name}» en este ordenador y compartir la carpeta {path} con sus miembros?',
'native.folder_confirm': '¿Compartir la carpeta {path} con los miembros de un grupo alojado en este ordenador? No se eligió en el selector de carpetas.',
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 e142dc3..c361fe4 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -1252,6 +1252,14 @@ export default {
'group.bundle_format_retired': 'Ce node détient votre identité dans un format que cette version ne lit plus. Demandez à son opérateur d\'exécuter « meshbay-node member unpin » pour votre compte et de vous envoyer un nouveau code d\'invitation.',
+ 'settings.browser_access': 'Accès depuis un navigateur',
+ 'settings.browser_access_on_hint': 'Un navigateur peut ouvrir vos groupes avec votre phrase secrète : cette application dépose votre identité sur chaque node, scellée de sorte que seule votre phrase secrète, avec votre compte sur le hub, puisse l\'ouvrir.',
+ 'settings.browser_access_off_hint': 'Vos identités restent sur cet appareil. Rien d\'elles n\'est déposé sur aucun node, et un navigateur ne peut pas ouvrir vos groupes avec votre seule phrase secrète. Un navigateur que vous approuvez depuis cette application reçoit ses propres identités.',
+ 'settings.browser_access_turn_on': 'Autoriser les navigateurs',
+ 'settings.browser_access_turn_off': 'Garder les identités sur cet appareil seulement',
+ 'settings.browser_access_next_open': 'Appliqué à chaque groupe la prochaine fois qu\'il s\'ouvre.',
+ 'settings.browser_access_off_in_browser': 'L\'accès depuis un navigateur est désactivé pour ce compte. Il s\'active dans l\'application de bureau MeshBay, qui peut aussi approuver ce navigateur pour lui-même.',
+ 'group.browser_access_off': 'L\'accès depuis un navigateur est désactivé pour ce compte. Activez-le dans l\'application de bureau MeshBay (Profil), ou approuvez ce navigateur depuis celle-ci.',
// Worded by the desktop main process for its own dialogs (main.js).
'native.attach_confirm': 'Héberger le groupe « {name} » sur cet ordinateur et partager le dossier {path} avec ses membres ?',
'native.folder_confirm': 'Partager le dossier {path} avec les membres d\'un groupe hébergé sur cet ordinateur ? Il n\'a pas été choisi dans le sélecteur de dossier.',
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 62800db..715eb81 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -1251,6 +1251,14 @@ export default {
'group.bundle_format_retired': 'Questo node conserva la tua identità in un formato che questa versione non legge più. Chiedi al suo operatore di eseguire «meshbay-node member unpin» per il tuo account e di inviarti un nuovo codice di invito.',
+ 'settings.browser_access': 'Accesso dal browser',
+ 'settings.browser_access_on_hint': 'Un browser può aprire i tuoi gruppi con la tua passphrase: questa applicazione lascia la tua identità su ogni node, sigillata in modo che solo la tua passphrase, insieme al tuo account sull\'hub, possa aprirla.',
+ 'settings.browser_access_off_hint': 'Le tue identità restano su questo dispositivo. Nulla di esse viene lasciato su alcun node, e un browser non può aprire i tuoi gruppi con la sola passphrase. Un browser che approvi da questa applicazione riceve identità proprie.',
+ 'settings.browser_access_turn_on': 'Consenti i browser',
+ 'settings.browser_access_turn_off': 'Tieni le identità solo su questo dispositivo',
+ 'settings.browser_access_next_open': 'Applicato a ogni gruppo la prossima volta che si apre.',
+ 'settings.browser_access_off_in_browser': 'L\'accesso dal browser è disattivato per questo account. Si attiva nell\'applicazione desktop di MeshBay, che può anche approvare questo browser per sé.',
+ 'group.browser_access_off': 'L\'accesso dal browser è disattivato per questo account. Attivalo nell\'applicazione desktop di MeshBay (Profilo) o approva questo browser da lì.',
// Worded by the desktop main process for its own dialogs (main.js).
'native.attach_confirm': 'Ospitare il gruppo «{name}» su questo computer e condividere la cartella {path} con i suoi membri?',
'native.folder_confirm': 'Condividere la cartella {path} con i membri di un gruppo ospitato su questo computer? Non è stata scelta nel selettore di cartelle.',
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 ad25af6..7331820 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -1235,6 +1235,14 @@ export default {
'group.bundle_format_retired': 'この node は、このバージョンでは読めなくなった形式であなたの ID を保持しています。運用者に、あなたのアカウントに対して「meshbay-node member unpin」を実行し、新しい招待コードを送るよう依頼してください。',
+ 'settings.browser_access': 'ブラウザーからのアクセス',
+ 'settings.browser_access_on_hint': 'ブラウザーはパスフレーズであなたのグループを開けます。このアプリは各 node にあなたの ID を残し、パスフレーズと hub のアカウントの両方がそろった場合にのみ開けるよう封印します。',
+ 'settings.browser_access_off_hint': 'あなたの ID はこのデバイスにとどまります。どの node にも何も残らず、ブラウザーはパスフレーズだけではグループを開けません。このアプリから承認したブラウザーには、専用の ID が作られます。',
+ 'settings.browser_access_turn_on': 'ブラウザーを許可',
+ 'settings.browser_access_turn_off': 'ID をこのデバイスだけに保持',
+ 'settings.browser_access_next_open': '各グループを次に開いたときに適用されます。',
+ 'settings.browser_access_off_in_browser': 'このアカウントではブラウザーからのアクセスがオフです。MeshBay デスクトップアプリでオンにできます。アプリからこのブラウザーだけを承認することもできます。',
+ 'group.browser_access_off': 'このアカウントではブラウザーからのアクセスがオフです。MeshBay デスクトップアプリ(プロフィール)でオンにするか、アプリからこのブラウザーを承認してください。',
// Worded by the desktop main process for its own dialogs (main.js).
'native.attach_confirm': 'このコンピューターでグループ「{name}」をホストし、フォルダー {path} をメンバーと共有しますか?',
'native.folder_confirm': 'このコンピューターでホストしているグループのメンバーとフォルダー {path} を共有しますか?このフォルダーはフォルダー選択画面で選ばれたものではありません。',
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 a6cfe02..d775aae 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -1253,6 +1253,14 @@ export default {
'group.bundle_format_retired': 'Deze node bewaart je identiteit in een formaat dat deze versie niet meer leest. Vraag de beheerder om "meshbay-node member unpin" voor je account uit te voeren en je een nieuwe uitnodigingscode te sturen.',
+ 'settings.browser_access': 'Browsertoegang',
+ 'settings.browser_access_on_hint': 'Een browser kan je groepen openen met je wachtzin: deze toepassing laat je identiteit achter op elke node, zo verzegeld dat alleen je wachtzin samen met je hub-account haar kan openen.',
+ 'settings.browser_access_off_hint': 'Je identiteiten blijven op dit apparaat. Er blijft niets van achter op een node, en een browser kan je groepen niet openen met alleen je wachtzin. Een browser die je vanuit deze toepassing goedkeurt, krijgt eigen identiteiten.',
+ 'settings.browser_access_turn_on': 'Browsers toestaan',
+ 'settings.browser_access_turn_off': 'Identiteiten alleen op dit apparaat houden',
+ 'settings.browser_access_next_open': 'Wordt toegepast op elke groep zodra die weer opent.',
+ 'settings.browser_access_off_in_browser': 'Browsertoegang staat uit voor dit account. Die wordt aangezet in de MeshBay-desktoptoepassing, die deze browser ook voor zichzelf kan goedkeuren.',
+ 'group.browser_access_off': 'Browsertoegang staat uit voor dit account. Zet die aan in de MeshBay-desktoptoepassing (Profiel), of keur deze browser daar goed.',
// Worded by the desktop main process for its own dialogs (main.js).
'native.attach_confirm': 'De groep "{name}" op deze computer hosten en de map {path} met de leden delen?',
'native.folder_confirm': 'De map {path} delen met de leden van een groep die op deze computer wordt gehost? Hij is niet gekozen in de mapkiezer.',
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 1d7a850..801e008 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -1279,6 +1279,14 @@ export default {
'group.bundle_format_retired': 'Ten node przechowuje Twoją tożsamość w formacie, którego ta wersja już nie odczytuje. Poproś jego operatora o uruchomienie „meshbay-node member unpin" dla Twojego konta i przesłanie nowego kodu zaproszenia.',
+ 'settings.browser_access': 'Dostęp z przeglądarki',
+ 'settings.browser_access_on_hint': 'Przeglądarka może otworzyć Twoje grupy Twoim hasłem: ta aplikacja zostawia Twoją tożsamość na każdym node, zapieczętowaną tak, by otworzyć ją mogło tylko Twoje hasło wraz z kontem na hubie.',
+ 'settings.browser_access_off_hint': 'Twoje tożsamości zostają na tym urządzeniu. Nic z nich nie trafia na żaden node, a przeglądarka nie otworzy Twoich grup samym hasłem. Przeglądarka zatwierdzona z tej aplikacji otrzymuje własne tożsamości.',
+ 'settings.browser_access_turn_on': 'Zezwól przeglądarkom',
+ 'settings.browser_access_turn_off': 'Trzymaj tożsamości tylko na tym urządzeniu',
+ 'settings.browser_access_next_open': 'Zostanie zastosowane w każdej grupie przy jej następnym otwarciu.',
+ 'settings.browser_access_off_in_browser': 'Dostęp z przeglądarki jest wyłączony dla tego konta. Włącza się go w aplikacji desktopowej MeshBay, która może też zatwierdzić tę przeglądarkę dla niej samej.',
+ 'group.browser_access_off': 'Dostęp z przeglądarki jest wyłączony dla tego konta. Włącz go w aplikacji desktopowej MeshBay (Profil) albo zatwierdź tę przeglądarkę w tej aplikacji.',
// Worded by the desktop main process for its own dialogs (main.js).
'native.attach_confirm': 'Hostować grupę „{name}" na tym komputerze i udostępnić jej członkom folder {path}?',
'native.folder_confirm': 'Udostępnić folder {path} członkom grupy hostowanej na tym komputerze? Nie został wybrany w oknie wyboru folderu.',
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 be40102..81a97d7 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
@@ -1238,6 +1238,14 @@ export default {
'group.bundle_format_retired': 'Este node guarda sua identidade em um formato que esta versão não lê mais. Peça ao operador que execute "meshbay-node member unpin" para sua conta e envie um novo código de convite.',
+ 'settings.browser_access': 'Acesso pelo navegador',
+ 'settings.browser_access_on_hint': 'Um navegador pode abrir seus grupos com sua frase secreta: este aplicativo deixa sua identidade em cada node, selada de forma que apenas sua frase secreta, junto com sua conta no hub, possa abri-la.',
+ 'settings.browser_access_off_hint': 'Suas identidades ficam neste dispositivo. Nada delas é deixado em nenhum node, e um navegador não consegue abrir seus grupos só com sua frase secreta. Um navegador que você aprovar por este aplicativo recebe identidades próprias.',
+ 'settings.browser_access_turn_on': 'Permitir navegadores',
+ 'settings.browser_access_turn_off': 'Manter as identidades só neste dispositivo',
+ 'settings.browser_access_next_open': 'Aplicado a cada grupo na próxima vez que ele abrir.',
+ 'settings.browser_access_off_in_browser': 'O acesso pelo navegador está desativado para esta conta. Ele é ativado no aplicativo de desktop do MeshBay, que também pode aprovar este navegador para si.',
+ 'group.browser_access_off': 'O acesso pelo navegador está desativado para esta conta. Ative-o no aplicativo de desktop do MeshBay (Perfil) ou aprove este navegador por ele.',
// Worded by the desktop main process for its own dialogs (main.js).
'native.attach_confirm': 'Hospedar o grupo "{name}" neste computador e compartilhar a pasta {path} com os membros?',
'native.folder_confirm': 'Compartilhar a pasta {path} com os membros de um grupo hospedado neste computador? Ela não foi escolhida no seletor de pastas.',
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 9b5b5dd..218b5f3 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
@@ -1224,6 +1224,14 @@ export default {
'group.bundle_format_retired': '此 node 以本版本不再读取的格式保存您的身份。请其运营者为您的账户运行“meshbay-node member unpin”,并向您发送新的邀请码。',
+ 'settings.browser_access': '浏览器访问',
+ 'settings.browser_access_on_hint': '浏览器可以用您的密码短语打开您的群组:本应用会在每个 node 上留下您的身份,并加以封存,只有您的密码短语配合您的 hub 账户才能打开。',
+ 'settings.browser_access_off_hint': '您的身份保留在本设备上。任何 node 上都不会留下它们的任何内容,浏览器仅凭密码短语无法打开您的群组。您从本应用批准的浏览器会获得自己的身份。',
+ 'settings.browser_access_turn_on': '允许浏览器',
+ 'settings.browser_access_turn_off': '仅在本设备上保留身份',
+ 'settings.browser_access_next_open': '将在每个群组下次打开时生效。',
+ 'settings.browser_access_off_in_browser': '此账户已关闭浏览器访问。可在 MeshBay 桌面应用中开启,该应用也可以单独批准此浏览器。',
+ 'group.browser_access_off': '此账户已关闭浏览器访问。请在 MeshBay 桌面应用(个人资料)中开启,或从该应用批准此浏览器。',
// Worded by the desktop main process for its own dialogs (main.js).
'native.attach_confirm': '在这台电脑上托管群组“{name}”,并与其成员共享文件夹 {path}?',
'native.folder_confirm': '与这台电脑上托管的群组成员共享文件夹 {path}?该文件夹不是在文件夹选择器中选择的。',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js b/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js
index cd00f03..7c36951 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js
@@ -6,7 +6,7 @@ import { ask } from './ask.js';
import { Icon } from './icon.js';
import * as platform from './platform.js';
import {
- hubFetch, HUB, session, setAuth,
+ hubFetch, HUB, session, setAuth, mirrorBrowserAccess,
_storeBundleKey, _loadBundleKey, _storeRecoveryKey,
} from './hub-client.js';
@@ -241,6 +241,46 @@ export function ProfilePage({ user, onLogout }) {
}
}, [rkInput, user]);
+ // ── Browser access (docs/MESHBAY_DESIGN.md §3.7) ────────────────────────
+ // Decided in the desktop application, which keeps the identities: there it
+ // is a switch, and turning it on is confirmed by the application itself. A
+ // browser only reads the hub's mirror, to say why it cannot open a group.
+ const [baNative, setBaNative] = useState(false);
+ const [baOn, setBaOn] = useState(null);
+ const [baBusy, setBaBusy] = useState(false);
+ const [baMsg, setBaMsg] = useState('');
+ useEffect(() => {
+ let gone = false;
+ (async () => {
+ const native = await platform.nativeKeys();
+ if (gone) return;
+ setBaNative(native);
+ if (native) {
+ const on = await platform.keys.browserAccess(user.userId);
+ if (!gone) setBaOn(on);
+ return;
+ }
+ try {
+ const prefs = await hubFetch('/v1/users/me/preferences', { token: user.token });
+ if (!gone) setBaOn((prefs || {}).browser_access !== 'off');
+ } catch { /* shown as nothing */ }
+ })();
+ return () => { gone = true; };
+ }, [user]);
+ const baSet = useCallback(async (on) => {
+ setBaBusy(true);
+ setBaMsg('');
+ try {
+ setBaOn(await platform.keys.setBrowserAccess(user.userId, on));
+ await mirrorBrowserAccess(user.token, user.userId);
+ setBaMsg(t('settings.browser_access_next_open'));
+ } catch (err) {
+ setBaMsg(platform.bridgeMessage(err));
+ } finally {
+ setBaBusy(false);
+ }
+ }, [user]);
+
useEffect(() => {
hubFetch('/v1/users/me', { token: user.token })
.then(data => {
@@ -539,6 +579,20 @@ export function ProfilePage({ user, onLogout }) {
</div>`}
</div>
+ ${baOn !== null && (baNative || baOn === false) && html`
+ <div class="settings-section">
+ <h3 class="settings-heading">${t('settings.browser_access')}</h3>
+ ${baNative ? html`
+ <p class="settings-hint">
+ ${baOn ? t('settings.browser_access_on_hint') : t('settings.browser_access_off_hint')}
+ </p>
+ <button class="btn-secondary" disabled=${baBusy} onClick=${() => baSet(!baOn)}>
+ ${baOn ? t('settings.browser_access_turn_off') : t('settings.browser_access_turn_on')}
+ </button>
+ ${baMsg && html`<p class="settings-hint">${baMsg}</p>`}
+ ` : html`<p class="settings-hint">${t('settings.browser_access_off_in_browser')}</p>`}
+ </div>`}
+
<div class="settings-section">
<h3 class="settings-heading">${t('settings.sessions_heading')}</h3>
<p class="settings-hint">${t('settings.sign_out_everywhere_hint')}</p>
diff --git a/packages/meshbay-hub/tests/test_bundle_pepper.py b/packages/meshbay-hub/tests/test_bundle_pepper.py
index e99799f..be9da19 100644
--- a/packages/meshbay-hub/tests/test_bundle_pepper.py
+++ b/packages/meshbay-hub/tests/test_bundle_pepper.py
@@ -167,3 +167,19 @@ async def test_it_is_never_logged(client):
lg.disabled = disabled
assert seen, "the handler saw nothing, so it proves nothing"
assert not any(login["bundle_pepper"] in m for m in seen)
+
+
+@pytest.mark.asyncio
+async def test_the_browser_access_mirror_is_a_preference_like_any_other(client):
+ """The desktop application writes what it holds, and a browser reads it to
+ say why it cannot open a group. Nothing on the hub or a node acts on it."""
+ await _register(client, "pepper_mirror")
+ login = await _login(client, "pepper_mirror")
+ headers = _bearer(login["access_token"])
+ r = await client.put("/v1/users/me/preferences/browser_access", headers=headers,
+ json={"value": "off"})
+ assert r.status_code == 200, r.text
+ prefs = (await client.get("/v1/users/me/preferences", headers=headers)).json()
+ assert prefs["browser_access"] == "off"
+ # Still handed the pepper: the mirror decides nothing.
+ assert "bundle_pepper" in await _login(client, "pepper_mirror")
diff --git a/packages/meshbay-hub/tests/test_desktop_shell.py b/packages/meshbay-hub/tests/test_desktop_shell.py
index 6e1c634..4426dd7 100644
--- a/packages/meshbay-hub/tests/test_desktop_shell.py
+++ b/packages/meshbay-hub/tests/test_desktop_shell.py
@@ -446,6 +446,17 @@ def test_the_native_dialogs_are_worded_in_every_language():
assert not missing, f"{catalogue.name} lacks {missing}"
+def test_browser_access_is_widened_only_by_the_person_natively():
+ """Turning it on puts every identity of the account on every node, for a
+ browser to open: the application asks, in a dialog the page cannot
+ answer. What the page may say unasked only narrows it."""
+ source = _main()
+ widen = source.split("handle('keys:set-browser-access'", 1)[1].split("\n handle(", 1)[0]
+ assert "confirmOrRefuse('native.browser_access_confirm')" in widen
+ here = source.split("handle('keys:created-here'", 1)[1].split("\n", 1)[0]
+ assert "setBrowserAccess(uid(u), false)" in here
+
+
def test_the_node_is_never_pointed_at_a_hub_the_page_names():
"""`node:start` writes the hub this application is signed in to, and a
username that cannot break out of a TOML string."""
@@ -680,3 +691,13 @@ def test_hardware_decoding_can_be_turned_off_without_a_rebuild():
catch needs an answer that is not "reinstall": config.json, the same file
every other client setting lives in."""
assert "config.videoAcceleration" in _main()
+
+
+def test_an_account_made_in_the_application_starts_without_browser_access():
+ """Registration tells the application, which then leaves nothing of the
+ account's identities on any node until the person turns it on."""
+ auth = (STATIC / "auth-page.js").read_text(encoding="utf-8")
+ register = auth.split("const reg = await window.MeshBayKeys.registerUser(", 1)[1][:900]
+ assert "platform.keys.createdHere(reg.userId)" in register
+ assert "userId" in (STATIC / "keyderive.js").read_text(encoding="utf-8").split(
+ "async function registerUser", 1)[1].split("\n}\n", 1)[0]