aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-client/src/preload.js
blob: 04dfe440008f38940381bd23f307c34019144399 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/**
 * 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,
  },

  // Ask the main process to call the hub. The renderer has an `app://` origin,
  // which CORS refuses and which is not a credential anyway.
  fetch: (url, init) => ipcRenderer.invoke('hub:fetch', url, init),

  // The device's hub key. Generated, held and used entirely in the main
  // process: the interface asks for a signature and never sees a key, because
  // it is the part of this application that parses hostile input.
  device: {
    ensure: () => ipcRenderer.invoke('device:ensure'),
    publicKey: () => ipcRenderer.invoke('device:public'),
    sign: (username) => ipcRenderer.invoke('device:sign', username),
    forget: () => ipcRenderer.invoke('device:forget'),
  },

  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'),
  },

  // Where downloads go, chosen once. The renderer never sees or sends a path —
  // it asks for a dialog and is told the folder's name for display only.
  folder: {
    choose: () => ipcRenderer.invoke('folder:choose'),
    get: () => ipcRenderer.invoke('folder:get'),
    forget: () => ipcRenderer.invoke('folder:forget'),
  },

  // Pick a directory to share as a group root. Returns { path, name } — the
  // path is forwarded over MNP to the node, which is the local machine for D5.
  rootPicker: {
    choose: () => ipcRenderer.invoke('root:choose'),
  },

  // The local node, if one is running. The renderer never sees the session
  // token — it names an operation and the main process executes it, the same
  // pattern as hub:fetch.
  node: {
    detect: () => ipcRenderer.invoke('node:detect'),
    call: (method, path, body) => ipcRenderer.invoke('node:call', method, path, body),
    pairingCode: () => ipcRenderer.invoke('node:pairing-code'),
    setPairingCode: (code) => ipcRenderer.invoke('node:set-pairing-code', code),
  },

  // A sink that writes to disk as chunks arrive, never a buffer handed over at
  // the end. `auto` uses the remembered folder without a dialog, which is what
  // "save automatically" means; without one, or when the person asked to be
  // prompted, a dialog opens. The renderer holds an id, not a path.
  saveFile: async (suggestedName, opts) => {
    const handle = await ipcRenderer.invoke('save:begin', suggestedName, opts);
    if (!handle) return null;
    return {
      name: handle.name,
      write: (chunk) => ipcRenderer.invoke('save:write', handle.id, chunk),
      close: () => ipcRenderer.invoke('save:end', handle.id),
      abort: () => ipcRenderer.invoke('save:abort', handle.id),
      open: () => ipcRenderer.invoke('save:open', handle.id),
    };
  },
});