diff options
Diffstat (limited to 'packages/meshbay-android/app/src/main/assets')
| -rw-r--r-- | packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js | 206 |
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 */ } + } +})(); |