From 2c6921aa2c35ffd41b6c453e6700574ef631ba2c Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 30 Sep 2026 12:57:58 +0200 Subject: fix(client): the page names node operations, and the app confirms what widens the node node:call is replaced by named operations with checked arguments; hosting a group, sharing an unpicked folder, key rotation, denylist clearing and a change of node account are confirmed by a native dialog. Every channel checks its sender, secrets:get/set/clear are gone, node:start writes the app's own hub. Co-Authored-By: Claude Opus 5.5 --- .../meshbay-hub/src/meshbay_hub/static/platform.js | 57 +++++++++++----------- 1 file changed, 29 insertions(+), 28 deletions(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/platform.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js index a3fb80b..8bf09b4 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js @@ -67,30 +67,15 @@ export const capabilities = { }; /** - * Where the identity keys live. + * The app's secret store, as far as the page may know it. * - * 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. + * The store is the main process's (the OS keychain through safeStorage) and it + * holds the device's hub key, which the page is never handed. The page used to + * be able to read and write it by name; nothing here did, and that was a way to + * both read the key and replace it. What is left is whether the OS protects it. */ 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. * @@ -257,9 +242,15 @@ export const node = { if (!bridge || !bridge.node) throw new Error('Node bridge not available'); return bridge.node.start(opts); }, - async call(method, path, body) { + /** + * One of the local node's operations, by name (`NODE_OPS` in the desktop + * client's main.js). The page never names a route: the main process checks + * the arguments, builds the request, and asks the person itself before + * anything that widens what the node shares. + */ + async op(name, args) { if (!bridge || !bridge.node) throw new Error('Node bridge not available'); - return bridge.node.call(method, path, body); + return bridge.node.op(name, args); }, async pairingCode() { return bridge && bridge.node ? bridge.node.pairingCode() : null; @@ -344,7 +335,7 @@ export async function watchIndexProgress(groupId, onUpdate, { intervalMs = 500 } for (;;) { let status; try { - status = await node.call('GET', `/api/groups/${groupId}/index-status`); + status = await node.op('groupIndexStatus', { groupId }); } catch { // The node went away mid-poll — stop rather than spin forever; the // caller's own connection-status handling already covers that case. @@ -376,12 +367,12 @@ export async function waitForGroupHosted(groupId, onProgress, const deadline = Date.now() + timeoutMs; for (;;) { try { - const status = await node.call('GET', `/api/groups/${groupId}/index-status`); + const status = await node.op('groupIndexStatus', { groupId }); if (onProgress) onProgress(status); } catch { /* keep waiting — the loopback API can be momentarily busy */ } try { - const list = await node.call('GET', '/api/groups'); + const list = await node.op('groups'); if (Array.isArray(list.groups) && list.groups.some((g) => g.id === groupId)) return; } catch { /* keep waiting */ } @@ -420,7 +411,7 @@ export async function waitForRootsIndexed(groupId, onProgress, for (;;) { let status; try { - status = await node.call('GET', `/api/groups/${groupId}/index-status`); + status = await node.op('groupIndexStatus', { groupId }); } catch { return; // the node went away mid-poll — same stance as watchIndexProgress } @@ -507,9 +498,19 @@ export async function setTrayLabels(labels) { return bridge.setTrayLabels(labels); } +/** + * Tell the app which language the interface is in. The confirmations its main + * process draws for itself are worded from the same catalogues, which it reads + * from the packaged files -- only the code crosses the bridge. + */ +export async function setUiLocale(code) { + if (!bridge || !bridge.setLocale) return false; + return bridge.setLocale(code); +} + export default { isNative, hubBase, capabilities, secrets, nativeSave, apiFetch, device, bridgeMessage, folder, rootPicker, node, - cast, minimizeToTray, setTrayLabels }; + cast, minimizeToTray, setTrayLabels, setUiLocale }; // Also a global, because `transport.js` is loaded as a classic script — it // predates the module graph and exposes `MeshBayTransport` the same way. The @@ -519,5 +520,5 @@ if (typeof window !== 'undefined') { window.MeshBayPlatform = { isNative, hubBase, capabilities, secrets, nativeSave, apiFetch, device, bridgeMessage, folder, rootPicker, node, - cast, minimizeToTray, setTrayLabels }; + cast, minimizeToTray, setTrayLabels, setUiLocale }; } -- cgit v1.2.3