aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/platform.js
blob: 0663a425b8e19fa4f480903822241b63ceee12cc (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
/**
 * 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 };