From d389bb93e440661129490ca45913d46ef361d905 Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Fri, 2 Oct 2020 11:44:19 +0200 Subject: [PATCH 01/14] docs: rework of the protocol documentation --- Readme.md | 443 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 341 insertions(+), 102 deletions(-) diff --git a/Readme.md b/Readme.md index 43709ac..e317ebe 100644 --- a/Readme.md +++ b/Readme.md @@ -3,150 +3,389 @@ This document describes the Socket.IO protocol. For a reference JavaScript implementation, take a look at - [socket.io-parser](https://github.com/learnboost/socket.io-parser), - [socket.io-client](https://github.com/learnboost/socket.io-client) - and [socket.io](https://github.com/learnboost/socket.io). + [socket.io-parser](https://github.com/socketio/socket.io-parser), + [socket.io-client](https://github.com/socketio/socket.io-client) + and [socket.io](https://github.com/socketio/socket.io). + +## Table of Contents + +- [Protocol version](#protocol-version) +- [Packet format](#packet-format) + - [Packet types](#packet-types) + - [CONNECT](#0---connect) + - [DISCONNECT](#1---disconnect) + - [EVENT](#2---event) + - [ACK](#3---ack) + - [ERROR](#4---error) + - [BINARY_EVENT](#5---binary_event) +- [Packet encoding](#packet-encoding) + - [Encoding format](#encoding-format) + - [Examples](#examples) +- [Exchange protocol](#exchange-protocol) + - [Connection to the default namespace](#connection-to-the-default-namespace) + - [Connection to a non-default namespace](#connection-to-a-non-default-namespace) + - [Disconnection from a non-default namespace](#disconnection-from-a-non-default-namespace) + - [Acknowledgement](#acknowledgement) +- [Difference between v3 and v2](#difference-between-v3-and-v2) ## Protocol version - **Current protocol revision:** `3`. +This is the revision **3** of the Socket.IO protocol, included in ̀`socket.io@1.0.0...1.0.2`. -## Parser API +The 4th revision (included in ̀`socket.io@1.0.3...latest`) can be found here: https://github.com/socketio/socket.io-protocol/tree/master -### Parser#Encoder +The 2nd revision (never released) can be found here: https://github.com/socketio/socket.io-protocol/tree/v2 - An object that encodes socket.io packets to engine.io-transportable form. - Its only public method is Encoder#encode. +It is built on top of the [3rd](https://github.com/socketio/engine.io-protocol/tree/v3) revision of the Engine.IO +protocol. -#### Encoder#encode(Object:packet, Function:callback) +While the Engine.IO protocol describes the low-level plumbing with WebSocket and HTTP long-polling, the Socket.IO +protocol adds another layer above in order to provide the following features: - Encodes a `Packet` object into an array of engine.io-compatible encodings. - If the object is pure JSON the array will contain a single item, a socket.io - encoded string. If the object contains binary data (ArrayBuffer, Buffer, - Blob, or File) the array's first item will be a string with packet-relevant - metadata and JSON with placeholders where the binary data was held in the - initial packet. The remaining items will be the raw binary data to fill in - the placeholders post-decoding. +- multiplexing (what we call [Namespace](https://socket.io/docs/namespaces/)) - The callback function is called with the encoded array as its only argument. - In the socket.io-parser implementation, the callback writes each item in the - array to engine.io for transport. The expectation for any implementation is - that each item in the array is transported sequentially. +Example of the Javascript API: -### Parser#Decoder +```js +// server-side +const nsp = io.of("/admin"); +nsp.on("connect", socket => {}); - An object that decodes data from engine.io into complete socket.io packets. +// client-side +const socket1 = io(); // default namespace +const socket2 = io("/admin"); +socket2.on("connect", () => {}); +``` - The expected workflow for using the Decoder is to call the `Decoder#add` - method upon receiving any encoding from engine.io (in the sequential order in - which the encodings are received) and to listen to the Decoder's 'decoded' - events to handle fully decoded packets. +- acknowledgement of packets -#### Decoder#add(Object:encoding) +Example of the Javascript API: - Decodes a single encoded object from engine.io transport. In the case of a - non-binary packet, the one encoding argument is used to reconstruct the full - packet. If the packet is of type `BINARY_EVENT` or `ACK`, then additional calls - to add are expected, one for each piece of binary data in the original packet. - Once the final binary data encoding is passed to add, the full socket.io - packet is reconstructed. +```js +// on one side +socket.emit("hello", 1, () => { console.log("received"); }); +// on the other side +socket.on("hello", (a, cb) => { cb(); }); +``` - After a packet is fully decoded, the Decoder emits a 'decoded' event (via - Emitter) with the decoded packet as the sole argument. Listeners to this event - should treat the packet as ready-to-go. +## Packet format -#### Decoder#destroy() +A packet contains the following fields: - Deallocates the Decoder instance's resources. Should be called in the event - of a disconnect mid-decoding, etc. to prevent memory leaks. +- a type (integer, see [below](#packet-types)) +- a namespace (string) +- optionally, a payload (string | Array) +- optionally, an acknowledgment id (integer) -### Parser#types +## Packet types - Array of packet type keys. +### 0 - CONNECT -### Packet +This event is sent: - Each packet is represented as a vanilla `Object` with a `nsp` key that - indicates what namespace it belongs to (see "Multiplexing") and a - `type` key that can be one of the following: +- by the client when requesting access to a namespace +- by the server when accepting the connection to a namespace - - `Packet#CONNECT` (`0`) - - `Packet#DISCONNECT` (`1`) - - `Packet#EVENT` (`2`) - - `Packet#ACK` (`3`) - - `Packet#ERROR` (`4`) - - `Packet#BINARY_EVENT` (`5`) +It does not contain any payload nor acknowledgement id. -#### EVENT +Example: - - `data` (`Array`) a list of arguments, the first of which is the event - name. Arguments can contain any type of field that can result of - `JSON` decoding, including objects and arrays of arbitrary size. +```json +{ + "type": 0, + "nsp": "/admin" +} +``` - - `id` (`Number`) if the `id` identifier is present, it indicates that the - server wishes to be acknowledged of the reception of this event. +The client may include additional information (i.e. for authentication purpose) in the namespace field. Example: -#### BINARY_EVENT +```json +{ + "type": 0, + "nsp": "/admin?token=1234&uid=abcd" +} +``` - - `data` (`Array`) see `EVENT` `data`, but with addition that any of the arguments - may contain non-JSON arbitrary binary data. For encoding, binary data is - considered either a Buffer, ArrayBuffer, Blob, or File. When decoding, all - binary data server-side is a Buffer; on modern clients binary data is an - ArrayBuffer. On older browsers that don't support binary, every binary data - item is replaced with an object like so: -`{base64: true, data: }`. When a `BINARY_EVENT` or `ACK` - packet is initially decoded, all of the binary data items will be placeholders, - and will be filled by additional calls to `Decoder#add`. +#### 1 - DISCONNECT - - `id` (`Number`) see `EVENT` `id`. +This event is used when one side wants to disconnect from a namespace. -#### ACK +It does not contain any payload nor acknowledgement id. - - `data` (`Array`) see `EVENT` `data`. Encoded in the `BINARY_EVENT` style in - case acknowledgement functions need binary data; see the notes above. - - `id` (`Number`) see `EVENT` `id`. +Example: -#### ERROR +```json +{ + "type": 1, + "nsp": "/admin" +} +``` - - `data` (`Mixed`) error data +#### 2 - EVENT -## Transport +This event is used when one side wants to transmit some data (without binary) to the other side. - The socket.io protocol can be delivered over a variety of transports. - [socket.io-client](http://github.com/learnboost/socket.io-client) - is the implementation of the protocol for the browser and Node.JS over - [engine.io-client](http://github.com/learnboost/engine.io-client). +It does contain a payload, and an optional acknowledgement id. - [socket.io](http://github.com/learnboost/socket.io) is the server - implementation of the protocol over - [engine.io](http://github.com/learnboost/engine.io). +Example: -## Multiplexing +```json +{ + "type": 2, + "nsp": "/", + "data": ["hello", 1] +} +``` - Socket.IO has built-in multiplexing support, which means that each packet - always belongs to a given `namespace`, identified by a path string (like - `/this`). The corresponding key in the `Packet` object is `nsp`. +With an acknowledgment id: - When the socket.io transport connection is established, a connection - attempt to the `/` namespace is assumed (ie: the server behaves as if - the client had sent a `CONNECT` packet to the `/` namespace). +```json +{ + "type": 2, + "nsp": "/admin", + "data": ["project:delete", 123], + "id": 456 +} +``` - In order to support multiplexing of multiple sockets under - the same transport, additional `CONNECT` packets can be sent by the - client to arbitrary namespace URIs (eg: `/another`). +#### 3 - ACK - When the server responds with a `CONNECT` packet to the corresponding - namespace, the multiplexed socket is considered connected. +This event is used when one side has received an EVENT or a BINARY_EVENT with an acknowledgement id. - Alternatively, the server can respond with an `ERROR` packet to indicate - a multiplexed socket connection error, such as authentication errors. - The associated error payload varies according to each error, and can - be user-defined. +It contains the acknowledgement id received in the previous packet, and may contain a payload (without binary). - After a `CONNECT` packet is received by the server for a given `nsp`, - the client can then send and receive `EVENT` packets. If any of the - parties receives an `EVENT` packet with an `id` field, an `ACK` packet is - expected to confirm the reception of said packet. +```json +{ + "type": 3, + "nsp": "/admin", + "data": [], + "id": 456 +} +``` + +#### 4 - ERROR + +This event is sent by the server when the connection to a namespace is refused. + +It may contain a payload indicating the reason of the refusal. + +Example: + +```json +{ + "type": 4, + "nsp": "/admin", + "data": "Not authorized" +} +``` + +#### 5 - BINARY_EVENT + +This event is used when one side wants to transmit some data (including binary) to the other side. + +It does contain a payload, and an optional acknowledgement id. + +Example: + +``` +{ + "type": 5, + "nsp": "/", + "data": ["hello", ] +} +``` + +With an acknowledgment id: + +``` +{ + "type": 5, + "nsp": "/admin", + "data": ["project:delete", ], + "id": 456 +} +``` + +## Packet encoding + +This section details the encoding used by the default parser which is included in Socket.IO server and client, and +whose source can be found [here](https://github.com/socketio/socket.io-parser). + +The JS server and client implementations also supports custom parsers, which have different tradeoffs and may benefit to +certain kind of applications. Please see [socket.io-json-parser](https://github.com/darrachequesne/socket.io-json-parser) +or [socket.io-msgpack-parser](https://github.com/darrachequesne/socket.io-msgpack-parser) for example. + +Please also note that each Socket.IO packet is sent as a Engine.IO `message` packet (more information [here](https://github.com/socketio/engine.io-protocol)), +so the encoded result will be prefixed by `4` when sent over the wire (in the request/response body with HTTP +long-polling, or in the WebSocket frame). + +### Encoding format + +``` +[<# of binary attachments>-][,][][JSON-stringified payload without binary] + ++ binary attachments extracted +``` + +Note: + +- the namespace is only included if it is different from the default namespace (`/`) + +### Examples + +- `CONNECT` packet for the default namespace + +```json +{ + "type": 0, + "nsp": "/" +} +``` + +is encoded to `0` + +- `CONNECT` packet for the `/admin` namespace + +```json +{ + "type": 0, + "nsp": "/admin" +} +``` + +is encoded to `0/admin` + +- `DISCONNECT` packet for the `/admin` namespace + +```json +{ + "type": 1, + "nsp": "/admin" +} +``` + +is encoded to `1/admin` + +- `EVENT` packet + +```json +{ + "type": 2, + "nsp": "/", + "data": ["hello", 1] +} +``` + +is encoded to `2["hello",1]` + +- `EVENT` packet with an acknowledgement id + +```json +{ + "type": 2, + "nsp": "/admin", + "data": ["project:delete", 123], + "id": 456 +} +``` + +is encoded to `2/admin,456["project:delete",123]` + +- `ACK` packet + +```json +{ + "type": 3, + "nsp": "/admin", + "data": [], + "id": 456 +} +``` + +is encoded to `3/admin,456[]` + +- `ERROR` packet + +```json +{ + "type": 4, + "nsp": "/admin", + "data": "Not authorized" +} +``` + +is encoded to `4/admin,"Not authorized"` + +- `BINARY_EVENT` packet + +``` +{ + "type": 5, + "nsp": "/", + "data": ["hello", ] +} +``` + +is encoded to `51-["hello",{"_placeholder":true,"num":0}]` + `` + +- `BINARY_EVENT` packet with an acknowledgement id + +``` +{ + "type": 5, + "nsp": "/admin", + "data": ["project:delete", ], + "id": 456 +} +``` + +is encoded to `51-/admin,456["project:delete",{"_placeholder":true,"num":0}]` + `` + + +## Exchange protocol + +### Connection to the default namespace + +The server always send a `CONNECT` packet for the default namespace (`/`) when the connection is established. + +That is, even if the client requests access to a non-default namespace, it will receive a `CONNECT` packet for the +default namespace first. + +``` +Server > { type: CONNECT, nsp: "/" } +``` + +No response is expected from the client. + +### Connection to a non-default namespace + +``` +Client > { type: CONNECT, nsp: "/admin" } +Server > { type: CONNECT, nsp: "/admin" } (if the connection is successful) +or +Server > { type: ERROR, nsp: "/admin", data: "Not authorized" } +``` + +### Disconnection from a non-default namespace + +``` +Client > { type: DISCONNECT, nsp: "/admin" } +``` + +And vice versa. No response is expected from the other-side. + +### Acknowledgement + +``` +Client > { type: EVENT, nsp: "/admin", data: ["hello"], id: 456 } +Server > { type: ACK, nsp: "/admin", data: [], id: 456 } +``` + +And vice versa. + +## Difference between v3 and v2 + +- remove the usage of msgpack to encode packets containing binary objects (see also [299849b](https://github.com/socketio/socket.io-parser/commit/299849b00294c3bc95817572441f3aca8ffb1f65)) ## License From 621aeba569e103f009fdf36010b7d0f63308bf0e Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Fri, 2 Oct 2020 12:32:42 +0200 Subject: [PATCH 02/14] docs: add a history of revisions Backported from master. --- Readme.md | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/Readme.md b/Readme.md index e317ebe..ec964fc 100644 --- a/Readme.md +++ b/Readme.md @@ -26,7 +26,10 @@ - [Connection to a non-default namespace](#connection-to-a-non-default-namespace) - [Disconnection from a non-default namespace](#disconnection-from-a-non-default-namespace) - [Acknowledgement](#acknowledgement) -- [Difference between v3 and v2](#difference-between-v3-and-v2) +- [History](#history) + - [Difference between v3 and v2](#difference-between-v3-and-v2) + - [Difference between v2 and v1](#difference-between-v2-and-v1) + - [Initial revision](#initial-revision) ## Protocol version @@ -34,7 +37,8 @@ This is the revision **3** of the Socket.IO protocol, included in ̀`socket.io@1 The 4th revision (included in ̀`socket.io@1.0.3...latest`) can be found here: https://github.com/socketio/socket.io-protocol/tree/master -The 2nd revision (never released) can be found here: https://github.com/socketio/socket.io-protocol/tree/v2 +Both the 1st and the 2nd revisions were part of the work towards Socket.IO 1.0 but were never included in a Socket.IO +release. It is built on top of the [3rd](https://github.com/socketio/engine.io-protocol/tree/v3) revision of the Engine.IO protocol. @@ -383,10 +387,25 @@ Server > { type: ACK, nsp: "/admin", data: [], id: 456 } And vice versa. -## Difference between v3 and v2 +## History + +### Difference between v3 and v2 - remove the usage of msgpack to encode packets containing binary objects (see also [299849b](https://github.com/socketio/socket.io-parser/commit/299849b00294c3bc95817572441f3aca8ffb1f65)) +### Difference between v2 and v1 + +- add a BINARY_EVENT packet type + +This was added during the work towards Socket.IO 1.0, in order to add support for binary objects. The BINARY_EVENT +packets were encoded with [msgpack](https://msgpack.org/). + +### Initial revision + +This first revision was the result of the split between the Engine.IO protocol (low-level plumbing with WebSocket / HTTP +long-polling, heartbeat) and the Socket.IO protocol. It was never included in a Socket.IO release, but paved the way for +the next iterations. + ## License MIT From bb28c298ea21bdfc941bee2768f2ebf3f9905808 Mon Sep 17 00:00:00 2001 From: Sleeyax Date: Sun, 2 Jul 2023 19:27:49 +0200 Subject: [PATCH 03/14] Copy tests from v5 --- test-suite/.gitignore | 1 + test-suite/index.html | 30 + test-suite/node-imports.js | 10 + test-suite/package-lock.json | 1936 ++++++++++++++++++++++++++++++++++ test-suite/package.json | 18 + test-suite/test-suite.js | 627 +++++++++++ 6 files changed, 2622 insertions(+) create mode 100755 test-suite/.gitignore create mode 100644 test-suite/index.html create mode 100644 test-suite/node-imports.js create mode 100644 test-suite/package-lock.json create mode 100644 test-suite/package.json create mode 100644 test-suite/test-suite.js diff --git a/test-suite/.gitignore b/test-suite/.gitignore new file mode 100755 index 0000000..3c3629e --- /dev/null +++ b/test-suite/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/test-suite/index.html b/test-suite/index.html new file mode 100644 index 0000000..49fdb6b --- /dev/null +++ b/test-suite/index.html @@ -0,0 +1,30 @@ + + + + + + + Test suite for the Socket.IO protocol + + + + +
+ + + + + + + + + + + + + diff --git a/test-suite/node-imports.js b/test-suite/node-imports.js new file mode 100644 index 0000000..fc509aa --- /dev/null +++ b/test-suite/node-imports.js @@ -0,0 +1,10 @@ +import fetch from "node-fetch"; +import { WebSocket } from "ws"; +import chai from "chai"; +import chaiString from "chai-string"; + +chai.use(chaiString); + +globalThis.fetch = fetch; +globalThis.WebSocket = WebSocket; +globalThis.chai = chai; diff --git a/test-suite/package-lock.json b/test-suite/package-lock.json new file mode 100644 index 0000000..aa08864 --- /dev/null +++ b/test-suite/package-lock.json @@ -0,0 +1,1936 @@ +{ + "name": "socket.io-protocol-test-suite", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "socket.io-protocol-test-suite", + "version": "0.0.1", + "devDependencies": { + "chai": "^4.3.6", + "chai-string": "^1.5.0", + "mocha": "^9.2.1", + "node-fetch": "^3.2.0", + "prettier": "^2.5.1", + "ws": "^8.5.0" + } + }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chai-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/chai-string/-/chai-string-1.5.0.tgz", + "integrity": "sha512-sydDC3S3pNAQMYwJrs6dQX0oBQ6KfIPuOZ78n7rocW0eJJlsHPh2t3kwW7xfwYA/1Bf6/arGtSUo16rxR2JFlw==", + "dev": true, + "peerDependencies": { + "chai": "^4.1.2" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", + "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fetch-blob": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.4.tgz", + "integrity": "sha512-Eq5Xv5+VlSrYWEqKrusxY1C3Hm/hjeAsCGVG3ft7pZahlUAChpGZT/Ms1WmSLnEAisEXszjzu/s+ce6HZB2VHA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true, + "engines": { + "node": ">=4.x" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.1.tgz", + "integrity": "sha512-T7uscqjJVS46Pq1XDXyo9Uvey9gd3huT/DD9cYBb4K2Xc/vbKRPUWK067bxDQRK0yIz6Jxk73IrnimvASzBNAQ==", + "dev": true, + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "3.0.4", + "ms": "2.1.3", + "nanoid": "3.2.0", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", + "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.0.tgz", + "integrity": "sha512-8xeimMwMItMw8hRrOl3C9/xzU49HV/yE6ORew/l+dxWimO5A4Ra8ld2rerlJvc/O7et5Z1zrWsPX43v1QBjCxw==", + "dev": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prettier": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", + "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz", + "integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "chai-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/chai-string/-/chai-string-1.5.0.tgz", + "integrity": "sha512-sydDC3S3pNAQMYwJrs6dQX0oBQ6KfIPuOZ78n7rocW0eJJlsHPh2t3kwW7xfwYA/1Bf6/arGtSUo16rxR2JFlw==", + "dev": true, + "requires": {} + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "data-uri-to-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", + "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==", + "dev": true + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "fetch-blob": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.4.tgz", + "integrity": "sha512-Eq5Xv5+VlSrYWEqKrusxY1C3Hm/hjeAsCGVG3ft7pZahlUAChpGZT/Ms1WmSLnEAisEXszjzu/s+ce6HZB2VHA==", + "dev": true, + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "requires": { + "fetch-blob": "^3.1.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mocha": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.1.tgz", + "integrity": "sha512-T7uscqjJVS46Pq1XDXyo9Uvey9gd3huT/DD9cYBb4K2Xc/vbKRPUWK067bxDQRK0yIz6Jxk73IrnimvASzBNAQ==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "3.0.4", + "ms": "2.1.3", + "nanoid": "3.2.0", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "nanoid": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", + "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==", + "dev": true + }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true + }, + "node-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.0.tgz", + "integrity": "sha512-8xeimMwMItMw8hRrOl3C9/xzU49HV/yE6ORew/l+dxWimO5A4Ra8ld2rerlJvc/O7et5Z1zrWsPX43v1QBjCxw==", + "dev": true, + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "prettier": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", + "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "web-streams-polyfill": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz", + "integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "dev": true, + "requires": {} + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/test-suite/package.json b/test-suite/package.json new file mode 100644 index 0000000..6b0391b --- /dev/null +++ b/test-suite/package.json @@ -0,0 +1,18 @@ +{ + "name": "socket.io-protocol-test-suite", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "format": "prettier -w *.js", + "test": "mocha test-suite.js" + }, + "devDependencies": { + "chai": "^4.3.6", + "chai-string": "^1.5.0", + "mocha": "^9.2.1", + "node-fetch": "^3.2.0", + "prettier": "^2.5.1", + "ws": "^8.5.0" + } +} diff --git a/test-suite/test-suite.js b/test-suite/test-suite.js new file mode 100644 index 0000000..313b3a2 --- /dev/null +++ b/test-suite/test-suite.js @@ -0,0 +1,627 @@ +const isNodejs = typeof window === "undefined"; + +if (isNodejs) { + // make the tests runnable in both the browser and Node.js + await import("./node-imports.js"); +} + +const { expect } = chai; + +const URL = "http://localhost:3000"; +const WS_URL = URL.replace("http", "ws"); + +const PING_INTERVAL = 300; +const PING_TIMEOUT = 200; + +function sleep(delay) { + return new Promise((resolve) => setTimeout(resolve, delay)); +} + +function waitFor(socket, eventType) { + return new Promise((resolve) => { + socket.addEventListener( + eventType, + (event) => { + resolve(event); + }, + { once: true } + ); + }); +} + +function waitForPackets(socket, count) { + const packets = []; + + return new Promise((resolve) => { + const handler = (event) => { + if (event.data === "2") { + // ignore PING packets + return; + } + packets.push(event.data); + if (packets.length === count) { + socket.removeEventListener("message", handler); + resolve(packets); + } + }; + socket.addEventListener("message", handler); + }); +} + +async function initLongPollingSession() { + const response = await fetch(`${URL}/socket.io/?EIO=4&transport=polling`); + const content = await response.text(); + return JSON.parse(content.substring(1)).sid; +} + +async function initSocketIOConnection() { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=websocket` + ); + socket.binaryType = "arraybuffer"; + + await waitFor(socket, "message"); // Engine.IO handshake + + socket.send("40"); + + await waitFor(socket, "message"); // Socket.IO handshake + await waitFor(socket, "message"); // "auth" packet + + return socket; +} + +describe("Engine.IO protocol", () => { + describe("handshake", () => { + describe("HTTP long-polling", () => { + it("should successfully open a session", async () => { + const response = await fetch( + `${URL}/socket.io/?EIO=4&transport=polling` + ); + + expect(response.status).to.eql(200); + + const content = await response.text(); + + expect(content).to.startsWith("0"); + + const value = JSON.parse(content.substring(1)); + + expect(value).to.have.all.keys( + "sid", + "upgrades", + "pingInterval", + "pingTimeout", + "maxPayload" + ); + expect(value.sid).to.be.a("string"); + expect(value.upgrades).to.eql(["websocket"]); + expect(value.pingInterval).to.eql(PING_INTERVAL); + expect(value.pingTimeout).to.eql(PING_TIMEOUT); + expect(value.maxPayload).to.eql(1000000); + }); + + it("should fail with an invalid 'EIO' query parameter", async () => { + const response = await fetch(`${URL}/socket.io/?transport=polling`); + + expect(response.status).to.eql(400); + + const response2 = await fetch( + `${URL}/socket.io/?EIO=abc&transport=polling` + ); + + expect(response2.status).to.eql(400); + }); + + it("should fail with an invalid 'transport' query parameter", async () => { + const response = await fetch(`${URL}/socket.io/?EIO=4`); + + expect(response.status).to.eql(400); + + const response2 = await fetch(`${URL}/socket.io/?EIO=4&transport=abc`); + + expect(response2.status).to.eql(400); + }); + + it("should fail with an invalid request method", async () => { + const response = await fetch( + `${URL}/socket.io/?EIO=4&transport=polling`, + { + method: "post", + } + ); + + expect(response.status).to.eql(400); + + const response2 = await fetch( + `${URL}/socket.io/?EIO=4&transport=polling`, + { + method: "put", + } + ); + + expect(response2.status).to.eql(400); + }); + }); + + describe("WebSocket", () => { + it("should successfully open a session", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=websocket` + ); + + const { data } = await waitFor(socket, "message"); + + expect(data).to.startsWith("0"); + + const value = JSON.parse(data.substring(1)); + + expect(value).to.have.all.keys( + "sid", + "upgrades", + "pingInterval", + "pingTimeout", + "maxPayload" + ); + expect(value.sid).to.be.a("string"); + expect(value.upgrades).to.eql([]); + expect(value.pingInterval).to.eql(PING_INTERVAL); + expect(value.pingTimeout).to.eql(PING_TIMEOUT); + expect(value.maxPayload).to.eql(1000000); + + socket.close(); + }); + + it("should fail with an invalid 'EIO' query parameter", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?transport=websocket` + ); + + if (isNodejs) { + socket.on("error", () => {}); + } + + waitFor(socket, "close"); + + const socket2 = new WebSocket( + `${WS_URL}/socket.io/?EIO=abc&transport=websocket` + ); + + if (isNodejs) { + socket2.on("error", () => {}); + } + + waitFor(socket2, "close"); + }); + + it("should fail with an invalid 'transport' query parameter", async () => { + const socket = new WebSocket(`${WS_URL}/socket.io/?EIO=4`); + + if (isNodejs) { + socket.on("error", () => {}); + } + + waitFor(socket, "close"); + + const socket2 = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=abc` + ); + + if (isNodejs) { + socket2.on("error", () => {}); + } + + waitFor(socket2, "close"); + }); + }); + }); + + describe("heartbeat", function () { + this.timeout(5000); + + describe("HTTP long-polling", () => { + it("should send ping/pong packets", async () => { + const sid = await initLongPollingSession(); + + for (let i = 0; i < 3; i++) { + const pollResponse = await fetch( + `${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}` + ); + + expect(pollResponse.status).to.eql(200); + + const pollContent = await pollResponse.text(); + + expect(pollContent).to.eql("2"); + + const pushResponse = await fetch( + `${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}`, + { + method: "post", + body: "3", + } + ); + + expect(pushResponse.status).to.eql(200); + } + }); + + it("should close the session upon ping timeout", async () => { + const sid = await initLongPollingSession(); + + await sleep(PING_INTERVAL + PING_TIMEOUT); + + const pollResponse = await fetch( + `${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}` + ); + + expect(pollResponse.status).to.eql(400); + }); + }); + + describe("WebSocket", () => { + it("should send ping/pong packets", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=websocket` + ); + + await waitFor(socket, "message"); // handshake + + for (let i = 0; i < 3; i++) { + const { data } = await waitFor(socket, "message"); + + expect(data).to.eql("2"); + + socket.send("3"); + } + + socket.close(); + }); + + it("should close the session upon ping timeout", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=websocket` + ); + + await waitFor(socket, "close"); // handshake + }); + }); + }); + + describe("close", () => { + describe("HTTP long-polling", () => { + it("should forcefully close the session", async () => { + const sid = await initLongPollingSession(); + + const [pollResponse] = await Promise.all([ + fetch(`${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}`), + fetch(`${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}`, { + method: "post", + body: "1", + }), + ]); + + expect(pollResponse.status).to.eql(200); + + const pullContent = await pollResponse.text(); + + expect(pullContent).to.eql("6"); + + const pollResponse2 = await fetch( + `${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}` + ); + + expect(pollResponse2.status).to.eql(400); + }); + }); + + describe("WebSocket", () => { + it("should forcefully close the session", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=websocket` + ); + + await waitFor(socket, "message"); // handshake + + socket.send("1"); + + await waitFor(socket, "close"); + }); + }); + }); + + describe("upgrade", () => { + it("should successfully upgrade from HTTP long-polling to WebSocket", async () => { + const sid = await initLongPollingSession(); + + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}` + ); + + await waitFor(socket, "open"); + + // send probe + socket.send("2probe"); + + const probeResponse = await waitFor(socket, "message"); + + expect(probeResponse.data).to.eql("3probe"); + + // complete upgrade + socket.send("5"); + }); + + it("should ignore HTTP requests with same sid after upgrade", async () => { + const sid = await initLongPollingSession(); + + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}` + ); + + await waitFor(socket, "open"); + socket.send("2probe"); + socket.send("5"); + + const pollResponse = await fetch( + `${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}` + ); + + expect(pollResponse.status).to.eql(400); + }); + + it("should ignore WebSocket connection with same sid after upgrade", async () => { + const sid = await initLongPollingSession(); + + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}` + ); + + await waitFor(socket, "open"); + socket.send("2probe"); + socket.send("5"); + + const socket2 = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}` + ); + + await waitFor(socket2, "close"); + }); + }); +}); + +describe("Socket.IO protocol", () => { + describe("connect", () => { + it("should allow connection to the main namespace", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=websocket` + ); + + await waitFor(socket, "message"); // Engine.IO handshake + + socket.send("40"); + + const { data } = await waitFor(socket, "message"); + + expect(data).to.startsWith("40"); + + const handshake = JSON.parse(data.substring(2)); + + expect(handshake).to.have.all.keys("sid"); + expect(handshake.sid).to.be.a("string"); + + const authPacket = await waitFor(socket, "message"); + + expect(authPacket.data).to.eql('42["auth",{}]'); + }); + + it("should allow connection to the main namespace with a payload", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=websocket` + ); + + await waitFor(socket, "message"); // Engine.IO handshake + + socket.send('40{"token":"123"}'); + + const { data } = await waitFor(socket, "message"); + + expect(data).to.startsWith("40"); + + const handshake = JSON.parse(data.substring(2)); + + expect(handshake).to.have.all.keys("sid"); + expect(handshake.sid).to.be.a("string"); + + const authPacket = await waitFor(socket, "message"); + + expect(authPacket.data).to.eql('42["auth",{"token":"123"}]'); + }); + + it("should allow connection to a custom namespace", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=websocket` + ); + + await waitFor(socket, "message"); // Engine.IO handshake + + socket.send("40/custom,"); + + const { data } = await waitFor(socket, "message"); + + expect(data).to.startsWith("40/custom,"); + + const handshake = JSON.parse(data.substring(10)); + + expect(handshake).to.have.all.keys("sid"); + expect(handshake.sid).to.be.a("string"); + + const authPacket = await waitFor(socket, "message"); + + expect(authPacket.data).to.eql('42/custom,["auth",{}]'); + }); + + it("should allow connection to a custom namespace with a payload", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=websocket` + ); + + await waitFor(socket, "message"); // Engine.IO handshake + + socket.send('40/custom,{"token":"abc"}'); + + const { data } = await waitFor(socket, "message"); + + expect(data).to.startsWith("40/custom,"); + + const handshake = JSON.parse(data.substring(10)); + + expect(handshake).to.have.all.keys("sid"); + expect(handshake.sid).to.be.a("string"); + + const authPacket = await waitFor(socket, "message"); + + expect(authPacket.data).to.eql('42/custom,["auth",{"token":"abc"}]'); + }); + + it("should disallow connection to an unknown namespace", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=websocket` + ); + + await waitFor(socket, "message"); // Engine.IO handshake + + socket.send("40/random"); + + const { data } = await waitFor(socket, "message"); + + expect(data).to.eql('44/random,{"message":"Invalid namespace"}'); + }); + + it("should disallow connection with an invalid handshake", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=4&transport=websocket` + ); + + await waitFor(socket, "message"); // Engine.IO handshake + + socket.send("4abc"); + + await waitFor(socket, "close"); + }); + }); + + describe("disconnect", () => { + it("should disconnect from the main namespace", async () => { + const socket = await initSocketIOConnection(); + + socket.send("41"); + + const { data } = await waitFor(socket, "message"); + + expect(data).to.eql("2"); + }); + + it("should connect then disconnect from a custom namespace", async () => { + const socket = await initSocketIOConnection(); + + await waitFor(socket, "message"); // ping + + socket.send("40/custom"); + + await waitFor(socket, "message"); // Socket.IO handshake + await waitFor(socket, "message"); // auth packet + + socket.send("41/custom"); + socket.send('42["message","message to main namespace"]'); + + const { data } = await waitFor(socket, "message"); + + expect(data).to.eql('42["message-back","message to main namespace"]'); + }); + }); + + describe("message", () => { + it("should send a plain-text packet", async () => { + const socket = await initSocketIOConnection(); + + socket.send('42["message",1,"2",{"3":[true]}]'); + + const { data } = await waitFor(socket, "message"); + + expect(data).to.eql('42["message-back",1,"2",{"3":[true]}]'); + }); + + it("should send a packet with binary attachments", async () => { + const socket = await initSocketIOConnection(); + + socket.send( + '452-["message",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' + ); + socket.send(Uint8Array.from([1, 2, 3])); + socket.send(Uint8Array.from([4, 5, 6])); + + const packets = await waitForPackets(socket, 3); + + expect(packets[0]).to.eql( + '452-["message-back",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' + ); + expect(packets[1]).to.eql(Uint8Array.from([1, 2, 3]).buffer); + expect(packets[2]).to.eql(Uint8Array.from([4, 5, 6]).buffer); + + socket.close(); + }); + + it("should send a plain-text packet with an ack", async () => { + const socket = await initSocketIOConnection(); + + socket.send('42456["message-with-ack",1,"2",{"3":[false]}]'); + + const { data } = await waitFor(socket, "message"); + + expect(data).to.eql('43456[1,"2",{"3":[false]}]'); + }); + + it("should send a packet with binary attachments and an ack", async () => { + const socket = await initSocketIOConnection(); + + socket.send( + '452-789["message-with-ack",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' + ); + socket.send(Uint8Array.from([1, 2, 3])); + socket.send(Uint8Array.from([4, 5, 6])); + + const packets = await waitForPackets(socket, 3); + + expect(packets[0]).to.eql( + '462-789[{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' + ); + expect(packets[1]).to.eql(Uint8Array.from([1, 2, 3]).buffer); + expect(packets[2]).to.eql(Uint8Array.from([4, 5, 6]).buffer); + + socket.close(); + }); + + it("should close the connection upon invalid format (unknown packet type)", async () => { + const socket = await initSocketIOConnection(); + + socket.send("4abc"); + + await waitFor(socket, "close"); + }); + + it("should close the connection upon invalid format (invalid payload format)", async () => { + const socket = await initSocketIOConnection(); + + socket.send("42{}"); + + await waitFor(socket, "close"); + }); + + it("should close the connection upon invalid format (invalid ack id)", async () => { + const socket = await initSocketIOConnection(); + + socket.send('42abc["message-with-ack",1,"2",{"3":[false]}]'); + + await waitFor(socket, "close"); + }); + }); +}); From 09cc91607ad5c6f75b822528e5aeefb6fafb2053 Mon Sep 17 00:00:00 2001 From: Sleeyax Date: Sun, 2 Jul 2023 22:22:39 +0200 Subject: [PATCH 04/14] Update tests to match v4 protocol --- test-suite/test-suite.js | 114 ++++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 54 deletions(-) diff --git a/test-suite/test-suite.js b/test-suite/test-suite.js index 313b3a2..8001d8d 100644 --- a/test-suite/test-suite.js +++ b/test-suite/test-suite.js @@ -48,15 +48,23 @@ function waitForPackets(socket, count) { }); } +function decodePayload(payload) { + const firstColonIndex = payload.indexOf(":"); + const length = payload.substring(0, firstColonIndex); + const packet = payload.substring(firstColonIndex + 1); + return [length, packet]; +} + async function initLongPollingSession() { - const response = await fetch(`${URL}/socket.io/?EIO=4&transport=polling`); - const content = await response.text(); + const response = await fetch(`${URL}/socket.io/?EIO=3&transport=polling`); + const text = await response.text(); + const [, content] = decodePayload(text); return JSON.parse(content.substring(1)).sid; } async function initSocketIOConnection() { const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=websocket` + `${WS_URL}/socket.io/?EIO=3&transport=websocket` ); socket.binaryType = "arraybuffer"; @@ -75,13 +83,15 @@ describe("Engine.IO protocol", () => { describe("HTTP long-polling", () => { it("should successfully open a session", async () => { const response = await fetch( - `${URL}/socket.io/?EIO=4&transport=polling` + `${URL}/socket.io/?EIO=3&transport=polling` ); expect(response.status).to.eql(200); - const content = await response.text(); + const text = await response.text(); + const [length, content] = decodePayload(text); + expect(length).to.eql(content.length.toString()); expect(content).to.startsWith("0"); const value = JSON.parse(content.substring(1)); @@ -113,18 +123,18 @@ describe("Engine.IO protocol", () => { }); it("should fail with an invalid 'transport' query parameter", async () => { - const response = await fetch(`${URL}/socket.io/?EIO=4`); + const response = await fetch(`${URL}/socket.io/?EIO=3`); expect(response.status).to.eql(400); - const response2 = await fetch(`${URL}/socket.io/?EIO=4&transport=abc`); + const response2 = await fetch(`${URL}/socket.io/?EIO=3&transport=abc`); expect(response2.status).to.eql(400); }); it("should fail with an invalid request method", async () => { const response = await fetch( - `${URL}/socket.io/?EIO=4&transport=polling`, + `${URL}/socket.io/?EIO=3&transport=polling`, { method: "post", } @@ -133,7 +143,7 @@ describe("Engine.IO protocol", () => { expect(response.status).to.eql(400); const response2 = await fetch( - `${URL}/socket.io/?EIO=4&transport=polling`, + `${URL}/socket.io/?EIO=3&transport=polling`, { method: "put", } @@ -146,7 +156,7 @@ describe("Engine.IO protocol", () => { describe("WebSocket", () => { it("should successfully open a session", async () => { const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=websocket` + `${WS_URL}/socket.io/?EIO=3&transport=websocket` ); const { data } = await waitFor(socket, "message"); @@ -194,7 +204,7 @@ describe("Engine.IO protocol", () => { }); it("should fail with an invalid 'transport' query parameter", async () => { - const socket = new WebSocket(`${WS_URL}/socket.io/?EIO=4`); + const socket = new WebSocket(`${WS_URL}/socket.io/?EIO=3`); if (isNodejs) { socket.on("error", () => {}); @@ -203,7 +213,7 @@ describe("Engine.IO protocol", () => { waitFor(socket, "close"); const socket2 = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=abc` + `${WS_URL}/socket.io/?EIO=3&transport=abc` ); if (isNodejs) { @@ -223,25 +233,25 @@ describe("Engine.IO protocol", () => { const sid = await initLongPollingSession(); for (let i = 0; i < 3; i++) { - const pollResponse = await fetch( - `${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}` - ); - - expect(pollResponse.status).to.eql(200); - - const pollContent = await pollResponse.text(); - - expect(pollContent).to.eql("2"); - const pushResponse = await fetch( - `${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}`, + `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}`, { method: "post", - body: "3", + body: "1:2", } ); expect(pushResponse.status).to.eql(200); + + const pollResponse = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` + ); + + expect(pollResponse.status).to.eql(200); + + const pollContent = await pollResponse.text(); + + expect(pollContent).to.eql("1:3"); } }); @@ -251,7 +261,7 @@ describe("Engine.IO protocol", () => { await sleep(PING_INTERVAL + PING_TIMEOUT); const pollResponse = await fetch( - `${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}` + `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(400); @@ -261,17 +271,17 @@ describe("Engine.IO protocol", () => { describe("WebSocket", () => { it("should send ping/pong packets", async () => { const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=websocket` + `${WS_URL}/socket.io/?EIO=3&transport=websocket` ); await waitFor(socket, "message"); // handshake for (let i = 0; i < 3; i++) { - const { data } = await waitFor(socket, "message"); + socket.send("2"); - expect(data).to.eql("2"); + const { data } = await waitFor(socket, "message"); - socket.send("3"); + expect(data).to.eql("3"); } socket.close(); @@ -279,7 +289,7 @@ describe("Engine.IO protocol", () => { it("should close the session upon ping timeout", async () => { const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=websocket` + `${WS_URL}/socket.io/?EIO=3&transport=websocket` ); await waitFor(socket, "close"); // handshake @@ -293,10 +303,10 @@ describe("Engine.IO protocol", () => { const sid = await initLongPollingSession(); const [pollResponse] = await Promise.all([ - fetch(`${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}`), - fetch(`${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}`, { + fetch(`${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}`), + fetch(`${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}`, { method: "post", - body: "1", + body: "1:1", }), ]); @@ -304,10 +314,10 @@ describe("Engine.IO protocol", () => { const pullContent = await pollResponse.text(); - expect(pullContent).to.eql("6"); + expect(pullContent).to.eql("1:6"); const pollResponse2 = await fetch( - `${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}` + `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` ); expect(pollResponse2.status).to.eql(400); @@ -317,7 +327,7 @@ describe("Engine.IO protocol", () => { describe("WebSocket", () => { it("should forcefully close the session", async () => { const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=websocket` + `${WS_URL}/socket.io/?EIO=3&transport=websocket` ); await waitFor(socket, "message"); // handshake @@ -334,7 +344,7 @@ describe("Engine.IO protocol", () => { const sid = await initLongPollingSession(); const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}` + `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` ); await waitFor(socket, "open"); @@ -354,7 +364,7 @@ describe("Engine.IO protocol", () => { const sid = await initLongPollingSession(); const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}` + `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` ); await waitFor(socket, "open"); @@ -362,7 +372,7 @@ describe("Engine.IO protocol", () => { socket.send("5"); const pollResponse = await fetch( - `${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}` + `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(400); @@ -372,7 +382,7 @@ describe("Engine.IO protocol", () => { const sid = await initLongPollingSession(); const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}` + `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` ); await waitFor(socket, "open"); @@ -380,7 +390,7 @@ describe("Engine.IO protocol", () => { socket.send("5"); const socket2 = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}` + `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` ); await waitFor(socket2, "close"); @@ -392,7 +402,7 @@ describe("Socket.IO protocol", () => { describe("connect", () => { it("should allow connection to the main namespace", async () => { const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=websocket` + `${WS_URL}/socket.io/?EIO=3&transport=websocket` ); await waitFor(socket, "message"); // Engine.IO handshake @@ -415,7 +425,7 @@ describe("Socket.IO protocol", () => { it("should allow connection to the main namespace with a payload", async () => { const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=websocket` + `${WS_URL}/socket.io/?EIO=3&transport=websocket` ); await waitFor(socket, "message"); // Engine.IO handshake @@ -438,7 +448,7 @@ describe("Socket.IO protocol", () => { it("should allow connection to a custom namespace", async () => { const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=websocket` + `${WS_URL}/socket.io/?EIO=3&transport=websocket` ); await waitFor(socket, "message"); // Engine.IO handshake @@ -461,7 +471,7 @@ describe("Socket.IO protocol", () => { it("should allow connection to a custom namespace with a payload", async () => { const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=websocket` + `${WS_URL}/socket.io/?EIO=3&transport=websocket` ); await waitFor(socket, "message"); // Engine.IO handshake @@ -484,7 +494,7 @@ describe("Socket.IO protocol", () => { it("should disallow connection to an unknown namespace", async () => { const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=websocket` + `${WS_URL}/socket.io/?EIO=3&transport=websocket` ); await waitFor(socket, "message"); // Engine.IO handshake @@ -498,7 +508,7 @@ describe("Socket.IO protocol", () => { it("should disallow connection with an invalid handshake", async () => { const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=4&transport=websocket` + `${WS_URL}/socket.io/?EIO=3&transport=websocket` ); await waitFor(socket, "message"); // Engine.IO handshake @@ -515,21 +525,17 @@ describe("Socket.IO protocol", () => { socket.send("41"); - const { data } = await waitFor(socket, "message"); - - expect(data).to.eql("2"); + await waitFor(socket, "close"); }); it("should connect then disconnect from a custom namespace", async () => { const socket = await initSocketIOConnection(); - await waitFor(socket, "message"); // ping - socket.send("40/custom"); - + await waitFor(socket, "message"); // Socket.IO handshake await waitFor(socket, "message"); // auth packet - + socket.send("41/custom"); socket.send('42["message","message to main namespace"]'); From 4ef5d9687052d1a684409159de558b6a756dfed5 Mon Sep 17 00:00:00 2001 From: Sleeyax Date: Mon, 3 Jul 2023 21:41:54 +0200 Subject: [PATCH 05/14] Remove auth packet checks CONNECT packets don't contain payloads in the v4 protocol. --- test-suite/test-suite.js | 79 +++++----------------------------------- 1 file changed, 10 insertions(+), 69 deletions(-) diff --git a/test-suite/test-suite.js b/test-suite/test-suite.js index 8001d8d..41546e1 100644 --- a/test-suite/test-suite.js +++ b/test-suite/test-suite.js @@ -68,12 +68,7 @@ async function initSocketIOConnection() { ); socket.binaryType = "arraybuffer"; - await waitFor(socket, "message"); // Engine.IO handshake - - socket.send("40"); - await waitFor(socket, "message"); // Socket.IO handshake - await waitFor(socket, "message"); // "auth" packet return socket; } @@ -251,7 +246,11 @@ describe("Engine.IO protocol", () => { const pollContent = await pollResponse.text(); - expect(pollContent).to.eql("1:3"); + if (i === 0) { + expect(pollContent).to.eql("2:401:3"); + } else { + expect(pollContent).to.eql("1:3"); + } } }); @@ -275,6 +274,7 @@ describe("Engine.IO protocol", () => { ); await waitFor(socket, "message"); // handshake + await waitFor(socket, "message"); // connect for (let i = 0; i < 3; i++) { socket.send("2"); @@ -314,7 +314,7 @@ describe("Engine.IO protocol", () => { const pullContent = await pollResponse.text(); - expect(pullContent).to.eql("1:6"); + expect(pullContent).to.eql("2:40"); const pollResponse2 = await fetch( `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` @@ -413,37 +413,9 @@ describe("Socket.IO protocol", () => { expect(data).to.startsWith("40"); - const handshake = JSON.parse(data.substring(2)); - - expect(handshake).to.have.all.keys("sid"); - expect(handshake.sid).to.be.a("string"); - - const authPacket = await waitFor(socket, "message"); + const connectionPacket = await waitFor(socket, "message"); - expect(authPacket.data).to.eql('42["auth",{}]'); - }); - - it("should allow connection to the main namespace with a payload", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); - - await waitFor(socket, "message"); // Engine.IO handshake - - socket.send('40{"token":"123"}'); - - const { data } = await waitFor(socket, "message"); - - expect(data).to.startsWith("40"); - - const handshake = JSON.parse(data.substring(2)); - - expect(handshake).to.have.all.keys("sid"); - expect(handshake.sid).to.be.a("string"); - - const authPacket = await waitFor(socket, "message"); - - expect(authPacket.data).to.eql('42["auth",{"token":"123"}]'); + expect(connectionPacket.data).to.eql('40'); }); it("should allow connection to a custom namespace", async () => { @@ -457,40 +429,9 @@ describe("Socket.IO protocol", () => { const { data } = await waitFor(socket, "message"); - expect(data).to.startsWith("40/custom,"); - - const handshake = JSON.parse(data.substring(10)); - - expect(handshake).to.have.all.keys("sid"); - expect(handshake.sid).to.be.a("string"); - - const authPacket = await waitFor(socket, "message"); - - expect(authPacket.data).to.eql('42/custom,["auth",{}]'); + expect(data).to.startsWith("40/custom"); }); - it("should allow connection to a custom namespace with a payload", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); - - await waitFor(socket, "message"); // Engine.IO handshake - - socket.send('40/custom,{"token":"abc"}'); - - const { data } = await waitFor(socket, "message"); - - expect(data).to.startsWith("40/custom,"); - - const handshake = JSON.parse(data.substring(10)); - - expect(handshake).to.have.all.keys("sid"); - expect(handshake.sid).to.be.a("string"); - - const authPacket = await waitFor(socket, "message"); - - expect(authPacket.data).to.eql('42/custom,["auth",{"token":"abc"}]'); - }); it("should disallow connection to an unknown namespace", async () => { const socket = new WebSocket( From 11a7b75417babd643ea055d606a05f9efcbddff0 Mon Sep 17 00:00:00 2001 From: Totodore Date: Sun, 15 Oct 2023 00:11:39 +0200 Subject: [PATCH 06/14] change ns test to should be connected by default to the main namespace --- test-suite/test-suite.js | 829 +++++++++++++++++++-------------------- 1 file changed, 413 insertions(+), 416 deletions(-) diff --git a/test-suite/test-suite.js b/test-suite/test-suite.js index 41546e1..55170ff 100644 --- a/test-suite/test-suite.js +++ b/test-suite/test-suite.js @@ -1,8 +1,8 @@ const isNodejs = typeof window === "undefined"; if (isNodejs) { - // make the tests runnable in both the browser and Node.js - await import("./node-imports.js"); + // make the tests runnable in both the browser and Node.js + await import("./node-imports.js"); } const { expect } = chai; @@ -14,561 +14,558 @@ const PING_INTERVAL = 300; const PING_TIMEOUT = 200; function sleep(delay) { - return new Promise((resolve) => setTimeout(resolve, delay)); + return new Promise((resolve) => setTimeout(resolve, delay)); } function waitFor(socket, eventType) { - return new Promise((resolve) => { - socket.addEventListener( - eventType, - (event) => { - resolve(event); - }, - { once: true } - ); - }); + return new Promise((resolve) => { + socket.addEventListener( + eventType, + (event) => { + resolve(event); + }, + { once: true } + ); + }); } function waitForPackets(socket, count) { - const packets = []; - - return new Promise((resolve) => { - const handler = (event) => { - if (event.data === "2") { - // ignore PING packets - return; - } - packets.push(event.data); - if (packets.length === count) { - socket.removeEventListener("message", handler); - resolve(packets); - } - }; - socket.addEventListener("message", handler); - }); + const packets = []; + + return new Promise((resolve) => { + const handler = (event) => { + if (event.data === "2") { + // ignore PING packets + return; + } + packets.push(event.data); + if (packets.length === count) { + socket.removeEventListener("message", handler); + resolve(packets); + } + }; + socket.addEventListener("message", handler); + }); } function decodePayload(payload) { - const firstColonIndex = payload.indexOf(":"); - const length = payload.substring(0, firstColonIndex); - const packet = payload.substring(firstColonIndex + 1); - return [length, packet]; + const firstColonIndex = payload.indexOf(":"); + const length = payload.substring(0, firstColonIndex); + const packet = payload.substring(firstColonIndex + 1); + return [length, packet]; } async function initLongPollingSession() { - const response = await fetch(`${URL}/socket.io/?EIO=3&transport=polling`); - const text = await response.text(); - const [, content] = decodePayload(text); - return JSON.parse(content.substring(1)).sid; + const response = await fetch(`${URL}/socket.io/?EIO=3&transport=polling`); + const text = await response.text(); + const [, content] = decodePayload(text); + return JSON.parse(content.substring(1)).sid; } async function initSocketIOConnection() { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); - socket.binaryType = "arraybuffer"; + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); + socket.binaryType = "arraybuffer"; - await waitFor(socket, "message"); // Socket.IO handshake + await waitFor(socket, "message"); // Socket.IO handshake - return socket; + return socket; } describe("Engine.IO protocol", () => { - describe("handshake", () => { - describe("HTTP long-polling", () => { - it("should successfully open a session", async () => { - const response = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling` - ); - - expect(response.status).to.eql(200); - - const text = await response.text(); - const [length, content] = decodePayload(text); - - expect(length).to.eql(content.length.toString()); - expect(content).to.startsWith("0"); - - const value = JSON.parse(content.substring(1)); - - expect(value).to.have.all.keys( - "sid", - "upgrades", - "pingInterval", - "pingTimeout", - "maxPayload" - ); - expect(value.sid).to.be.a("string"); - expect(value.upgrades).to.eql(["websocket"]); - expect(value.pingInterval).to.eql(PING_INTERVAL); - expect(value.pingTimeout).to.eql(PING_TIMEOUT); - expect(value.maxPayload).to.eql(1000000); - }); - - it("should fail with an invalid 'EIO' query parameter", async () => { - const response = await fetch(`${URL}/socket.io/?transport=polling`); - - expect(response.status).to.eql(400); - - const response2 = await fetch( - `${URL}/socket.io/?EIO=abc&transport=polling` - ); - - expect(response2.status).to.eql(400); - }); - - it("should fail with an invalid 'transport' query parameter", async () => { - const response = await fetch(`${URL}/socket.io/?EIO=3`); - - expect(response.status).to.eql(400); - - const response2 = await fetch(`${URL}/socket.io/?EIO=3&transport=abc`); - - expect(response2.status).to.eql(400); - }); - - it("should fail with an invalid request method", async () => { - const response = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling`, - { - method: "post", - } - ); - - expect(response.status).to.eql(400); - - const response2 = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling`, - { - method: "put", - } - ); - - expect(response2.status).to.eql(400); - }); - }); - - describe("WebSocket", () => { - it("should successfully open a session", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); + describe("handshake", () => { + describe("HTTP long-polling", () => { + it("should successfully open a session", async () => { + const response = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling` + ); + + expect(response.status).to.eql(200); + + const text = await response.text(); + const [length, content] = decodePayload(text); + + expect(length).to.eql(content.length.toString()); + expect(content).to.startsWith("0"); + + const value = JSON.parse(content.substring(1)); + + expect(value).to.have.all.keys( + "sid", + "upgrades", + "pingInterval", + "pingTimeout", + "maxPayload" + ); + expect(value.sid).to.be.a("string"); + expect(value.upgrades).to.eql(["websocket"]); + expect(value.pingInterval).to.eql(PING_INTERVAL); + expect(value.pingTimeout).to.eql(PING_TIMEOUT); + expect(value.maxPayload).to.eql(1000000); + }); - const { data } = await waitFor(socket, "message"); + it("should fail with an invalid 'EIO' query parameter", async () => { + const response = await fetch(`${URL}/socket.io/?transport=polling`); - expect(data).to.startsWith("0"); + expect(response.status).to.eql(400); + + const response2 = await fetch( + `${URL}/socket.io/?EIO=abc&transport=polling` + ); + + expect(response2.status).to.eql(400); + }); - const value = JSON.parse(data.substring(1)); + it("should fail with an invalid 'transport' query parameter", async () => { + const response = await fetch(`${URL}/socket.io/?EIO=3`); + + expect(response.status).to.eql(400); + + const response2 = await fetch(`${URL}/socket.io/?EIO=3&transport=abc`); + + expect(response2.status).to.eql(400); + }); + + it("should fail with an invalid request method", async () => { + const response = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling`, + { + method: "post", + } + ); + + expect(response.status).to.eql(400); + + const response2 = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling`, + { + method: "put", + } + ); + + expect(response2.status).to.eql(400); + }); + }); + + describe("WebSocket", () => { + it("should successfully open a session", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); + + const { data } = await waitFor(socket, "message"); + + expect(data).to.startsWith("0"); + + const value = JSON.parse(data.substring(1)); + + expect(value).to.have.all.keys( + "sid", + "upgrades", + "pingInterval", + "pingTimeout", + "maxPayload" + ); + expect(value.sid).to.be.a("string"); + expect(value.upgrades).to.eql([]); + expect(value.pingInterval).to.eql(PING_INTERVAL); + expect(value.pingTimeout).to.eql(PING_TIMEOUT); + expect(value.maxPayload).to.eql(1000000); + + socket.close(); + }); - expect(value).to.have.all.keys( - "sid", - "upgrades", - "pingInterval", - "pingTimeout", - "maxPayload" - ); - expect(value.sid).to.be.a("string"); - expect(value.upgrades).to.eql([]); - expect(value.pingInterval).to.eql(PING_INTERVAL); - expect(value.pingTimeout).to.eql(PING_TIMEOUT); - expect(value.maxPayload).to.eql(1000000); - - socket.close(); - }); - - it("should fail with an invalid 'EIO' query parameter", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?transport=websocket` - ); + it("should fail with an invalid 'EIO' query parameter", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?transport=websocket` + ); - if (isNodejs) { - socket.on("error", () => {}); - } + if (isNodejs) { + socket.on("error", () => { }); + } - waitFor(socket, "close"); + waitFor(socket, "close"); + + const socket2 = new WebSocket( + `${WS_URL}/socket.io/?EIO=abc&transport=websocket` + ); + + if (isNodejs) { + socket2.on("error", () => { }); + } + + waitFor(socket2, "close"); + }); - const socket2 = new WebSocket( - `${WS_URL}/socket.io/?EIO=abc&transport=websocket` - ); + it("should fail with an invalid 'transport' query parameter", async () => { + const socket = new WebSocket(`${WS_URL}/socket.io/?EIO=3`); - if (isNodejs) { - socket2.on("error", () => {}); - } + if (isNodejs) { + socket.on("error", () => { }); + } - waitFor(socket2, "close"); - }); + waitFor(socket, "close"); - it("should fail with an invalid 'transport' query parameter", async () => { - const socket = new WebSocket(`${WS_URL}/socket.io/?EIO=3`); + const socket2 = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=abc` + ); - if (isNodejs) { - socket.on("error", () => {}); - } + if (isNodejs) { + socket2.on("error", () => { }); + } - waitFor(socket, "close"); - - const socket2 = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=abc` - ); - - if (isNodejs) { - socket2.on("error", () => {}); - } - - waitFor(socket2, "close"); - }); + waitFor(socket2, "close"); + }); + }); }); - }); - describe("heartbeat", function () { - this.timeout(5000); + describe("heartbeat", function () { + this.timeout(5000); - describe("HTTP long-polling", () => { - it("should send ping/pong packets", async () => { - const sid = await initLongPollingSession(); + describe("HTTP long-polling", () => { + it("should send ping/pong packets", async () => { + const sid = await initLongPollingSession(); - for (let i = 0; i < 3; i++) { - const pushResponse = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}`, - { - method: "post", - body: "1:2", - } - ); + for (let i = 0; i < 3; i++) { + const pushResponse = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}`, + { + method: "post", + body: "1:2", + } + ); - expect(pushResponse.status).to.eql(200); + expect(pushResponse.status).to.eql(200); - const pollResponse = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` - ); + const pollResponse = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` + ); - expect(pollResponse.status).to.eql(200); + expect(pollResponse.status).to.eql(200); - const pollContent = await pollResponse.text(); + const pollContent = await pollResponse.text(); - if (i === 0) { - expect(pollContent).to.eql("2:401:3"); - } else { - expect(pollContent).to.eql("1:3"); - } - } - }); + if (i === 0) { + expect(pollContent).to.eql("2:401:3"); + } else { + expect(pollContent).to.eql("1:3"); + } + } + }); - it("should close the session upon ping timeout", async () => { - const sid = await initLongPollingSession(); + it("should close the session upon ping timeout", async () => { + const sid = await initLongPollingSession(); - await sleep(PING_INTERVAL + PING_TIMEOUT); + await sleep(PING_INTERVAL + PING_TIMEOUT); - const pollResponse = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` - ); + const pollResponse = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` + ); - expect(pollResponse.status).to.eql(400); - }); - }); + expect(pollResponse.status).to.eql(400); + }); + }); - describe("WebSocket", () => { - it("should send ping/pong packets", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); + describe("WebSocket", () => { + it("should send ping/pong packets", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); - await waitFor(socket, "message"); // handshake - await waitFor(socket, "message"); // connect + await waitFor(socket, "message"); // handshake + await waitFor(socket, "message"); // connect - for (let i = 0; i < 3; i++) { - socket.send("2"); + for (let i = 0; i < 3; i++) { + socket.send("2"); - const { data } = await waitFor(socket, "message"); + const { data } = await waitFor(socket, "message"); - expect(data).to.eql("3"); - } + expect(data).to.eql("3"); + } - socket.close(); - }); + socket.close(); + }); - it("should close the session upon ping timeout", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); + it("should close the session upon ping timeout", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); - await waitFor(socket, "close"); // handshake - }); + await waitFor(socket, "close"); // handshake + }); + }); }); - }); - describe("close", () => { - describe("HTTP long-polling", () => { - it("should forcefully close the session", async () => { - const sid = await initLongPollingSession(); + describe("close", () => { + describe("HTTP long-polling", () => { + it("should forcefully close the session", async () => { + const sid = await initLongPollingSession(); - const [pollResponse] = await Promise.all([ - fetch(`${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}`), - fetch(`${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}`, { - method: "post", - body: "1:1", - }), - ]); + const [pollResponse] = await Promise.all([ + fetch(`${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}`), + fetch(`${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}`, { + method: "post", + body: "1:1", + }), + ]); - expect(pollResponse.status).to.eql(200); + expect(pollResponse.status).to.eql(200); - const pullContent = await pollResponse.text(); + const pullContent = await pollResponse.text(); - expect(pullContent).to.eql("2:40"); + expect(pullContent).to.eql("2:40"); - const pollResponse2 = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` - ); + const pollResponse2 = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` + ); - expect(pollResponse2.status).to.eql(400); - }); - }); + expect(pollResponse2.status).to.eql(400); + }); + }); - describe("WebSocket", () => { - it("should forcefully close the session", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); + describe("WebSocket", () => { + it("should forcefully close the session", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); - await waitFor(socket, "message"); // handshake + await waitFor(socket, "message"); // handshake - socket.send("1"); + socket.send("1"); - await waitFor(socket, "close"); - }); + await waitFor(socket, "close"); + }); + }); }); - }); - describe("upgrade", () => { - it("should successfully upgrade from HTTP long-polling to WebSocket", async () => { - const sid = await initLongPollingSession(); + describe("upgrade", () => { + it("should successfully upgrade from HTTP long-polling to WebSocket", async () => { + const sid = await initLongPollingSession(); - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` - ); + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` + ); - await waitFor(socket, "open"); + await waitFor(socket, "open"); - // send probe - socket.send("2probe"); + // send probe + socket.send("2probe"); - const probeResponse = await waitFor(socket, "message"); + const probeResponse = await waitFor(socket, "message"); - expect(probeResponse.data).to.eql("3probe"); + expect(probeResponse.data).to.eql("3probe"); - // complete upgrade - socket.send("5"); - }); + // complete upgrade + socket.send("5"); + }); - it("should ignore HTTP requests with same sid after upgrade", async () => { - const sid = await initLongPollingSession(); + it("should ignore HTTP requests with same sid after upgrade", async () => { + const sid = await initLongPollingSession(); - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` - ); + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` + ); - await waitFor(socket, "open"); - socket.send("2probe"); - socket.send("5"); + await waitFor(socket, "open"); + socket.send("2probe"); + socket.send("5"); - const pollResponse = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` - ); + const pollResponse = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` + ); - expect(pollResponse.status).to.eql(400); - }); + expect(pollResponse.status).to.eql(400); + }); - it("should ignore WebSocket connection with same sid after upgrade", async () => { - const sid = await initLongPollingSession(); + it("should ignore WebSocket connection with same sid after upgrade", async () => { + const sid = await initLongPollingSession(); - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` - ); + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` + ); - await waitFor(socket, "open"); - socket.send("2probe"); - socket.send("5"); + await waitFor(socket, "open"); + socket.send("2probe"); + socket.send("5"); - const socket2 = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` - ); + const socket2 = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` + ); - await waitFor(socket2, "close"); + await waitFor(socket2, "close"); + }); }); - }); }); describe("Socket.IO protocol", () => { - describe("connect", () => { - it("should allow connection to the main namespace", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); + describe("connect", () => { + it("should be connected by default to the main namespace", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); - await waitFor(socket, "message"); // Engine.IO handshake + await waitFor(socket, "message"); // Engine.IO handshake - socket.send("40"); + const { data } = await waitFor(socket, "message"); // Socket.IO / namespace handshake + expect(data).to.startsWith("40"); - const { data } = await waitFor(socket, "message"); + socket.send('42["message","message to main namespace"]'); + const { data: echo } = await waitFor(socket, "message"); + expect(echo).to.eql('42["message-back","message to main namespace"]'); + }); - expect(data).to.startsWith("40"); + it("should allow connection to a custom namespace", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); - const connectionPacket = await waitFor(socket, "message"); + await waitFor(socket, "message"); // Engine.IO handshake - expect(connectionPacket.data).to.eql('40'); - }); + socket.send("40/custom,"); - it("should allow connection to a custom namespace", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); + const { data } = await waitFor(socket, "message"); - await waitFor(socket, "message"); // Engine.IO handshake + expect(data).to.startsWith("40/custom"); + }); - socket.send("40/custom,"); - const { data } = await waitFor(socket, "message"); + it("should disallow connection to an unknown namespace", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); - expect(data).to.startsWith("40/custom"); - }); + await waitFor(socket, "message"); // Engine.IO handshake + socket.send("40/random"); - it("should disallow connection to an unknown namespace", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); + const { data } = await waitFor(socket, "message"); - await waitFor(socket, "message"); // Engine.IO handshake + expect(data).to.eql('44/random,{"message":"Invalid namespace"}'); + }); - socket.send("40/random"); + it("should disallow connection with an invalid handshake", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); - const { data } = await waitFor(socket, "message"); + await waitFor(socket, "message"); // Engine.IO handshake - expect(data).to.eql('44/random,{"message":"Invalid namespace"}'); + socket.send("4abc"); + + await waitFor(socket, "close"); + }); }); - it("should disallow connection with an invalid handshake", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); + describe("disconnect", () => { + it("should disconnect from the main namespace", async () => { + const socket = await initSocketIOConnection(); - await waitFor(socket, "message"); // Engine.IO handshake + socket.send("41"); - socket.send("4abc"); + await waitFor(socket, "close"); + }); - await waitFor(socket, "close"); - }); - }); + it("should connect then disconnect from a custom namespace", async () => { + const socket = await initSocketIOConnection(); - describe("disconnect", () => { - it("should disconnect from the main namespace", async () => { - const socket = await initSocketIOConnection(); + socket.send("40/custom"); - socket.send("41"); + await waitFor(socket, "message"); // Socket.IO handshake + await waitFor(socket, "message"); // auth packet - await waitFor(socket, "close"); - }); + socket.send("41/custom"); + socket.send('42["message","message to main namespace"]'); - it("should connect then disconnect from a custom namespace", async () => { - const socket = await initSocketIOConnection(); + const { data } = await waitFor(socket, "message"); - socket.send("40/custom"); - - await waitFor(socket, "message"); // Socket.IO handshake - await waitFor(socket, "message"); // auth packet - - socket.send("41/custom"); - socket.send('42["message","message to main namespace"]'); - - const { data } = await waitFor(socket, "message"); - - expect(data).to.eql('42["message-back","message to main namespace"]'); + expect(data).to.eql('42["message-back","message to main namespace"]'); + }); }); - }); - describe("message", () => { - it("should send a plain-text packet", async () => { - const socket = await initSocketIOConnection(); + describe("message", () => { + it("should send a plain-text packet", async () => { + const socket = await initSocketIOConnection(); - socket.send('42["message",1,"2",{"3":[true]}]'); + socket.send('42["message",1,"2",{"3":[true]}]'); - const { data } = await waitFor(socket, "message"); + const { data } = await waitFor(socket, "message"); - expect(data).to.eql('42["message-back",1,"2",{"3":[true]}]'); - }); + expect(data).to.eql('42["message-back",1,"2",{"3":[true]}]'); + }); - it("should send a packet with binary attachments", async () => { - const socket = await initSocketIOConnection(); + it("should send a packet with binary attachments", async () => { + const socket = await initSocketIOConnection(); - socket.send( - '452-["message",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' - ); - socket.send(Uint8Array.from([1, 2, 3])); - socket.send(Uint8Array.from([4, 5, 6])); + socket.send( + '452-["message",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' + ); + socket.send(Uint8Array.from([1, 2, 3])); + socket.send(Uint8Array.from([4, 5, 6])); - const packets = await waitForPackets(socket, 3); + const packets = await waitForPackets(socket, 3); - expect(packets[0]).to.eql( - '452-["message-back",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' - ); - expect(packets[1]).to.eql(Uint8Array.from([1, 2, 3]).buffer); - expect(packets[2]).to.eql(Uint8Array.from([4, 5, 6]).buffer); + expect(packets[0]).to.eql( + '452-["message-back",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' + ); + expect(packets[1]).to.eql(Uint8Array.from([1, 2, 3]).buffer); + expect(packets[2]).to.eql(Uint8Array.from([4, 5, 6]).buffer); - socket.close(); - }); + socket.close(); + }); - it("should send a plain-text packet with an ack", async () => { - const socket = await initSocketIOConnection(); + it("should send a plain-text packet with an ack", async () => { + const socket = await initSocketIOConnection(); - socket.send('42456["message-with-ack",1,"2",{"3":[false]}]'); + socket.send('42456["message-with-ack",1,"2",{"3":[false]}]'); - const { data } = await waitFor(socket, "message"); + const { data } = await waitFor(socket, "message"); - expect(data).to.eql('43456[1,"2",{"3":[false]}]'); - }); + expect(data).to.eql('43456[1,"2",{"3":[false]}]'); + }); - it("should send a packet with binary attachments and an ack", async () => { - const socket = await initSocketIOConnection(); + it("should send a packet with binary attachments and an ack", async () => { + const socket = await initSocketIOConnection(); - socket.send( - '452-789["message-with-ack",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' - ); - socket.send(Uint8Array.from([1, 2, 3])); - socket.send(Uint8Array.from([4, 5, 6])); + socket.send( + '452-789["message-with-ack",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' + ); + socket.send(Uint8Array.from([1, 2, 3])); + socket.send(Uint8Array.from([4, 5, 6])); - const packets = await waitForPackets(socket, 3); + const packets = await waitForPackets(socket, 3); - expect(packets[0]).to.eql( - '462-789[{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' - ); - expect(packets[1]).to.eql(Uint8Array.from([1, 2, 3]).buffer); - expect(packets[2]).to.eql(Uint8Array.from([4, 5, 6]).buffer); + expect(packets[0]).to.eql( + '462-789[{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' + ); + expect(packets[1]).to.eql(Uint8Array.from([1, 2, 3]).buffer); + expect(packets[2]).to.eql(Uint8Array.from([4, 5, 6]).buffer); - socket.close(); - }); + socket.close(); + }); - it("should close the connection upon invalid format (unknown packet type)", async () => { - const socket = await initSocketIOConnection(); + it("should close the connection upon invalid format (unknown packet type)", async () => { + const socket = await initSocketIOConnection(); - socket.send("4abc"); + socket.send("4abc"); - await waitFor(socket, "close"); - }); + await waitFor(socket, "close"); + }); - it("should close the connection upon invalid format (invalid payload format)", async () => { - const socket = await initSocketIOConnection(); + it("should close the connection upon invalid format (invalid payload format)", async () => { + const socket = await initSocketIOConnection(); - socket.send("42{}"); + socket.send("42{}"); - await waitFor(socket, "close"); - }); + await waitFor(socket, "close"); + }); - it("should close the connection upon invalid format (invalid ack id)", async () => { - const socket = await initSocketIOConnection(); + it("should close the connection upon invalid format (invalid ack id)", async () => { + const socket = await initSocketIOConnection(); - socket.send('42abc["message-with-ack",1,"2",{"3":[false]}]'); + socket.send('42abc["message-with-ack",1,"2",{"3":[false]}]'); - await waitFor(socket, "close"); + await waitFor(socket, "close"); + }); }); - }); }); From 5a3bf1577760e9f2677a9c03a85b419615908faa Mon Sep 17 00:00:00 2001 From: Totodore Date: Sun, 15 Oct 2023 14:15:48 +0200 Subject: [PATCH 07/14] chore: reformat test-suite.js --- test-suite/test-suite.js | 824 +++++++++++++++++++-------------------- 1 file changed, 412 insertions(+), 412 deletions(-) diff --git a/test-suite/test-suite.js b/test-suite/test-suite.js index 55170ff..117a265 100644 --- a/test-suite/test-suite.js +++ b/test-suite/test-suite.js @@ -1,8 +1,8 @@ const isNodejs = typeof window === "undefined"; if (isNodejs) { - // make the tests runnable in both the browser and Node.js - await import("./node-imports.js"); + // make the tests runnable in both the browser and Node.js + await import("./node-imports.js"); } const { expect } = chai; @@ -14,558 +14,558 @@ const PING_INTERVAL = 300; const PING_TIMEOUT = 200; function sleep(delay) { - return new Promise((resolve) => setTimeout(resolve, delay)); + return new Promise((resolve) => setTimeout(resolve, delay)); } function waitFor(socket, eventType) { - return new Promise((resolve) => { - socket.addEventListener( - eventType, - (event) => { - resolve(event); - }, - { once: true } - ); - }); + return new Promise((resolve) => { + socket.addEventListener( + eventType, + (event) => { + resolve(event); + }, + { once: true } + ); + }); } function waitForPackets(socket, count) { - const packets = []; - - return new Promise((resolve) => { - const handler = (event) => { - if (event.data === "2") { - // ignore PING packets - return; - } - packets.push(event.data); - if (packets.length === count) { - socket.removeEventListener("message", handler); - resolve(packets); - } - }; - socket.addEventListener("message", handler); - }); + const packets = []; + + return new Promise((resolve) => { + const handler = (event) => { + if (event.data === "2") { + // ignore PING packets + return; + } + packets.push(event.data); + if (packets.length === count) { + socket.removeEventListener("message", handler); + resolve(packets); + } + }; + socket.addEventListener("message", handler); + }); } function decodePayload(payload) { - const firstColonIndex = payload.indexOf(":"); - const length = payload.substring(0, firstColonIndex); - const packet = payload.substring(firstColonIndex + 1); - return [length, packet]; + const firstColonIndex = payload.indexOf(":"); + const length = payload.substring(0, firstColonIndex); + const packet = payload.substring(firstColonIndex + 1); + return [length, packet]; } async function initLongPollingSession() { - const response = await fetch(`${URL}/socket.io/?EIO=3&transport=polling`); - const text = await response.text(); - const [, content] = decodePayload(text); - return JSON.parse(content.substring(1)).sid; + const response = await fetch(`${URL}/socket.io/?EIO=3&transport=polling`); + const text = await response.text(); + const [, content] = decodePayload(text); + return JSON.parse(content.substring(1)).sid; } async function initSocketIOConnection() { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); - socket.binaryType = "arraybuffer"; + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); + socket.binaryType = "arraybuffer"; - await waitFor(socket, "message"); // Socket.IO handshake + await waitFor(socket, "message"); // Socket.IO handshake - return socket; + return socket; } describe("Engine.IO protocol", () => { - describe("handshake", () => { - describe("HTTP long-polling", () => { - it("should successfully open a session", async () => { - const response = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling` - ); - - expect(response.status).to.eql(200); - - const text = await response.text(); - const [length, content] = decodePayload(text); - - expect(length).to.eql(content.length.toString()); - expect(content).to.startsWith("0"); - - const value = JSON.parse(content.substring(1)); - - expect(value).to.have.all.keys( - "sid", - "upgrades", - "pingInterval", - "pingTimeout", - "maxPayload" - ); - expect(value.sid).to.be.a("string"); - expect(value.upgrades).to.eql(["websocket"]); - expect(value.pingInterval).to.eql(PING_INTERVAL); - expect(value.pingTimeout).to.eql(PING_TIMEOUT); - expect(value.maxPayload).to.eql(1000000); - }); + describe("handshake", () => { + describe("HTTP long-polling", () => { + it("should successfully open a session", async () => { + const response = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling` + ); + + expect(response.status).to.eql(200); + + const text = await response.text(); + const [length, content] = decodePayload(text); + + expect(length).to.eql(content.length.toString()); + expect(content).to.startsWith("0"); - it("should fail with an invalid 'EIO' query parameter", async () => { - const response = await fetch(`${URL}/socket.io/?transport=polling`); + const value = JSON.parse(content.substring(1)); - expect(response.status).to.eql(400); - - const response2 = await fetch( - `${URL}/socket.io/?EIO=abc&transport=polling` - ); - - expect(response2.status).to.eql(400); - }); + expect(value).to.have.all.keys( + "sid", + "upgrades", + "pingInterval", + "pingTimeout", + "maxPayload" + ); + expect(value.sid).to.be.a("string"); + expect(value.upgrades).to.eql(["websocket"]); + expect(value.pingInterval).to.eql(PING_INTERVAL); + expect(value.pingTimeout).to.eql(PING_TIMEOUT); + expect(value.maxPayload).to.eql(1000000); + }); + + it("should fail with an invalid 'EIO' query parameter", async () => { + const response = await fetch(`${URL}/socket.io/?transport=polling`); - it("should fail with an invalid 'transport' query parameter", async () => { - const response = await fetch(`${URL}/socket.io/?EIO=3`); - - expect(response.status).to.eql(400); - - const response2 = await fetch(`${URL}/socket.io/?EIO=3&transport=abc`); - - expect(response2.status).to.eql(400); - }); - - it("should fail with an invalid request method", async () => { - const response = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling`, - { - method: "post", - } - ); - - expect(response.status).to.eql(400); - - const response2 = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling`, - { - method: "put", - } - ); - - expect(response2.status).to.eql(400); - }); - }); - - describe("WebSocket", () => { - it("should successfully open a session", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); - - const { data } = await waitFor(socket, "message"); - - expect(data).to.startsWith("0"); - - const value = JSON.parse(data.substring(1)); - - expect(value).to.have.all.keys( - "sid", - "upgrades", - "pingInterval", - "pingTimeout", - "maxPayload" - ); - expect(value.sid).to.be.a("string"); - expect(value.upgrades).to.eql([]); - expect(value.pingInterval).to.eql(PING_INTERVAL); - expect(value.pingTimeout).to.eql(PING_TIMEOUT); - expect(value.maxPayload).to.eql(1000000); - - socket.close(); - }); + expect(response.status).to.eql(400); - it("should fail with an invalid 'EIO' query parameter", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?transport=websocket` - ); + const response2 = await fetch( + `${URL}/socket.io/?EIO=abc&transport=polling` + ); - if (isNodejs) { - socket.on("error", () => { }); - } + expect(response2.status).to.eql(400); + }); - waitFor(socket, "close"); - - const socket2 = new WebSocket( - `${WS_URL}/socket.io/?EIO=abc&transport=websocket` - ); - - if (isNodejs) { - socket2.on("error", () => { }); - } - - waitFor(socket2, "close"); - }); + it("should fail with an invalid 'transport' query parameter", async () => { + const response = await fetch(`${URL}/socket.io/?EIO=3`); - it("should fail with an invalid 'transport' query parameter", async () => { - const socket = new WebSocket(`${WS_URL}/socket.io/?EIO=3`); + expect(response.status).to.eql(400); - if (isNodejs) { - socket.on("error", () => { }); - } + const response2 = await fetch(`${URL}/socket.io/?EIO=3&transport=abc`); - waitFor(socket, "close"); + expect(response2.status).to.eql(400); + }); - const socket2 = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=abc` - ); + it("should fail with an invalid request method", async () => { + const response = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling`, + { + method: "post", + } + ); - if (isNodejs) { - socket2.on("error", () => { }); - } + expect(response.status).to.eql(400); - waitFor(socket2, "close"); - }); - }); + const response2 = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling`, + { + method: "put", + } + ); + + expect(response2.status).to.eql(400); + }); }); - describe("heartbeat", function () { - this.timeout(5000); + describe("WebSocket", () => { + it("should successfully open a session", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); + + const { data } = await waitFor(socket, "message"); + + expect(data).to.startsWith("0"); + + const value = JSON.parse(data.substring(1)); + + expect(value).to.have.all.keys( + "sid", + "upgrades", + "pingInterval", + "pingTimeout", + "maxPayload" + ); + expect(value.sid).to.be.a("string"); + expect(value.upgrades).to.eql([]); + expect(value.pingInterval).to.eql(PING_INTERVAL); + expect(value.pingTimeout).to.eql(PING_TIMEOUT); + expect(value.maxPayload).to.eql(1000000); + + socket.close(); + }); + + it("should fail with an invalid 'EIO' query parameter", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?transport=websocket` + ); + + if (isNodejs) { + socket.on("error", () => { }); + } + + waitFor(socket, "close"); + + const socket2 = new WebSocket( + `${WS_URL}/socket.io/?EIO=abc&transport=websocket` + ); - describe("HTTP long-polling", () => { - it("should send ping/pong packets", async () => { - const sid = await initLongPollingSession(); + if (isNodejs) { + socket2.on("error", () => { }); + } - for (let i = 0; i < 3; i++) { - const pushResponse = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}`, - { - method: "post", - body: "1:2", - } - ); + waitFor(socket2, "close"); + }); - expect(pushResponse.status).to.eql(200); + it("should fail with an invalid 'transport' query parameter", async () => { + const socket = new WebSocket(`${WS_URL}/socket.io/?EIO=3`); - const pollResponse = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` - ); + if (isNodejs) { + socket.on("error", () => { }); + } - expect(pollResponse.status).to.eql(200); + waitFor(socket, "close"); - const pollContent = await pollResponse.text(); + const socket2 = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=abc` + ); - if (i === 0) { - expect(pollContent).to.eql("2:401:3"); - } else { - expect(pollContent).to.eql("1:3"); - } - } - }); + if (isNodejs) { + socket2.on("error", () => { }); + } - it("should close the session upon ping timeout", async () => { - const sid = await initLongPollingSession(); + waitFor(socket2, "close"); + }); + }); + }); - await sleep(PING_INTERVAL + PING_TIMEOUT); + describe("heartbeat", function () { + this.timeout(5000); - const pollResponse = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` - ); + describe("HTTP long-polling", () => { + it("should send ping/pong packets", async () => { + const sid = await initLongPollingSession(); - expect(pollResponse.status).to.eql(400); - }); - }); + for (let i = 0; i < 3; i++) { + const pushResponse = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}`, + { + method: "post", + body: "1:2", + } + ); - describe("WebSocket", () => { - it("should send ping/pong packets", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); + expect(pushResponse.status).to.eql(200); - await waitFor(socket, "message"); // handshake - await waitFor(socket, "message"); // connect + const pollResponse = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` + ); - for (let i = 0; i < 3; i++) { - socket.send("2"); + expect(pollResponse.status).to.eql(200); - const { data } = await waitFor(socket, "message"); + const pollContent = await pollResponse.text(); - expect(data).to.eql("3"); - } + if (i === 0) { + expect(pollContent).to.eql("2:401:3"); + } else { + expect(pollContent).to.eql("1:3"); + } + } + }); - socket.close(); - }); + it("should close the session upon ping timeout", async () => { + const sid = await initLongPollingSession(); - it("should close the session upon ping timeout", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); + await sleep(PING_INTERVAL + PING_TIMEOUT); - await waitFor(socket, "close"); // handshake - }); - }); + const pollResponse = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` + ); + + expect(pollResponse.status).to.eql(400); + }); + }); + + describe("WebSocket", () => { + it("should send ping/pong packets", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); + + await waitFor(socket, "message"); // handshake + await waitFor(socket, "message"); // connect + + for (let i = 0; i < 3; i++) { + socket.send("2"); + + const { data } = await waitFor(socket, "message"); + + expect(data).to.eql("3"); + } + + socket.close(); + }); + + it("should close the session upon ping timeout", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); + + await waitFor(socket, "close"); // handshake + }); }); + }); - describe("close", () => { - describe("HTTP long-polling", () => { - it("should forcefully close the session", async () => { - const sid = await initLongPollingSession(); + describe("close", () => { + describe("HTTP long-polling", () => { + it("should forcefully close the session", async () => { + const sid = await initLongPollingSession(); - const [pollResponse] = await Promise.all([ - fetch(`${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}`), - fetch(`${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}`, { - method: "post", - body: "1:1", - }), - ]); + const [pollResponse] = await Promise.all([ + fetch(`${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}`), + fetch(`${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}`, { + method: "post", + body: "1:1", + }), + ]); - expect(pollResponse.status).to.eql(200); + expect(pollResponse.status).to.eql(200); - const pullContent = await pollResponse.text(); + const pullContent = await pollResponse.text(); - expect(pullContent).to.eql("2:40"); + expect(pullContent).to.eql("2:40"); - const pollResponse2 = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` - ); + const pollResponse2 = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` + ); - expect(pollResponse2.status).to.eql(400); - }); - }); + expect(pollResponse2.status).to.eql(400); + }); + }); - describe("WebSocket", () => { - it("should forcefully close the session", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); + describe("WebSocket", () => { + it("should forcefully close the session", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); - await waitFor(socket, "message"); // handshake + await waitFor(socket, "message"); // handshake - socket.send("1"); + socket.send("1"); - await waitFor(socket, "close"); - }); - }); + await waitFor(socket, "close"); + }); }); + }); - describe("upgrade", () => { - it("should successfully upgrade from HTTP long-polling to WebSocket", async () => { - const sid = await initLongPollingSession(); + describe("upgrade", () => { + it("should successfully upgrade from HTTP long-polling to WebSocket", async () => { + const sid = await initLongPollingSession(); - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` - ); + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` + ); - await waitFor(socket, "open"); + await waitFor(socket, "open"); - // send probe - socket.send("2probe"); + // send probe + socket.send("2probe"); - const probeResponse = await waitFor(socket, "message"); + const probeResponse = await waitFor(socket, "message"); - expect(probeResponse.data).to.eql("3probe"); + expect(probeResponse.data).to.eql("3probe"); - // complete upgrade - socket.send("5"); - }); + // complete upgrade + socket.send("5"); + }); - it("should ignore HTTP requests with same sid after upgrade", async () => { - const sid = await initLongPollingSession(); + it("should ignore HTTP requests with same sid after upgrade", async () => { + const sid = await initLongPollingSession(); - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` - ); + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` + ); - await waitFor(socket, "open"); - socket.send("2probe"); - socket.send("5"); + await waitFor(socket, "open"); + socket.send("2probe"); + socket.send("5"); - const pollResponse = await fetch( - `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` - ); + const pollResponse = await fetch( + `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` + ); - expect(pollResponse.status).to.eql(400); - }); + expect(pollResponse.status).to.eql(400); + }); - it("should ignore WebSocket connection with same sid after upgrade", async () => { - const sid = await initLongPollingSession(); + it("should ignore WebSocket connection with same sid after upgrade", async () => { + const sid = await initLongPollingSession(); - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` - ); + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` + ); - await waitFor(socket, "open"); - socket.send("2probe"); - socket.send("5"); + await waitFor(socket, "open"); + socket.send("2probe"); + socket.send("5"); - const socket2 = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` - ); + const socket2 = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket&sid=${sid}` + ); - await waitFor(socket2, "close"); - }); + await waitFor(socket2, "close"); }); + }); }); describe("Socket.IO protocol", () => { - describe("connect", () => { - it("should be connected by default to the main namespace", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); + describe("connect", () => { + it("should be connected by default to the main namespace", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); - await waitFor(socket, "message"); // Engine.IO handshake + await waitFor(socket, "message"); // Engine.IO handshake - const { data } = await waitFor(socket, "message"); // Socket.IO / namespace handshake - expect(data).to.startsWith("40"); + const { data } = await waitFor(socket, "message"); // Socket.IO / namespace handshake + expect(data).to.startsWith("40"); - socket.send('42["message","message to main namespace"]'); - const { data: echo } = await waitFor(socket, "message"); - expect(echo).to.eql('42["message-back","message to main namespace"]'); - }); + socket.send('42["message","message to main namespace"]'); + const { data: echo } = await waitFor(socket, "message"); + expect(echo).to.eql('42["message-back","message to main namespace"]'); + }); - it("should allow connection to a custom namespace", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); + it("should allow connection to a custom namespace", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); - await waitFor(socket, "message"); // Engine.IO handshake + await waitFor(socket, "message"); // Engine.IO handshake - socket.send("40/custom,"); + socket.send("40/custom,"); - const { data } = await waitFor(socket, "message"); + const { data } = await waitFor(socket, "message"); - expect(data).to.startsWith("40/custom"); - }); + expect(data).to.startsWith("40/custom"); + }); - it("should disallow connection to an unknown namespace", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); + it("should disallow connection to an unknown namespace", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); - await waitFor(socket, "message"); // Engine.IO handshake + await waitFor(socket, "message"); // Engine.IO handshake - socket.send("40/random"); + socket.send("40/random"); - const { data } = await waitFor(socket, "message"); + const { data } = await waitFor(socket, "message"); - expect(data).to.eql('44/random,{"message":"Invalid namespace"}'); - }); + expect(data).to.eql('44/random,{"message":"Invalid namespace"}'); + }); - it("should disallow connection with an invalid handshake", async () => { - const socket = new WebSocket( - `${WS_URL}/socket.io/?EIO=3&transport=websocket` - ); + it("should disallow connection with an invalid handshake", async () => { + const socket = new WebSocket( + `${WS_URL}/socket.io/?EIO=3&transport=websocket` + ); - await waitFor(socket, "message"); // Engine.IO handshake + await waitFor(socket, "message"); // Engine.IO handshake - socket.send("4abc"); + socket.send("4abc"); - await waitFor(socket, "close"); - }); + await waitFor(socket, "close"); }); + }); - describe("disconnect", () => { - it("should disconnect from the main namespace", async () => { - const socket = await initSocketIOConnection(); + describe("disconnect", () => { + it("should disconnect from the main namespace", async () => { + const socket = await initSocketIOConnection(); - socket.send("41"); + socket.send("41"); - await waitFor(socket, "close"); - }); + await waitFor(socket, "close"); + }); - it("should connect then disconnect from a custom namespace", async () => { - const socket = await initSocketIOConnection(); + it("should connect then disconnect from a custom namespace", async () => { + const socket = await initSocketIOConnection(); - socket.send("40/custom"); + socket.send("40/custom"); - await waitFor(socket, "message"); // Socket.IO handshake - await waitFor(socket, "message"); // auth packet + await waitFor(socket, "message"); // Socket.IO handshake + await waitFor(socket, "message"); // auth packet - socket.send("41/custom"); - socket.send('42["message","message to main namespace"]'); + socket.send("41/custom"); + socket.send('42["message","message to main namespace"]'); - const { data } = await waitFor(socket, "message"); + const { data } = await waitFor(socket, "message"); - expect(data).to.eql('42["message-back","message to main namespace"]'); - }); + expect(data).to.eql('42["message-back","message to main namespace"]'); }); + }); - describe("message", () => { - it("should send a plain-text packet", async () => { - const socket = await initSocketIOConnection(); + describe("message", () => { + it("should send a plain-text packet", async () => { + const socket = await initSocketIOConnection(); - socket.send('42["message",1,"2",{"3":[true]}]'); + socket.send('42["message",1,"2",{"3":[true]}]'); - const { data } = await waitFor(socket, "message"); + const { data } = await waitFor(socket, "message"); - expect(data).to.eql('42["message-back",1,"2",{"3":[true]}]'); - }); + expect(data).to.eql('42["message-back",1,"2",{"3":[true]}]'); + }); - it("should send a packet with binary attachments", async () => { - const socket = await initSocketIOConnection(); + it("should send a packet with binary attachments", async () => { + const socket = await initSocketIOConnection(); - socket.send( - '452-["message",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' - ); - socket.send(Uint8Array.from([1, 2, 3])); - socket.send(Uint8Array.from([4, 5, 6])); + socket.send( + '452-["message",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' + ); + socket.send(Uint8Array.from([1, 2, 3])); + socket.send(Uint8Array.from([4, 5, 6])); - const packets = await waitForPackets(socket, 3); + const packets = await waitForPackets(socket, 3); - expect(packets[0]).to.eql( - '452-["message-back",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' - ); - expect(packets[1]).to.eql(Uint8Array.from([1, 2, 3]).buffer); - expect(packets[2]).to.eql(Uint8Array.from([4, 5, 6]).buffer); + expect(packets[0]).to.eql( + '452-["message-back",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' + ); + expect(packets[1]).to.eql(Uint8Array.from([1, 2, 3]).buffer); + expect(packets[2]).to.eql(Uint8Array.from([4, 5, 6]).buffer); - socket.close(); - }); + socket.close(); + }); - it("should send a plain-text packet with an ack", async () => { - const socket = await initSocketIOConnection(); + it("should send a plain-text packet with an ack", async () => { + const socket = await initSocketIOConnection(); - socket.send('42456["message-with-ack",1,"2",{"3":[false]}]'); + socket.send('42456["message-with-ack",1,"2",{"3":[false]}]'); - const { data } = await waitFor(socket, "message"); + const { data } = await waitFor(socket, "message"); - expect(data).to.eql('43456[1,"2",{"3":[false]}]'); - }); + expect(data).to.eql('43456[1,"2",{"3":[false]}]'); + }); - it("should send a packet with binary attachments and an ack", async () => { - const socket = await initSocketIOConnection(); + it("should send a packet with binary attachments and an ack", async () => { + const socket = await initSocketIOConnection(); - socket.send( - '452-789["message-with-ack",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' - ); - socket.send(Uint8Array.from([1, 2, 3])); - socket.send(Uint8Array.from([4, 5, 6])); + socket.send( + '452-789["message-with-ack",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' + ); + socket.send(Uint8Array.from([1, 2, 3])); + socket.send(Uint8Array.from([4, 5, 6])); - const packets = await waitForPackets(socket, 3); + const packets = await waitForPackets(socket, 3); - expect(packets[0]).to.eql( - '462-789[{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' - ); - expect(packets[1]).to.eql(Uint8Array.from([1, 2, 3]).buffer); - expect(packets[2]).to.eql(Uint8Array.from([4, 5, 6]).buffer); + expect(packets[0]).to.eql( + '462-789[{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' + ); + expect(packets[1]).to.eql(Uint8Array.from([1, 2, 3]).buffer); + expect(packets[2]).to.eql(Uint8Array.from([4, 5, 6]).buffer); - socket.close(); - }); + socket.close(); + }); - it("should close the connection upon invalid format (unknown packet type)", async () => { - const socket = await initSocketIOConnection(); + it("should close the connection upon invalid format (unknown packet type)", async () => { + const socket = await initSocketIOConnection(); - socket.send("4abc"); + socket.send("4abc"); - await waitFor(socket, "close"); - }); + await waitFor(socket, "close"); + }); - it("should close the connection upon invalid format (invalid payload format)", async () => { - const socket = await initSocketIOConnection(); + it("should close the connection upon invalid format (invalid payload format)", async () => { + const socket = await initSocketIOConnection(); - socket.send("42{}"); + socket.send("42{}"); - await waitFor(socket, "close"); - }); + await waitFor(socket, "close"); + }); - it("should close the connection upon invalid format (invalid ack id)", async () => { - const socket = await initSocketIOConnection(); + it("should close the connection upon invalid format (invalid ack id)", async () => { + const socket = await initSocketIOConnection(); - socket.send('42abc["message-with-ack",1,"2",{"3":[false]}]'); + socket.send('42abc["message-with-ack",1,"2",{"3":[false]}]'); - await waitFor(socket, "close"); - }); + await waitFor(socket, "close"); }); + }); }); From e144b2bce800ad9f391560148df43d1ae90651db Mon Sep 17 00:00:00 2001 From: Totodore Date: Sun, 15 Oct 2023 15:06:59 +0200 Subject: [PATCH 08/14] fix: wait for ns auth echo message --- test-suite/test-suite.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/test-suite/test-suite.js b/test-suite/test-suite.js index 117a265..cb4938d 100644 --- a/test-suite/test-suite.js +++ b/test-suite/test-suite.js @@ -69,6 +69,8 @@ async function initSocketIOConnection() { socket.binaryType = "arraybuffer"; await waitFor(socket, "message"); // Socket.IO handshake + await waitFor(socket, "message"); // Socket.IO / namespace handshake + await waitFor(socket, "message"); // auth packet return socket; } @@ -247,7 +249,7 @@ describe("Engine.IO protocol", () => { const pollContent = await pollResponse.text(); if (i === 0) { - expect(pollContent).to.eql("2:401:3"); + expect(pollContent).to.eql(`2:4013:42["auth",{}]1:3`); } else { expect(pollContent).to.eql("1:3"); } @@ -275,6 +277,7 @@ describe("Engine.IO protocol", () => { await waitFor(socket, "message"); // handshake await waitFor(socket, "message"); // connect + await waitFor(socket, "message"); // ns auth echo for (let i = 0; i < 3; i++) { socket.send("2"); @@ -314,7 +317,7 @@ describe("Engine.IO protocol", () => { const pullContent = await pollResponse.text(); - expect(pullContent).to.eql("2:40"); + expect(pullContent).to.eql(`2:4013:42["auth",{}]`); const pollResponse2 = await fetch( `${URL}/socket.io/?EIO=3&transport=polling&sid=${sid}` @@ -407,12 +410,12 @@ describe("Socket.IO protocol", () => { await waitFor(socket, "message"); // Engine.IO handshake - const { data } = await waitFor(socket, "message"); // Socket.IO / namespace handshake - expect(data).to.startsWith("40"); + await waitFor(socket, "message"); // Socket.IO / namespace handshake + await waitFor(socket, "message"); // auth packet socket.send('42["message","message to main namespace"]'); - const { data: echo } = await waitFor(socket, "message"); - expect(echo).to.eql('42["message-back","message to main namespace"]'); + const { data } = await waitFor(socket, "message"); + expect(data).to.eql('42["message-back","message to main namespace"]'); }); it("should allow connection to a custom namespace", async () => { @@ -421,6 +424,8 @@ describe("Socket.IO protocol", () => { ); await waitFor(socket, "message"); // Engine.IO handshake + await waitFor(socket, "message"); // Socket.IO / namespace handshake + await waitFor(socket, "message"); // auth packet socket.send("40/custom,"); @@ -436,6 +441,8 @@ describe("Socket.IO protocol", () => { ); await waitFor(socket, "message"); // Engine.IO handshake + await waitFor(socket, "message"); // Socket.IO / namespace handshake + await waitFor(socket, "message"); // auth packet socket.send("40/random"); @@ -450,6 +457,8 @@ describe("Socket.IO protocol", () => { ); await waitFor(socket, "message"); // Engine.IO handshake + await waitFor(socket, "message"); // Socket.IO / namespace handshake + await waitFor(socket, "message"); // auth packet socket.send("4abc"); From a0d8d7b7f1d2ed049da9fe4bd668674155b8da22 Mon Sep 17 00:00:00 2001 From: Totodore Date: Sun, 15 Oct 2023 15:14:53 +0200 Subject: [PATCH 09/14] fix: waitFor message latency causing missed packets --- test-suite/node-imports.js | 26 +++++++++++++++++++++++--- test-suite/test-suite.js | 17 +++++++++++++++-- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/test-suite/node-imports.js b/test-suite/node-imports.js index fc509aa..08b4355 100644 --- a/test-suite/node-imports.js +++ b/test-suite/node-imports.js @@ -1,10 +1,30 @@ import fetch from "node-fetch"; -import { WebSocket } from "ws"; +import { WebSocket, createWebSocketStream } from "ws"; import chai from "chai"; import chaiString from "chai-string"; + +// Wrap WebSocket to provide a async iterator to yield messages +// This is a workaround for the lack of support for async iterators in ws +// It avoids the need to spawn a new event handler + promise for each message +class WebSocketStream extends WebSocket { + constructor(url) { + super(url); + this.stream = createWebSocketStream(this, { objectMode: true, readableObjectMode: true }); + // ignore errors + this.stream.on("error", () => { }); + } + + // implement async iterator + async* iterator() { + for await (const message of this.stream.iterator()) { + yield message; + } + } +} + chai.use(chaiString); globalThis.fetch = fetch; -globalThis.WebSocket = WebSocket; -globalThis.chai = chai; +globalThis.WebSocket = WebSocketStream; +globalThis.chai = chai; \ No newline at end of file diff --git a/test-suite/test-suite.js b/test-suite/test-suite.js index cb4938d..9c97038 100644 --- a/test-suite/test-suite.js +++ b/test-suite/test-suite.js @@ -17,7 +17,12 @@ function sleep(delay) { return new Promise((resolve) => setTimeout(resolve, delay)); } -function waitFor(socket, eventType) { +async function waitFor(socket, eventType) { + if (eventType == "message" && isNodejs) { + const { value: data } = await socket.iterator().next(); + return { data }; + } + return new Promise((resolve) => { socket.addEventListener( eventType, @@ -29,8 +34,16 @@ function waitFor(socket, eventType) { }); } -function waitForPackets(socket, count) { +async function waitForPackets(socket, count) { const packets = []; + if (isNodejs) { + for (let i = 0; i < count; i++) { + const data = await socket.iterator().next(); + packets.push(data); + console.log("packet", data); + } + return packets; + } return new Promise((resolve) => { const handler = (event) => { From a976454431a19b39163341eeaff74ef36b8da695 Mon Sep 17 00:00:00 2001 From: Totodore Date: Sun, 15 Oct 2023 15:28:14 +0200 Subject: [PATCH 10/14] fix: binary encoding according to engine.io v3 --- test-suite/test-suite.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test-suite/test-suite.js b/test-suite/test-suite.js index 9c97038..5b569d1 100644 --- a/test-suite/test-suite.js +++ b/test-suite/test-suite.js @@ -522,16 +522,16 @@ describe("Socket.IO protocol", () => { socket.send( '452-["message",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' ); - socket.send(Uint8Array.from([1, 2, 3])); - socket.send(Uint8Array.from([4, 5, 6])); + socket.send(Uint8Array.from([4, 1, 2, 3])); + socket.send(Uint8Array.from([4, 4, 5, 6])); const packets = await waitForPackets(socket, 3); expect(packets[0]).to.eql( '452-["message-back",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' ); - expect(packets[1]).to.eql(Uint8Array.from([1, 2, 3]).buffer); - expect(packets[2]).to.eql(Uint8Array.from([4, 5, 6]).buffer); + expect(packets[1]).to.eql(Uint8Array.from([4, 1, 2, 3]).buffer); + expect(packets[2]).to.eql(Uint8Array.from([4, 4, 5, 6]).buffer); socket.close(); }); @@ -552,16 +552,16 @@ describe("Socket.IO protocol", () => { socket.send( '452-789["message-with-ack",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' ); - socket.send(Uint8Array.from([1, 2, 3])); - socket.send(Uint8Array.from([4, 5, 6])); + socket.send(Uint8Array.from([4, 1, 2, 3])); + socket.send(Uint8Array.from([4, 4, 5, 6])); const packets = await waitForPackets(socket, 3); expect(packets[0]).to.eql( '462-789[{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' ); - expect(packets[1]).to.eql(Uint8Array.from([1, 2, 3]).buffer); - expect(packets[2]).to.eql(Uint8Array.from([4, 5, 6]).buffer); + expect(packets[1]).to.eql(Uint8Array.from([4, 1, 2, 3]).buffer); + expect(packets[2]).to.eql(Uint8Array.from([4, 4, 5, 6]).buffer); socket.close(); }); From 721433ab8fae4cc2ae410d3a1d8b47e9a7b6b1b3 Mon Sep 17 00:00:00 2001 From: Totodore Date: Sun, 15 Oct 2023 15:30:56 +0200 Subject: [PATCH 11/14] fix: wait for ns auth echo message --- test-suite/test-suite.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test-suite/test-suite.js b/test-suite/test-suite.js index 5b569d1..76902cf 100644 --- a/test-suite/test-suite.js +++ b/test-suite/test-suite.js @@ -37,12 +37,12 @@ async function waitFor(socket, eventType) { async function waitForPackets(socket, count) { const packets = []; if (isNodejs) { - for (let i = 0; i < count; i++) { - const data = await socket.iterator().next(); - packets.push(data); - console.log("packet", data); - } + for await (const packet of socket.iterator()) { + packets.push(packet); + if (packets.length === count) { return packets; + } + } } return new Promise((resolve) => { From bced5d87a4c3708911aab13f3a9003cae0f4d224 Mon Sep 17 00:00:00 2001 From: Totodore Date: Sun, 15 Oct 2023 15:31:19 +0200 Subject: [PATCH 12/14] fix: engine.io string connect error --- test-suite/test-suite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-suite/test-suite.js b/test-suite/test-suite.js index 76902cf..ad0b9f6 100644 --- a/test-suite/test-suite.js +++ b/test-suite/test-suite.js @@ -461,7 +461,7 @@ describe("Socket.IO protocol", () => { const { data } = await waitFor(socket, "message"); - expect(data).to.eql('44/random,{"message":"Invalid namespace"}'); + expect(data).to.eql('44/random,Invalid namespace'); }); it("should disallow connection with an invalid handshake", async () => { From cc7c138cc93e44bb9d08c4dfabd7422498deb3c4 Mon Sep 17 00:00:00 2001 From: Totodore Date: Sun, 15 Oct 2023 15:46:26 +0200 Subject: [PATCH 13/14] chore: merge Readme.md from v4 branch --- Readme.md | 157 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 151 insertions(+), 6 deletions(-) diff --git a/Readme.md b/Readme.md index ec964fc..42c6306 100644 --- a/Readme.md +++ b/Readme.md @@ -18,6 +18,7 @@ - [ACK](#3---ack) - [ERROR](#4---error) - [BINARY_EVENT](#5---binary_event) + - [BINARY_ACK](#6---binary_ack) - [Packet encoding](#packet-encoding) - [Encoding format](#encoding-format) - [Examples](#examples) @@ -26,16 +27,18 @@ - [Connection to a non-default namespace](#connection-to-a-non-default-namespace) - [Disconnection from a non-default namespace](#disconnection-from-a-non-default-namespace) - [Acknowledgement](#acknowledgement) +- [Sample session](#sample-session) - [History](#history) + - [Difference between v4 and v3](#difference-between-v4-and-v3) - [Difference between v3 and v2](#difference-between-v3-and-v2) - [Difference between v2 and v1](#difference-between-v2-and-v1) - [Initial revision](#initial-revision) ## Protocol version -This is the revision **3** of the Socket.IO protocol, included in ̀`socket.io@1.0.0...1.0.2`. +This is the revision **4** of the Socket.IO protocol, included in `socket.io@1.0.3...latest`. -The 4th revision (included in ̀`socket.io@1.0.3...latest`) can be found here: https://github.com/socketio/socket.io-protocol/tree/master +The 3rd revision (included in `socket.io@1.0.0...1.0.2`) can be found here: https://github.com/socketio/socket.io-protocol/tree/v3 Both the 1st and the 2nd revisions were part of the work towards Socket.IO 1.0 but were never included in a Socket.IO release. @@ -210,6 +213,23 @@ With an acknowledgment id: } ``` +#### 6 - BINARY_ACK + +This event is used when one side has received an EVENT or a BINARY_EVENT with an acknowledgement id. + +It contains the acknowledgement id received in the previous packet, and contain a payload including binary. + +Example: + +``` +{ + "type": 6, + "nsp": "/admin", + "data": [], + "id": 456 +} +``` + ## Packet encoding This section details the encoding used by the default parser which is included in Socket.IO server and client, and @@ -257,7 +277,7 @@ is encoded to `0` } ``` -is encoded to `0/admin` +is encoded to `0/admin,` - `DISCONNECT` packet for the `/admin` namespace @@ -268,7 +288,7 @@ is encoded to `0/admin` } ``` -is encoded to `1/admin` +is encoded to `1/admin,` - `EVENT` packet @@ -345,6 +365,19 @@ is encoded to `51-["hello",{"_placeholder":true,"num":0}]` + `` is encoded to `51-/admin,456["project:delete",{"_placeholder":true,"num":0}]` + `` +- `BINARY_ACK` packet + +``` +{ + "type": 6, + "nsp": "/admin", + "data": [], + "id": 456 +} +``` + +is encoded to `61-/admin,456[{"_placeholder":true,"num":0}]` + `` + ## Exchange protocol @@ -383,21 +416,133 @@ And vice versa. No response is expected from the other-side. ``` Client > { type: EVENT, nsp: "/admin", data: ["hello"], id: 456 } Server > { type: ACK, nsp: "/admin", data: [], id: 456 } +or +Server > { type: BINARY_ACK, nsp: "/admin", data: [ ], id: 456 } ``` And vice versa. +## Sample session + +Here is an example of what is sent over the wire when combining both the Engine.IO and the Socket.IO protocols. + +- Request n°1 (open packet) + +``` +GET /socket.io/?EIO=3&transport=polling&t=N8hyd6w +< HTTP/1.1 200 OK +< Content-Type: text/plain; charset=UTF-8 +96:0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000}2:40 +``` + +Details: + +``` +96 => number of characters (not bytes) of the first message +: => separator +0 => Engine.IO "open" packet type +{"sid":... => the Engine.IO handshake data +2 => number of characters of the 2nd message +: => separator +4 => Engine.IO "message" packet type +0 => Socket.IO "CONNECT" packet type +``` + +Note: the `t` query param is used to ensure that the request is not cached by the browser. + +- Request n°2 (message in): + +`socket.emit('hey', 'Jude')` is executed on the server: + +``` +GET /socket.io/?EIO=3&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC +< HTTP/1.1 200 OK +< Content-Type: text/plain; charset=UTF-8 +16:42["hey","Jude"] +``` + +Details: + +``` +16 => number of characters +: => separator +4 => Engine.IO "message" packet type +2 => Socket.IO "EVENT" packet type +[...] => content +``` + +- Request n°3 (message out) + +`socket.emit('hello'); socket.emit('world');` is executed on the client: + +``` +POST /socket.io/?EIO=3&transport=polling&t=N8hzxke&sid=lv_VI97HAXpY6yYWAAAC +> Content-Type: text/plain; charset=UTF-8 +11:42["hello"]11:42["world"] +< HTTP/1.1 200 OK +< Content-Type: text/plain; charset=UTF-8 +ok +``` + +Details: + +``` +11 => number of characters of the 1st packet +: => separator +4 => Engine.IO "message" packet type +2 => Socket.IO "EVENT" packet type +["hello"] => the 1st content +11 => number of characters of the 2nd packet +: => separator +4 => Engine.IO "message" packet type +2 => Socket.IO "EVENT" packet type +["world"] => the 2nd content +``` + +- Request n°4 (WebSocket upgrade) + +``` +GET /socket.io/?EIO=3&transport=websocket&sid=lv_VI97HAXpY6yYWAAAC +< HTTP/1.1 101 Switching Protocols +``` + +WebSocket frames: + +``` +< 2probe => Engine.IO probe request +> 3probe => Engine.IO probe response +> 5 => Engine.IO "upgrade" packet type +> 42["hello"] +> 42["world"] +> 40/admin, => request access to the admin namespace (Socket.IO "CONNECT" packet) +< 40/admin, => grant access to the admin namespace +> 42/admin,1["tellme"] => Socket.IO "EVENT" packet with acknowledgement +< 461-/admin,1[{"_placeholder":true,"num":0}] => Socket.IO "BINARY_ACK" packet with a placeholder +< => the binary attachment (sent in the following frame) +... after a while without message +> 2 => Engine.IO "ping" packet type +< 3 => Engine.IO "pong" packet type +> 1 => Engine.IO "close" packet type +``` + ## History +### Difference between v4 and v3 + +- add a `BINARY_ACK` packet type + +Previously, an `ACK` packet was always treated as if it may contain binary objects, with recursive search for such +objects, which could hurt performance. + ### Difference between v3 and v2 - remove the usage of msgpack to encode packets containing binary objects (see also [299849b](https://github.com/socketio/socket.io-parser/commit/299849b00294c3bc95817572441f3aca8ffb1f65)) ### Difference between v2 and v1 -- add a BINARY_EVENT packet type +- add a `BINARY_EVENT` packet type -This was added during the work towards Socket.IO 1.0, in order to add support for binary objects. The BINARY_EVENT +This was added during the work towards Socket.IO 1.0, in order to add support for binary objects. The `BINARY_EVENT` packets were encoded with [msgpack](https://msgpack.org/). ### Initial revision From 9514179512e60982a9024e551b661349128790ce Mon Sep 17 00:00:00 2001 From: Totodore Date: Mon, 16 Oct 2023 15:18:22 +0200 Subject: [PATCH 14/14] Revert "fix: engine.io string connect error" This reverts commit bced5d87a4c3708911aab13f3a9003cae0f4d224. --- test-suite/test-suite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-suite/test-suite.js b/test-suite/test-suite.js index ad0b9f6..76902cf 100644 --- a/test-suite/test-suite.js +++ b/test-suite/test-suite.js @@ -461,7 +461,7 @@ describe("Socket.IO protocol", () => { const { data } = await waitFor(socket, "message"); - expect(data).to.eql('44/random,Invalid namespace'); + expect(data).to.eql('44/random,{"message":"Invalid namespace"}'); }); it("should disallow connection with an invalid handshake", async () => {