/** * Does the browser's real `uploadFile` produce frames a real node can open — * and does it read back what that node actually answered? * * Drives **the real `MeshBayTransport` over the real `crypto.js`**. Only the DOM * and the DataChannel are stand-ins; the msgpack encode, the HKDF, the AES-GCM * and the whole upload loop are the shipped code. * * It exists because a source-reading test cannot show either half of MNP 2.0's * upload. `test_transport_contracts` can see that `sealGroup` is called; only * this can show that what comes out opens under `meshbay_common.groupbox` — and * that the caller of `uploadFile` is told the name the *node* chose, which now * arrives sealed and would otherwise be `undefined` with nothing to notice it. * * node upload_seal_probe.mjs * * Two modes, because the node half runs in Python between them: * "send" — run the upload, print every frame it emits, answer nothing * "receive" — run it again, answering with acks Python built. Their * `upload_id` is retargeted to this run's; it is outside the * seal, so the ciphertext stays exactly the one Python produced. */ import fs from 'fs'; const STATIC = process.argv[2]; const input = JSON.parse(fs.readFileSync(process.argv[3], 'utf8')); for (const level of ['log', 'warn', 'error', 'info', 'debug']) { console[level] = (...args) => process.stderr.write(args.join(' ') + '\n'); } globalThis.window = globalThis; globalThis.addEventListener = () => {}; globalThis.removeEventListener = () => {}; globalThis.location = { hash: '' }; globalThis.document = { addEventListener() {}, removeEventListener() {}, visibilityState: 'visible', }; new Function(fs.readFileSync(`${STATIC}/crypto.js`, 'utf8'))(); const transportSrc = fs.readFileSync(`${STATIC}/transport.js`, 'utf8'); new Function(transportSrc)(); // The msgpack codec is private to transport.js — pulled out the same way the // groupbox parity harness pulls out sealGroup, so this probe encodes and // decodes with the codec that is actually shipped rather than a second one. const { msgpack_encode, msgpack_decode } = new Function(transportSrc + '\nreturn { msgpack_encode, msgpack_decode };')(); const hex = (s) => Uint8Array.from(s.match(/../g).map((b) => parseInt(b, 16))); const toHex = (u8) => Array.from(u8).map((b) => b.toString(16).padStart(2, '0')).join(''); // Just enough of a File: a name, a size, and slices that yield ArrayBuffers. const bytes = hex(input.file.data); const file = { name: input.file.name, size: bytes.length, slice(a, b) { const part = bytes.slice(a, b); return { arrayBuffer: async () => part.buffer.slice( part.byteOffset, part.byteOffset + part.byteLength) }; }, }; const tp = new window.MeshBayTransport('', 'token'); tp._connected = true; tp._channel = { readyState: 'open', bufferedAmount: 0, send() {}, close() {} }; tp._pc = { close() {} }; tp._gekRaw = hex(input.gek); tp._connectArgs = { groupId: input.group_id }; const frames = []; let uploadId = null; let answered = 0; tp._send = (msg) => { frames.push(toHex(msgpack_encode(msg))); if (msg.upload_id) uploadId = msg.upload_id; if (input.mode !== 'receive') { // Nothing answers in this mode -- except the probe, which the client waits // five seconds for. A node that predates it refuses the index, and that // refusal is a plain error rather than a sealed ack, so the harness can // produce it honestly. It is also the degradation path worth exercising. if (msg.chunk_index === -1) { // With `probe_ack`, answer it the way a node holding part of this file // does; without, the way one that predates the probe does. const reply = input.probe_ack ? Object.assign(msgpack_decode(hex(input.probe_ack)), { upload_id: uploadId }) : { type: 'error', upload_id: uploadId, code: 'bad_chunk_index', detail: 'Unexpected chunk index' }; setImmediate(() => tp._dispatch(reply)); } return; } // Answer as the node did, on the next turn of the loop so the send path // finishes first — which is also how a real ack arrives. // // By position, not by `chunk_index`: the node answers every frame including // the probe, whose index is -1, and the two lists are built from the same // sequence of frames. const ack = msgpack_decode(hex(input.acks[answered++])); ack.upload_id = uploadId; // Through the real `_dispatch`, so the routing under test — matching an // ack to its uploader by `upload_id` — is the shipped one. setImmediate(() => tp._dispatch(ack)); }; const out = { frames, mode: input.mode }; const done = tp.uploadFile(file, { chunkSize: input.chunk_size, dir: input.dir, root: input.root }); if (input.mode === 'receive') { done.then((stored) => { out.state = 'resolved'; out.stored = stored; }) .catch((e) => { out.state = 'rejected'; out.message = e.message; }) .finally(() => { out.upload_id = uploadId; process.stdout.write(JSON.stringify(out)); }); } else { // Nothing will answer, so let the send loop run itself out and report. done.catch((e) => { out.state = 'rejected'; out.message = e.message; }); setTimeout(() => { out.upload_id = uploadId; process.stdout.write(JSON.stringify(out)); }, 250); }