summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-client/src/preload.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-client/src/preload.js')
-rw-r--r--packages/meshbay-client/src/preload.js66
1 files changed, 66 insertions, 0 deletions
diff --git a/packages/meshbay-client/src/preload.js b/packages/meshbay-client/src/preload.js
new file mode 100644
index 0000000..9c273e2
--- /dev/null
+++ b/packages/meshbay-client/src/preload.js
@@ -0,0 +1,66 @@
+/**
+ * The bridge, and the whole of it.
+ *
+ * `contextIsolation` puts this in its own world, so what is exposed here is all
+ * the page can reach — page script cannot read the closure, cannot replace
+ * these functions for other code, and cannot call an IPC channel that is not
+ * named below. That is what makes the enumeration meaningful rather than
+ * decorative.
+ *
+ * The rule for anything added here: **the renderer never names a path, a file
+ * handle or a process.** It asks for a dialog and receives an opaque id. The
+ * renderer parses decrypted content from nodes — video, images, filenames —
+ * which is attacker-controlled input, so it is treated as hostile even though
+ * it is our own code.
+ */
+
+'use strict';
+
+const { contextBridge, ipcRenderer } = require('electron');
+
+const HUB_BASE = (process.argv.find(a => a.startsWith('--meshbay-hub=')) || '')
+ .slice('--meshbay-hub='.length);
+
+contextBridge.exposeInMainWorld('meshbay', {
+ // Where the hub is. The interface prefixes every API path with this; in a
+ // browser the same function returns '' and relative paths go to the origin
+ // that served the page.
+ //
+ // Read from a process argument, not over IPC: the interface asks for this
+ // while its modules are still loading, before anything can await, and
+ // synchronous IPC would block the renderer for a value that cannot change
+ // within a run.
+ hubBase: () => HUB_BASE,
+ setHubBase: (base) => ipcRenderer.invoke('hub:set', base),
+
+ // What this build can do that a browser cannot. The interface renders
+ // features gated on these nowhere at all in a browser, rather than offering
+ // something that fails when clicked.
+ capabilities: {
+ nodeAdmin: true,
+ localFolders: true,
+ nativeSave: true,
+ },
+
+ secrets: {
+ get: (name) => ipcRenderer.invoke('secrets:get', name),
+ set: (name, value) => ipcRenderer.invoke('secrets:set', name, value),
+ clear: (name) => ipcRenderer.invoke('secrets:clear', name),
+ // 'unprotected_fallback' means safeStorage found no keyring and is using a
+ // fixed key. Encrypted on disk, by a key that is not a secret — the
+ // interface says so rather than letting someone believe otherwise.
+ backend: () => ipcRenderer.invoke('secrets:backend'),
+ },
+
+ // A save dialog and a write that never passes back through the page. The
+ // renderer holds an id, not a path.
+ saveFile: async (suggestedName) => {
+ const handle = await ipcRenderer.invoke('save:begin', suggestedName);
+ if (!handle) return null;
+ return {
+ name: handle.name,
+ write: (chunk) => ipcRenderer.invoke('save:write', handle.id, chunk),
+ close: () => ipcRenderer.invoke('save:end', handle.id),
+ };
+ },
+});