/** * Native Messaging protocol: Chrome/Firefox spec. * - 4-byte little-endian unsigned 32-bit length prefix * - UTF-8 JSON payload * - Max 1 MB message from native host to browser */ const MAX_MESSAGE_LENGTH = 1024 * 1024; // 1 MB /** * Create a messenger that reads length-prefixed JSON from input and writes to output. * @param {object} options * @param {import('stream').Readable} options.input - e.g. process.stdin * @param {import('stream').Writable} options.output - e.g. process.stdout * @param {function(object): void} options.onMessage - called with parsed JSON message * @param {function(Error): void} [options.onError] - called on parse/read errors */ function createMessenger({ input, output, onMessage, onError }) { let buffer = Buffer.alloc(0); let needed = 4; // first read: 4-byte length let readingLength = true; function fail(err) { if (onError) onError(err); else { try { process.stderr.write(String(err && err.stack ? err.stack : err) + '\n'); } catch (_) {} } } function toBuffer(chunk) { if (!chunk) return null; if (Buffer.isBuffer(chunk)) return chunk; if (chunk instanceof Uint8Array) return Buffer.from(chunk); if (typeof chunk === 'string') return Buffer.from(chunk, 'latin1'); return Buffer.from(chunk); } function processChunk(chunk) { const buf = toBuffer(chunk); if (!buf || buf.length === 0) return; buffer = Buffer.concat([buffer, buf]); while (buffer.length >= needed) { const slice = buffer.subarray(0, needed); buffer = buffer.subarray(needed); if (readingLength) { readingLength = false; needed = slice.readUInt32LE(0); if (needed > MAX_MESSAGE_LENGTH) { fail(new Error(`Message length ${needed} exceeds max ${MAX_MESSAGE_LENGTH}`)); return; } if (needed === 0) { readingLength = true; needed = 4; } continue; } try { const msg = JSON.parse(slice.toString('utf8')); onMessage(msg); } catch (e) { fail(e); } readingLength = true; needed = 4; } } input.on('data', processChunk); input.on('error', fail); if (typeof input.resume === 'function') input.resume(); return { /** * Send a message to the browser (length prefix + JSON). * @param {object} msg - JSON-serializable object */ send(msg) { try { const json = JSON.stringify(msg); const body = Buffer.from(json, 'utf8'); const len = Buffer.allocUnsafe(4); len.writeUInt32LE(body.length, 0); output.write(Buffer.concat([len, body])); } catch (e) { fail(e); } }, destroy() { input.removeAllListeners('data'); input.removeAllListeners('error'); }, }; } module.exports = { createMessenger, MAX_MESSAGE_LENGTH };