diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub')
13 files changed, 169 insertions, 3 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py index 392e8d1..7028071 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py @@ -23,8 +23,12 @@ router = APIRouter(tags=["webapp"]) # Assets the shell pulls in, in load order. Everything else is imported by # app.js and rides on the same query string via window.__MB_ASSET_V. +# Every module the page loads. A file missing from here is a file whose change +# does not move the URL, so a browser holding the old one never asks for it — +# which is the failure this list exists to prevent, and it is silent. _ASSETS = ("style.css", "keyderive.js", "crypto.js", "transport.js", "app.js", - "i18n.js", "downloads.js", "transfers.js", "zipstream.js") + "i18n.js", "downloads.js", "transfers.js", "zipstream.js", + "platform.js") def _asset_version() -> str: diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index a48535a..abb3374 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -6,10 +6,15 @@ import { t, getLocale, setLocale, initLocale, LOCALES } from './i18n.js'; import { ZipStream, entriesUnder } from './zipstream.js'; import { transfers, formatSpeed } from './transfers.js'; import * as downloads from './downloads.js'; +import * as platform from './platform.js'; // ── Constants ──────────────────────────────────────────────────────────────── -const HUB = ''; +// Where the hub is. Empty in a browser — it served this page, so a relative +// path cannot be pointed at the wrong place. In the installed app the page +// comes from disk and has no origin of its own, so the base is configured. +// See platform.js. +const HUB = platform.hubBase(); const AUTH_KEY = 'mb_auth'; // Renew an access token with this much life left rather than waiting for it to // fail. Generous against a one-hour token: a film is watched without the hub @@ -1323,7 +1328,9 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth, // connection at all. Renewals are shared, so if one is already in // flight this waits for it instead of starting a second. const live = (await ensureFreshToken()) || token; - const transport = new window.MeshBayTransport('', live); + // The same base the API calls use: signaling is a hub endpoint like + // any other, and two sources for one address is how they drift. + const transport = new window.MeshBayTransport(HUB, live); transportRef.current = transport; const ack = await transport.connect( @@ -3941,6 +3948,15 @@ function SettingsPage({ user, theme, onThemeChange, groups }) { // Read from the hub rather than written here: the two constants that used to // sit in this markup said 0.1.0 and MNP 0.1 long after both had moved on. const [hubInfo, setHubInfo] = useState(null); + // On a desktop build, whether the OS is really holding the keys. Electron's + // safeStorage falls back to a fixed key when no keyring is running — a + // 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(''); + useEffect(() => { + if (!platform.secrets.available) return; + platform.secrets.backend().then(setKeyBackend).catch(() => {}); + }, []); useEffect(() => { hubFetch('/v1/hub/version').then(setHubInfo).catch(() => {}); @@ -4043,6 +4059,19 @@ function SettingsPage({ user, theme, onThemeChange, groups }) { </div> `} + ${keyBackend && html` + <div class="settings-section"> + <h3 class="settings-heading">${t('settings.keys_heading')}</h3> + <div class="settings-row"> + <span class="settings-label">${t('settings.keys_where')}</span> + <span class="settings-value">${keyBackend}</span> + </div> + ${keyBackend === 'unprotected_fallback' && html` + <p class="error-msg">${t('settings.keys_unprotected')}</p> + `} + </div> + `} + <div class="settings-section"> <h3 class="settings-heading">${t('settings.about')}</h3> <div class="settings-row"> 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 5a946f7..ae356ba 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -198,6 +198,9 @@ export default { 'settings.theme_system': 'System', 'settings.language': 'Sprache', 'settings.about': 'Über', + 'settings.keys_heading': 'Keys on this device', + '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.version': 'Version', 'settings.protocol': 'Protokoll', 'settings.role': 'Rolle', 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 fd2d6ec..fe6bd0f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -190,6 +190,9 @@ export default { 'settings.theme_system': 'System', 'settings.language': 'Language', 'settings.about': 'About', + 'settings.keys_heading': 'Keys on this device', + '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.version': 'Version', 'settings.protocol': 'Protocol', 'settings.role': 'Role', 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 e564164..3066963 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -194,6 +194,9 @@ export default { 'settings.theme_system': 'Sistema', 'settings.language': 'Idioma', 'settings.about': 'Acerca de', + 'settings.keys_heading': 'Keys on this device', + '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.version': 'Versión', 'settings.protocol': 'Protocolo', 'settings.role': 'Rol', 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 5848f9d..c94089d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -198,6 +198,9 @@ export default { 'settings.theme_system': 'Système', 'settings.language': 'Langue', 'settings.about': 'À propos', + 'settings.keys_heading': 'Clés sur cet appareil', + '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.version': 'Version', 'settings.protocol': 'Protocole', 'settings.role': 'Rôle', 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 61913e3..56beb8d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -197,6 +197,9 @@ export default { 'settings.theme_system': 'Sistema', 'settings.language': 'Lingua', 'settings.about': 'Informazioni', + 'settings.keys_heading': 'Keys on this device', + '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.version': 'Versione', 'settings.protocol': 'Protocollo', 'settings.role': 'Ruolo', 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 aa656ee..5d83bb9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -192,6 +192,9 @@ export default { 'settings.theme_system': 'システムに合わせる', 'settings.language': '言語', 'settings.about': 'このアプリについて', + 'settings.keys_heading': 'Keys on this device', + '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.version': 'バージョン', 'settings.protocol': 'プロトコル', 'settings.role': '権限', 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 dae903f..55c7d81 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -198,6 +198,9 @@ export default { 'settings.theme_system': 'Systeem', 'settings.language': 'Taal', 'settings.about': 'Over', + 'settings.keys_heading': 'Keys on this device', + '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.version': 'Versie', 'settings.protocol': 'Protocol', 'settings.role': 'Rol', 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 65c81ca..3cf909f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -204,6 +204,9 @@ export default { 'settings.theme_system': 'Systemowy', 'settings.language': 'Język', 'settings.about': 'O programie', + 'settings.keys_heading': 'Keys on this device', + '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.version': 'Wersja', 'settings.protocol': 'Protokół', 'settings.role': 'Rola', 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 729a7fc..714dff6 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 @@ -196,6 +196,9 @@ export default { 'settings.theme_system': 'Sistema', 'settings.language': 'Idioma', 'settings.about': 'Sobre', + 'settings.keys_heading': 'Keys on this device', + '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.version': 'Versão', 'settings.protocol': 'Protocolo', 'settings.role': 'Função', 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 0ff5a49..c3d9494 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 @@ -183,6 +183,9 @@ export default { 'settings.theme_system': '跟随系统', 'settings.language': '语言', 'settings.about': '关于', + 'settings.keys_heading': 'Keys on this device', + '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.version': '版本', 'settings.protocol': '协议', 'settings.role': '角色', 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 }; |