├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── SECURITY.md ├── aftman.toml ├── default.project.json ├── package.json ├── selene.toml ├── server ├── index.js ├── package-lock.json └── package.json ├── sourcemap.json ├── src ├── Maid.lua ├── Signal.lua ├── Signature.lua ├── init.lua └── reader │ ├── Dictionary.lua │ ├── Errors.lua │ └── init.lua └── wally.toml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [sturmgeisty] 2 | custom: https://paypal.me/ajihaede1 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.md 2 | .github/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 RoSocket 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 |

2 | 3 | 4 | 5 |

6 | 7 |

8 | GitHub License 9 | GitHub Downloads 10 |

11 | 12 | --- 13 | 14 |

15 | API documentation • 16 | Examples • 17 | Self-hosting 18 |

19 | 20 | ## Installation 21 | 22 | Wally: 23 | 24 | ```toml 25 | [dependencies] 26 | Socket = "RoSocket/rosocket@1.0.1" 27 | ``` 28 | 29 | Roblox Model: 30 | Click [here](https://create.roblox.com/store/asset/17132752732/RoSocket) or 31 | Download from [Releases](https://github.com/RoSocket/rosocket/releases) 32 | (we recommend you get the marketplace one which will always be the latest one) 33 | 34 | ## API 35 | 36 | For RoSocket to work correctly, you must enable in-game HTTP requests & self-host the server! 37 | If you want faster replies, then navigate to the **reader module** > **SOCKET_SERVER_UPDATES**, set it to 0.10 or less, minimum is 0.02 before ratelimits start to appear. 38 | 39 | **Functions:** 40 | 41 | ```Lua 42 | function RoSocket.Connect(socket: string): (any?) -> (table) 43 | ``` 44 | 45 | **Keys:** 46 | 47 | ```Lua 48 | string RoSocket.Version 49 | ``` 50 | 51 | **Socket:** 52 | ```Lua 53 | function socket.Disconnect(...: any): (boolean) -> (boolean) 54 | function socket.Send(msg: string?): (boolean) -> (boolean) 55 | RBXScriptSignal socket.OnDisconnect() 56 | RBXScriptSignal socket.OnMessageReceived(msg: string?) 57 | RBXScriptSignal socket.OnErrorReceived(err: string?) 58 | string socket.UUID -- Universal Unique Identifier 59 | string socket.Socket -- Socket link (e.g: wss://hello.com) 60 | string socket.binaryType -- buffer (doesn't modify way of requests) 61 | string socket.readyState -- OPEN/CLOSED 62 | object socket.Messages 63 | object socket.Errors 64 | ``` 65 | 66 | ## Simple Example 67 | 68 | ```Lua 69 | local RoSocket = require(script.RoSocket) 70 | 71 | -- Http service requests should be enabled for this to work, and a correct server should be set in the Reader module. 72 | local Success, Socket = pcall(RoSocket.Connect, "wss://echo.websocket.org") 73 | if Success ~= false then 74 | print(`Socket's Universal Unique Identifier: {Socket.UUID}`) -- ... 75 | print(`Socket's URL is: {Socket.Socket}`) -- wss://echo.websocket.org 76 | print(`Socket's state is: {Socket.readyState}`) -- OPEN 77 | print(`Socket's binary Type is: {Socket.binaryType}`) -- buffer (read-only) 78 | print(`Socket's amount of messages: {#Socket.Messages}`) 79 | print(`Socket's amount of errors: {#Socket.Errors}`) 80 | Socket.OnDisconnect:Connect(function(...: any?) 81 | warn(`Socket {Socket.Socket} was disconnected!`) 82 | end) 83 | Socket.OnMessageReceived:Connect(function(msg: string?) 84 | warn(`Message from {Socket.Socket}: {tostring(msg)}`) 85 | end) 86 | Socket.OnErrorReceived:Connect(function(err: string?) 87 | error(`Error from {Socket.Socket}: {tostring(err)}`) 88 | end) 89 | local Suc1 = Socket.Send("Hello World!") -- First message 90 | print(`Socket first message {Suc1 == true and "has been sent successfully!" or "has failed to send!"}`) 91 | local Suc2 = Socket.Send("Hello World!") -- Repeated message 92 | print(`Socket repeated message {Suc2 == true and "has been sent successfully!" or "has failed to send!"}`) 93 | local Suc3 = Socket.Send("Goodbye World!") -- Second message 94 | print(`Socket second message {Suc3 == true and "has been sent successfully!" or "has failed to send!"}`) 95 | Socket.Disconnect() 96 | Socket.Send("Hello World!") -- Throws a warning in the output saying you can't send messages to a disconnected socket 97 | print(`Socket's state is: {Socket.readyState}`) -- CLOSED 98 | print(`Socket's amount of messages: {#Socket.Messages}`) 99 | else 100 | warn("Failed to connect to websocket!") 101 | end 102 | ``` 103 | 104 | ## Self-hosting the server 105 | 1. Download the entire RoSocket repository by clicking on **Code** > **Download ZIP** 106 | 2. Extract the ZIP file, and cut the "server" folder. Paste the contents of the folder inside a directory of your choice/folder. 107 | 3. Open a shell and run: 108 | ```npm 109 | npm install express ws 110 | ``` 111 | 4. You're good to go! Optional is to change the default port & default host.
112 | 113 | [(Back to top)](#installation) 114 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.0.x | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability / Issue 10 | 11 | Open a new issue. 12 | Arrival time for a response from our team is around 1 up to 2 days, or even a few hours. Additionally, contact us on discord for faster responses! 13 | -------------------------------------------------------------------------------- /aftman.toml: -------------------------------------------------------------------------------- 1 | # This file lists tools managed by Aftman, a cross-platform toolchain manager. 2 | # For more information, see https://github.com/LPGhatguy/aftman 3 | 4 | # To add a new tool, add an entry to this table. 5 | [tools] 6 | rojo = "rojo-rbx/rojo@7.3.0" 7 | run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0" 8 | wally = "upliftgames/wally@0.3.2" 9 | selene = "Kampfkarren/selene@0.25.0" 10 | stylua = "JohnnyMorganz/stylua@0.18.1" 11 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RoSocket", 3 | "tree": { 4 | "$path": "src" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RoSocket/rosocket", 3 | "version": "1.0.1", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/RoSocket/rosocket.git" 8 | }, 9 | "contributors": [ 10 | "sturmgeisty" 11 | ], 12 | "bugs": { 13 | "url": "https://github.com/RoSocket/rosocket/issues" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "roblox" 2 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const WebSocket = require('ws'); 3 | 4 | const app = express(); 5 | app.use(express.json()); 6 | const port = 6214; 7 | 8 | const connections = {}; 9 | 10 | function isValidWebSocketURL(url) { 11 | return url.startsWith("wss://"); 12 | } 13 | 14 | function generateUUID() { 15 | return Math.random().toString(36).substring(2, 10); 16 | } // TODO: remove this and use uuid npm package 17 | 18 | function handleWebSocketConnection(UUID, socket) { 19 | socket.on('message', (message) => { 20 | if (connections[UUID]) { 21 | const messageString = message.toString(); 22 | connections[UUID].messages.push({ 23 | id: generateUUID(), 24 | message: messageString, 25 | step: connections[UUID].messages.length + 1 26 | }); 27 | } 28 | }); 29 | socket.on('error', (error) => { 30 | console.error(`WebSocket error for UUID: ${UUID}`, error); 31 | if (connections[UUID]) { 32 | connections[UUID].errors.push({ 33 | id: generateUUID(), 34 | message: error, 35 | step: connections[UUID].errors.length + 1 36 | }); 37 | } 38 | }); 39 | } 40 | 41 | 42 | app.post('/connect', async (req, res) => { 43 | const { Socket } = req.body; 44 | if (!Socket) { 45 | return res.status(400).json({ success: false, error: "No WebSocket URL provided!" }); 46 | } 47 | if (!isValidWebSocketURL(Socket)) { 48 | return res.status(400).json({ success: false, error: "Invalid WebSocket URL" }); 49 | } 50 | 51 | const UUID = generateUUID(); 52 | const socket = new WebSocket(Socket); 53 | 54 | try { 55 | await new Promise((resolve, reject) => { 56 | socket.on('error', (error) => { 57 | console.error(`WebSocket error for UUID: ${UUID}`, error); 58 | reject(error); 59 | }); 60 | socket.on('open', () => { 61 | resolve(); 62 | }); 63 | }); 64 | } catch (error) { 65 | return res.status(500).json({ success: false, error: "WebSocket connection error" }); 66 | } 67 | 68 | connections[UUID] = { socket: socket, messages: [] }; 69 | handleWebSocketConnection(UUID, socket); 70 | 71 | res.json({ UUID, Socket, success: true }); 72 | }); 73 | 74 | 75 | app.post('/disconnect', (req, res) => { 76 | const { UUID } = req.body; 77 | if (!UUID) { 78 | return res.status(400).json({ success: false, error: "No UUID provided!" }); 79 | } 80 | if (!connections[UUID]) { 81 | return res.status(404).json({ success: false, error: "UUID not found" }); 82 | } 83 | 84 | connections[UUID].socket.close(); 85 | delete connections[UUID]; 86 | 87 | res.json({ UUID, success: true }); 88 | }); 89 | 90 | app.post('/send', (req, res) => { 91 | const { UUID, Message } = req.body; 92 | if (!UUID || !Message) { 93 | return res.status(400).json({ success: false, error: "UUID or Message not provided!" }); 94 | } 95 | if (!connections[UUID] || connections[UUID].socket.readyState !== WebSocket.OPEN) { 96 | return res.status(404).json({ success: false, error: "Invalid UUID or WebSocket connection closed" }); 97 | } 98 | 99 | connections[UUID].socket.send(Message); 100 | res.json(true); 101 | }); 102 | 103 | app.post('/get', (req, res) => { 104 | const { UUID } = req.body; 105 | if (!UUID) { 106 | return res.status(400).json({ success: false, error: "No UUID provided!" }); 107 | } 108 | if (!connections[UUID]) { 109 | return res.status(404).json({ success: false, error: "Invalid UUID" }); 110 | } 111 | 112 | res.json(connections[UUID].messages); 113 | }); 114 | app.post('/errors', (req, res) => { 115 | const { UUID } = req.body; 116 | if (!UUID) { 117 | return res.status(400).json({ success: false, error: "No UUID provided!" }); 118 | } 119 | if (!connections[UUID]) { 120 | return res.status(404).json({ success: false, error: "Invalid UUID" }); 121 | } 122 | 123 | res.json(connections[UUID].errors); 124 | }); 125 | 126 | app.listen(port, () => { 127 | console.log(`Server running on port ${port}`); 128 | }); 129 | -------------------------------------------------------------------------------- /server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rosocket-server", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "rosocket-server", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "express": "^4.19.2", 13 | "ws": "^8.17.1" 14 | } 15 | }, 16 | "node_modules/accepts": { 17 | "version": "1.3.8", 18 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 19 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 20 | "dependencies": { 21 | "mime-types": "~2.1.34", 22 | "negotiator": "0.6.3" 23 | }, 24 | "engines": { 25 | "node": ">= 0.6" 26 | } 27 | }, 28 | "node_modules/array-flatten": { 29 | "version": "1.1.1", 30 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 31 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 32 | }, 33 | "node_modules/body-parser": { 34 | "version": "1.20.2", 35 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", 36 | "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", 37 | "dependencies": { 38 | "bytes": "3.1.2", 39 | "content-type": "~1.0.5", 40 | "debug": "2.6.9", 41 | "depd": "2.0.0", 42 | "destroy": "1.2.0", 43 | "http-errors": "2.0.0", 44 | "iconv-lite": "0.4.24", 45 | "on-finished": "2.4.1", 46 | "qs": "6.11.0", 47 | "raw-body": "2.5.2", 48 | "type-is": "~1.6.18", 49 | "unpipe": "1.0.0" 50 | }, 51 | "engines": { 52 | "node": ">= 0.8", 53 | "npm": "1.2.8000 || >= 1.4.16" 54 | } 55 | }, 56 | "node_modules/bytes": { 57 | "version": "3.1.2", 58 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 59 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 60 | "engines": { 61 | "node": ">= 0.8" 62 | } 63 | }, 64 | "node_modules/call-bind": { 65 | "version": "1.0.7", 66 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 67 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 68 | "dependencies": { 69 | "es-define-property": "^1.0.0", 70 | "es-errors": "^1.3.0", 71 | "function-bind": "^1.1.2", 72 | "get-intrinsic": "^1.2.4", 73 | "set-function-length": "^1.2.1" 74 | }, 75 | "engines": { 76 | "node": ">= 0.4" 77 | }, 78 | "funding": { 79 | "url": "https://github.com/sponsors/ljharb" 80 | } 81 | }, 82 | "node_modules/content-disposition": { 83 | "version": "0.5.4", 84 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 85 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 86 | "dependencies": { 87 | "safe-buffer": "5.2.1" 88 | }, 89 | "engines": { 90 | "node": ">= 0.6" 91 | } 92 | }, 93 | "node_modules/content-type": { 94 | "version": "1.0.5", 95 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 96 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 97 | "engines": { 98 | "node": ">= 0.6" 99 | } 100 | }, 101 | "node_modules/cookie": { 102 | "version": "0.6.0", 103 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", 104 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", 105 | "engines": { 106 | "node": ">= 0.6" 107 | } 108 | }, 109 | "node_modules/cookie-signature": { 110 | "version": "1.0.6", 111 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 112 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 113 | }, 114 | "node_modules/debug": { 115 | "version": "2.6.9", 116 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 117 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 118 | "dependencies": { 119 | "ms": "2.0.0" 120 | } 121 | }, 122 | "node_modules/define-data-property": { 123 | "version": "1.1.4", 124 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 125 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 126 | "dependencies": { 127 | "es-define-property": "^1.0.0", 128 | "es-errors": "^1.3.0", 129 | "gopd": "^1.0.1" 130 | }, 131 | "engines": { 132 | "node": ">= 0.4" 133 | }, 134 | "funding": { 135 | "url": "https://github.com/sponsors/ljharb" 136 | } 137 | }, 138 | "node_modules/depd": { 139 | "version": "2.0.0", 140 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 141 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 142 | "engines": { 143 | "node": ">= 0.8" 144 | } 145 | }, 146 | "node_modules/destroy": { 147 | "version": "1.2.0", 148 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 149 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 150 | "engines": { 151 | "node": ">= 0.8", 152 | "npm": "1.2.8000 || >= 1.4.16" 153 | } 154 | }, 155 | "node_modules/ee-first": { 156 | "version": "1.1.1", 157 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 158 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 159 | }, 160 | "node_modules/encodeurl": { 161 | "version": "1.0.2", 162 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 163 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 164 | "engines": { 165 | "node": ">= 0.8" 166 | } 167 | }, 168 | "node_modules/es-define-property": { 169 | "version": "1.0.0", 170 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", 171 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", 172 | "dependencies": { 173 | "get-intrinsic": "^1.2.4" 174 | }, 175 | "engines": { 176 | "node": ">= 0.4" 177 | } 178 | }, 179 | "node_modules/es-errors": { 180 | "version": "1.3.0", 181 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 182 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 183 | "engines": { 184 | "node": ">= 0.4" 185 | } 186 | }, 187 | "node_modules/escape-html": { 188 | "version": "1.0.3", 189 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 190 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 191 | }, 192 | "node_modules/etag": { 193 | "version": "1.8.1", 194 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 195 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 196 | "engines": { 197 | "node": ">= 0.6" 198 | } 199 | }, 200 | "node_modules/express": { 201 | "version": "4.19.2", 202 | "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", 203 | "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", 204 | "dependencies": { 205 | "accepts": "~1.3.8", 206 | "array-flatten": "1.1.1", 207 | "body-parser": "1.20.2", 208 | "content-disposition": "0.5.4", 209 | "content-type": "~1.0.4", 210 | "cookie": "0.6.0", 211 | "cookie-signature": "1.0.6", 212 | "debug": "2.6.9", 213 | "depd": "2.0.0", 214 | "encodeurl": "~1.0.2", 215 | "escape-html": "~1.0.3", 216 | "etag": "~1.8.1", 217 | "finalhandler": "1.2.0", 218 | "fresh": "0.5.2", 219 | "http-errors": "2.0.0", 220 | "merge-descriptors": "1.0.1", 221 | "methods": "~1.1.2", 222 | "on-finished": "2.4.1", 223 | "parseurl": "~1.3.3", 224 | "path-to-regexp": "0.1.7", 225 | "proxy-addr": "~2.0.7", 226 | "qs": "6.11.0", 227 | "range-parser": "~1.2.1", 228 | "safe-buffer": "5.2.1", 229 | "send": "0.18.0", 230 | "serve-static": "1.15.0", 231 | "setprototypeof": "1.2.0", 232 | "statuses": "2.0.1", 233 | "type-is": "~1.6.18", 234 | "utils-merge": "1.0.1", 235 | "vary": "~1.1.2" 236 | }, 237 | "engines": { 238 | "node": ">= 0.10.0" 239 | } 240 | }, 241 | "node_modules/finalhandler": { 242 | "version": "1.2.0", 243 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 244 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 245 | "dependencies": { 246 | "debug": "2.6.9", 247 | "encodeurl": "~1.0.2", 248 | "escape-html": "~1.0.3", 249 | "on-finished": "2.4.1", 250 | "parseurl": "~1.3.3", 251 | "statuses": "2.0.1", 252 | "unpipe": "~1.0.0" 253 | }, 254 | "engines": { 255 | "node": ">= 0.8" 256 | } 257 | }, 258 | "node_modules/forwarded": { 259 | "version": "0.2.0", 260 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 261 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 262 | "engines": { 263 | "node": ">= 0.6" 264 | } 265 | }, 266 | "node_modules/fresh": { 267 | "version": "0.5.2", 268 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 269 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 270 | "engines": { 271 | "node": ">= 0.6" 272 | } 273 | }, 274 | "node_modules/function-bind": { 275 | "version": "1.1.2", 276 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 277 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 278 | "funding": { 279 | "url": "https://github.com/sponsors/ljharb" 280 | } 281 | }, 282 | "node_modules/get-intrinsic": { 283 | "version": "1.2.4", 284 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", 285 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", 286 | "dependencies": { 287 | "es-errors": "^1.3.0", 288 | "function-bind": "^1.1.2", 289 | "has-proto": "^1.0.1", 290 | "has-symbols": "^1.0.3", 291 | "hasown": "^2.0.0" 292 | }, 293 | "engines": { 294 | "node": ">= 0.4" 295 | }, 296 | "funding": { 297 | "url": "https://github.com/sponsors/ljharb" 298 | } 299 | }, 300 | "node_modules/gopd": { 301 | "version": "1.0.1", 302 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 303 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 304 | "dependencies": { 305 | "get-intrinsic": "^1.1.3" 306 | }, 307 | "funding": { 308 | "url": "https://github.com/sponsors/ljharb" 309 | } 310 | }, 311 | "node_modules/has-property-descriptors": { 312 | "version": "1.0.2", 313 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 314 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 315 | "dependencies": { 316 | "es-define-property": "^1.0.0" 317 | }, 318 | "funding": { 319 | "url": "https://github.com/sponsors/ljharb" 320 | } 321 | }, 322 | "node_modules/has-proto": { 323 | "version": "1.0.3", 324 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", 325 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", 326 | "engines": { 327 | "node": ">= 0.4" 328 | }, 329 | "funding": { 330 | "url": "https://github.com/sponsors/ljharb" 331 | } 332 | }, 333 | "node_modules/has-symbols": { 334 | "version": "1.0.3", 335 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 336 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 337 | "engines": { 338 | "node": ">= 0.4" 339 | }, 340 | "funding": { 341 | "url": "https://github.com/sponsors/ljharb" 342 | } 343 | }, 344 | "node_modules/hasown": { 345 | "version": "2.0.2", 346 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 347 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 348 | "dependencies": { 349 | "function-bind": "^1.1.2" 350 | }, 351 | "engines": { 352 | "node": ">= 0.4" 353 | } 354 | }, 355 | "node_modules/http-errors": { 356 | "version": "2.0.0", 357 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 358 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 359 | "dependencies": { 360 | "depd": "2.0.0", 361 | "inherits": "2.0.4", 362 | "setprototypeof": "1.2.0", 363 | "statuses": "2.0.1", 364 | "toidentifier": "1.0.1" 365 | }, 366 | "engines": { 367 | "node": ">= 0.8" 368 | } 369 | }, 370 | "node_modules/iconv-lite": { 371 | "version": "0.4.24", 372 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 373 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 374 | "dependencies": { 375 | "safer-buffer": ">= 2.1.2 < 3" 376 | }, 377 | "engines": { 378 | "node": ">=0.10.0" 379 | } 380 | }, 381 | "node_modules/inherits": { 382 | "version": "2.0.4", 383 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 384 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 385 | }, 386 | "node_modules/ipaddr.js": { 387 | "version": "1.9.1", 388 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 389 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 390 | "engines": { 391 | "node": ">= 0.10" 392 | } 393 | }, 394 | "node_modules/media-typer": { 395 | "version": "0.3.0", 396 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 397 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 398 | "engines": { 399 | "node": ">= 0.6" 400 | } 401 | }, 402 | "node_modules/merge-descriptors": { 403 | "version": "1.0.1", 404 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 405 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 406 | }, 407 | "node_modules/methods": { 408 | "version": "1.1.2", 409 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 410 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 411 | "engines": { 412 | "node": ">= 0.6" 413 | } 414 | }, 415 | "node_modules/mime": { 416 | "version": "1.6.0", 417 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 418 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 419 | "bin": { 420 | "mime": "cli.js" 421 | }, 422 | "engines": { 423 | "node": ">=4" 424 | } 425 | }, 426 | "node_modules/mime-db": { 427 | "version": "1.52.0", 428 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 429 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 430 | "engines": { 431 | "node": ">= 0.6" 432 | } 433 | }, 434 | "node_modules/mime-types": { 435 | "version": "2.1.35", 436 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 437 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 438 | "dependencies": { 439 | "mime-db": "1.52.0" 440 | }, 441 | "engines": { 442 | "node": ">= 0.6" 443 | } 444 | }, 445 | "node_modules/ms": { 446 | "version": "2.0.0", 447 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 448 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 449 | }, 450 | "node_modules/negotiator": { 451 | "version": "0.6.3", 452 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 453 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 454 | "engines": { 455 | "node": ">= 0.6" 456 | } 457 | }, 458 | "node_modules/object-inspect": { 459 | "version": "1.13.1", 460 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", 461 | "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", 462 | "funding": { 463 | "url": "https://github.com/sponsors/ljharb" 464 | } 465 | }, 466 | "node_modules/on-finished": { 467 | "version": "2.4.1", 468 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 469 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 470 | "dependencies": { 471 | "ee-first": "1.1.1" 472 | }, 473 | "engines": { 474 | "node": ">= 0.8" 475 | } 476 | }, 477 | "node_modules/parseurl": { 478 | "version": "1.3.3", 479 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 480 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 481 | "engines": { 482 | "node": ">= 0.8" 483 | } 484 | }, 485 | "node_modules/path-to-regexp": { 486 | "version": "0.1.7", 487 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 488 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 489 | }, 490 | "node_modules/proxy-addr": { 491 | "version": "2.0.7", 492 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 493 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 494 | "dependencies": { 495 | "forwarded": "0.2.0", 496 | "ipaddr.js": "1.9.1" 497 | }, 498 | "engines": { 499 | "node": ">= 0.10" 500 | } 501 | }, 502 | "node_modules/qs": { 503 | "version": "6.11.0", 504 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 505 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 506 | "dependencies": { 507 | "side-channel": "^1.0.4" 508 | }, 509 | "engines": { 510 | "node": ">=0.6" 511 | }, 512 | "funding": { 513 | "url": "https://github.com/sponsors/ljharb" 514 | } 515 | }, 516 | "node_modules/range-parser": { 517 | "version": "1.2.1", 518 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 519 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 520 | "engines": { 521 | "node": ">= 0.6" 522 | } 523 | }, 524 | "node_modules/raw-body": { 525 | "version": "2.5.2", 526 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 527 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 528 | "dependencies": { 529 | "bytes": "3.1.2", 530 | "http-errors": "2.0.0", 531 | "iconv-lite": "0.4.24", 532 | "unpipe": "1.0.0" 533 | }, 534 | "engines": { 535 | "node": ">= 0.8" 536 | } 537 | }, 538 | "node_modules/safe-buffer": { 539 | "version": "5.2.1", 540 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 541 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 542 | "funding": [ 543 | { 544 | "type": "github", 545 | "url": "https://github.com/sponsors/feross" 546 | }, 547 | { 548 | "type": "patreon", 549 | "url": "https://www.patreon.com/feross" 550 | }, 551 | { 552 | "type": "consulting", 553 | "url": "https://feross.org/support" 554 | } 555 | ] 556 | }, 557 | "node_modules/safer-buffer": { 558 | "version": "2.1.2", 559 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 560 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 561 | }, 562 | "node_modules/send": { 563 | "version": "0.18.0", 564 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 565 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 566 | "dependencies": { 567 | "debug": "2.6.9", 568 | "depd": "2.0.0", 569 | "destroy": "1.2.0", 570 | "encodeurl": "~1.0.2", 571 | "escape-html": "~1.0.3", 572 | "etag": "~1.8.1", 573 | "fresh": "0.5.2", 574 | "http-errors": "2.0.0", 575 | "mime": "1.6.0", 576 | "ms": "2.1.3", 577 | "on-finished": "2.4.1", 578 | "range-parser": "~1.2.1", 579 | "statuses": "2.0.1" 580 | }, 581 | "engines": { 582 | "node": ">= 0.8.0" 583 | } 584 | }, 585 | "node_modules/send/node_modules/ms": { 586 | "version": "2.1.3", 587 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 588 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 589 | }, 590 | "node_modules/serve-static": { 591 | "version": "1.15.0", 592 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 593 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 594 | "dependencies": { 595 | "encodeurl": "~1.0.2", 596 | "escape-html": "~1.0.3", 597 | "parseurl": "~1.3.3", 598 | "send": "0.18.0" 599 | }, 600 | "engines": { 601 | "node": ">= 0.8.0" 602 | } 603 | }, 604 | "node_modules/set-function-length": { 605 | "version": "1.2.2", 606 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 607 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 608 | "dependencies": { 609 | "define-data-property": "^1.1.4", 610 | "es-errors": "^1.3.0", 611 | "function-bind": "^1.1.2", 612 | "get-intrinsic": "^1.2.4", 613 | "gopd": "^1.0.1", 614 | "has-property-descriptors": "^1.0.2" 615 | }, 616 | "engines": { 617 | "node": ">= 0.4" 618 | } 619 | }, 620 | "node_modules/setprototypeof": { 621 | "version": "1.2.0", 622 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 623 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 624 | }, 625 | "node_modules/side-channel": { 626 | "version": "1.0.6", 627 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 628 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 629 | "dependencies": { 630 | "call-bind": "^1.0.7", 631 | "es-errors": "^1.3.0", 632 | "get-intrinsic": "^1.2.4", 633 | "object-inspect": "^1.13.1" 634 | }, 635 | "engines": { 636 | "node": ">= 0.4" 637 | }, 638 | "funding": { 639 | "url": "https://github.com/sponsors/ljharb" 640 | } 641 | }, 642 | "node_modules/statuses": { 643 | "version": "2.0.1", 644 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 645 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 646 | "engines": { 647 | "node": ">= 0.8" 648 | } 649 | }, 650 | "node_modules/toidentifier": { 651 | "version": "1.0.1", 652 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 653 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 654 | "engines": { 655 | "node": ">=0.6" 656 | } 657 | }, 658 | "node_modules/type-is": { 659 | "version": "1.6.18", 660 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 661 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 662 | "dependencies": { 663 | "media-typer": "0.3.0", 664 | "mime-types": "~2.1.24" 665 | }, 666 | "engines": { 667 | "node": ">= 0.6" 668 | } 669 | }, 670 | "node_modules/unpipe": { 671 | "version": "1.0.0", 672 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 673 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 674 | "engines": { 675 | "node": ">= 0.8" 676 | } 677 | }, 678 | "node_modules/utils-merge": { 679 | "version": "1.0.1", 680 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 681 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 682 | "engines": { 683 | "node": ">= 0.4.0" 684 | } 685 | }, 686 | "node_modules/vary": { 687 | "version": "1.1.2", 688 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 689 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 690 | "engines": { 691 | "node": ">= 0.8" 692 | } 693 | }, 694 | "node_modules/ws": { 695 | "version": "8.17.1", 696 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", 697 | "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", 698 | "engines": { 699 | "node": ">=10.0.0" 700 | }, 701 | "peerDependencies": { 702 | "bufferutil": "^4.0.1", 703 | "utf-8-validate": ">=5.0.2" 704 | }, 705 | "peerDependenciesMeta": { 706 | "bufferutil": { 707 | "optional": true 708 | }, 709 | "utf-8-validate": { 710 | "optional": true 711 | } 712 | } 713 | } 714 | } 715 | } 716 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rosocket-server", 3 | "version": "1.0.0", 4 | "description": "Node backend server for RoSocket.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo Hello World!" 8 | }, 9 | "keywords": [ 10 | "server", 11 | "node.js server", 12 | "rosocket" 13 | ], 14 | "author": "sturmgeisty", 15 | "license": "MIT", 16 | "dependencies": { 17 | "express": "^4.19.2", 18 | "ws": "^8.17.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sourcemap.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RoSocket", 3 | "className": "ModuleScript", 4 | "filePaths": [ 5 | "src\\init.lua", 6 | "default.project.json" 7 | ], 8 | "children": [ 9 | { 10 | "name": "Reader", 11 | "className": "ModuleScript", 12 | "filePaths": [ 13 | "src\\reader\\init.lua" 14 | ], 15 | "children": [ 16 | { 17 | "name": "Dictionary", 18 | "className": "ModuleScript", 19 | "filePaths": [ 20 | "src\\reader\\Dictionary.lua" 21 | ] 22 | }, 23 | { 24 | "name": "Errors", 25 | "className": "ModuleScript", 26 | "filePaths": [ 27 | "src\\reader\\Errors.lua" 28 | ] 29 | } 30 | ] 31 | }, 32 | { 33 | "name": "Maid", 34 | "className": "ModuleScript", 35 | "filePaths": [ 36 | "src\\Maid.lua" 37 | ] 38 | }, 39 | { 40 | "name": "Signal", 41 | "className": "ModuleScript", 42 | "filePaths": [ 43 | "src\\Signal.lua" 44 | ] 45 | }, 46 | { 47 | "name": "Signature", 48 | "className": "ModuleScript", 49 | "filePaths": [ 50 | "src\\Signature.lua" 51 | ] 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /src/Maid.lua: -------------------------------------------------------------------------------- 1 | --- Manages the cleaning of events and other things. 2 | -- Useful for encapsulating state and make deconstructors easy 3 | -- @classmod Maid 4 | -- @see Signal 5 | 6 | local Maid = {} 7 | Maid.ClassName = "Maid" 8 | 9 | --- Returns a new Maid object 10 | -- @constructor Maid.new() 11 | -- @treturn Maid 12 | function Maid.new() 13 | return setmetatable({ 14 | _tasks = {} 15 | }, Maid) 16 | end 17 | 18 | function Maid.isMaid(value) 19 | return type(value) == "table" and value.ClassName == "Maid" 20 | end 21 | 22 | --- Returns Maid[key] if not part of Maid metatable 23 | -- @return Maid[key] value 24 | function Maid:__index(index) 25 | if Maid[index] then 26 | return Maid[index] 27 | else 28 | return self._tasks[index] 29 | end 30 | end 31 | 32 | --- Add a task to clean up. Tasks given to a maid will be cleaned when 33 | -- maid[index] is set to a different value. 34 | -- @usage 35 | -- Maid[key] = (function) Adds a task to perform 36 | -- Maid[key] = (event connection) Manages an event connection 37 | -- Maid[key] = (Maid) Maids can act as an event connection, allowing a Maid to have other maids to clean up. 38 | -- Maid[key] = (Object) Maids can cleanup objects with a `Destroy` method 39 | -- Maid[key] = nil Removes a named task. If the task is an event, it is disconnected. If it is an object, 40 | -- it is destroyed. 41 | function Maid:__newindex(index, newTask) 42 | if Maid[index] ~= nil then 43 | error(("'%s' is reserved"):format(tostring(index)), 2) 44 | end 45 | 46 | local tasks = self._tasks 47 | local oldTask = tasks[index] 48 | 49 | if oldTask == newTask then 50 | return 51 | end 52 | 53 | tasks[index] = newTask 54 | 55 | if oldTask then 56 | if type(oldTask) == "function" then 57 | oldTask() 58 | elseif typeof(oldTask) == "RBXScriptConnection" then 59 | oldTask:Disconnect() 60 | elseif oldTask.Destroy then 61 | oldTask:Destroy() 62 | end 63 | end 64 | end 65 | 66 | --- Same as indexing, but uses an incremented number as a key. 67 | -- @param task An item to clean 68 | -- @treturn number taskId 69 | function Maid:GiveTask(task) 70 | if not task then 71 | error("Task cannot be false or nil", 2) 72 | end 73 | 74 | local taskId = #self._tasks+1 75 | self[taskId] = task 76 | 77 | if type(task) == "table" and (not task.Destroy) then 78 | warn("[Maid.GiveTask] - Gave table task without .Destroy\n\n" .. debug.traceback()) 79 | end 80 | 81 | return taskId 82 | end 83 | 84 | function Maid:GivePromise(promise) 85 | if not promise:IsPending() then 86 | return promise 87 | end 88 | 89 | local newPromise = promise.resolved(promise) 90 | local id = self:GiveTask(newPromise) 91 | 92 | -- Ensure GC 93 | newPromise:Finally(function() 94 | self[id] = nil 95 | end) 96 | 97 | return newPromise 98 | end 99 | 100 | --- Cleans up all tasks. 101 | -- @alias Destroy 102 | function Maid:DoCleaning() 103 | local tasks = self._tasks 104 | 105 | -- Disconnect all events first as we know this is safe 106 | for index, task in pairs(tasks) do 107 | if typeof(task) == "RBXScriptConnection" then 108 | tasks[index] = nil 109 | task:Disconnect() 110 | end 111 | end 112 | 113 | -- Clear out tasks table completely, even if clean up tasks add more tasks to the maid 114 | local index, task = next(tasks) 115 | while task ~= nil do 116 | tasks[index] = nil 117 | if type(task) == "function" then 118 | task() 119 | elseif typeof(task) == "RBXScriptConnection" then 120 | task:Disconnect() 121 | elseif task.Destroy then 122 | task:Destroy() 123 | end 124 | index, task = next(tasks) 125 | end 126 | end 127 | 128 | --- Alias for DoCleaning() 129 | -- @function Destroy 130 | Maid.Destroy = Maid.DoCleaning 131 | 132 | return Maid -------------------------------------------------------------------------------- /src/Signal.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -- Batched Yield-Safe Signal Implementation -- 3 | -- This is a Signal class which has effectively identical behavior to a -- 4 | -- normal RBXScriptSignal, with the only difference being a couple extra -- 5 | -- stack frames at the bottom of the stack trace when an error is thrown. -- 6 | -- This implementation caches runner coroutines, so the ability to yield in -- 7 | -- the signal handlers comes at minimal extra cost over a naive signal -- 8 | -- implementation that either always or never spawns a thread. -- 9 | -- -- 10 | -- API: -- 11 | -- local Signal = require(THIS MODULE) -- 12 | -- local sig = Signal.new() -- 13 | -- local connection = sig:Connect(function(arg1, arg2, ...) ... end) -- 14 | -- sig:Fire(arg1, arg2, ...) -- 15 | -- connection:Disconnect() -- 16 | -- sig:DisconnectAll() -- 17 | -- local arg1, arg2, ... = sig:Wait() -- 18 | -- -- 19 | -- Licence: -- 20 | -- Licenced under the MIT licence. -- 21 | -- -- 22 | -- Authors: -- 23 | -- stravant - July 31st, 2021 - Created the file. -- 24 | -------------------------------------------------------------------------------- 25 | 26 | -- The currently idle thread to run the next handler on 27 | local freeRunnerThread = nil 28 | 29 | -- Function which acquires the currently idle handler runner thread, runs the 30 | -- function fn on it, and then releases the thread, returning it to being the 31 | -- currently idle one. 32 | -- If there was a currently idle runner thread already, that's okay, that old 33 | -- one will just get thrown and eventually GCed. 34 | local function acquireRunnerThreadAndCallEventHandler(fn, ...) 35 | local acquiredRunnerThread = freeRunnerThread 36 | freeRunnerThread = nil 37 | fn(...) 38 | -- The handler finished running, this runner thread is free again. 39 | freeRunnerThread = acquiredRunnerThread 40 | end 41 | 42 | -- Coroutine runner that we create coroutines of. The coroutine can be 43 | -- repeatedly resumed with functions to run followed by the argument to run 44 | -- them with. 45 | local function runEventHandlerInFreeThread(...) 46 | acquireRunnerThreadAndCallEventHandler(...) 47 | while true do 48 | acquireRunnerThreadAndCallEventHandler(coroutine.yield()) 49 | end 50 | end 51 | 52 | -- Connection class 53 | local Connection = {} 54 | Connection.__index = Connection 55 | 56 | function Connection.new(signal, fn) 57 | return setmetatable({ 58 | _connected = true, 59 | _signal = signal, 60 | _fn = fn, 61 | _next = false, 62 | }, Connection) 63 | end 64 | 65 | function Connection:Disconnect() 66 | assert(self._connected, "Can't disconnect a connection twice.", 2) 67 | self._connected = false 68 | 69 | -- Unhook the node, but DON'T clear it. That way any fire calls that are 70 | -- currently sitting on this node will be able to iterate forwards off of 71 | -- it, but any subsequent fire calls will not hit it, and it will be GCed 72 | -- when no more fire calls are sitting on it. 73 | if self._signal._handlerListHead == self then 74 | self._signal._handlerListHead = self._next 75 | else 76 | local prev = self._signal._handlerListHead 77 | while prev and prev._next ~= self do 78 | prev = prev._next 79 | end 80 | if prev then 81 | prev._next = self._next 82 | end 83 | end 84 | end 85 | 86 | -- Make Connection strict 87 | setmetatable(Connection, { 88 | __index = function(tb, key) 89 | error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2) 90 | end, 91 | __newindex = function(tb, key, value) 92 | error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2) 93 | end 94 | }) 95 | 96 | -- Signal class 97 | local Signal = {} 98 | Signal.__index = Signal 99 | 100 | function Signal.new() 101 | return setmetatable({ 102 | _handlerListHead = false, 103 | }, Signal) 104 | end 105 | 106 | function Signal:Connect(fn) 107 | local connection = Connection.new(self, fn) 108 | if self._handlerListHead then 109 | connection._next = self._handlerListHead 110 | self._handlerListHead = connection 111 | else 112 | self._handlerListHead = connection 113 | end 114 | return connection 115 | end 116 | 117 | -- Disconnect all handlers. Since we use a linked list it suffices to clear the 118 | -- reference to the head handler. 119 | function Signal:DisconnectAll() 120 | self._handlerListHead = false 121 | end 122 | 123 | -- Signal:Fire(...) implemented by running the handler functions on the 124 | -- coRunnerThread, and any time the resulting thread yielded without returning 125 | -- to us, that means that it yielded to the Roblox scheduler and has been taken 126 | -- over by Roblox scheduling, meaning we have to make a new coroutine runner. 127 | function Signal:Fire(...) 128 | local item = self._handlerListHead 129 | while item do 130 | if item._connected then 131 | if not freeRunnerThread then 132 | freeRunnerThread = coroutine.create(runEventHandlerInFreeThread) 133 | end 134 | task.spawn(freeRunnerThread, item._fn, ...) 135 | end 136 | item = item._next 137 | end 138 | end 139 | 140 | -- Implement Signal:Wait() in terms of a temporary connection using 141 | -- a Signal:Connect() which disconnects itself. 142 | function Signal:Wait() 143 | local waitingCoroutine = coroutine.running() 144 | local cn; 145 | cn = self:Connect(function(...) 146 | cn:Disconnect() 147 | task.spawn(waitingCoroutine, ...) 148 | end) 149 | return coroutine.yield() 150 | end 151 | 152 | -- Make signal strict 153 | setmetatable(Signal, { 154 | __index = function(tb, key) 155 | error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2) 156 | end, 157 | __newindex = function(tb, key, value) 158 | error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2) 159 | end 160 | }) 161 | 162 | return Signal -------------------------------------------------------------------------------- /src/Signature.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | - RO SOCKET - 3 | Module responsible to return the signature for standard outputting. 4 | You can modify to your liking, such as: the roblox websocket -> Connection successfull! 5 | 6 | • Creator: @binarychunk 7 | ]]-- 8 | 9 | return { 10 | Signature = "[RoSocket]", 11 | Splitter = "::" 12 | } -------------------------------------------------------------------------------- /src/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | - RO SOCKET - 3 | A blazing fast implementation of WebSockets in roblox, similar to the "ws" library in Node. 4 | Supports client and server implementation. 5 | Backend uses the "ws" library aswell, providing proper socket support, and is coded in Node. 6 | 7 | • Creator: @binarychunk 8 | 9 | - CHANGELOG - 10 | 11 | v1.0.0: 12 | Initial release 13 | v1.0.1: 14 | Improved code readability 15 | Improved code speed by removing some useless portions 16 | Made it send all variable arguments from inside the Send function to the Reader 17 | Added more intelli sense stuff 18 | Added custom error messages when trying to: 19 | - send messages to a disconnected socket 20 | - disconnect a already-disconnected socket 21 | ]]-- 22 | 23 | local RoSocket = {} 24 | local Reader = require(script.Reader) 25 | local Errors = require(script.Reader.Errors) 26 | local Signal = require(script.Signal) 27 | local Maid = require(script.Maid) 28 | 29 | local HttpService = game:GetService("HttpService") 30 | local RunService = game:GetService("RunService") 31 | local Players = game:GetService("Players") 32 | local StarterGui = game:GetService("StarterGui") 33 | 34 | local SOCKET_SERVER_UPDATES = 0.10 35 | 36 | if RunService:IsServer() == false then 37 | error(Reader:FormatText(Errors.INVALID_REQUIREMENT_CONTEXT)) 38 | end 39 | if not HttpService.HttpEnabled then 40 | error(Reader:FormatText(Errors.HTTP_SERVICE_DISABLED)) 41 | end 42 | 43 | local MaidSocket = Maid.new() 44 | local Sockets = {} 45 | RoSocket.Version = "1.0.1" 46 | RoSocket.Connect = function(socket: string): (any?) -> (table) 47 | local validsocket = true 48 | 49 | if validsocket ~= false then 50 | local data = Reader:Connect(socket) 51 | if data.success ~= false then 52 | local dis = false 53 | local uuid = data.UUID 54 | local localmsgs = {} 55 | local localerrors = {} 56 | local tbl = {} 57 | tbl.readyState = dis 58 | coroutine.resume(coroutine.create(function() 59 | while tbl do 60 | tbl.readyState = dis and "CLOSED" or "OPEN" 61 | task.wait(0.05) 62 | end 63 | end)) 64 | tbl.binaryType = "buffer" 65 | local OnDisconnect : RBXScriptSignal = Signal.new() 66 | tbl.OnDisconnect = OnDisconnect 67 | local OnMessageReceived : RBXScriptSignal = Signal.new() 68 | tbl.OnMessageReceived = OnMessageReceived 69 | local OnErrorReceived : RBXScriptSignal = Signal.new() 70 | tbl.OnErrorReceived = OnErrorReceived 71 | 72 | local elapsedTimer = Sockets[uuid] and Sockets[uuid].elapsedtimer or 0 73 | 74 | MaidSocket[uuid] = RunService.Heartbeat:Connect(function(deltaTime) 75 | 76 | if elapsedTimer >= SOCKET_SERVER_UPDATES then 77 | elapsedTimer = 0 78 | end 79 | elapsedTimer += deltaTime 80 | if elapsedTimer >= SOCKET_SERVER_UPDATES then 81 | if dis == false then 82 | -- messages 83 | local suc, Msgs = pcall(Reader.Get, Reader, uuid) 84 | if typeof(Msgs) == "table" then 85 | for _, msgobj in ipairs(Msgs) do 86 | local existsAlready = false 87 | for i,msg in ipairs(Sockets[uuid].msgs) do 88 | if msg.id == msgobj.id then 89 | existsAlready = true 90 | break 91 | end 92 | end 93 | 94 | if existsAlready == false then 95 | tbl.OnMessageReceived:Fire(msgobj.message) 96 | table.insert(Sockets[uuid].msgs, { 97 | id = msgobj.id, 98 | message = msgobj.message, 99 | }) 100 | table.insert(localmsgs, { 101 | id = msgobj.id, 102 | message = msgobj.message, 103 | }) 104 | end 105 | end 106 | end 107 | -- errors 108 | local suc, Msgs = pcall(Reader.GetErrors, Reader, uuid) 109 | if typeof(Msgs) == "table" then 110 | for _, msgobj in ipairs(Msgs) do 111 | local existsAlready = false 112 | for i,msg in ipairs(Sockets[uuid].errors) do 113 | if msg.id == msgobj.id then 114 | existsAlready = true 115 | break 116 | end 117 | end 118 | 119 | if existsAlready == false then 120 | tbl.OnErrorReceived:Fire(msgobj.message) 121 | table.insert(Sockets[uuid].errors, { 122 | id = msgobj.id, 123 | message = msgobj.message, 124 | }) 125 | table.insert(localerrors, { 126 | id = msgobj.id, 127 | message = msgobj.message, 128 | }) 129 | end 130 | end 131 | end 132 | else 133 | 134 | end 135 | end 136 | end) 137 | 138 | tbl.UUID = uuid 139 | tbl.Socket = data.Socket 140 | tbl.Disconnect = function(...) 141 | if dis == true then 142 | warn(Reader:FormatText("You cannot disconnect a disconnected socket!")) 143 | return false 144 | else 145 | local success = Reader:Disconnect(uuid) 146 | Sockets[uuid] = nil 147 | MaidSocket[uuid] = nil 148 | tbl.OnDisconnect:Fire() 149 | dis = true 150 | return true 151 | end 152 | 153 | end 154 | tbl.Send = function(...) 155 | if dis == false then 156 | local success = Reader:Send(uuid, ...) 157 | return success 158 | else 159 | warn(Reader:FormatText("You cannot send messages to a disconnected socket!")) 160 | return false 161 | end 162 | end 163 | tbl.Messages = localmsgs or {} 164 | tbl.Errors = localerrors or {} 165 | 166 | setmetatable(tbl, { 167 | __call = function(self, index, ...) 168 | return tbl[index](...) 169 | end, 170 | __metatable = "This is a protected metatable!" 171 | }) 172 | Sockets[uuid] = { 173 | sockettbl = tbl, 174 | msgs = {}, 175 | errors = {}, 176 | elapsedtimer = 0 177 | } 178 | 179 | return tbl 180 | end 181 | else 182 | return {} 183 | end 184 | end 185 | setmetatable(RoSocket, { 186 | __call = function(self, ...) 187 | return RoSocket.Connect(...) 188 | end 189 | }) 190 | table.freeze(RoSocket) 191 | ----------------------------------------------- 192 | return RoSocket 193 | -------------------------------------------------------------------------------- /src/reader/Dictionary.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | - RO SOCKET - 3 | Module responsible for helping reader module with API endpoints. 4 | 5 | • Creator: @binarychunk 6 | ]]-- 7 | 8 | return { 9 | Connection = "/connect", 10 | Disconnection = "/disconnect", 11 | 12 | Validation = "/validation", 13 | Send = "/send", 14 | Get = "/get", 15 | GetErrors = "/errors" 16 | } -------------------------------------------------------------------------------- /src/reader/Errors.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | - RO SOCKET - 3 | Module responsible for helping reader module to throw out errors. 4 | 5 | • Creator: @binarychunk 6 | ]]-- 7 | 8 | return { 9 | INVALID_ARGUMENT_TYPE = "Argument \"%s\" expected to be type %s, instead got %s!", 10 | 11 | EMPTY_WSS_LINK_TO_VALIDATE = "wss link expected, received none. Regex pattern can't be executed.", 12 | EMPTY_TEXT_TO_FORMAT = "text expected to be formatted, received none. Text formatting can't operate further more.", 13 | EMPTY_ID_TO_VALIDATE = "id expected, received none. ID validation can't operate further more.", 14 | 15 | INVALID_WSS_LINK = "invalid wss link received. Try connecting with a proper link!", 16 | 17 | HTTP_SERVICE_DISABLED = "RoSocket must have HTTP enabled for it to operate correctly. To resolve this problem, navigate to the top left corner, select FILE ➡ Game Settings ➡ Security ➡ = { 18 | Success: boolean?, 19 | StatusCode: number?, 20 | StatusMessage: string?, 21 | Headers: {[string?]: string}, 22 | Body: T 23 | } 24 | export type ConnectionData = { 25 | UUID: string?, 26 | Socket: string?, 27 | Success: boolean? 28 | } 29 | export type DisconnectionData = { 30 | UUID: string?, 31 | Socket: string? 32 | } 33 | ----------------------------------------------- 34 | local SOCKET_SERVER_URL = "" --You can change this if you self host 35 | ----------------------------------------------- 36 | local WSS_PATTERN = "^wss://[%w%.]" 37 | ----------------------------------------------- 38 | function Reader:FormatText(text: string?, ...: any?): string 39 | assert(text, Errors.EMPTY_TEXT_TO_FORMAT) 40 | assert(typeof(text) == "string", string.format(Errors.INVALID_ARGUMENT_TYPE, "text", "string", typeof(text))) 41 | 42 | return tostring(`{Signature.Signature} {Signature.Splitter} {text}`) 43 | end 44 | assert(SOCKET_SERVER_URL ~= "", Reader:FormatText("Invalid backend URL. You can use heroku or replit to deploy the node backend!")) 45 | function Reader:ValidateWSSLink(link: string?, ...: any?): boolean 46 | assert(link, Errors.EMPTY_WSS_LINK_TO_VALIDATE) 47 | assert(typeof(link) == "string", string.format(Errors.INVALID_ARGUMENT_TYPE, "link", "string", typeof(link))) 48 | 49 | return string.match(link, WSS_PATTERN) and true or false 50 | end 51 | 52 | function Reader:Connect(socket: string?, ...: any?) 53 | local ValidLink = self:ValidateWSSLink(tostring(socket)) 54 | if ValidLink == false then 55 | return error(self:FormatText(`Invalid socket link passed. Their format is: wss://hostname/path.`)) 56 | end 57 | local Response : RequestResponse = HttpService:RequestAsync({ 58 | Url = `{SOCKET_SERVER_URL}{Dictionary.Connection}`, 59 | Method = "POST", 60 | Headers = { 61 | ["Content-Type"] = "application/json", 62 | }, 63 | Body = HttpService:JSONEncode({Socket = tostring(socket)}) 64 | }) 65 | 66 | if Response.Success == true then 67 | warn(self:FormatText(`Successfully connected!`)) 68 | local DecodedSuccess, DecodedResult : ConnectionData = pcall(function() 69 | return HttpService:JSONDecode(Response.Body) 70 | end) 71 | 72 | if DecodedSuccess == true then 73 | return DecodedResult 74 | elseif DecodedSuccess == false then 75 | return error(self:FormatText(`Failed to decode response | Error {tostring(DecodedResult)}`)) 76 | end 77 | elseif Response.Success == false then 78 | return error(self:FormatText(`Failed to connect to socket {tostring(socket)} | Status Code {tostring(Response.StatusCode)}`)) 79 | end 80 | end 81 | function Reader:Disconnect(id: string?, ...: any?): boolean 82 | local ValidID = true 83 | 84 | local Response : RequestResponse = HttpService:RequestAsync({ 85 | Url = `{SOCKET_SERVER_URL}{Dictionary.Disconnection}`, 86 | Method = "POST", 87 | Headers = { 88 | ["Content-Type"] = "application/json", 89 | }, 90 | Body = HttpService:JSONEncode({UUID = tostring(id)}) 91 | }) 92 | 93 | if Response.Success == true then 94 | local DecodedSuccess, DecodedResult : DisconnectionData = pcall(function() 95 | return HttpService:JSONDecode(Response.Body) 96 | end) 97 | 98 | if DecodedSuccess == true then 99 | return DecodedResult 100 | elseif DecodedSuccess == false then 101 | return error(self:FormatText(`Failed to decode response | Error {tostring(DecodedResult)}`)) 102 | end 103 | elseif Response.Success == false then 104 | return error(self:FormatText(`Failed to disconnect ID {tostring(id)} | Status Code {tostring(Response.StatusCode)}`)) 105 | end 106 | end 107 | function Reader:Send(id: string?, message: string?, ...): boolean 108 | local Response : RequestResponse = HttpService:RequestAsync({ 109 | Url = `{SOCKET_SERVER_URL}{Dictionary.Send}`, 110 | Method = "POST", 111 | Headers = { 112 | ["Content-Type"] = "application/json", 113 | }, 114 | Body = HttpService:JSONEncode({UUID = tostring(id), Message = tostring(message)}) 115 | }) 116 | 117 | if Response.Success == true then 118 | local DecodedSuccess, DecodedResult = pcall(function() 119 | return HttpService:JSONDecode(Response.Body) 120 | end) 121 | 122 | if DecodedSuccess == true then 123 | return DecodedResult 124 | elseif DecodedSuccess == false then 125 | return error(self:FormatText(`Failed to decode response | Error {tostring(DecodedResult)}`)) 126 | end 127 | elseif Response.Success == false then 128 | return error(self:FormatText(`Failed to send message! This socket is probably disconnected.`)) 129 | end 130 | end 131 | function Reader:Get(id: string?, ...): any 132 | local Response : RequestResponse = HttpService:RequestAsync({ 133 | Url = `{SOCKET_SERVER_URL}{Dictionary.Get}`, 134 | Method = "POST", 135 | Headers = { 136 | ["Content-Type"] = "application/json", 137 | }, 138 | Body = HttpService:JSONEncode({UUID = tostring(id)}) 139 | }) 140 | 141 | if Response.Success == true then 142 | local DecodedSuccess, DecodedResult = pcall(function() 143 | return HttpService:JSONDecode(Response.Body) 144 | end) 145 | 146 | if DecodedSuccess == true then 147 | return DecodedResult 148 | elseif DecodedSuccess == false then 149 | return error(self:FormatText(`Failed to decode response | Error {tostring(DecodedResult)}`)) 150 | end 151 | elseif Response.Success == false then 152 | return error(self:FormatText(`Failed to get messages! This socket is probably disconnected.`)) 153 | end 154 | end 155 | function Reader:GetErrors(id: string?, ...): any 156 | local Response : RequestResponse = HttpService:RequestAsync({ 157 | Url = `{SOCKET_SERVER_URL}{Dictionary.GetErrors}`, 158 | Method = "POST", 159 | Headers = { 160 | ["Content-Type"] = "application/json", 161 | }, 162 | Body = HttpService:JSONEncode({UUID = tostring(id)}) 163 | }) 164 | 165 | if Response.Success == true then 166 | local DecodedSuccess, DecodedResult = pcall(function() 167 | return HttpService:JSONDecode(Response.Body) 168 | end) 169 | 170 | if DecodedSuccess == true then 171 | return DecodedResult 172 | elseif DecodedSuccess == false then 173 | return error(self:FormatText(`Failed to decode response | Error {tostring(DecodedResult)}`)) 174 | end 175 | elseif Response.Success == false then 176 | return error(self:FormatText(`Failed to get errors! This socket is probably disconnected.`)) 177 | end 178 | end 179 | 180 | return Reader 181 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "RoSocket/rosocket" 3 | description = "Roblox websocket support in 1 module" 4 | version = "1.0.1" 5 | license = "MIT" 6 | authors = ["sturmgeisty"] 7 | registry = "https://github.com/upliftgames/wally-index" 8 | realm = "shared" 9 | include = ["src", "src/**", "wally.toml", "wally.lock", "default.project.json"] 10 | exclude = ["**"] 11 | --------------------------------------------------------------------------------