UNPKG

acq-sdk

Version:
1,738 lines (1,605 loc) 252 kB
'use strict'; var axios = require('axios'); var require$$0 = require('fs'); var require$$1 = require('url'); var require$$2 = require('child_process'); var require$$3 = require('http'); var require$$4 = require('https'); var require$$0$1 = require('stream'); var require$$0$2 = require('zlib'); var require$$0$3 = require('buffer'); var require$$1$1 = require('crypto'); var require$$0$4 = require('events'); var require$$3$1 = require('net'); var require$$4$1 = require('tls'); function _mergeNamespaces(n, m) { m.forEach(function (e) { e && typeof e !== 'string' && !Array.isArray(e) && Object.keys(e).forEach(function (k) { if (k !== 'default' && !(k in n)) { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); }); return Object.freeze(n); } /** * Erro customizado para a API ACQ */ class AcqApiError extends Error { constructor(error, status) { super(error.message); this.name = "AcqApiError"; this.code = error.error; this.details = error.details; this.status = status; } } /** * Erro de validação de entrada */ class AcqValidationError extends Error { constructor(message) { super(message); this.name = "AcqValidationError"; } } /** * Erro de configuração */ class AcqConfigError extends Error { constructor(message) { super(message); this.name = "AcqConfigError"; } } /** * Cliente HTTP base para comunicação com a API */ class HttpClient { constructor(config) { if (!config.apiKey) { throw new AcqConfigError("API key é obrigatória"); } this.client = axios.create({ baseURL: config.baseUrl || "https://api.acq.lat", timeout: config.timeout || 30000, headers: { Authorization: `Bearer ${config.apiKey}`, "Content-Type": "application/json", "User-Agent": "acq-sdk/1.2.0", }, }); // Interceptor para tratamento de erros this.client.interceptors.response.use(response => response, error => { if (error.response?.data) { const apiError = error.response.data; throw new AcqApiError(apiError, error.response.status); } throw error; }); } /** * Realiza uma requisição GET */ async get(url, params) { const response = await this.client.get(url, { params }); return response.data; } /** * Realiza uma requisição POST */ async post(url, data) { const response = await this.client.post(url, data); return response.data; } /** * Realiza uma requisição POST que retorna um buffer (para imagens) */ async postBuffer(url, data) { const response = await this.client.post(url, data, { responseType: "arraybuffer", }); return Buffer.from(response.data); } /** * Realiza uma requisição DELETE */ async delete(url, params) { const response = await this.client.delete(url, { params, }); return response.data; } } /** * Implementação do serviço de renderização */ class RenderServiceImpl { constructor(httpClient) { this.httpClient = httpClient; } async htmlToImage(options) { if (!options.html || options.html.trim().length === 0) { throw new AcqValidationError("HTML é obrigatório e não pode estar vazio"); } return this.httpClient.postBuffer("/render", { html: options.html, }); } } /** * Implementação do serviço de emails */ class MailsServiceImpl { constructor(httpClient) { this.httpClient = httpClient; } async list() { return this.httpClient.get("/mails"); } async create(options) { const data = options?.domain ? { domain: options.domain } : undefined; return this.httpClient.post("/mail", data); } async delete(email) { if (!email || !this.isValidEmail(email)) { throw new AcqValidationError("Email inválido"); } return this.httpClient.delete("/mail", { mail: email, }); } async getMessages(options) { if (!options.mail || !this.isValidEmail(options.mail)) { throw new AcqValidationError("Email inválido"); } return this.httpClient.post("/mailbox", { mail: options.mail, }); } async deleteMessages(email) { if (!email || !this.isValidEmail(email)) { throw new AcqValidationError("Email inválido"); } return this.httpClient.delete("/mailbox", { mail: email, }); } isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } } /** * Cliente principal do SDK ACQ */ class AcqClient { /** * Cria uma nova instância do cliente ACQ * @param config Configuração do cliente */ constructor(config) { this.httpClient = new HttpClient(config); this.render = new RenderServiceImpl(this.httpClient); this.mails = new MailsServiceImpl(this.httpClient); } } function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } /** * Wrapper for built-in http.js to emulate the browser XMLHttpRequest object. * * This can be used with JS designed for browsers to improve reuse of code and * allow the use of existing libraries. * * Usage: include("XMLHttpRequest.js") and use XMLHttpRequest per W3C specs. * * @author Dan DeFelippi <dan@driverdan.com> * @contributor David Ellis <d.f.ellis@ieee.org> * @license MIT */ var fs = require$$0; var Url = require$$1; var spawn = require$$2.spawn; /** * Module exports. */ var XMLHttpRequest_1 = XMLHttpRequest$2; // backwards-compat XMLHttpRequest$2.XMLHttpRequest = XMLHttpRequest$2; /** * `XMLHttpRequest` constructor. * * Supported options for the `opts` object are: * * - `agent`: An http.Agent instance; http.globalAgent may be used; if 'undefined', agent usage is disabled * * @param {Object} opts optional "options" object */ function XMLHttpRequest$2(opts) { opts = opts || {}; /** * Private variables */ var self = this; var http = require$$3; var https = require$$4; // Holds http.js objects var request; var response; // Request settings var settings = {}; // Disable header blacklist. // Not part of XHR specs. var disableHeaderCheck = false; // Set some default headers var defaultHeaders = { "User-Agent": "node-XMLHttpRequest", "Accept": "*/*" }; var headers = Object.assign({}, defaultHeaders); // These headers are not user setable. // The following are allowed but banned in the spec: // * user-agent var forbiddenRequestHeaders = [ "accept-charset", "accept-encoding", "access-control-request-headers", "access-control-request-method", "connection", "content-length", "content-transfer-encoding", "cookie", "cookie2", "date", "expect", "host", "keep-alive", "origin", "referer", "te", "trailer", "transfer-encoding", "upgrade", "via" ]; // These request methods are not allowed var forbiddenRequestMethods = [ "TRACE", "TRACK", "CONNECT" ]; // Send flag var sendFlag = false; // Error flag, used when errors occur or abort is called var errorFlag = false; var abortedFlag = false; // Event listeners var listeners = {}; /** * Constants */ this.UNSENT = 0; this.OPENED = 1; this.HEADERS_RECEIVED = 2; this.LOADING = 3; this.DONE = 4; /** * Public vars */ // Current state this.readyState = this.UNSENT; // default ready state change handler in case one is not set or is set late this.onreadystatechange = null; // Result & response this.responseText = ""; this.responseXML = ""; this.response = Buffer.alloc(0); this.status = null; this.statusText = null; /** * Private methods */ /** * Check if the specified header is allowed. * * @param string header Header to validate * @return boolean False if not allowed, otherwise true */ var isAllowedHttpHeader = function(header) { return disableHeaderCheck || (header && forbiddenRequestHeaders.indexOf(header.toLowerCase()) === -1); }; /** * Check if the specified method is allowed. * * @param string method Request method to validate * @return boolean False if not allowed, otherwise true */ var isAllowedHttpMethod = function(method) { return (method && forbiddenRequestMethods.indexOf(method) === -1); }; /** * Public methods */ /** * Open the connection. Currently supports local server requests. * * @param string method Connection method (eg GET, POST) * @param string url URL for the connection. * @param boolean async Asynchronous connection. Default is true. * @param string user Username for basic authentication (optional) * @param string password Password for basic authentication (optional) */ this.open = function(method, url, async, user, password) { this.abort(); errorFlag = false; abortedFlag = false; // Check for valid request method if (!isAllowedHttpMethod(method)) { throw new Error("SecurityError: Request method not allowed"); } settings = { "method": method, "url": url.toString(), "async": (typeof async !== "boolean" ? true : async), "user": user || null, "password": password || null }; setState(this.OPENED); }; /** * Disables or enables isAllowedHttpHeader() check the request. Enabled by default. * This does not conform to the W3C spec. * * @param boolean state Enable or disable header checking. */ this.setDisableHeaderCheck = function(state) { disableHeaderCheck = state; }; /** * Sets a header for the request. * * @param string header Header name * @param string value Header value * @return boolean Header added */ this.setRequestHeader = function(header, value) { if (this.readyState != this.OPENED) { throw new Error("INVALID_STATE_ERR: setRequestHeader can only be called when state is OPEN"); } if (!isAllowedHttpHeader(header)) { console.warn('Refused to set unsafe header "' + header + '"'); return false; } if (sendFlag) { throw new Error("INVALID_STATE_ERR: send flag is true"); } headers[header] = value; return true; }; /** * Gets a header from the server response. * * @param string header Name of header to get. * @return string Text of the header or null if it doesn't exist. */ this.getResponseHeader = function(header) { if (typeof header === "string" && this.readyState > this.OPENED && response.headers[header.toLowerCase()] && !errorFlag ) { return response.headers[header.toLowerCase()]; } return null; }; /** * Gets all the response headers. * * @return string A string with all response headers separated by CR+LF */ this.getAllResponseHeaders = function() { if (this.readyState < this.HEADERS_RECEIVED || errorFlag) { return ""; } var result = ""; for (var i in response.headers) { // Cookie headers are excluded if (i !== "set-cookie" && i !== "set-cookie2") { result += i + ": " + response.headers[i] + "\r\n"; } } return result.substr(0, result.length - 2); }; /** * Gets a request header * * @param string name Name of header to get * @return string Returns the request header or empty string if not set */ this.getRequestHeader = function(name) { // @TODO Make this case insensitive if (typeof name === "string" && headers[name]) { return headers[name]; } return ""; }; /** * Sends the request to the server. * * @param string data Optional data to send as request body. */ this.send = function(data) { if (this.readyState != this.OPENED) { throw new Error("INVALID_STATE_ERR: connection must be opened before send() is called"); } if (sendFlag) { throw new Error("INVALID_STATE_ERR: send has already been called"); } var ssl = false, local = false; var url = Url.parse(settings.url); var host; // Determine the server switch (url.protocol) { case 'https:': ssl = true; // SSL & non-SSL both need host, no break here. case 'http:': host = url.hostname; break; case 'file:': local = true; break; case undefined: case '': host = "localhost"; break; default: throw new Error("Protocol not supported."); } // Load files off the local filesystem (file://) if (local) { if (settings.method !== "GET") { throw new Error("XMLHttpRequest: Only GET method is supported"); } if (settings.async) { fs.readFile(unescape(url.pathname), function(error, data) { if (error) { self.handleError(error, error.errno || -1); } else { self.status = 200; self.responseText = data.toString('utf8'); self.response = data; setState(self.DONE); } }); } else { try { this.response = fs.readFileSync(unescape(url.pathname)); this.responseText = this.response.toString('utf8'); this.status = 200; setState(self.DONE); } catch(e) { this.handleError(e, e.errno || -1); } } return; } // Default to port 80. If accessing localhost on another port be sure // to use http://localhost:port/path var port = url.port || (ssl ? 443 : 80); // Add query string if one is used var uri = url.pathname + (url.search ? url.search : ''); // Set the Host header or the server may reject the request headers["Host"] = host; if (!((ssl && port === 443) || port === 80)) { headers["Host"] += ':' + url.port; } // Set Basic Auth if necessary if (settings.user) { if (typeof settings.password == "undefined") { settings.password = ""; } var authBuf = new Buffer(settings.user + ":" + settings.password); headers["Authorization"] = "Basic " + authBuf.toString("base64"); } // Set content length header if (settings.method === "GET" || settings.method === "HEAD") { data = null; } else if (data) { headers["Content-Length"] = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data); var headersKeys = Object.keys(headers); if (!headersKeys.some(function (h) { return h.toLowerCase() === 'content-type' })) { headers["Content-Type"] = "text/plain;charset=UTF-8"; } } else if (settings.method === "POST") { // For a post with no data set Content-Length: 0. // This is required by buggy servers that don't meet the specs. headers["Content-Length"] = 0; } var agent = opts.agent || false; var options = { host: host, port: port, path: uri, method: settings.method, headers: headers, agent: agent }; if (ssl) { options.pfx = opts.pfx; options.key = opts.key; options.passphrase = opts.passphrase; options.cert = opts.cert; options.ca = opts.ca; options.ciphers = opts.ciphers; options.rejectUnauthorized = opts.rejectUnauthorized === false ? false : true; } // Reset error flag errorFlag = false; // Handle async requests if (settings.async) { // Use the proper protocol var doRequest = ssl ? https.request : http.request; // Request is being sent, set send flag sendFlag = true; // As per spec, this is called here for historical reasons. self.dispatchEvent("readystatechange"); // Handler for the response var responseHandler = function(resp) { // Set response var to the response we got back // This is so it remains accessable outside this scope response = resp; // Check for redirect // @TODO Prevent looped redirects if (response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) { // Change URL to the redirect location settings.url = response.headers.location; var url = Url.parse(settings.url); // Set host var in case it's used later host = url.hostname; // Options for the new request var newOptions = { hostname: url.hostname, port: url.port, path: url.path, method: response.statusCode === 303 ? 'GET' : settings.method, headers: headers }; if (ssl) { newOptions.pfx = opts.pfx; newOptions.key = opts.key; newOptions.passphrase = opts.passphrase; newOptions.cert = opts.cert; newOptions.ca = opts.ca; newOptions.ciphers = opts.ciphers; newOptions.rejectUnauthorized = opts.rejectUnauthorized === false ? false : true; } // Issue the new request request = doRequest(newOptions, responseHandler).on('error', errorHandler); request.end(); // @TODO Check if an XHR event needs to be fired here return; } setState(self.HEADERS_RECEIVED); self.status = response.statusCode; response.on('data', function(chunk) { // Make sure there's some data if (chunk) { var data = Buffer.from(chunk); self.response = Buffer.concat([self.response, data]); } // Don't emit state changes if the connection has been aborted. if (sendFlag) { setState(self.LOADING); } }); response.on('end', function() { if (sendFlag) { // The sendFlag needs to be set before setState is called. Otherwise if we are chaining callbacks // there can be a timing issue (the callback is called and a new call is made before the flag is reset). sendFlag = false; // Discard the 'end' event if the connection has been aborted setState(self.DONE); // Construct responseText from response self.responseText = self.response.toString('utf8'); } }); response.on('error', function(error) { self.handleError(error); }); }; // Error handler for the request var errorHandler = function(error) { // In the case of https://nodejs.org/api/http.html#requestreusedsocket triggering an ECONNRESET, // don't fail the xhr request, attempt again. if (request.reusedSocket && error.code === 'ECONNRESET') return doRequest(options, responseHandler).on('error', errorHandler); self.handleError(error); }; // Create the request request = doRequest(options, responseHandler).on('error', errorHandler); if (opts.autoUnref) { request.on('socket', (socket) => { socket.unref(); }); } // Node 0.4 and later won't accept empty data. Make sure it's needed. if (data) { request.write(data); } request.end(); self.dispatchEvent("loadstart"); } else { // Synchronous // Create a temporary file for communication with the other Node process var contentFile = ".node-xmlhttprequest-content-" + process.pid; var syncFile = ".node-xmlhttprequest-sync-" + process.pid; fs.writeFileSync(syncFile, "", "utf8"); // The async request the other Node process executes var execString = "var http = require('http'), https = require('https'), fs = require('fs');" + "var doRequest = http" + (ssl ? "s" : "") + ".request;" + "var options = " + JSON.stringify(options) + ";" + "var responseText = '';" + "var responseData = Buffer.alloc(0);" + "var req = doRequest(options, function(response) {" + "response.on('data', function(chunk) {" + " var data = Buffer.from(chunk);" + " responseText += data.toString('utf8');" + " responseData = Buffer.concat([responseData, data]);" + "});" + "response.on('end', function() {" + "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText, data: responseData.toString('base64')}}), 'utf8');" + "fs.unlinkSync('" + syncFile + "');" + "});" + "response.on('error', function(error) {" + "fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');" + "fs.unlinkSync('" + syncFile + "');" + "});" + "}).on('error', function(error) {" + "fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');" + "fs.unlinkSync('" + syncFile + "');" + "});" + (data ? "req.write('" + JSON.stringify(data).slice(1,-1).replace(/'/g, "\\'") + "');":"") + "req.end();"; // Start the other Node Process, executing this string var syncProc = spawn(process.argv[0], ["-e", execString]); while(fs.existsSync(syncFile)) { // Wait while the sync file is empty } self.responseText = fs.readFileSync(contentFile, 'utf8'); // Kill the child process once the file has data syncProc.stdin.end(); // Remove the temporary file fs.unlinkSync(contentFile); if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR:/)) { // If the file returned an error, handle it var errorObj = JSON.parse(self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, "")); self.handleError(errorObj, 503); } else { // If the file returned okay, parse its data and move to the DONE state self.status = self.responseText.replace(/^NODE-XMLHTTPREQUEST-STATUS:([0-9]*),.*/, "$1"); var resp = JSON.parse(self.responseText.replace(/^NODE-XMLHTTPREQUEST-STATUS:[0-9]*,(.*)/, "$1")); response = { statusCode: self.status, headers: resp.data.headers }; self.responseText = resp.data.text; self.response = Buffer.from(resp.data.data, 'base64'); setState(self.DONE); } } }; /** * Called when an error is encountered to deal with it. * @param status {number} HTTP status code to use rather than the default (0) for XHR errors. */ this.handleError = function(error, status) { this.status = status || 0; this.statusText = error; this.responseText = error.stack; errorFlag = true; setState(this.DONE); }; /** * Aborts a request. */ this.abort = function() { if (request) { request.abort(); request = null; } headers = Object.assign({}, defaultHeaders); this.responseText = ""; this.responseXML = ""; this.response = Buffer.alloc(0); errorFlag = abortedFlag = true; if (this.readyState !== this.UNSENT && (this.readyState !== this.OPENED || sendFlag) && this.readyState !== this.DONE) { sendFlag = false; setState(this.DONE); } this.readyState = this.UNSENT; }; /** * Adds an event listener. Preferred method of binding to events. */ this.addEventListener = function(event, callback) { if (!(event in listeners)) { listeners[event] = []; } // Currently allows duplicate callbacks. Should it? listeners[event].push(callback); }; /** * Remove an event callback that has already been bound. * Only works on the matching funciton, cannot be a copy. */ this.removeEventListener = function(event, callback) { if (event in listeners) { // Filter will return a new array with the callback removed listeners[event] = listeners[event].filter(function(ev) { return ev !== callback; }); } }; /** * Dispatch any events, including both "on" methods and events attached using addEventListener. */ this.dispatchEvent = function (event) { if (typeof self["on" + event] === "function") { if (this.readyState === this.DONE && settings.async) setTimeout(function() { self["on" + event](); }, 0); else self["on" + event](); } if (event in listeners) { for (let i = 0, len = listeners[event].length; i < len; i++) { if (this.readyState === this.DONE) setTimeout(function() { listeners[event][i].call(self); }, 0); else listeners[event][i].call(self); } } }; /** * Changes readyState and calls onreadystatechange. * * @param int state New state */ var setState = function(state) { if ((self.readyState === state) || (self.readyState === self.UNSENT && abortedFlag)) return self.readyState = state; if (settings.async || self.readyState < self.OPENED || self.readyState === self.DONE) { self.dispatchEvent("readystatechange"); } if (self.readyState === self.DONE) { let fire; if (abortedFlag) fire = "abort"; else if (errorFlag) fire = "error"; else fire = "load"; self.dispatchEvent(fire); // @TODO figure out InspectorInstrumentation::didLoadXHR(cookie) self.dispatchEvent("loadend"); } }; } var XMLHttpRequest$3 = /*@__PURE__*/getDefaultExportFromCjs(XMLHttpRequest_1); var XMLHttpRequestModule = /*#__PURE__*/_mergeNamespaces({ __proto__: null, default: XMLHttpRequest$3 }, [XMLHttpRequest_1]); const PACKET_TYPES = Object.create(null); // no Map = no polyfill PACKET_TYPES["open"] = "0"; PACKET_TYPES["close"] = "1"; PACKET_TYPES["ping"] = "2"; PACKET_TYPES["pong"] = "3"; PACKET_TYPES["message"] = "4"; PACKET_TYPES["upgrade"] = "5"; PACKET_TYPES["noop"] = "6"; const PACKET_TYPES_REVERSE = Object.create(null); Object.keys(PACKET_TYPES).forEach((key) => { PACKET_TYPES_REVERSE[PACKET_TYPES[key]] = key; }); const ERROR_PACKET = { type: "error", data: "parser error" }; const encodePacket = ({ type, data }, supportsBinary, callback) => { if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) { return callback(supportsBinary ? data : "b" + toBuffer$3(data, true).toString("base64")); } // plain string return callback(PACKET_TYPES[type] + (data || "")); }; const toBuffer$3 = (data, forceBufferConversion) => { if (Buffer.isBuffer(data) || (data instanceof Uint8Array && !forceBufferConversion)) { return data; } else if (data instanceof ArrayBuffer) { return Buffer.from(data); } else { return Buffer.from(data.buffer, data.byteOffset, data.byteLength); } }; let TEXT_ENCODER; function encodePacketToBinary(packet, callback) { if (packet.data instanceof ArrayBuffer || ArrayBuffer.isView(packet.data)) { return callback(toBuffer$3(packet.data, false)); } encodePacket(packet, true, (encoded) => { if (!TEXT_ENCODER) { // lazily created for compatibility with Node.js 10 TEXT_ENCODER = new TextEncoder(); } callback(TEXT_ENCODER.encode(encoded)); }); } const decodePacket = (encodedPacket, binaryType) => { if (typeof encodedPacket !== "string") { return { type: "message", data: mapBinary(encodedPacket, binaryType), }; } const type = encodedPacket.charAt(0); if (type === "b") { const buffer = Buffer.from(encodedPacket.substring(1), "base64"); return { type: "message", data: mapBinary(buffer, binaryType), }; } if (!PACKET_TYPES_REVERSE[type]) { return ERROR_PACKET; } return encodedPacket.length > 1 ? { type: PACKET_TYPES_REVERSE[type], data: encodedPacket.substring(1), } : { type: PACKET_TYPES_REVERSE[type], }; }; const mapBinary = (data, binaryType) => { switch (binaryType) { case "arraybuffer": if (data instanceof ArrayBuffer) { // from WebSocket & binaryType "arraybuffer" return data; } else if (Buffer.isBuffer(data)) { // from HTTP long-polling return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength); } else { // from WebTransport (Uint8Array) return data.buffer; } case "nodebuffer": default: if (Buffer.isBuffer(data)) { // from HTTP long-polling or WebSocket & binaryType "nodebuffer" (default) return data; } else { // from WebTransport (Uint8Array) return Buffer.from(data); } } }; const SEPARATOR = String.fromCharCode(30); // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text const encodePayload = (packets, callback) => { // some packets may be added to the array while encoding, so the initial length must be saved const length = packets.length; const encodedPackets = new Array(length); let count = 0; packets.forEach((packet, i) => { // force base64 encoding for binary packets encodePacket(packet, false, (encodedPacket) => { encodedPackets[i] = encodedPacket; if (++count === length) { callback(encodedPackets.join(SEPARATOR)); } }); }); }; const decodePayload = (encodedPayload, binaryType) => { const encodedPackets = encodedPayload.split(SEPARATOR); const packets = []; for (let i = 0; i < encodedPackets.length; i++) { const decodedPacket = decodePacket(encodedPackets[i], binaryType); packets.push(decodedPacket); if (decodedPacket.type === "error") { break; } } return packets; }; function createPacketEncoderStream() { return new TransformStream({ transform(packet, controller) { encodePacketToBinary(packet, (encodedPacket) => { const payloadLength = encodedPacket.length; let header; // inspired by the WebSocket format: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#decoding_payload_length if (payloadLength < 126) { header = new Uint8Array(1); new DataView(header.buffer).setUint8(0, payloadLength); } else if (payloadLength < 65536) { header = new Uint8Array(3); const view = new DataView(header.buffer); view.setUint8(0, 126); view.setUint16(1, payloadLength); } else { header = new Uint8Array(9); const view = new DataView(header.buffer); view.setUint8(0, 127); view.setBigUint64(1, BigInt(payloadLength)); } // first bit indicates whether the payload is plain text (0) or binary (1) if (packet.data && typeof packet.data !== "string") { header[0] |= 0x80; } controller.enqueue(header); controller.enqueue(encodedPacket); }); }, }); } let TEXT_DECODER; function totalLength(chunks) { return chunks.reduce((acc, chunk) => acc + chunk.length, 0); } function concatChunks(chunks, size) { if (chunks[0].length === size) { return chunks.shift(); } const buffer = new Uint8Array(size); let j = 0; for (let i = 0; i < size; i++) { buffer[i] = chunks[0][j++]; if (j === chunks[0].length) { chunks.shift(); j = 0; } } if (chunks.length && j < chunks[0].length) { chunks[0] = chunks[0].slice(j); } return buffer; } function createPacketDecoderStream(maxPayload, binaryType) { if (!TEXT_DECODER) { TEXT_DECODER = new TextDecoder(); } const chunks = []; let state = 0 /* State.READ_HEADER */; let expectedLength = -1; let isBinary = false; return new TransformStream({ transform(chunk, controller) { chunks.push(chunk); while (true) { if (state === 0 /* State.READ_HEADER */) { if (totalLength(chunks) < 1) { break; } const header = concatChunks(chunks, 1); isBinary = (header[0] & 0x80) === 0x80; expectedLength = header[0] & 0x7f; if (expectedLength < 126) { state = 3 /* State.READ_PAYLOAD */; } else if (expectedLength === 126) { state = 1 /* State.READ_EXTENDED_LENGTH_16 */; } else { state = 2 /* State.READ_EXTENDED_LENGTH_64 */; } } else if (state === 1 /* State.READ_EXTENDED_LENGTH_16 */) { if (totalLength(chunks) < 2) { break; } const headerArray = concatChunks(chunks, 2); expectedLength = new DataView(headerArray.buffer, headerArray.byteOffset, headerArray.length).getUint16(0); state = 3 /* State.READ_PAYLOAD */; } else if (state === 2 /* State.READ_EXTENDED_LENGTH_64 */) { if (totalLength(chunks) < 8) { break; } const headerArray = concatChunks(chunks, 8); const view = new DataView(headerArray.buffer, headerArray.byteOffset, headerArray.length); const n = view.getUint32(0); if (n > Math.pow(2, 53 - 32) - 1) { // the maximum safe integer in JavaScript is 2^53 - 1 controller.enqueue(ERROR_PACKET); break; } expectedLength = n * Math.pow(2, 32) + view.getUint32(4); state = 3 /* State.READ_PAYLOAD */; } else { if (totalLength(chunks) < expectedLength) { break; } const data = concatChunks(chunks, expectedLength); controller.enqueue(decodePacket(isBinary ? data : TEXT_DECODER.decode(data), binaryType)); state = 0 /* State.READ_HEADER */; } if (expectedLength === 0 || expectedLength > maxPayload) { controller.enqueue(ERROR_PACKET); break; } } }, }); } const protocol$1 = 4; /** * Initialize a new `Emitter`. * * @api public */ function Emitter(obj) { if (obj) return mixin(obj); } /** * Mixin the emitter properties. * * @param {Object} obj * @return {Object} * @api private */ function mixin(obj) { for (var key in Emitter.prototype) { obj[key] = Emitter.prototype[key]; } return obj; } /** * Listen on the given `event` with `fn`. * * @param {String} event * @param {Function} fn * @return {Emitter} * @api public */ Emitter.prototype.on = Emitter.prototype.addEventListener = function(event, fn){ this._callbacks = this._callbacks || {}; (this._callbacks['$' + event] = this._callbacks['$' + event] || []) .push(fn); return this; }; /** * Adds an `event` listener that will be invoked a single * time then automatically removed. * * @param {String} event * @param {Function} fn * @return {Emitter} * @api public */ Emitter.prototype.once = function(event, fn){ function on() { this.off(event, on); fn.apply(this, arguments); } on.fn = fn; this.on(event, on); return this; }; /** * Remove the given callback for `event` or all * registered callbacks. * * @param {String} event * @param {Function} fn * @return {Emitter} * @api public */ Emitter.prototype.off = Emitter.prototype.removeListener = Emitter.prototype.removeAllListeners = Emitter.prototype.removeEventListener = function(event, fn){ this._callbacks = this._callbacks || {}; // all if (0 == arguments.length) { this._callbacks = {}; return this; } // specific event var callbacks = this._callbacks['$' + event]; if (!callbacks) return this; // remove all handlers if (1 == arguments.length) { delete this._callbacks['$' + event]; return this; } // remove specific handler var cb; for (var i = 0; i < callbacks.length; i++) { cb = callbacks[i]; if (cb === fn || cb.fn === fn) { callbacks.splice(i, 1); break; } } // Remove event specific arrays for event types that no // one is subscribed for to avoid memory leak. if (callbacks.length === 0) { delete this._callbacks['$' + event]; } return this; }; /** * Emit `event` with the given args. * * @param {String} event * @param {Mixed} ... * @return {Emitter} */ Emitter.prototype.emit = function(event){ this._callbacks = this._callbacks || {}; var args = new Array(arguments.length - 1) , callbacks = this._callbacks['$' + event]; for (var i = 1; i < arguments.length; i++) { args[i - 1] = arguments[i]; } if (callbacks) { callbacks = callbacks.slice(0); for (var i = 0, len = callbacks.length; i < len; ++i) { callbacks[i].apply(this, args); } } return this; }; // alias used for reserved events (protected method) Emitter.prototype.emitReserved = Emitter.prototype.emit; /** * Return array of callbacks for `event`. * * @param {String} event * @return {Array} * @api public */ Emitter.prototype.listeners = function(event){ this._callbacks = this._callbacks || {}; return this._callbacks['$' + event] || []; }; /** * Check if this emitter has `event` handlers. * * @param {String} event * @return {Boolean} * @api public */ Emitter.prototype.hasListeners = function(event){ return !! this.listeners(event).length; }; const nextTick = process.nextTick; const globalThisShim = global; const defaultBinaryType = "nodebuffer"; function createCookieJar() { return new CookieJar(); } /** * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie */ function parse$3(setCookieString) { const parts = setCookieString.split("; "); const i = parts[0].indexOf("="); if (i === -1) { return; } const name = parts[0].substring(0, i).trim(); if (!name.length) { return; } let value = parts[0].substring(i + 1).trim(); if (value.charCodeAt(0) === 0x22) { // remove double quotes value = value.slice(1, -1); } const cookie = { name, value, }; for (let j = 1; j < parts.length; j++) { const subParts = parts[j].split("="); if (subParts.length !== 2) { continue; } const key = subParts[0].trim(); const value = subParts[1].trim(); switch (key) { case "Expires": cookie.expires = new Date(value); break; case "Max-Age": const expiration = new Date(); expiration.setUTCSeconds(expiration.getUTCSeconds() + parseInt(value, 10)); cookie.expires = expiration; break; // ignore other keys } } return cookie; } class CookieJar { constructor() { this._cookies = new Map(); } parseCookies(values) { if (!values) { return; } values.forEach((value) => { const parsed = parse$3(value); if (parsed) { this._cookies.set(parsed.name, parsed); } }); } get cookies() { const now = Date.now(); this._cookies.forEach((cookie, name) => { var _a; if (((_a = cookie.expires) === null || _a === void 0 ? void 0 : _a.getTime()) < now) { this._cookies.delete(name); } }); return this._cookies.entries(); } addCookies(xhr) { const cookies = []; for (const [name, cookie] of this.cookies) { cookies.push(`${name}=${cookie.value}`); } if (cookies.length) { xhr.setDisableHeaderCheck(true); xhr.setRequestHeader("cookie", cookies.join("; ")); } } appendCookies(headers) { for (const [name, cookie] of this.cookies) { headers.append("cookie", `${name}=${cookie.value}`); } } } function pick(obj, ...attr) { return attr.reduce((acc, k) => { if (obj.hasOwnProperty(k)) { acc[k] = obj[k]; } return acc; }, {}); } // Keep a reference to the real timeout functions so they can be used when overridden const NATIVE_SET_TIMEOUT = globalThisShim.setTimeout; const NATIVE_CLEAR_TIMEOUT = globalThisShim.clearTimeout; function installTimerFunctions(obj, opts) { if (opts.useNativeTimers) { obj.setTimeoutFn = NATIVE_SET_TIMEOUT.bind(globalThisShim); obj.clearTimeoutFn = NATIVE_CLEAR_TIMEOUT.bind(globalThisShim); } else { obj.setTimeoutFn = globalThisShim.setTimeout.bind(globalThisShim); obj.clearTimeoutFn = globalThisShim.clearTimeout.bind(globalThisShim); } } // base64 encoded buffers are about 33% bigger (https://en.wikipedia.org/wiki/Base64) const BASE64_OVERHEAD = 1.33; // we could also have used `new Blob([obj]).size`, but it isn't supported in IE9 function byteLength(obj) { if (typeof obj === "string") { return utf8Length(obj); } // arraybuffer or blob return Math.ceil((obj.byteLength || obj.size) * BASE64_OVERHEAD); } function utf8Length(str) { let c = 0, length = 0; for (let i = 0, l = str.length; i < l; i++) { c = str.charCodeAt(i); if (c < 0x80) { length += 1; } else if (c < 0x800) { length += 2; } else if (c < 0xd800 || c >= 0xe000) { length += 3; } else { i++; length += 4; } } return length; } /** * Generates a random 8-characters string. */ function randomString() { return (Date.now().toString(36).substring(3) + Math.random().toString(36).substring(2, 5)); } // imported from https://github.com/galkn/querystring /** * Compiles a querystring * Returns string representation of the object * * @param {Object} * @api private */ function encode(obj) { let str = ''; for (let i in obj) { if (obj.hasOwnProperty(i)) { if (str.length) str += '&'; str += encodeURIComponent(i) + '=' + encodeURIComponent(obj[i]); } } return str; } /** * Parses a simple querystring into an object * * @param {String} qs * @api private */ function decode(qs) { let qry = {}; let pairs = qs.split('&'); for (let i = 0, l = pairs.length; i < l; i++) { let pair = pairs[i].split('='); qry[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); } return qry; } class TransportError extends Error { constructor(reason, description, context) { super(reason); this.description = description; this.context = context; this.type = "TransportError"; } } class Transport extends Emitter { /** * Transport abstract constructor. * * @param {Object} opts - options * @protected */ constructor(opts) { super(); this.writable = false; installTimerFunctions(this, opts); this.opts = opts; this.query = opts.query; this.socket = opts.socket; this.supportsBinary = !opts.forceBase64; } /** * Emits an error. * * @param {String} reason * @param description * @param context - the error context * @return {Transport} for chaining * @protected */ onError(reason, description, context) { super.emitReserved("error", new TransportError(reason, description, context)); return this; } /** * Opens the transport. */ open() { this.readyState = "opening"; this.doOpen(); return this; } /** * Closes the transport. */ close() { if (this.readyState === "opening" || this.readyState === "open") { this.doClose(); this.onClose(); } return this; } /** * Sends multiple packets. * * @param {Array} packets */ send(packets) { if (this.readyState === "open") { this.write(packets); } } /** * Called upon open * * @protected */ onOpen() { this.readyState = "open"; this.writable = true; super.emitReserved("open"); } /** * Called with data. * * @param {String} data * @protected */ onData(data) { const packet = decodePacket(data, this.socket.binaryType); this.onPacket(packet); } /** * Called with a decoded packet. * * @protected */ onPacket(packet) { super.emitReserved("packet", packet); } /** * Called upon close. * * @protected */ onClose(details) { this.readyState = "closed"; super.emitReserved("close", details); } /** * Pauses the transport, in order not to lose packets during an upgrade. * * @param onPause */ pause(onPause) { } createUri(schema, query = {}) { return (schema + "://" + this._hostname() + this._port() + this.opts.path + this._query(query)); } _hostname() { const hostname = this.opts.hostname; return hostname.indexOf(":") === -1 ? hostname : "[" + hostname + "]"; } _port() { if (this.opts.port && ((this.opts.secure && Number(this.opts.port !== 443)) || (!this.opts.secure && Number(this.opts.port) !== 80))) { return ":" + this.opts.port; } else { return ""; } } _query(query) { const encodedQuery = encode(query); return encodedQuery.length ? "?" + encodedQuery : ""; } } class Polling extends Transport { constructor() { super(...arguments); this._polling = false; } get name() { return "polling"; } /** * Opens the socket (triggers polling). We write a PING message to determine * when the transport is open. * * @protected */ doOpen() { this._poll(); } /** * Pauses polling. * * @param {Function} onPause - callback upon buffers are flushed and transport is paused * @package */ pause(onPause) { this.readyState = "pausing"; const pause = () => { this.readyState = "paused"; onPause(); }; if (this._polling || !this.writable) { let total = 0; if (this._polling) { total++; this.once("pollComplete", function () { --total || pause(); }); } if (!this.writable) { total++; this.once("drain", function () { --total || pause(); }); } } else { pause(); } } /** * Starts polling cycle. * * @private */ _poll() { this._polling = true; this.doPoll(); this.emitReserved("poll"); } /** * Overloads onData to detect payloads. * * @protected */ onData(data) { const callback = (packet) => { // if its the first message we consider the transport open if ("opening" === this.readyState && packet.type === "open") { this.onOpen(); } // if its a close packet, we close the ongoing requests if ("close" === packet.type) { this.onClose({ description: "transport closed by the server" }); return false; } // otherwise bypass onData and handle the message this.onPacket(packet); }; // decode payload decodePayload(data, this.socket.binaryType).forEach(callback); // if an event did not trigger closing if ("closed" !== this.readyState) { // if we got data we're not polling this._polling = false; this.emitReserved("pollComplete"); if ("open" === this.readyState) { this._poll(); } } } /** * For polling, send a close packet. * * @protected */ doClose() {