aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/main/assets/bridge
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-android/app/src/main/assets/bridge')
-rw-r--r--packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js206
1 files changed, 206 insertions, 0 deletions
diff --git a/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js b/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js
new file mode 100644
index 0000000..4755531
--- /dev/null
+++ b/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js
@@ -0,0 +1,206 @@
+/**
+ * The bridge, and the whole of it — the Android counterpart of
+ * meshbay-client/src/preload.js, with the same shape wherever it offers
+ * something at all.
+ *
+ * Injected at document start into documents of the packaged origin, before any
+ * page script. It takes the native port the listener injected, hides the
+ * global, and exposes `window.meshbay` frozen. There is no context isolation
+ * on Android: page script runs in the same world, so what this buys is that
+ * nothing can reach the raw port by name, not that this file is out of reach.
+ * The confinement that matters is native — the listener answers the packaged
+ * origin's top-level document only, and checks every argument.
+ *
+ * What the desktop offers and this build does not is ABSENT, not a function
+ * that refuses: `platform.js` decides what to show from whether an object
+ * exists (`platform.node.available`, `platform.folder.available`, …).
+ *
+ * `HUB_BASE`, `BINARY` and `CAST` are prepended by the shell when it injects
+ * this file: the interface asks for the hub while its modules load, before
+ * anything can await; BINARY says whether the WebView carries ArrayBuffer
+ * messages; CAST whether this device can cast at all.
+ */
+(function () {
+ 'use strict';
+ const port = window.meshbayNative;
+ try { delete window.meshbayNative; } catch (e) { /* already gone */ }
+ // A same-origin child frame gets the port too; it gets no bridge, and native
+ // refuses whatever it sends anyway.
+ if (!port || window.top !== window) return;
+
+ const pending = new Map();
+ let seq = 0;
+ port.onmessage = (event) => {
+ let reply;
+ try { reply = JSON.parse(event.data); } catch (e) { return; }
+ const waiter = pending.get(reply.id);
+ if (!waiter) return;
+ pending.delete(reply.id);
+ if (reply.ok) waiter.resolve(reply.value);
+ else waiter.reject(new Error(reply.error));
+ };
+ const call = (channel, ...args) => new Promise((resolve, reject) => {
+ const id = ++seq;
+ pending.set(id, { resolve, reject });
+ port.postMessage(JSON.stringify({ id, ch: channel, args }));
+ });
+
+ // A write is a binary message: "MBB1" | id | channel | 0 | handle | bytes,
+ // big-endian — one copy, no JSON, no base64 (spike S-3: 136 MB/s awaited
+ // per 48 KB chunk). Without ArrayBuffer messages, base64 over JSON.
+ const SAVE_WRITE = 1;
+ const CAST_PUSH = 2;
+ const bytesOf = (chunk) => (chunk instanceof Uint8Array ? chunk
+ : ArrayBuffer.isView(chunk) ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)
+ : new Uint8Array(chunk));
+ const base64Of = (bytes) => {
+ let s = '';
+ for (let i = 0; i < bytes.length; i += 0x8000) s += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
+ return btoa(s);
+ };
+ const sendBinary = (channel, handle, bytes) => new Promise((resolve, reject) => {
+ const id = ++seq;
+ pending.set(id, { resolve, reject });
+ const frame = new ArrayBuffer(16 + bytes.length);
+ const head = new DataView(frame);
+ head.setUint32(0, 0x4d424231); // "MBB1"
+ head.setUint32(4, id);
+ head.setUint16(8, channel);
+ head.setUint32(12, handle);
+ new Uint8Array(frame, 16).set(bytes);
+ port.postMessage(frame);
+ });
+ const writeChunk = (handle, chunk) => {
+ const bytes = bytesOf(chunk);
+ return BINARY ? sendBinary(SAVE_WRITE, handle, bytes) : call('save:write', handle, base64Of(bytes));
+ };
+ const pushSegment = (chunk) => {
+ const bytes = bytesOf(chunk);
+ return BINARY ? sendBinary(CAST_PUSH, 0, bytes) : call('cast:push', base64Of(bytes));
+ };
+
+ const meshbay = {
+ hubBase: () => HUB_BASE,
+ setHubBase: (base) => call('hub:set', base),
+
+ capabilities: {
+ nodeAdmin: false, // no node runs on a phone (§11.3)
+ localFolders: false,
+ nativeSave: true,
+ lanCast: CAST, // false where the vendor's play services are absent
+ tray: false,
+ },
+
+ setLocale: (code) => call('ui:locale', code),
+
+ // The page's origin is refused by the hub's absent CORS, and is not a
+ // credential anyway: native goes, to the signed-in hub only.
+ fetch: (url, init) => call('hub:fetch', url, init),
+
+ resolveStun: (urls) => call('ice:resolve-stun', urls),
+
+ // The device's hub key: generated, held and used natively. The page asks
+ // for a signature and never sees a key — it parses hostile input.
+ device: {
+ ensure: () => call('device:ensure'),
+ publicKey: () => call('device:public'),
+ sign: (username) => call('device:sign', username),
+ forget: () => call('device:forget'),
+ },
+
+ // The bundle key and the identity on every node, held natively: the page
+ // is told public keys and handed signatures and agreements. A signature is
+ // asked for by kind and fields, never by bytes.
+ keys: {
+ available: () => call('keys:available'),
+ deriveSession: (o) => call('keys:derive-session', o),
+ commitPending: (u) => call('keys:commit-pending', u),
+ dropPending: (u) => call('keys:drop-pending', u),
+ hasSession: (u) => call('keys:has-session', u),
+ forgetSession: (u) => call('keys:forget-session', u),
+ identity: (u, n) => call('keys:identity', u, n),
+ openBundle: (u, n, o) => call('keys:open-bundle', u, n, o),
+ mint: (u, n) => call('keys:mint', u, n),
+ sealBundle: (u, n, o) => call('keys:seal-bundle', u, n, o),
+ sealRecovery: (u, n, m, name) => call('keys:seal-recovery', u, n, m, name),
+ markSealed: (u, n, fp) => call('keys:mark-sealed', u, n, fp),
+ fingerprint: (u) => call('keys:fingerprint', u),
+ sign: (u, n, kind, fields) => call('keys:sign', u, n, kind, fields),
+ shared: (u, n, peer) => call('keys:shared', u, n, peer),
+ playlistKey: (u) => call('keys:playlist-key', u),
+ browserAccess: (u) => call('keys:browser-access', u),
+ setBrowserAccess: (u, on) => call('keys:set-browser-access', u, on),
+ createdHere: (u) => call('keys:created-here', u),
+ },
+
+ // Whether the OS protects what is stored. The store itself is not
+ // reachable from here.
+ secrets: {
+ backend: () => call('secrets:backend'),
+ },
+
+ // LAN cast relay: the page feeds it decrypted segments, a receiver on the
+ // same Wi-Fi plays from the URL. Present only where casting can work —
+ // `platform.cast.available` is whether this object exists.
+ ...(CAST ? { cast: {
+ start: (opts) => {
+ const o = Object.assign({}, opts || {});
+ if (o.initSegment) o.initSegment = base64Of(bytesOf(o.initSegment));
+ return call('cast:start', o);
+ },
+ push: (data) => pushSegment(data),
+ stop: () => call('cast:stop'),
+ subtitle: (sub) => call('cast:subtitle', sub),
+ finish: () => call('cast:finish'),
+ status: () => call('cast:status'),
+ scan: () => call('cast:scan'),
+ devices: () => call('cast:devices'),
+ chromecastConnect: (opts) => call('cast:chromecast:connect', opts),
+ chromecastReload: (opts) => call('cast:chromecast:reload', opts),
+ chromecastDisconnect: () => call('cast:chromecast:disconnect'),
+ chromecastPause: () => call('cast:chromecast:pause'),
+ chromecastPlay: () => call('cast:chromecast:play'),
+ } } : {}),
+
+ // Where downloads go, chosen once. A display name comes back, never a URI.
+ folder: {
+ choose: () => call('folder:choose'),
+ get: () => call('folder:get'),
+ forget: () => call('folder:forget'),
+ },
+
+ // A sink that writes to disk as chunks arrive, never a buffer handed over
+ // at the end. The page holds an id. `open` exists only where the target
+ // says the file may be opened — a type that runs nothing.
+ saveFile: async (suggestedName, opts) => {
+ const handle = await call('save:begin', suggestedName, opts);
+ if (!handle) return null;
+ const sink = {
+ name: handle.name,
+ write: (chunk) => writeChunk(handle.id, chunk),
+ close: () => call('save:end', handle.id),
+ abort: () => call('save:abort', handle.id),
+ };
+ if (handle.openable) sink.open = () => call('save:open', handle.id);
+ return sink;
+ },
+ };
+
+ const freeze = (o) => {
+ Object.freeze(o);
+ for (const v of Object.values(o)) if (v && typeof v === 'object' && !Object.isFrozen(v)) freeze(v);
+ return o;
+ };
+ Object.defineProperty(window, 'meshbay', {
+ value: freeze(meshbay), writable: false, configurable: false, enumerable: false,
+ });
+
+ // The WebView exposes File System Access and cannot back it with anything a
+ // person can see. Left in place, `downloads.SUPPORTED` reads true and a
+ // download could take a path that fails — or reach the blob floor silently.
+ for (const name of ['showDirectoryPicker', 'showSaveFilePicker', 'showOpenFilePicker']) {
+ try {
+ Object.defineProperty(window, name, { value: undefined, writable: false, configurable: false });
+ } catch (e) { /* not definable: leave it, native save comes first anyway */ }
+ }
+})();