summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/platform.js
blob: fcc866ed0b41472db2d114ae36fbcbb8953a3d92 (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
/**
 * 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();
  },
};

/**
 * Call the hub.
 *
 * In a browser this is `fetch`, unchanged — the page came from the hub, so the
 * request is same-origin and nothing is in the way.
 *
 * In the application the page's origin is `app://meshbay`, and a browser fetch
 * from it is refused by CORS. The hub has **no CORS middleware at all**, and
 * that is worth keeping: its API is reachable from no web origin whatever.
 * Widening it for `app://meshbay` would be worse than it appears, because that
 * origin is not a credential — any Electron application can claim the same
 * scheme and host name.
 *
 * So the main process makes the call. It returns a small object rather than a
 * Response, and this shapes it back into something with `.ok`, `.status` and
 * `.json()`, so callers do not have to know which one they got.
 */
export async function apiFetch(url, init) {
  if (!bridge || !bridge.fetch) return fetch(url, init);
  const raw = await bridge.fetch(String(url), init && {
    method: init.method,
    headers: init.headers,
    body: init.body,
  });
  return {
    ok: raw.ok,
    status: raw.status,
    statusText: String(raw.status),
    headers: new Headers(raw.headers || {}),
    text: async () => raw.body,
    json: async () => JSON.parse(raw.body),
  };
}

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

// Also a global, because `transport.js` is loaded as a classic script — it
// predates the module graph and exposes `MeshBayTransport` the same way. The
// alternative was a second fetch path there, which is how two callers of one
// hub end up disagreeing about how to reach it.
if (typeof window !== 'undefined') {
  window.MeshBayPlatform = { isNative, hubBase, capabilities, secrets,
                             nativeSave, apiFetch };
}