aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-client/src/preload.js
blob: b390bfbbe0ac87f4d4d69cc86f209c712b9050fa (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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/**
 * 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,
    lanCast: true,
    // Linux and Windows have a tray to hide into; declared per platform
    // rather than always, so the button never appears somewhere the desktop
    // shows no indicator, which would hide the window for good.
    tray: process.platform === 'linux' || process.platform === 'win32',
  },

  // Labels are passed in because the main process has no i18n; see main.js.
  minimizeToTray: (labels) => ipcRenderer.invoke('window:minimize-to-tray', labels),

  // 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),

  // Resolve STUN `stun:host:port` URLs to `stun:ip:port` using Node's resolver.
  // Chromium's P2P socket manager fails STUN hostnames outright in some
  // restricted-resolver environments; the renderer cannot do DNS, so it asks
  // here. Unresolvable entries are dropped.
  resolveStun: (urls) => ipcRenderer.invoke('ice:resolve-stun', urls),

  // 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'),
    installed: () => ipcRenderer.invoke('node:installed'),
    start: (opts) => ipcRenderer.invoke('node:start', opts),
    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),
    // The daemon's lifecycle as seen from outside it: the systemd unit (Linux)
    // or, on Windows, a probe of the daemon plus whether the Startup launcher
    // is in place — reachable even while the daemon itself is stopped or
    // crash-looping, which `call()` above is not.
    service: {
      status: () => ipcRenderer.invoke('node:service-status'),
      stop: () => ipcRenderer.invoke('node:service-stop'),
      restart: () => ipcRenderer.invoke('node:service-restart'),
    },
    // Windows only: the "run at every sign-in" Startup-folder launcher.
    // action: 'install' | 'remove' | 'status' (default). Elsewhere returns
    // { supported: false }.
    autostart: (action) => ipcRenderer.invoke('node:autostart', action),
    // Windows only: switch into/out of the boot-time service (Scheduled Task
    // + firewall, one elevation). action: 'install' | 'remove'. Elsewhere
    // returns { supported: false }.
    serviceMode: (action) => ipcRenderer.invoke('node:service-mode', action),
  },

  // LAN cast relay. The main process runs a local HTTP server and the
  // renderer feeds it decrypted segments. A Chromecast or Smart TV on the
  // same Wi-Fi plays from the URL.
  cast: {
    start: (opts) => ipcRenderer.invoke('cast:start', opts),
    push: (data) => ipcRenderer.invoke('cast:push', data),
    stop: () => ipcRenderer.invoke('cast:stop'),
    finish: () => ipcRenderer.invoke('cast:finish'),
    status: () => ipcRenderer.invoke('cast:status'),
    discover: () => ipcRenderer.invoke('cast:discover'),
    chromecastConnect: (opts) => ipcRenderer.invoke('cast:chromecast:connect', opts),
    chromecastReload: (opts) => ipcRenderer.invoke('cast:chromecast:reload', opts),
    chromecastDisconnect: () => ipcRenderer.invoke('cast:chromecast:disconnect'),
  },

  // 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),
    };
  },
});