/** * 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'), } } : {}), // 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 */ } } })();