aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/platform.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/platform.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/platform.js103
1 files changed, 103 insertions, 0 deletions
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 };