├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jeremy Kithome 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # signalling-server 2 | 3 | A simple server for signalling between WebRTC clients 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const WebSocket = require("ws"); 3 | const http = require("http"); 4 | const uuidv4 = require("uuid/v4"); 5 | 6 | const app = express(); 7 | 8 | const port = process.env.PORT || 9000; 9 | 10 | //initialize a http server 11 | const server = http.createServer(app); 12 | 13 | //initialize the WebSocket server instance 14 | const wss = new WebSocket.Server({ server }); 15 | 16 | let users = {}; 17 | 18 | const sendTo = (connection, message) => { 19 | connection.send(JSON.stringify(message)); 20 | }; 21 | 22 | const sendToAll = (clients, type, { id, name: userName }) => { 23 | Object.values(clients).forEach(client => { 24 | if(client.name !== userName) { 25 | client.send( 26 | JSON.stringify({ 27 | type, 28 | user: { id, userName } 29 | }) 30 | ) 31 | } 32 | }) 33 | }; 34 | 35 | wss.on("connection", ws => { 36 | ws.on("message", msg => { 37 | console.log("Received message: %s", msg); 38 | let data; 39 | 40 | //accepting only JSON messages 41 | try { 42 | data = JSON.parse(msg); 43 | } catch (e) { 44 | console.log("Invalid JSON"); 45 | data = {}; 46 | } 47 | const { type, name, offer, answer, candidate } = data; 48 | switch (type) { 49 | //when a user tries to login 50 | case "login": 51 | //Check if username is available 52 | if (users[name]) { 53 | sendTo(ws, { 54 | type: "login", 55 | success: false, 56 | message: "Username is unavailable" 57 | }); 58 | } else { 59 | const id = uuidv4(); 60 | const loggedIn = Object.values( 61 | users 62 | ).map(({ id, name: userName }) => ({ id, userName })); 63 | // const loggedIn = Object.keys(users).map(user => ({ userName: user })); 64 | users[name] = ws; 65 | ws.name = name; 66 | ws.id = id; 67 | 68 | sendTo(ws, { 69 | type: "login", 70 | success: true, 71 | users: loggedIn 72 | }); 73 | sendToAll(users, "updateUsers", ws); 74 | } 75 | break; 76 | case "offer": 77 | //if UserBexists then send him offer details 78 | const offerRecipient = users[name]; 79 | 80 | if (!!offerRecipient) { 81 | //setting that sender connected with cecipient 82 | ws.otherName = name; 83 | sendTo(offerRecipient, { 84 | type: "offer", 85 | offer, 86 | name: ws.name 87 | }); 88 | } 89 | break; 90 | case "answer": 91 | //for ex. UserB answers UserA 92 | const answerRecipient = users[name]; 93 | 94 | if (!!answerRecipient) { 95 | ws.otherName = name; 96 | sendTo(answerRecipient, { 97 | type: "answer", 98 | answer 99 | }); 100 | } 101 | break; 102 | case "candidate": 103 | const candidateRecipient = users[name]; 104 | 105 | if (!!candidateRecipient) { 106 | sendTo(candidateRecipient, { 107 | type: "candidate", 108 | candidate 109 | }); 110 | } 111 | break; 112 | case "leave": 113 | recipient = users[name]; 114 | 115 | //notify the other user so he can disconnect his peer connection 116 | if (!!recipient) { 117 | recipient.otherName = null; 118 | sendTo(recipient, { 119 | type: "leave" 120 | }); 121 | } 122 | break; 123 | default: 124 | sendTo(ws, { 125 | type: "error", 126 | message: "Command not found: " + type 127 | }); 128 | break; 129 | } 130 | }); 131 | 132 | ws.on("close", function() { 133 | if (ws.name) { 134 | delete users[ws.name]; 135 | if (ws.otherName) { 136 | console.log("Disconnecting from ", ws.otherName); 137 | const recipient = users[ws.otherName]; 138 | if (!!recipient) { 139 | recipient.otherName = null; 140 | } 141 | } 142 | sendToAll(users, "removeUser", ws); 143 | } 144 | }); 145 | //send immediatly a feedback to the incoming connection 146 | ws.send( 147 | JSON.stringify({ 148 | type: "connect", 149 | message: "Well hello there, I am a WebSocket server" 150 | }) 151 | ); 152 | }); 153 | 154 | //start our server 155 | server.listen(port, () => { 156 | console.log(`Server started on port ${server.address().port} :)`); 157 | }); 158 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "signalling-server", 3 | "version": "1.0.0", 4 | "description": "signalling server for webrtc clients", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/jkithome/signalling-server.git" 13 | }, 14 | "author": "Jeremy Kithome", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/jkithome/signalling-server/issues" 18 | }, 19 | "homepage": "https://github.com/jkithome/signalling-server#readme", 20 | "dependencies": { 21 | "body-parser": "^1.19.0", 22 | "express": "^4.17.1", 23 | "uuid": "^3.4.0", 24 | "ws": "^7.2.1" 25 | }, 26 | "engines": { 27 | "node": ">=8.16" 28 | }, 29 | "devDependencies": { 30 | "errorhandler": "^1.5.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@~1.3.7: 6 | version "1.3.7" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" 8 | dependencies: 9 | mime-types "~2.1.24" 10 | negotiator "0.6.2" 11 | 12 | array-flatten@1.1.1: 13 | version "1.1.1" 14 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 15 | 16 | body-parser@1.19.0, body-parser@^1.19.0: 17 | version "1.19.0" 18 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" 19 | dependencies: 20 | bytes "3.1.0" 21 | content-type "~1.0.4" 22 | debug "2.6.9" 23 | depd "~1.1.2" 24 | http-errors "1.7.2" 25 | iconv-lite "0.4.24" 26 | on-finished "~2.3.0" 27 | qs "6.7.0" 28 | raw-body "2.4.0" 29 | type-is "~1.6.17" 30 | 31 | bytes@3.1.0: 32 | version "3.1.0" 33 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" 34 | 35 | content-disposition@0.5.3: 36 | version "0.5.3" 37 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" 38 | dependencies: 39 | safe-buffer "5.1.2" 40 | 41 | content-type@~1.0.4: 42 | version "1.0.4" 43 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 44 | 45 | cookie-signature@1.0.6: 46 | version "1.0.6" 47 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 48 | 49 | cookie@0.4.0: 50 | version "0.4.0" 51 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" 52 | 53 | debug@2.6.9: 54 | version "2.6.9" 55 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 56 | dependencies: 57 | ms "2.0.0" 58 | 59 | depd@~1.1.2: 60 | version "1.1.2" 61 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 62 | 63 | destroy@~1.0.4: 64 | version "1.0.4" 65 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 66 | 67 | ee-first@1.1.1: 68 | version "1.1.1" 69 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 70 | 71 | encodeurl@~1.0.2: 72 | version "1.0.2" 73 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 74 | 75 | errorhandler@^1.5.1: 76 | version "1.5.1" 77 | resolved "https://registry.yarnpkg.com/errorhandler/-/errorhandler-1.5.1.tgz#b9ba5d17cf90744cd1e851357a6e75bf806a9a91" 78 | dependencies: 79 | accepts "~1.3.7" 80 | escape-html "~1.0.3" 81 | 82 | escape-html@~1.0.3: 83 | version "1.0.3" 84 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 85 | 86 | etag@~1.8.1: 87 | version "1.8.1" 88 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 89 | 90 | express@^4.17.1: 91 | version "4.17.1" 92 | resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" 93 | dependencies: 94 | accepts "~1.3.7" 95 | array-flatten "1.1.1" 96 | body-parser "1.19.0" 97 | content-disposition "0.5.3" 98 | content-type "~1.0.4" 99 | cookie "0.4.0" 100 | cookie-signature "1.0.6" 101 | debug "2.6.9" 102 | depd "~1.1.2" 103 | encodeurl "~1.0.2" 104 | escape-html "~1.0.3" 105 | etag "~1.8.1" 106 | finalhandler "~1.1.2" 107 | fresh "0.5.2" 108 | merge-descriptors "1.0.1" 109 | methods "~1.1.2" 110 | on-finished "~2.3.0" 111 | parseurl "~1.3.3" 112 | path-to-regexp "0.1.7" 113 | proxy-addr "~2.0.5" 114 | qs "6.7.0" 115 | range-parser "~1.2.1" 116 | safe-buffer "5.1.2" 117 | send "0.17.1" 118 | serve-static "1.14.1" 119 | setprototypeof "1.1.1" 120 | statuses "~1.5.0" 121 | type-is "~1.6.18" 122 | utils-merge "1.0.1" 123 | vary "~1.1.2" 124 | 125 | finalhandler@~1.1.2: 126 | version "1.1.2" 127 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" 128 | dependencies: 129 | debug "2.6.9" 130 | encodeurl "~1.0.2" 131 | escape-html "~1.0.3" 132 | on-finished "~2.3.0" 133 | parseurl "~1.3.3" 134 | statuses "~1.5.0" 135 | unpipe "~1.0.0" 136 | 137 | forwarded@~0.1.2: 138 | version "0.1.2" 139 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 140 | 141 | fresh@0.5.2: 142 | version "0.5.2" 143 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 144 | 145 | http-errors@1.7.2: 146 | version "1.7.2" 147 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" 148 | dependencies: 149 | depd "~1.1.2" 150 | inherits "2.0.3" 151 | setprototypeof "1.1.1" 152 | statuses ">= 1.5.0 < 2" 153 | toidentifier "1.0.0" 154 | 155 | http-errors@~1.7.2: 156 | version "1.7.3" 157 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" 158 | dependencies: 159 | depd "~1.1.2" 160 | inherits "2.0.4" 161 | setprototypeof "1.1.1" 162 | statuses ">= 1.5.0 < 2" 163 | toidentifier "1.0.0" 164 | 165 | iconv-lite@0.4.24: 166 | version "0.4.24" 167 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 168 | dependencies: 169 | safer-buffer ">= 2.1.2 < 3" 170 | 171 | inherits@2.0.3: 172 | version "2.0.3" 173 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 174 | 175 | inherits@2.0.4: 176 | version "2.0.4" 177 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 178 | 179 | ipaddr.js@1.9.0: 180 | version "1.9.0" 181 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" 182 | 183 | media-typer@0.3.0: 184 | version "0.3.0" 185 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 186 | 187 | merge-descriptors@1.0.1: 188 | version "1.0.1" 189 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 190 | 191 | methods@~1.1.2: 192 | version "1.1.2" 193 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 194 | 195 | mime-db@1.43.0: 196 | version "1.43.0" 197 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" 198 | 199 | mime-types@~2.1.24: 200 | version "2.1.26" 201 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" 202 | dependencies: 203 | mime-db "1.43.0" 204 | 205 | mime@1.6.0: 206 | version "1.6.0" 207 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 208 | 209 | ms@2.0.0: 210 | version "2.0.0" 211 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 212 | 213 | ms@2.1.1: 214 | version "2.1.1" 215 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" 216 | 217 | negotiator@0.6.2: 218 | version "0.6.2" 219 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" 220 | 221 | on-finished@~2.3.0: 222 | version "2.3.0" 223 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 224 | dependencies: 225 | ee-first "1.1.1" 226 | 227 | parseurl@~1.3.3: 228 | version "1.3.3" 229 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 230 | 231 | path-to-regexp@0.1.7: 232 | version "0.1.7" 233 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 234 | 235 | proxy-addr@~2.0.5: 236 | version "2.0.5" 237 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" 238 | dependencies: 239 | forwarded "~0.1.2" 240 | ipaddr.js "1.9.0" 241 | 242 | qs@6.7.0: 243 | version "6.7.0" 244 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" 245 | 246 | range-parser@~1.2.1: 247 | version "1.2.1" 248 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 249 | 250 | raw-body@2.4.0: 251 | version "2.4.0" 252 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" 253 | dependencies: 254 | bytes "3.1.0" 255 | http-errors "1.7.2" 256 | iconv-lite "0.4.24" 257 | unpipe "1.0.0" 258 | 259 | safe-buffer@5.1.2: 260 | version "5.1.2" 261 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 262 | 263 | "safer-buffer@>= 2.1.2 < 3": 264 | version "2.1.2" 265 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 266 | 267 | send@0.17.1: 268 | version "0.17.1" 269 | resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" 270 | dependencies: 271 | debug "2.6.9" 272 | depd "~1.1.2" 273 | destroy "~1.0.4" 274 | encodeurl "~1.0.2" 275 | escape-html "~1.0.3" 276 | etag "~1.8.1" 277 | fresh "0.5.2" 278 | http-errors "~1.7.2" 279 | mime "1.6.0" 280 | ms "2.1.1" 281 | on-finished "~2.3.0" 282 | range-parser "~1.2.1" 283 | statuses "~1.5.0" 284 | 285 | serve-static@1.14.1: 286 | version "1.14.1" 287 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" 288 | dependencies: 289 | encodeurl "~1.0.2" 290 | escape-html "~1.0.3" 291 | parseurl "~1.3.3" 292 | send "0.17.1" 293 | 294 | setprototypeof@1.1.1: 295 | version "1.1.1" 296 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" 297 | 298 | "statuses@>= 1.5.0 < 2", statuses@~1.5.0: 299 | version "1.5.0" 300 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 301 | 302 | toidentifier@1.0.0: 303 | version "1.0.0" 304 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" 305 | 306 | type-is@~1.6.17, type-is@~1.6.18: 307 | version "1.6.18" 308 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 309 | dependencies: 310 | media-typer "0.3.0" 311 | mime-types "~2.1.24" 312 | 313 | unpipe@1.0.0, unpipe@~1.0.0: 314 | version "1.0.0" 315 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 316 | 317 | utils-merge@1.0.1: 318 | version "1.0.1" 319 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 320 | 321 | uuid@^3.4.0: 322 | version "3.4.0" 323 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" 324 | 325 | vary@~1.1.2: 326 | version "1.1.2" 327 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 328 | 329 | ws@^7.2.1: 330 | version "7.2.1" 331 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e" 332 | --------------------------------------------------------------------------------