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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
|
/**
* 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` and `BINARY` are prepended by the shell when it injects this
* file: the interface asks for the hub while its modules load, before anything
* can await, and BINARY says whether the WebView carries ArrayBuffer messages.
*/
(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 bytesOf = (chunk) => (chunk instanceof Uint8Array ? chunk
: ArrayBuffer.isView(chunk) ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)
: new Uint8Array(chunk));
const writeChunk = (handle, chunk) => {
const bytes = bytesOf(chunk);
if (!BINARY) {
let s = '';
for (let i = 0; i < bytes.length; i += 0x8000) s += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
return call('save:write', handle, btoa(s));
}
return 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, SAVE_WRITE);
head.setUint32(12, handle);
new Uint8Array(frame, 16).set(bytes);
port.postMessage(frame);
});
};
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: false, // phase 3
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'),
},
// 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 */ }
}
})();
|