├── .gitignore
├── README.md
├── constants
└── user.constant.ts
├── image-main.png
├── image.png
├── package-lock.json
├── package.json
├── server.ts
├── src
├── app.ts
├── config
│ ├── db.ts
│ └── dotenv.config.ts
├── controllers
│ ├── auth.controller.ts
│ ├── messages.controller.ts
│ ├── server.controller.ts
│ ├── statistics.controller.ts
│ └── users.controller.ts
├── middleware
│ ├── auth.ts
│ └── errorHandler.ts
├── models
│ ├── invitation.model.ts
│ ├── messages.model.ts
│ ├── server.model.ts
│ ├── statistics.model.ts
│ └── user.model.ts
├── routes
│ ├── auth.routes.ts
│ ├── messages.route.ts
│ └── server.routes.ts
├── sockets
│ └── index.socket.ts
├── types
│ ├── error.class.ts
│ ├── message.interface.ts
│ ├── server-socket.interface.ts
│ └── server.interface.ts
└── utils
│ ├── constants.ts
│ ├── index.util.ts
│ ├── messages.utli.ts
│ ├── server.util.ts
│ └── user.util.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | .env.*
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | # 🕵️♂️ Secret Room - Backend
3 |
4 |
5 |
10 |
11 |
Built with these tools and technologies:
12 |
13 |
14 |

15 |

16 |

17 |

18 |

19 |
20 |
21 |
22 |

23 |

24 |

25 |

26 |

27 |
28 |
29 |
30 | **Secret Room** is a fully anonymous, real-time chat web app that lets users create temporary servers and invite others to chat without ever revealing their identity. This is the backend service powering the chat experience, built with **Node.js**, **Express**, **MongoDB**, and **Socket.io**.
31 |
32 | > 💡 This is my first personal Node.js backend project. It focuses heavily on secure communication, temporary server lifecycles, and anonymous user management.
33 |
34 | ---
35 |
36 | ## 🚀 Features
37 |
38 | - 🔒 JWT Authentication for secure access
39 | - 🧠 Anonymous user identification via fingerprinting
40 | - 💬 Real-time chat between host and guests (powered by Socket.io)
41 | - ⚡ Instant join/leave/typing notifications
42 | - ⏳ Self-destructing servers based on a lifespan
43 | - 📨 Global & Unique server invitation system
44 | - 👻 Deterministic anonymous usernames per server
45 | - 📦 Clean modular structure: Models, Routes, Controllers, Middleware
46 |
47 | ---
48 |
49 | ## 📁 Project Structure
50 |
51 | ```
52 | .
53 | ├── controllers/
54 | │ ├── userController.js
55 | │ ├── serverController.js
56 | │ └── messagesController.js
57 | ├── middleware/
58 | │ └── authenticateJWT.js
59 | ├── models/
60 | │ ├── User.js
61 | │ ├── Server.js
62 | │ ├── Message.js
63 | │ └── Invitation.js
64 | ├── socket/
65 | │ └── socketHandler.js
66 | ├── routes/
67 | │ └── ...
68 | ```
69 |
70 | ---
71 |
72 | ## 📦 Tech Stack
73 |
74 | - **Node.js**
75 | - **Express**
76 | - **MongoDB** (Mongoose)
77 | - **Socket.io** for real-time communication
78 | - **JWT** for authentication
79 | - **UUID + Custom Encryption** for anonymous ID and secure communication
80 |
81 | ---
82 |
83 | ## 🔐 Authentication
84 |
85 | - Uses JWT tokens to secure private routes
86 | - Fingerprinting used to generate a unique ID for users (without collecting sensitive info)
87 |
88 | ---
89 |
90 | ## 💬 Real-time Communication (Socket.io)
91 |
92 | - Users join a room corresponding to the serverId
93 | - On joining, a deterministic **anonymous username** is generated using a `serverId-userId` combo (see below)
94 | - Events include:
95 | - `user:join` – Notify server members of a new user
96 | - `user:leave` – Broadcast when a user disconnects
97 | - `user:active` - Broadcast when the user is active
98 | - `server:deleted` - Broadcast when the server is deleted
99 | - `message:send` – Relay messages between users
100 | - `message:read` – Relay messages between users
101 | - Socket connections are JWT-authenticated and scoped to server rooms
102 |
103 | ---
104 |
105 | ## 👻 Username Generation (Anonymity)
106 |
107 | Usernames are deterministically generated per server using a salted pattern based on the combination: `serverId-userId`.
108 |
109 | ```ts
110 | // utils/generateUsername.ts
111 | export function generateUsername(salt: string): string {
112 | // uses custom hash + either consonant-vowel or alphanumeric pattern
113 | }
114 | ```
115 |
116 | ### Why this matters:
117 | - Ensures each user has a **unique, anonymous identity** _per server_
118 | - Zero chance of real user data leaks
119 | - No usernames are ever reused across different servers
120 |
121 | ---
122 |
123 | ## 🧪 API Endpoints
124 |
125 | ### 🔐 Authentication Required
126 |
127 | | Method | Endpoint |
128 | |--------|----------|
129 | | `POST` | `/` – Create a new server |
130 | | `POST` | `/invitation/:globalInvitationId` – Join with a global invite |
131 | | `POST` | `/unique-invitation/:inviteCode` – Join with a unique invite |
132 | | `GET` | `/:serverId` – Get server data |
133 | | `GET` | `/:serverId/active-users` – Get current active users |
134 | | `GET` | `/:serverId/generate-unique-server-invitation-id` – Generate a unique invite |
135 | | `GET` | `/:serverId/messages` – Fetch messages in the server |
136 | | `DELETE` | `/:serverId` – Delete a server |
137 |
138 | ---
139 |
140 | ## 🧬 Mongoose Models
141 |
142 | ### 🧑💻 User
143 | ```ts
144 | userId, isOnline, lastSeen, currentServer, createdAt, bgColor, textColor
145 | ```
146 |
147 | ### 💬 Message
148 | ```ts
149 | serverId, senderId, receiverId, content, attachmentUrl, sent, readBySender, readByReceiver
150 | ```
151 |
152 | ### 🏠 Server
153 | ```ts
154 | serverId, owner, serverName, salt, globalInvitationId, expiresAt, approvedUsers, allUsers
155 | ```
156 |
157 | ### 🎟️ Invitation
158 | ```ts
159 | inviteCode, used, serverId, expiresAt
160 | ```
161 |
162 | ---
163 |
164 | ## ⚙️ Setup Instructions
165 |
166 | ```bash
167 | # 1. Clone the repo
168 | git clone [https://github.com/your-username/secret-room-backend](https://github.com/fabiconcept/secret-room-backend.git)
169 |
170 | # 2. Navigate into the folder
171 | cd secret-room-backend
172 |
173 | # 3. Install dependencies
174 | npm install
175 |
176 | # 4. Create a `.env` file and add:
177 | # PORT
178 | # HOST
179 | # CORS_ORIGIN
180 | # JWT_SECRET
181 | # MONGODB_URI
182 | # API_KEY
183 |
184 | # 5. Run the server
185 | npm run dev
186 | ```
187 |
188 |
189 | ---
190 |
191 | ## 🙌 Author
192 |
193 | Made with ❤️ by [Favour Tochukwu Ajokubi](https://github.com/FavourBE)
194 |
195 | ---
196 |
197 | ## 🛡️ License
198 |
199 | This project is open-source and available under the [MIT License](LICENSE).
200 |
--------------------------------------------------------------------------------
/constants/user.constant.ts:
--------------------------------------------------------------------------------
1 | export const adjectives = [
2 | // Original adjectives
3 | 'cool', 'dark', 'epic', 'fast', 'gold', 'holy', 'iron', 'jade', 'kind', 'lost',
4 | 'mega', 'neon', 'okay', 'pure', 'quit', 'rare', 'safe', 'true', 'uber', 'void',
5 |
6 | // Natural elements
7 | 'azure', 'blaze', 'coral', 'ember', 'frost', 'glade', 'haze', 'icy', 'lunar', 'misty',
8 | 'north', 'ocean', 'polar', 'quake', 'river', 'storm', 'tidal', 'urban', 'vapor', 'wind',
9 |
10 | // Colors and visual properties
11 | 'amber', 'black', 'chrome', 'denim', 'ebony', 'faded', 'green', 'honey', 'ivory', 'jet',
12 | 'khaki', 'lilac', 'mauve', 'navy', 'opal', 'pink', 'quartz', 'ruby', 'silver', 'teal',
13 |
14 | // Qualities and characteristics
15 | 'able', 'bold', 'calm', 'deft', 'eager', 'fierce', 'grand', 'hardy', 'ideal', 'jolly',
16 | 'keen', 'lively', 'mighty', 'noble', 'odd', 'prime', 'quick', 'royal', 'sharp', 'tough',
17 |
18 | // Fantasy and sci-fi themed
19 | 'astral', 'brave', 'cyber', 'dire', 'ether', 'feral', 'gamma', 'hyper', 'intel', 'jedi',
20 | 'krewe', 'laser', 'mecha', 'nexus', 'omega', 'phase', 'quantum', 'rune', 'solar', 'twin',
21 |
22 | // Emotional states
23 | 'angry', 'busy', 'crazy', 'dizzy', 'eager', 'funky', 'giddy', 'happy', 'irate', 'jolly',
24 | 'kooky', 'loopy', 'moody', 'nervy', 'peppy', 'quirky', 'rowdy', 'silly', 'testy', 'wild'
25 | ];
26 |
27 | export const nouns = [
28 | // Original nouns
29 | 'ace', 'bear', 'cat', 'dove', 'elf', 'fox', 'goat', 'hawk', 'ibex', 'jay',
30 | 'king', 'lion', 'mage', 'nuke', 'owl', 'puma', 'rook', 'sage', 'tide', 'wolf',
31 |
32 | // More animals
33 | 'ape', 'bison', 'crow', 'drake', 'eagle', 'fawn', 'gecko', 'hound', 'impala', 'jaguar',
34 | 'koala', 'lynx', 'moose', 'newt', 'orca', 'panda', 'quail', 'rhino', 'shark', 'tiger',
35 |
36 | // Fantasy creatures
37 | 'angel', 'beast', 'cipher', 'demon', 'eidolon', 'fairy', 'giant', 'hobbit', 'imp', 'joker',
38 | 'knight', 'lucifer', 'mimic', 'ninja', 'oracle', 'pirate', 'queen', 'rebel', 'specter', 'titan',
39 |
40 | // Objects and items
41 | 'arrow', 'blade', 'charm', 'dagger', 'ember', 'flare', 'glyph', 'hammer', 'icon', 'jewel',
42 | 'katana', 'locket', 'mallet', 'needle', 'orb', 'prism', 'quiver', 'relic', 'shield', 'torch',
43 |
44 | // Natural elements and phenomena
45 | 'ash', 'breeze', 'comet', 'dune', 'echo', 'flame', 'gust', 'hail', 'ice', 'jungle',
46 | 'kite', 'leaf', 'mist', 'nova', 'oasis', 'peak', 'quake', 'reef', 'stone', 'tempest',
47 |
48 | // Technology and modern terms
49 | 'atom', 'byte', 'code', 'data', 'echo', 'flux', 'grid', 'hack', 'intel', 'joule',
50 | 'key', 'link', 'mech', 'node', 'optic', 'pulse', 'query', 'router', 'server', 'tech'
51 | ];
--------------------------------------------------------------------------------
/image-main.png:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fabiconcept/secret-room-backend/b781f85e513b4da479d98fbd90697ddc36ffc82e/image.png
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "server",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "@types/cors": "^2.8.17",
13 | "@types/express": "^5.0.0",
14 | "@types/jsonwebtoken": "^9.0.9",
15 | "@types/node": "^22.13.9",
16 | "cors": "^2.8.5",
17 | "dotenv": "^16.4.7",
18 | "express": "^4.21.2",
19 | "jsonwebtoken": "^9.0.2",
20 | "mongoose": "^8.12.1",
21 | "node-cron": "^3.0.3",
22 | "nodemon": "^3.1.9",
23 | "socket.io": "^4.8.1",
24 | "typescript": "^5.8.2",
25 | "uuid": "^11.1.0"
26 | },
27 | "devDependencies": {
28 | "@types/node-cron": "^3.0.11",
29 | "ts-node": "^10.9.2"
30 | }
31 | },
32 | "node_modules/@cspotcode/source-map-support": {
33 | "version": "0.8.1",
34 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
35 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
36 | "dev": true,
37 | "license": "MIT",
38 | "dependencies": {
39 | "@jridgewell/trace-mapping": "0.3.9"
40 | },
41 | "engines": {
42 | "node": ">=12"
43 | }
44 | },
45 | "node_modules/@jridgewell/resolve-uri": {
46 | "version": "3.1.2",
47 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
48 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
49 | "dev": true,
50 | "license": "MIT",
51 | "engines": {
52 | "node": ">=6.0.0"
53 | }
54 | },
55 | "node_modules/@jridgewell/sourcemap-codec": {
56 | "version": "1.5.0",
57 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
58 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
59 | "dev": true,
60 | "license": "MIT"
61 | },
62 | "node_modules/@jridgewell/trace-mapping": {
63 | "version": "0.3.9",
64 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
65 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
66 | "dev": true,
67 | "license": "MIT",
68 | "dependencies": {
69 | "@jridgewell/resolve-uri": "^3.0.3",
70 | "@jridgewell/sourcemap-codec": "^1.4.10"
71 | }
72 | },
73 | "node_modules/@mongodb-js/saslprep": {
74 | "version": "1.2.0",
75 | "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz",
76 | "integrity": "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==",
77 | "license": "MIT",
78 | "dependencies": {
79 | "sparse-bitfield": "^3.0.3"
80 | }
81 | },
82 | "node_modules/@socket.io/component-emitter": {
83 | "version": "3.1.2",
84 | "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
85 | "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
86 | "license": "MIT"
87 | },
88 | "node_modules/@tsconfig/node10": {
89 | "version": "1.0.11",
90 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
91 | "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
92 | "dev": true,
93 | "license": "MIT"
94 | },
95 | "node_modules/@tsconfig/node12": {
96 | "version": "1.0.11",
97 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
98 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
99 | "dev": true,
100 | "license": "MIT"
101 | },
102 | "node_modules/@tsconfig/node14": {
103 | "version": "1.0.3",
104 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
105 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
106 | "dev": true,
107 | "license": "MIT"
108 | },
109 | "node_modules/@tsconfig/node16": {
110 | "version": "1.0.4",
111 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
112 | "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
113 | "dev": true,
114 | "license": "MIT"
115 | },
116 | "node_modules/@types/body-parser": {
117 | "version": "1.19.5",
118 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
119 | "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
120 | "license": "MIT",
121 | "dependencies": {
122 | "@types/connect": "*",
123 | "@types/node": "*"
124 | }
125 | },
126 | "node_modules/@types/connect": {
127 | "version": "3.4.38",
128 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
129 | "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
130 | "license": "MIT",
131 | "dependencies": {
132 | "@types/node": "*"
133 | }
134 | },
135 | "node_modules/@types/cors": {
136 | "version": "2.8.17",
137 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
138 | "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
139 | "license": "MIT",
140 | "dependencies": {
141 | "@types/node": "*"
142 | }
143 | },
144 | "node_modules/@types/express": {
145 | "version": "5.0.0",
146 | "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz",
147 | "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==",
148 | "license": "MIT",
149 | "dependencies": {
150 | "@types/body-parser": "*",
151 | "@types/express-serve-static-core": "^5.0.0",
152 | "@types/qs": "*",
153 | "@types/serve-static": "*"
154 | }
155 | },
156 | "node_modules/@types/express-serve-static-core": {
157 | "version": "5.0.6",
158 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz",
159 | "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==",
160 | "license": "MIT",
161 | "dependencies": {
162 | "@types/node": "*",
163 | "@types/qs": "*",
164 | "@types/range-parser": "*",
165 | "@types/send": "*"
166 | }
167 | },
168 | "node_modules/@types/http-errors": {
169 | "version": "2.0.4",
170 | "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
171 | "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
172 | "license": "MIT"
173 | },
174 | "node_modules/@types/jsonwebtoken": {
175 | "version": "9.0.9",
176 | "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz",
177 | "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==",
178 | "license": "MIT",
179 | "dependencies": {
180 | "@types/ms": "*",
181 | "@types/node": "*"
182 | }
183 | },
184 | "node_modules/@types/mime": {
185 | "version": "1.3.5",
186 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
187 | "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
188 | "license": "MIT"
189 | },
190 | "node_modules/@types/ms": {
191 | "version": "2.1.0",
192 | "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
193 | "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
194 | "license": "MIT"
195 | },
196 | "node_modules/@types/node": {
197 | "version": "22.13.9",
198 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
199 | "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
200 | "license": "MIT",
201 | "dependencies": {
202 | "undici-types": "~6.20.0"
203 | }
204 | },
205 | "node_modules/@types/node-cron": {
206 | "version": "3.0.11",
207 | "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz",
208 | "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==",
209 | "dev": true,
210 | "license": "MIT"
211 | },
212 | "node_modules/@types/qs": {
213 | "version": "6.9.18",
214 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
215 | "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==",
216 | "license": "MIT"
217 | },
218 | "node_modules/@types/range-parser": {
219 | "version": "1.2.7",
220 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
221 | "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
222 | "license": "MIT"
223 | },
224 | "node_modules/@types/send": {
225 | "version": "0.17.4",
226 | "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
227 | "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
228 | "license": "MIT",
229 | "dependencies": {
230 | "@types/mime": "^1",
231 | "@types/node": "*"
232 | }
233 | },
234 | "node_modules/@types/serve-static": {
235 | "version": "1.15.7",
236 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
237 | "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
238 | "license": "MIT",
239 | "dependencies": {
240 | "@types/http-errors": "*",
241 | "@types/node": "*",
242 | "@types/send": "*"
243 | }
244 | },
245 | "node_modules/@types/webidl-conversions": {
246 | "version": "7.0.3",
247 | "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
248 | "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
249 | "license": "MIT"
250 | },
251 | "node_modules/@types/whatwg-url": {
252 | "version": "11.0.5",
253 | "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
254 | "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
255 | "license": "MIT",
256 | "dependencies": {
257 | "@types/webidl-conversions": "*"
258 | }
259 | },
260 | "node_modules/accepts": {
261 | "version": "1.3.8",
262 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
263 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
264 | "license": "MIT",
265 | "dependencies": {
266 | "mime-types": "~2.1.34",
267 | "negotiator": "0.6.3"
268 | },
269 | "engines": {
270 | "node": ">= 0.6"
271 | }
272 | },
273 | "node_modules/acorn": {
274 | "version": "8.14.1",
275 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
276 | "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
277 | "dev": true,
278 | "license": "MIT",
279 | "bin": {
280 | "acorn": "bin/acorn"
281 | },
282 | "engines": {
283 | "node": ">=0.4.0"
284 | }
285 | },
286 | "node_modules/acorn-walk": {
287 | "version": "8.3.4",
288 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
289 | "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
290 | "dev": true,
291 | "license": "MIT",
292 | "dependencies": {
293 | "acorn": "^8.11.0"
294 | },
295 | "engines": {
296 | "node": ">=0.4.0"
297 | }
298 | },
299 | "node_modules/anymatch": {
300 | "version": "3.1.3",
301 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
302 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
303 | "license": "ISC",
304 | "dependencies": {
305 | "normalize-path": "^3.0.0",
306 | "picomatch": "^2.0.4"
307 | },
308 | "engines": {
309 | "node": ">= 8"
310 | }
311 | },
312 | "node_modules/arg": {
313 | "version": "4.1.3",
314 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
315 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
316 | "dev": true,
317 | "license": "MIT"
318 | },
319 | "node_modules/array-flatten": {
320 | "version": "1.1.1",
321 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
322 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
323 | "license": "MIT"
324 | },
325 | "node_modules/balanced-match": {
326 | "version": "1.0.2",
327 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
328 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
329 | "license": "MIT"
330 | },
331 | "node_modules/base64id": {
332 | "version": "2.0.0",
333 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
334 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
335 | "license": "MIT",
336 | "engines": {
337 | "node": "^4.5.0 || >= 5.9"
338 | }
339 | },
340 | "node_modules/binary-extensions": {
341 | "version": "2.3.0",
342 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
343 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
344 | "license": "MIT",
345 | "engines": {
346 | "node": ">=8"
347 | },
348 | "funding": {
349 | "url": "https://github.com/sponsors/sindresorhus"
350 | }
351 | },
352 | "node_modules/body-parser": {
353 | "version": "1.20.3",
354 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
355 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
356 | "license": "MIT",
357 | "dependencies": {
358 | "bytes": "3.1.2",
359 | "content-type": "~1.0.5",
360 | "debug": "2.6.9",
361 | "depd": "2.0.0",
362 | "destroy": "1.2.0",
363 | "http-errors": "2.0.0",
364 | "iconv-lite": "0.4.24",
365 | "on-finished": "2.4.1",
366 | "qs": "6.13.0",
367 | "raw-body": "2.5.2",
368 | "type-is": "~1.6.18",
369 | "unpipe": "1.0.0"
370 | },
371 | "engines": {
372 | "node": ">= 0.8",
373 | "npm": "1.2.8000 || >= 1.4.16"
374 | }
375 | },
376 | "node_modules/brace-expansion": {
377 | "version": "1.1.11",
378 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
379 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
380 | "license": "MIT",
381 | "dependencies": {
382 | "balanced-match": "^1.0.0",
383 | "concat-map": "0.0.1"
384 | }
385 | },
386 | "node_modules/braces": {
387 | "version": "3.0.3",
388 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
389 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
390 | "license": "MIT",
391 | "dependencies": {
392 | "fill-range": "^7.1.1"
393 | },
394 | "engines": {
395 | "node": ">=8"
396 | }
397 | },
398 | "node_modules/bson": {
399 | "version": "6.10.3",
400 | "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz",
401 | "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==",
402 | "license": "Apache-2.0",
403 | "engines": {
404 | "node": ">=16.20.1"
405 | }
406 | },
407 | "node_modules/buffer-equal-constant-time": {
408 | "version": "1.0.1",
409 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
410 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
411 | "license": "BSD-3-Clause"
412 | },
413 | "node_modules/bytes": {
414 | "version": "3.1.2",
415 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
416 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
417 | "license": "MIT",
418 | "engines": {
419 | "node": ">= 0.8"
420 | }
421 | },
422 | "node_modules/call-bind-apply-helpers": {
423 | "version": "1.0.2",
424 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
425 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
426 | "license": "MIT",
427 | "dependencies": {
428 | "es-errors": "^1.3.0",
429 | "function-bind": "^1.1.2"
430 | },
431 | "engines": {
432 | "node": ">= 0.4"
433 | }
434 | },
435 | "node_modules/call-bound": {
436 | "version": "1.0.4",
437 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
438 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
439 | "license": "MIT",
440 | "dependencies": {
441 | "call-bind-apply-helpers": "^1.0.2",
442 | "get-intrinsic": "^1.3.0"
443 | },
444 | "engines": {
445 | "node": ">= 0.4"
446 | },
447 | "funding": {
448 | "url": "https://github.com/sponsors/ljharb"
449 | }
450 | },
451 | "node_modules/chokidar": {
452 | "version": "3.6.0",
453 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
454 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
455 | "license": "MIT",
456 | "dependencies": {
457 | "anymatch": "~3.1.2",
458 | "braces": "~3.0.2",
459 | "glob-parent": "~5.1.2",
460 | "is-binary-path": "~2.1.0",
461 | "is-glob": "~4.0.1",
462 | "normalize-path": "~3.0.0",
463 | "readdirp": "~3.6.0"
464 | },
465 | "engines": {
466 | "node": ">= 8.10.0"
467 | },
468 | "funding": {
469 | "url": "https://paulmillr.com/funding/"
470 | },
471 | "optionalDependencies": {
472 | "fsevents": "~2.3.2"
473 | }
474 | },
475 | "node_modules/concat-map": {
476 | "version": "0.0.1",
477 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
478 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
479 | "license": "MIT"
480 | },
481 | "node_modules/content-disposition": {
482 | "version": "0.5.4",
483 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
484 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
485 | "license": "MIT",
486 | "dependencies": {
487 | "safe-buffer": "5.2.1"
488 | },
489 | "engines": {
490 | "node": ">= 0.6"
491 | }
492 | },
493 | "node_modules/content-type": {
494 | "version": "1.0.5",
495 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
496 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
497 | "license": "MIT",
498 | "engines": {
499 | "node": ">= 0.6"
500 | }
501 | },
502 | "node_modules/cookie": {
503 | "version": "0.7.1",
504 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
505 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
506 | "license": "MIT",
507 | "engines": {
508 | "node": ">= 0.6"
509 | }
510 | },
511 | "node_modules/cookie-signature": {
512 | "version": "1.0.6",
513 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
514 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
515 | "license": "MIT"
516 | },
517 | "node_modules/cors": {
518 | "version": "2.8.5",
519 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
520 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
521 | "license": "MIT",
522 | "dependencies": {
523 | "object-assign": "^4",
524 | "vary": "^1"
525 | },
526 | "engines": {
527 | "node": ">= 0.10"
528 | }
529 | },
530 | "node_modules/create-require": {
531 | "version": "1.1.1",
532 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
533 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
534 | "dev": true,
535 | "license": "MIT"
536 | },
537 | "node_modules/debug": {
538 | "version": "2.6.9",
539 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
540 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
541 | "license": "MIT",
542 | "dependencies": {
543 | "ms": "2.0.0"
544 | }
545 | },
546 | "node_modules/depd": {
547 | "version": "2.0.0",
548 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
549 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
550 | "license": "MIT",
551 | "engines": {
552 | "node": ">= 0.8"
553 | }
554 | },
555 | "node_modules/destroy": {
556 | "version": "1.2.0",
557 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
558 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
559 | "license": "MIT",
560 | "engines": {
561 | "node": ">= 0.8",
562 | "npm": "1.2.8000 || >= 1.4.16"
563 | }
564 | },
565 | "node_modules/diff": {
566 | "version": "4.0.2",
567 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
568 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
569 | "dev": true,
570 | "license": "BSD-3-Clause",
571 | "engines": {
572 | "node": ">=0.3.1"
573 | }
574 | },
575 | "node_modules/dotenv": {
576 | "version": "16.4.7",
577 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
578 | "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
579 | "license": "BSD-2-Clause",
580 | "engines": {
581 | "node": ">=12"
582 | },
583 | "funding": {
584 | "url": "https://dotenvx.com"
585 | }
586 | },
587 | "node_modules/dunder-proto": {
588 | "version": "1.0.1",
589 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
590 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
591 | "license": "MIT",
592 | "dependencies": {
593 | "call-bind-apply-helpers": "^1.0.1",
594 | "es-errors": "^1.3.0",
595 | "gopd": "^1.2.0"
596 | },
597 | "engines": {
598 | "node": ">= 0.4"
599 | }
600 | },
601 | "node_modules/ecdsa-sig-formatter": {
602 | "version": "1.0.11",
603 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
604 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
605 | "license": "Apache-2.0",
606 | "dependencies": {
607 | "safe-buffer": "^5.0.1"
608 | }
609 | },
610 | "node_modules/ee-first": {
611 | "version": "1.1.1",
612 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
613 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
614 | "license": "MIT"
615 | },
616 | "node_modules/encodeurl": {
617 | "version": "2.0.0",
618 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
619 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
620 | "license": "MIT",
621 | "engines": {
622 | "node": ">= 0.8"
623 | }
624 | },
625 | "node_modules/engine.io": {
626 | "version": "6.6.4",
627 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
628 | "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
629 | "license": "MIT",
630 | "dependencies": {
631 | "@types/cors": "^2.8.12",
632 | "@types/node": ">=10.0.0",
633 | "accepts": "~1.3.4",
634 | "base64id": "2.0.0",
635 | "cookie": "~0.7.2",
636 | "cors": "~2.8.5",
637 | "debug": "~4.3.1",
638 | "engine.io-parser": "~5.2.1",
639 | "ws": "~8.17.1"
640 | },
641 | "engines": {
642 | "node": ">=10.2.0"
643 | }
644 | },
645 | "node_modules/engine.io-parser": {
646 | "version": "5.2.3",
647 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
648 | "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
649 | "license": "MIT",
650 | "engines": {
651 | "node": ">=10.0.0"
652 | }
653 | },
654 | "node_modules/engine.io/node_modules/cookie": {
655 | "version": "0.7.2",
656 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
657 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
658 | "license": "MIT",
659 | "engines": {
660 | "node": ">= 0.6"
661 | }
662 | },
663 | "node_modules/engine.io/node_modules/debug": {
664 | "version": "4.3.7",
665 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
666 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
667 | "license": "MIT",
668 | "dependencies": {
669 | "ms": "^2.1.3"
670 | },
671 | "engines": {
672 | "node": ">=6.0"
673 | },
674 | "peerDependenciesMeta": {
675 | "supports-color": {
676 | "optional": true
677 | }
678 | }
679 | },
680 | "node_modules/engine.io/node_modules/ms": {
681 | "version": "2.1.3",
682 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
683 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
684 | "license": "MIT"
685 | },
686 | "node_modules/es-define-property": {
687 | "version": "1.0.1",
688 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
689 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
690 | "license": "MIT",
691 | "engines": {
692 | "node": ">= 0.4"
693 | }
694 | },
695 | "node_modules/es-errors": {
696 | "version": "1.3.0",
697 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
698 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
699 | "license": "MIT",
700 | "engines": {
701 | "node": ">= 0.4"
702 | }
703 | },
704 | "node_modules/es-object-atoms": {
705 | "version": "1.1.1",
706 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
707 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
708 | "license": "MIT",
709 | "dependencies": {
710 | "es-errors": "^1.3.0"
711 | },
712 | "engines": {
713 | "node": ">= 0.4"
714 | }
715 | },
716 | "node_modules/escape-html": {
717 | "version": "1.0.3",
718 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
719 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
720 | "license": "MIT"
721 | },
722 | "node_modules/etag": {
723 | "version": "1.8.1",
724 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
725 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
726 | "license": "MIT",
727 | "engines": {
728 | "node": ">= 0.6"
729 | }
730 | },
731 | "node_modules/express": {
732 | "version": "4.21.2",
733 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
734 | "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
735 | "license": "MIT",
736 | "dependencies": {
737 | "accepts": "~1.3.8",
738 | "array-flatten": "1.1.1",
739 | "body-parser": "1.20.3",
740 | "content-disposition": "0.5.4",
741 | "content-type": "~1.0.4",
742 | "cookie": "0.7.1",
743 | "cookie-signature": "1.0.6",
744 | "debug": "2.6.9",
745 | "depd": "2.0.0",
746 | "encodeurl": "~2.0.0",
747 | "escape-html": "~1.0.3",
748 | "etag": "~1.8.1",
749 | "finalhandler": "1.3.1",
750 | "fresh": "0.5.2",
751 | "http-errors": "2.0.0",
752 | "merge-descriptors": "1.0.3",
753 | "methods": "~1.1.2",
754 | "on-finished": "2.4.1",
755 | "parseurl": "~1.3.3",
756 | "path-to-regexp": "0.1.12",
757 | "proxy-addr": "~2.0.7",
758 | "qs": "6.13.0",
759 | "range-parser": "~1.2.1",
760 | "safe-buffer": "5.2.1",
761 | "send": "0.19.0",
762 | "serve-static": "1.16.2",
763 | "setprototypeof": "1.2.0",
764 | "statuses": "2.0.1",
765 | "type-is": "~1.6.18",
766 | "utils-merge": "1.0.1",
767 | "vary": "~1.1.2"
768 | },
769 | "engines": {
770 | "node": ">= 0.10.0"
771 | },
772 | "funding": {
773 | "type": "opencollective",
774 | "url": "https://opencollective.com/express"
775 | }
776 | },
777 | "node_modules/fill-range": {
778 | "version": "7.1.1",
779 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
780 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
781 | "license": "MIT",
782 | "dependencies": {
783 | "to-regex-range": "^5.0.1"
784 | },
785 | "engines": {
786 | "node": ">=8"
787 | }
788 | },
789 | "node_modules/finalhandler": {
790 | "version": "1.3.1",
791 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
792 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
793 | "license": "MIT",
794 | "dependencies": {
795 | "debug": "2.6.9",
796 | "encodeurl": "~2.0.0",
797 | "escape-html": "~1.0.3",
798 | "on-finished": "2.4.1",
799 | "parseurl": "~1.3.3",
800 | "statuses": "2.0.1",
801 | "unpipe": "~1.0.0"
802 | },
803 | "engines": {
804 | "node": ">= 0.8"
805 | }
806 | },
807 | "node_modules/forwarded": {
808 | "version": "0.2.0",
809 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
810 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
811 | "license": "MIT",
812 | "engines": {
813 | "node": ">= 0.6"
814 | }
815 | },
816 | "node_modules/fresh": {
817 | "version": "0.5.2",
818 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
819 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
820 | "license": "MIT",
821 | "engines": {
822 | "node": ">= 0.6"
823 | }
824 | },
825 | "node_modules/fsevents": {
826 | "version": "2.3.3",
827 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
828 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
829 | "hasInstallScript": true,
830 | "license": "MIT",
831 | "optional": true,
832 | "os": [
833 | "darwin"
834 | ],
835 | "engines": {
836 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
837 | }
838 | },
839 | "node_modules/function-bind": {
840 | "version": "1.1.2",
841 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
842 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
843 | "license": "MIT",
844 | "funding": {
845 | "url": "https://github.com/sponsors/ljharb"
846 | }
847 | },
848 | "node_modules/get-intrinsic": {
849 | "version": "1.3.0",
850 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
851 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
852 | "license": "MIT",
853 | "dependencies": {
854 | "call-bind-apply-helpers": "^1.0.2",
855 | "es-define-property": "^1.0.1",
856 | "es-errors": "^1.3.0",
857 | "es-object-atoms": "^1.1.1",
858 | "function-bind": "^1.1.2",
859 | "get-proto": "^1.0.1",
860 | "gopd": "^1.2.0",
861 | "has-symbols": "^1.1.0",
862 | "hasown": "^2.0.2",
863 | "math-intrinsics": "^1.1.0"
864 | },
865 | "engines": {
866 | "node": ">= 0.4"
867 | },
868 | "funding": {
869 | "url": "https://github.com/sponsors/ljharb"
870 | }
871 | },
872 | "node_modules/get-proto": {
873 | "version": "1.0.1",
874 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
875 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
876 | "license": "MIT",
877 | "dependencies": {
878 | "dunder-proto": "^1.0.1",
879 | "es-object-atoms": "^1.0.0"
880 | },
881 | "engines": {
882 | "node": ">= 0.4"
883 | }
884 | },
885 | "node_modules/glob-parent": {
886 | "version": "5.1.2",
887 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
888 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
889 | "license": "ISC",
890 | "dependencies": {
891 | "is-glob": "^4.0.1"
892 | },
893 | "engines": {
894 | "node": ">= 6"
895 | }
896 | },
897 | "node_modules/gopd": {
898 | "version": "1.2.0",
899 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
900 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
901 | "license": "MIT",
902 | "engines": {
903 | "node": ">= 0.4"
904 | },
905 | "funding": {
906 | "url": "https://github.com/sponsors/ljharb"
907 | }
908 | },
909 | "node_modules/has-flag": {
910 | "version": "3.0.0",
911 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
912 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
913 | "license": "MIT",
914 | "engines": {
915 | "node": ">=4"
916 | }
917 | },
918 | "node_modules/has-symbols": {
919 | "version": "1.1.0",
920 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
921 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
922 | "license": "MIT",
923 | "engines": {
924 | "node": ">= 0.4"
925 | },
926 | "funding": {
927 | "url": "https://github.com/sponsors/ljharb"
928 | }
929 | },
930 | "node_modules/hasown": {
931 | "version": "2.0.2",
932 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
933 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
934 | "license": "MIT",
935 | "dependencies": {
936 | "function-bind": "^1.1.2"
937 | },
938 | "engines": {
939 | "node": ">= 0.4"
940 | }
941 | },
942 | "node_modules/http-errors": {
943 | "version": "2.0.0",
944 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
945 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
946 | "license": "MIT",
947 | "dependencies": {
948 | "depd": "2.0.0",
949 | "inherits": "2.0.4",
950 | "setprototypeof": "1.2.0",
951 | "statuses": "2.0.1",
952 | "toidentifier": "1.0.1"
953 | },
954 | "engines": {
955 | "node": ">= 0.8"
956 | }
957 | },
958 | "node_modules/iconv-lite": {
959 | "version": "0.4.24",
960 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
961 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
962 | "license": "MIT",
963 | "dependencies": {
964 | "safer-buffer": ">= 2.1.2 < 3"
965 | },
966 | "engines": {
967 | "node": ">=0.10.0"
968 | }
969 | },
970 | "node_modules/ignore-by-default": {
971 | "version": "1.0.1",
972 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
973 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
974 | "license": "ISC"
975 | },
976 | "node_modules/inherits": {
977 | "version": "2.0.4",
978 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
979 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
980 | "license": "ISC"
981 | },
982 | "node_modules/ipaddr.js": {
983 | "version": "1.9.1",
984 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
985 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
986 | "license": "MIT",
987 | "engines": {
988 | "node": ">= 0.10"
989 | }
990 | },
991 | "node_modules/is-binary-path": {
992 | "version": "2.1.0",
993 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
994 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
995 | "license": "MIT",
996 | "dependencies": {
997 | "binary-extensions": "^2.0.0"
998 | },
999 | "engines": {
1000 | "node": ">=8"
1001 | }
1002 | },
1003 | "node_modules/is-extglob": {
1004 | "version": "2.1.1",
1005 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1006 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1007 | "license": "MIT",
1008 | "engines": {
1009 | "node": ">=0.10.0"
1010 | }
1011 | },
1012 | "node_modules/is-glob": {
1013 | "version": "4.0.3",
1014 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1015 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1016 | "license": "MIT",
1017 | "dependencies": {
1018 | "is-extglob": "^2.1.1"
1019 | },
1020 | "engines": {
1021 | "node": ">=0.10.0"
1022 | }
1023 | },
1024 | "node_modules/is-number": {
1025 | "version": "7.0.0",
1026 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
1027 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
1028 | "license": "MIT",
1029 | "engines": {
1030 | "node": ">=0.12.0"
1031 | }
1032 | },
1033 | "node_modules/jsonwebtoken": {
1034 | "version": "9.0.2",
1035 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
1036 | "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
1037 | "license": "MIT",
1038 | "dependencies": {
1039 | "jws": "^3.2.2",
1040 | "lodash.includes": "^4.3.0",
1041 | "lodash.isboolean": "^3.0.3",
1042 | "lodash.isinteger": "^4.0.4",
1043 | "lodash.isnumber": "^3.0.3",
1044 | "lodash.isplainobject": "^4.0.6",
1045 | "lodash.isstring": "^4.0.1",
1046 | "lodash.once": "^4.0.0",
1047 | "ms": "^2.1.1",
1048 | "semver": "^7.5.4"
1049 | },
1050 | "engines": {
1051 | "node": ">=12",
1052 | "npm": ">=6"
1053 | }
1054 | },
1055 | "node_modules/jsonwebtoken/node_modules/ms": {
1056 | "version": "2.1.3",
1057 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1058 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1059 | "license": "MIT"
1060 | },
1061 | "node_modules/jwa": {
1062 | "version": "1.4.1",
1063 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
1064 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
1065 | "license": "MIT",
1066 | "dependencies": {
1067 | "buffer-equal-constant-time": "1.0.1",
1068 | "ecdsa-sig-formatter": "1.0.11",
1069 | "safe-buffer": "^5.0.1"
1070 | }
1071 | },
1072 | "node_modules/jws": {
1073 | "version": "3.2.2",
1074 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
1075 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
1076 | "license": "MIT",
1077 | "dependencies": {
1078 | "jwa": "^1.4.1",
1079 | "safe-buffer": "^5.0.1"
1080 | }
1081 | },
1082 | "node_modules/kareem": {
1083 | "version": "2.6.3",
1084 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
1085 | "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
1086 | "license": "Apache-2.0",
1087 | "engines": {
1088 | "node": ">=12.0.0"
1089 | }
1090 | },
1091 | "node_modules/lodash.includes": {
1092 | "version": "4.3.0",
1093 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
1094 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
1095 | "license": "MIT"
1096 | },
1097 | "node_modules/lodash.isboolean": {
1098 | "version": "3.0.3",
1099 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
1100 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
1101 | "license": "MIT"
1102 | },
1103 | "node_modules/lodash.isinteger": {
1104 | "version": "4.0.4",
1105 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
1106 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
1107 | "license": "MIT"
1108 | },
1109 | "node_modules/lodash.isnumber": {
1110 | "version": "3.0.3",
1111 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
1112 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
1113 | "license": "MIT"
1114 | },
1115 | "node_modules/lodash.isplainobject": {
1116 | "version": "4.0.6",
1117 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
1118 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
1119 | "license": "MIT"
1120 | },
1121 | "node_modules/lodash.isstring": {
1122 | "version": "4.0.1",
1123 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
1124 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
1125 | "license": "MIT"
1126 | },
1127 | "node_modules/lodash.once": {
1128 | "version": "4.1.1",
1129 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
1130 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
1131 | "license": "MIT"
1132 | },
1133 | "node_modules/make-error": {
1134 | "version": "1.3.6",
1135 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
1136 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
1137 | "dev": true,
1138 | "license": "ISC"
1139 | },
1140 | "node_modules/math-intrinsics": {
1141 | "version": "1.1.0",
1142 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1143 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1144 | "license": "MIT",
1145 | "engines": {
1146 | "node": ">= 0.4"
1147 | }
1148 | },
1149 | "node_modules/media-typer": {
1150 | "version": "0.3.0",
1151 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
1152 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
1153 | "license": "MIT",
1154 | "engines": {
1155 | "node": ">= 0.6"
1156 | }
1157 | },
1158 | "node_modules/memory-pager": {
1159 | "version": "1.5.0",
1160 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
1161 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
1162 | "license": "MIT"
1163 | },
1164 | "node_modules/merge-descriptors": {
1165 | "version": "1.0.3",
1166 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
1167 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
1168 | "license": "MIT",
1169 | "funding": {
1170 | "url": "https://github.com/sponsors/sindresorhus"
1171 | }
1172 | },
1173 | "node_modules/methods": {
1174 | "version": "1.1.2",
1175 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
1176 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
1177 | "license": "MIT",
1178 | "engines": {
1179 | "node": ">= 0.6"
1180 | }
1181 | },
1182 | "node_modules/mime": {
1183 | "version": "1.6.0",
1184 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
1185 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
1186 | "license": "MIT",
1187 | "bin": {
1188 | "mime": "cli.js"
1189 | },
1190 | "engines": {
1191 | "node": ">=4"
1192 | }
1193 | },
1194 | "node_modules/mime-db": {
1195 | "version": "1.52.0",
1196 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1197 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1198 | "license": "MIT",
1199 | "engines": {
1200 | "node": ">= 0.6"
1201 | }
1202 | },
1203 | "node_modules/mime-types": {
1204 | "version": "2.1.35",
1205 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1206 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1207 | "license": "MIT",
1208 | "dependencies": {
1209 | "mime-db": "1.52.0"
1210 | },
1211 | "engines": {
1212 | "node": ">= 0.6"
1213 | }
1214 | },
1215 | "node_modules/minimatch": {
1216 | "version": "3.1.2",
1217 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
1218 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
1219 | "license": "ISC",
1220 | "dependencies": {
1221 | "brace-expansion": "^1.1.7"
1222 | },
1223 | "engines": {
1224 | "node": "*"
1225 | }
1226 | },
1227 | "node_modules/mongodb": {
1228 | "version": "6.14.2",
1229 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz",
1230 | "integrity": "sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q==",
1231 | "license": "Apache-2.0",
1232 | "dependencies": {
1233 | "@mongodb-js/saslprep": "^1.1.9",
1234 | "bson": "^6.10.3",
1235 | "mongodb-connection-string-url": "^3.0.0"
1236 | },
1237 | "engines": {
1238 | "node": ">=16.20.1"
1239 | },
1240 | "peerDependencies": {
1241 | "@aws-sdk/credential-providers": "^3.188.0",
1242 | "@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
1243 | "gcp-metadata": "^5.2.0",
1244 | "kerberos": "^2.0.1",
1245 | "mongodb-client-encryption": ">=6.0.0 <7",
1246 | "snappy": "^7.2.2",
1247 | "socks": "^2.7.1"
1248 | },
1249 | "peerDependenciesMeta": {
1250 | "@aws-sdk/credential-providers": {
1251 | "optional": true
1252 | },
1253 | "@mongodb-js/zstd": {
1254 | "optional": true
1255 | },
1256 | "gcp-metadata": {
1257 | "optional": true
1258 | },
1259 | "kerberos": {
1260 | "optional": true
1261 | },
1262 | "mongodb-client-encryption": {
1263 | "optional": true
1264 | },
1265 | "snappy": {
1266 | "optional": true
1267 | },
1268 | "socks": {
1269 | "optional": true
1270 | }
1271 | }
1272 | },
1273 | "node_modules/mongodb-connection-string-url": {
1274 | "version": "3.0.2",
1275 | "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
1276 | "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
1277 | "license": "Apache-2.0",
1278 | "dependencies": {
1279 | "@types/whatwg-url": "^11.0.2",
1280 | "whatwg-url": "^14.1.0 || ^13.0.0"
1281 | }
1282 | },
1283 | "node_modules/mongoose": {
1284 | "version": "8.12.1",
1285 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.12.1.tgz",
1286 | "integrity": "sha512-UW22y8QFVYmrb36hm8cGncfn4ARc/XsYWQwRTaj0gxtQk1rDuhzDO1eBantS+hTTatfAIS96LlRCJrcNHvW5+Q==",
1287 | "license": "MIT",
1288 | "dependencies": {
1289 | "bson": "^6.10.3",
1290 | "kareem": "2.6.3",
1291 | "mongodb": "~6.14.0",
1292 | "mpath": "0.9.0",
1293 | "mquery": "5.0.0",
1294 | "ms": "2.1.3",
1295 | "sift": "17.1.3"
1296 | },
1297 | "engines": {
1298 | "node": ">=16.20.1"
1299 | },
1300 | "funding": {
1301 | "type": "opencollective",
1302 | "url": "https://opencollective.com/mongoose"
1303 | }
1304 | },
1305 | "node_modules/mongoose/node_modules/ms": {
1306 | "version": "2.1.3",
1307 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1308 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1309 | "license": "MIT"
1310 | },
1311 | "node_modules/mpath": {
1312 | "version": "0.9.0",
1313 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
1314 | "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
1315 | "license": "MIT",
1316 | "engines": {
1317 | "node": ">=4.0.0"
1318 | }
1319 | },
1320 | "node_modules/mquery": {
1321 | "version": "5.0.0",
1322 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
1323 | "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
1324 | "license": "MIT",
1325 | "dependencies": {
1326 | "debug": "4.x"
1327 | },
1328 | "engines": {
1329 | "node": ">=14.0.0"
1330 | }
1331 | },
1332 | "node_modules/mquery/node_modules/debug": {
1333 | "version": "4.4.0",
1334 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
1335 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
1336 | "license": "MIT",
1337 | "dependencies": {
1338 | "ms": "^2.1.3"
1339 | },
1340 | "engines": {
1341 | "node": ">=6.0"
1342 | },
1343 | "peerDependenciesMeta": {
1344 | "supports-color": {
1345 | "optional": true
1346 | }
1347 | }
1348 | },
1349 | "node_modules/mquery/node_modules/ms": {
1350 | "version": "2.1.3",
1351 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1352 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1353 | "license": "MIT"
1354 | },
1355 | "node_modules/ms": {
1356 | "version": "2.0.0",
1357 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1358 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
1359 | "license": "MIT"
1360 | },
1361 | "node_modules/negotiator": {
1362 | "version": "0.6.3",
1363 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1364 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
1365 | "license": "MIT",
1366 | "engines": {
1367 | "node": ">= 0.6"
1368 | }
1369 | },
1370 | "node_modules/node-cron": {
1371 | "version": "3.0.3",
1372 | "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
1373 | "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
1374 | "license": "ISC",
1375 | "dependencies": {
1376 | "uuid": "8.3.2"
1377 | },
1378 | "engines": {
1379 | "node": ">=6.0.0"
1380 | }
1381 | },
1382 | "node_modules/node-cron/node_modules/uuid": {
1383 | "version": "8.3.2",
1384 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
1385 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
1386 | "license": "MIT",
1387 | "bin": {
1388 | "uuid": "dist/bin/uuid"
1389 | }
1390 | },
1391 | "node_modules/nodemon": {
1392 | "version": "3.1.9",
1393 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
1394 | "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==",
1395 | "license": "MIT",
1396 | "dependencies": {
1397 | "chokidar": "^3.5.2",
1398 | "debug": "^4",
1399 | "ignore-by-default": "^1.0.1",
1400 | "minimatch": "^3.1.2",
1401 | "pstree.remy": "^1.1.8",
1402 | "semver": "^7.5.3",
1403 | "simple-update-notifier": "^2.0.0",
1404 | "supports-color": "^5.5.0",
1405 | "touch": "^3.1.0",
1406 | "undefsafe": "^2.0.5"
1407 | },
1408 | "bin": {
1409 | "nodemon": "bin/nodemon.js"
1410 | },
1411 | "engines": {
1412 | "node": ">=10"
1413 | },
1414 | "funding": {
1415 | "type": "opencollective",
1416 | "url": "https://opencollective.com/nodemon"
1417 | }
1418 | },
1419 | "node_modules/nodemon/node_modules/debug": {
1420 | "version": "4.4.0",
1421 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
1422 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
1423 | "license": "MIT",
1424 | "dependencies": {
1425 | "ms": "^2.1.3"
1426 | },
1427 | "engines": {
1428 | "node": ">=6.0"
1429 | },
1430 | "peerDependenciesMeta": {
1431 | "supports-color": {
1432 | "optional": true
1433 | }
1434 | }
1435 | },
1436 | "node_modules/nodemon/node_modules/ms": {
1437 | "version": "2.1.3",
1438 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1439 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1440 | "license": "MIT"
1441 | },
1442 | "node_modules/normalize-path": {
1443 | "version": "3.0.0",
1444 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1445 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1446 | "license": "MIT",
1447 | "engines": {
1448 | "node": ">=0.10.0"
1449 | }
1450 | },
1451 | "node_modules/object-assign": {
1452 | "version": "4.1.1",
1453 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1454 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1455 | "license": "MIT",
1456 | "engines": {
1457 | "node": ">=0.10.0"
1458 | }
1459 | },
1460 | "node_modules/object-inspect": {
1461 | "version": "1.13.4",
1462 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1463 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1464 | "license": "MIT",
1465 | "engines": {
1466 | "node": ">= 0.4"
1467 | },
1468 | "funding": {
1469 | "url": "https://github.com/sponsors/ljharb"
1470 | }
1471 | },
1472 | "node_modules/on-finished": {
1473 | "version": "2.4.1",
1474 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1475 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1476 | "license": "MIT",
1477 | "dependencies": {
1478 | "ee-first": "1.1.1"
1479 | },
1480 | "engines": {
1481 | "node": ">= 0.8"
1482 | }
1483 | },
1484 | "node_modules/parseurl": {
1485 | "version": "1.3.3",
1486 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1487 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1488 | "license": "MIT",
1489 | "engines": {
1490 | "node": ">= 0.8"
1491 | }
1492 | },
1493 | "node_modules/path-to-regexp": {
1494 | "version": "0.1.12",
1495 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
1496 | "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
1497 | "license": "MIT"
1498 | },
1499 | "node_modules/picomatch": {
1500 | "version": "2.3.1",
1501 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1502 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1503 | "license": "MIT",
1504 | "engines": {
1505 | "node": ">=8.6"
1506 | },
1507 | "funding": {
1508 | "url": "https://github.com/sponsors/jonschlinkert"
1509 | }
1510 | },
1511 | "node_modules/proxy-addr": {
1512 | "version": "2.0.7",
1513 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1514 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1515 | "license": "MIT",
1516 | "dependencies": {
1517 | "forwarded": "0.2.0",
1518 | "ipaddr.js": "1.9.1"
1519 | },
1520 | "engines": {
1521 | "node": ">= 0.10"
1522 | }
1523 | },
1524 | "node_modules/pstree.remy": {
1525 | "version": "1.1.8",
1526 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
1527 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
1528 | "license": "MIT"
1529 | },
1530 | "node_modules/punycode": {
1531 | "version": "2.3.1",
1532 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
1533 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
1534 | "license": "MIT",
1535 | "engines": {
1536 | "node": ">=6"
1537 | }
1538 | },
1539 | "node_modules/qs": {
1540 | "version": "6.13.0",
1541 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
1542 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
1543 | "license": "BSD-3-Clause",
1544 | "dependencies": {
1545 | "side-channel": "^1.0.6"
1546 | },
1547 | "engines": {
1548 | "node": ">=0.6"
1549 | },
1550 | "funding": {
1551 | "url": "https://github.com/sponsors/ljharb"
1552 | }
1553 | },
1554 | "node_modules/range-parser": {
1555 | "version": "1.2.1",
1556 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1557 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1558 | "license": "MIT",
1559 | "engines": {
1560 | "node": ">= 0.6"
1561 | }
1562 | },
1563 | "node_modules/raw-body": {
1564 | "version": "2.5.2",
1565 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
1566 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
1567 | "license": "MIT",
1568 | "dependencies": {
1569 | "bytes": "3.1.2",
1570 | "http-errors": "2.0.0",
1571 | "iconv-lite": "0.4.24",
1572 | "unpipe": "1.0.0"
1573 | },
1574 | "engines": {
1575 | "node": ">= 0.8"
1576 | }
1577 | },
1578 | "node_modules/readdirp": {
1579 | "version": "3.6.0",
1580 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1581 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1582 | "license": "MIT",
1583 | "dependencies": {
1584 | "picomatch": "^2.2.1"
1585 | },
1586 | "engines": {
1587 | "node": ">=8.10.0"
1588 | }
1589 | },
1590 | "node_modules/safe-buffer": {
1591 | "version": "5.2.1",
1592 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1593 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1594 | "funding": [
1595 | {
1596 | "type": "github",
1597 | "url": "https://github.com/sponsors/feross"
1598 | },
1599 | {
1600 | "type": "patreon",
1601 | "url": "https://www.patreon.com/feross"
1602 | },
1603 | {
1604 | "type": "consulting",
1605 | "url": "https://feross.org/support"
1606 | }
1607 | ],
1608 | "license": "MIT"
1609 | },
1610 | "node_modules/safer-buffer": {
1611 | "version": "2.1.2",
1612 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1613 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1614 | "license": "MIT"
1615 | },
1616 | "node_modules/semver": {
1617 | "version": "7.7.1",
1618 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
1619 | "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
1620 | "license": "ISC",
1621 | "bin": {
1622 | "semver": "bin/semver.js"
1623 | },
1624 | "engines": {
1625 | "node": ">=10"
1626 | }
1627 | },
1628 | "node_modules/send": {
1629 | "version": "0.19.0",
1630 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
1631 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
1632 | "license": "MIT",
1633 | "dependencies": {
1634 | "debug": "2.6.9",
1635 | "depd": "2.0.0",
1636 | "destroy": "1.2.0",
1637 | "encodeurl": "~1.0.2",
1638 | "escape-html": "~1.0.3",
1639 | "etag": "~1.8.1",
1640 | "fresh": "0.5.2",
1641 | "http-errors": "2.0.0",
1642 | "mime": "1.6.0",
1643 | "ms": "2.1.3",
1644 | "on-finished": "2.4.1",
1645 | "range-parser": "~1.2.1",
1646 | "statuses": "2.0.1"
1647 | },
1648 | "engines": {
1649 | "node": ">= 0.8.0"
1650 | }
1651 | },
1652 | "node_modules/send/node_modules/encodeurl": {
1653 | "version": "1.0.2",
1654 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
1655 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
1656 | "license": "MIT",
1657 | "engines": {
1658 | "node": ">= 0.8"
1659 | }
1660 | },
1661 | "node_modules/send/node_modules/ms": {
1662 | "version": "2.1.3",
1663 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1664 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1665 | "license": "MIT"
1666 | },
1667 | "node_modules/serve-static": {
1668 | "version": "1.16.2",
1669 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
1670 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
1671 | "license": "MIT",
1672 | "dependencies": {
1673 | "encodeurl": "~2.0.0",
1674 | "escape-html": "~1.0.3",
1675 | "parseurl": "~1.3.3",
1676 | "send": "0.19.0"
1677 | },
1678 | "engines": {
1679 | "node": ">= 0.8.0"
1680 | }
1681 | },
1682 | "node_modules/setprototypeof": {
1683 | "version": "1.2.0",
1684 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1685 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1686 | "license": "ISC"
1687 | },
1688 | "node_modules/side-channel": {
1689 | "version": "1.1.0",
1690 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1691 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1692 | "license": "MIT",
1693 | "dependencies": {
1694 | "es-errors": "^1.3.0",
1695 | "object-inspect": "^1.13.3",
1696 | "side-channel-list": "^1.0.0",
1697 | "side-channel-map": "^1.0.1",
1698 | "side-channel-weakmap": "^1.0.2"
1699 | },
1700 | "engines": {
1701 | "node": ">= 0.4"
1702 | },
1703 | "funding": {
1704 | "url": "https://github.com/sponsors/ljharb"
1705 | }
1706 | },
1707 | "node_modules/side-channel-list": {
1708 | "version": "1.0.0",
1709 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1710 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1711 | "license": "MIT",
1712 | "dependencies": {
1713 | "es-errors": "^1.3.0",
1714 | "object-inspect": "^1.13.3"
1715 | },
1716 | "engines": {
1717 | "node": ">= 0.4"
1718 | },
1719 | "funding": {
1720 | "url": "https://github.com/sponsors/ljharb"
1721 | }
1722 | },
1723 | "node_modules/side-channel-map": {
1724 | "version": "1.0.1",
1725 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1726 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1727 | "license": "MIT",
1728 | "dependencies": {
1729 | "call-bound": "^1.0.2",
1730 | "es-errors": "^1.3.0",
1731 | "get-intrinsic": "^1.2.5",
1732 | "object-inspect": "^1.13.3"
1733 | },
1734 | "engines": {
1735 | "node": ">= 0.4"
1736 | },
1737 | "funding": {
1738 | "url": "https://github.com/sponsors/ljharb"
1739 | }
1740 | },
1741 | "node_modules/side-channel-weakmap": {
1742 | "version": "1.0.2",
1743 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1744 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1745 | "license": "MIT",
1746 | "dependencies": {
1747 | "call-bound": "^1.0.2",
1748 | "es-errors": "^1.3.0",
1749 | "get-intrinsic": "^1.2.5",
1750 | "object-inspect": "^1.13.3",
1751 | "side-channel-map": "^1.0.1"
1752 | },
1753 | "engines": {
1754 | "node": ">= 0.4"
1755 | },
1756 | "funding": {
1757 | "url": "https://github.com/sponsors/ljharb"
1758 | }
1759 | },
1760 | "node_modules/sift": {
1761 | "version": "17.1.3",
1762 | "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
1763 | "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
1764 | "license": "MIT"
1765 | },
1766 | "node_modules/simple-update-notifier": {
1767 | "version": "2.0.0",
1768 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1769 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1770 | "license": "MIT",
1771 | "dependencies": {
1772 | "semver": "^7.5.3"
1773 | },
1774 | "engines": {
1775 | "node": ">=10"
1776 | }
1777 | },
1778 | "node_modules/socket.io": {
1779 | "version": "4.8.1",
1780 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
1781 | "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
1782 | "license": "MIT",
1783 | "dependencies": {
1784 | "accepts": "~1.3.4",
1785 | "base64id": "~2.0.0",
1786 | "cors": "~2.8.5",
1787 | "debug": "~4.3.2",
1788 | "engine.io": "~6.6.0",
1789 | "socket.io-adapter": "~2.5.2",
1790 | "socket.io-parser": "~4.2.4"
1791 | },
1792 | "engines": {
1793 | "node": ">=10.2.0"
1794 | }
1795 | },
1796 | "node_modules/socket.io-adapter": {
1797 | "version": "2.5.5",
1798 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
1799 | "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
1800 | "license": "MIT",
1801 | "dependencies": {
1802 | "debug": "~4.3.4",
1803 | "ws": "~8.17.1"
1804 | }
1805 | },
1806 | "node_modules/socket.io-adapter/node_modules/debug": {
1807 | "version": "4.3.7",
1808 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
1809 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
1810 | "license": "MIT",
1811 | "dependencies": {
1812 | "ms": "^2.1.3"
1813 | },
1814 | "engines": {
1815 | "node": ">=6.0"
1816 | },
1817 | "peerDependenciesMeta": {
1818 | "supports-color": {
1819 | "optional": true
1820 | }
1821 | }
1822 | },
1823 | "node_modules/socket.io-adapter/node_modules/ms": {
1824 | "version": "2.1.3",
1825 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1826 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1827 | "license": "MIT"
1828 | },
1829 | "node_modules/socket.io-parser": {
1830 | "version": "4.2.4",
1831 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
1832 | "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
1833 | "license": "MIT",
1834 | "dependencies": {
1835 | "@socket.io/component-emitter": "~3.1.0",
1836 | "debug": "~4.3.1"
1837 | },
1838 | "engines": {
1839 | "node": ">=10.0.0"
1840 | }
1841 | },
1842 | "node_modules/socket.io-parser/node_modules/debug": {
1843 | "version": "4.3.7",
1844 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
1845 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
1846 | "license": "MIT",
1847 | "dependencies": {
1848 | "ms": "^2.1.3"
1849 | },
1850 | "engines": {
1851 | "node": ">=6.0"
1852 | },
1853 | "peerDependenciesMeta": {
1854 | "supports-color": {
1855 | "optional": true
1856 | }
1857 | }
1858 | },
1859 | "node_modules/socket.io-parser/node_modules/ms": {
1860 | "version": "2.1.3",
1861 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1862 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1863 | "license": "MIT"
1864 | },
1865 | "node_modules/socket.io/node_modules/debug": {
1866 | "version": "4.3.7",
1867 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
1868 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
1869 | "license": "MIT",
1870 | "dependencies": {
1871 | "ms": "^2.1.3"
1872 | },
1873 | "engines": {
1874 | "node": ">=6.0"
1875 | },
1876 | "peerDependenciesMeta": {
1877 | "supports-color": {
1878 | "optional": true
1879 | }
1880 | }
1881 | },
1882 | "node_modules/socket.io/node_modules/ms": {
1883 | "version": "2.1.3",
1884 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1885 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1886 | "license": "MIT"
1887 | },
1888 | "node_modules/sparse-bitfield": {
1889 | "version": "3.0.3",
1890 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
1891 | "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
1892 | "license": "MIT",
1893 | "dependencies": {
1894 | "memory-pager": "^1.0.2"
1895 | }
1896 | },
1897 | "node_modules/statuses": {
1898 | "version": "2.0.1",
1899 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1900 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1901 | "license": "MIT",
1902 | "engines": {
1903 | "node": ">= 0.8"
1904 | }
1905 | },
1906 | "node_modules/supports-color": {
1907 | "version": "5.5.0",
1908 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
1909 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
1910 | "license": "MIT",
1911 | "dependencies": {
1912 | "has-flag": "^3.0.0"
1913 | },
1914 | "engines": {
1915 | "node": ">=4"
1916 | }
1917 | },
1918 | "node_modules/to-regex-range": {
1919 | "version": "5.0.1",
1920 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1921 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1922 | "license": "MIT",
1923 | "dependencies": {
1924 | "is-number": "^7.0.0"
1925 | },
1926 | "engines": {
1927 | "node": ">=8.0"
1928 | }
1929 | },
1930 | "node_modules/toidentifier": {
1931 | "version": "1.0.1",
1932 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1933 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1934 | "license": "MIT",
1935 | "engines": {
1936 | "node": ">=0.6"
1937 | }
1938 | },
1939 | "node_modules/touch": {
1940 | "version": "3.1.1",
1941 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
1942 | "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
1943 | "license": "ISC",
1944 | "bin": {
1945 | "nodetouch": "bin/nodetouch.js"
1946 | }
1947 | },
1948 | "node_modules/tr46": {
1949 | "version": "5.0.0",
1950 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
1951 | "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
1952 | "license": "MIT",
1953 | "dependencies": {
1954 | "punycode": "^2.3.1"
1955 | },
1956 | "engines": {
1957 | "node": ">=18"
1958 | }
1959 | },
1960 | "node_modules/ts-node": {
1961 | "version": "10.9.2",
1962 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
1963 | "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
1964 | "dev": true,
1965 | "license": "MIT",
1966 | "dependencies": {
1967 | "@cspotcode/source-map-support": "^0.8.0",
1968 | "@tsconfig/node10": "^1.0.7",
1969 | "@tsconfig/node12": "^1.0.7",
1970 | "@tsconfig/node14": "^1.0.0",
1971 | "@tsconfig/node16": "^1.0.2",
1972 | "acorn": "^8.4.1",
1973 | "acorn-walk": "^8.1.1",
1974 | "arg": "^4.1.0",
1975 | "create-require": "^1.1.0",
1976 | "diff": "^4.0.1",
1977 | "make-error": "^1.1.1",
1978 | "v8-compile-cache-lib": "^3.0.1",
1979 | "yn": "3.1.1"
1980 | },
1981 | "bin": {
1982 | "ts-node": "dist/bin.js",
1983 | "ts-node-cwd": "dist/bin-cwd.js",
1984 | "ts-node-esm": "dist/bin-esm.js",
1985 | "ts-node-script": "dist/bin-script.js",
1986 | "ts-node-transpile-only": "dist/bin-transpile.js",
1987 | "ts-script": "dist/bin-script-deprecated.js"
1988 | },
1989 | "peerDependencies": {
1990 | "@swc/core": ">=1.2.50",
1991 | "@swc/wasm": ">=1.2.50",
1992 | "@types/node": "*",
1993 | "typescript": ">=2.7"
1994 | },
1995 | "peerDependenciesMeta": {
1996 | "@swc/core": {
1997 | "optional": true
1998 | },
1999 | "@swc/wasm": {
2000 | "optional": true
2001 | }
2002 | }
2003 | },
2004 | "node_modules/type-is": {
2005 | "version": "1.6.18",
2006 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
2007 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
2008 | "license": "MIT",
2009 | "dependencies": {
2010 | "media-typer": "0.3.0",
2011 | "mime-types": "~2.1.24"
2012 | },
2013 | "engines": {
2014 | "node": ">= 0.6"
2015 | }
2016 | },
2017 | "node_modules/typescript": {
2018 | "version": "5.8.2",
2019 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
2020 | "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
2021 | "license": "Apache-2.0",
2022 | "bin": {
2023 | "tsc": "bin/tsc",
2024 | "tsserver": "bin/tsserver"
2025 | },
2026 | "engines": {
2027 | "node": ">=14.17"
2028 | }
2029 | },
2030 | "node_modules/undefsafe": {
2031 | "version": "2.0.5",
2032 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
2033 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
2034 | "license": "MIT"
2035 | },
2036 | "node_modules/undici-types": {
2037 | "version": "6.20.0",
2038 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
2039 | "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
2040 | "license": "MIT"
2041 | },
2042 | "node_modules/unpipe": {
2043 | "version": "1.0.0",
2044 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
2045 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
2046 | "license": "MIT",
2047 | "engines": {
2048 | "node": ">= 0.8"
2049 | }
2050 | },
2051 | "node_modules/utils-merge": {
2052 | "version": "1.0.1",
2053 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
2054 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
2055 | "license": "MIT",
2056 | "engines": {
2057 | "node": ">= 0.4.0"
2058 | }
2059 | },
2060 | "node_modules/uuid": {
2061 | "version": "11.1.0",
2062 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
2063 | "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
2064 | "funding": [
2065 | "https://github.com/sponsors/broofa",
2066 | "https://github.com/sponsors/ctavan"
2067 | ],
2068 | "license": "MIT",
2069 | "bin": {
2070 | "uuid": "dist/esm/bin/uuid"
2071 | }
2072 | },
2073 | "node_modules/v8-compile-cache-lib": {
2074 | "version": "3.0.1",
2075 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
2076 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
2077 | "dev": true,
2078 | "license": "MIT"
2079 | },
2080 | "node_modules/vary": {
2081 | "version": "1.1.2",
2082 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
2083 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
2084 | "license": "MIT",
2085 | "engines": {
2086 | "node": ">= 0.8"
2087 | }
2088 | },
2089 | "node_modules/webidl-conversions": {
2090 | "version": "7.0.0",
2091 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
2092 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
2093 | "license": "BSD-2-Clause",
2094 | "engines": {
2095 | "node": ">=12"
2096 | }
2097 | },
2098 | "node_modules/whatwg-url": {
2099 | "version": "14.1.1",
2100 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz",
2101 | "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==",
2102 | "license": "MIT",
2103 | "dependencies": {
2104 | "tr46": "^5.0.0",
2105 | "webidl-conversions": "^7.0.0"
2106 | },
2107 | "engines": {
2108 | "node": ">=18"
2109 | }
2110 | },
2111 | "node_modules/ws": {
2112 | "version": "8.17.1",
2113 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
2114 | "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
2115 | "license": "MIT",
2116 | "engines": {
2117 | "node": ">=10.0.0"
2118 | },
2119 | "peerDependencies": {
2120 | "bufferutil": "^4.0.1",
2121 | "utf-8-validate": ">=5.0.2"
2122 | },
2123 | "peerDependenciesMeta": {
2124 | "bufferutil": {
2125 | "optional": true
2126 | },
2127 | "utf-8-validate": {
2128 | "optional": true
2129 | }
2130 | }
2131 | },
2132 | "node_modules/yn": {
2133 | "version": "3.1.1",
2134 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
2135 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
2136 | "dev": true,
2137 | "license": "MIT",
2138 | "engines": {
2139 | "node": ">=6"
2140 | }
2141 | }
2142 | }
2143 | }
2144 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "nodemon dist/server.js",
9 | "build": "tsc",
10 | "dev": "nodemon server.ts"
11 | },
12 | "dependencies": {
13 | "@types/cors": "^2.8.17",
14 | "@types/express": "^5.0.0",
15 | "@types/jsonwebtoken": "^9.0.9",
16 | "@types/node": "^22.13.9",
17 | "cors": "^2.8.5",
18 | "dotenv": "^16.4.7",
19 | "express": "^4.21.2",
20 | "jsonwebtoken": "^9.0.2",
21 | "mongoose": "^8.12.1",
22 | "node-cron": "^3.0.3",
23 | "nodemon": "^3.1.9",
24 | "socket.io": "^4.8.1",
25 | "typescript": "^5.8.2",
26 | "uuid": "^11.1.0"
27 | },
28 | "devDependencies": {
29 | "@types/node-cron": "^3.0.11",
30 | "ts-node": "^10.9.2"
31 | },
32 | "keywords": [],
33 | "author": "",
34 | "license": "ISC",
35 | "type": "commonjs"
36 | }
37 |
--------------------------------------------------------------------------------
/server.ts:
--------------------------------------------------------------------------------
1 | import "./src/config/dotenv.config";
2 | import app from "./src/app";
3 | import { createServer } from "http";
4 | import { Server } from "socket.io";
5 | import { config } from "./src/config/dotenv.config";
6 | import connectDB from "./src/config/db";
7 | import cron from "node-cron";
8 | import { CheckAndDestroyExpiredServers } from "./src/controllers/server.controller";
9 |
10 | // Constants
11 | const PORT = config.PORT;
12 | const HOST = config.HOST;
13 |
14 | // HTTP Server
15 | const server = createServer(app);
16 |
17 | // Socket.IO
18 | export const io = new Server(server, {
19 | cors: {
20 | origin: process.env.CORS_ORIGIN,
21 | credentials: true
22 | }
23 | });
24 |
25 | import "./src/sockets/index.socket";
26 |
27 |
28 | // Set up cron job to check for expired servers every minute
29 | cron.schedule('* * * * *', async () => {
30 | console.log("Checking for expired servers...");
31 | await CheckAndDestroyExpiredServers();
32 | console.log("Cleaner job completed");
33 | });
34 |
35 | // Start the server
36 | server.listen(PORT, async () => {
37 | await connectDB();
38 | console.log(`Server is running on http://${HOST}:${PORT}`);
39 | });
--------------------------------------------------------------------------------
/src/app.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import cors from "cors";
3 | import errorHandler from "./middleware/errorHandler";
4 | import { config } from "./config/dotenv.config";
5 | import * as ServerRoute from "./routes/server.routes";
6 | import messagesRouter from "./routes/messages.route";
7 | import authRouter from "./routes/auth.routes";
8 |
9 | const app = express();
10 |
11 | // Middlewares
12 | app.use(express.json());
13 | app.use(express.urlencoded({ extended: false }));
14 | app.use(cors({
15 | origin: config.CORS_ORIGIN,
16 | credentials: true
17 | }));
18 |
19 | app.use("/api/server", ServerRoute.default);
20 | app.use("/api/messages", messagesRouter);
21 | app.use("/api/auth", authRouter);
22 | app.use("/", (_, res) => {
23 | res.status(404).send("How TF did you get here? 😂");
24 | });
25 |
26 | app.use(errorHandler);
27 |
28 | export default app;
--------------------------------------------------------------------------------
/src/config/db.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import { config } from "./dotenv.config";
3 |
4 | const connectDB = async () => {
5 | try {
6 | await mongoose.connect(config.MONGODB_URI);
7 | console.log("MongoDB connected");
8 | } catch (error) {
9 | console.error("MongoDB connection error:", error);
10 | process.exit(1);
11 | }
12 | };
13 |
14 | export default connectDB;
--------------------------------------------------------------------------------
/src/config/dotenv.config.ts:
--------------------------------------------------------------------------------
1 | import dotenv from "dotenv";
2 |
3 | (() => {
4 | dotenv.config();
5 | })();
6 |
7 | export const config = {
8 | PORT: parseInt(process.env.PORT || "3001"),
9 | HOST: process.env.HOST || "localhost",
10 | JWT_SECRET: process.env.JWT_SECRET || "supersecretkey",
11 | MONGODB_URI: process.env.MONGODB_URI || "mongodb://localhost:27017/secret-room",
12 | CORS_ORIGIN: process.env.CORS_ORIGIN || "http://localhost:3000",
13 | API_KEY: process.env.API_KEY || "supersecretkey",
14 | STATS_ID: process.env.STATS_ID || "app-demo-stats",
15 | }
--------------------------------------------------------------------------------
/src/controllers/auth.controller.ts:
--------------------------------------------------------------------------------
1 | import { Server } from "../models/server.model";
2 | import { AuthRequest } from "../middleware/auth";
3 | import { authMiddleware } from "../middleware/auth";
4 | import AppError from "../types/error.class";
5 | import { isUserInServer } from "./users.controller";
6 | import { User } from "../models/user.model";
7 | import { NextFunction, Request, Response } from "express";
8 |
9 | export class AuthController {
10 | private static instance: AuthController;
11 | private constructor() {}
12 |
13 | public static getInstance(): AuthController {
14 | if (!AuthController.instance) {
15 | AuthController.instance = new AuthController();
16 | }
17 | return AuthController.instance;
18 | }
19 |
20 | public refreshToken = async (req: Request, res: Response, next: NextFunction): Promise => {
21 | try {
22 | const { userId, serverId } = req.body;
23 |
24 | if (!serverId) {
25 | throw new AppError(400, 'Server ID is required');
26 | }
27 |
28 | const server = await Server.findOne({ serverId });
29 |
30 | if (!server) {
31 | throw new AppError(404, 'Server not found');
32 | }
33 |
34 | const user = await User.findOne({ userId });
35 | if (!user) {
36 | throw new AppError(404, 'User not found');
37 | }
38 | const isServerUser = await isUserInServer(serverId, userId);
39 |
40 | if (!isServerUser) {
41 | throw new AppError(403, 'You are not a member of this server');
42 | }
43 | const token = authMiddleware.generateToken({ userId, serverId });
44 | res.status(200).json({
45 | message: 'Token generated successfully',
46 | token
47 | });
48 | } catch (error) {
49 | next(error);
50 | }
51 | }
52 | }
53 |
54 | export const authController = AuthController.getInstance();
55 |
56 | export const refreshToken = (req: Request, res: Response, next: NextFunction): Promise =>
57 | authController.refreshToken(req, res, next);
58 |
--------------------------------------------------------------------------------
/src/controllers/messages.controller.ts:
--------------------------------------------------------------------------------
1 | import { Message } from "../models/messages.model";
2 | import AppError from "../types/error.class";
3 | import { Server } from "../models/server.model";
4 | import { isUserInServer } from "./users.controller";
5 | import { getSocketService } from "../sockets/index.socket";
6 | import { NextFunction, Response } from "express";
7 | import { AuthRequest } from "../middleware/auth";
8 | import { decryptMessage } from "../utils/messages.utli";
9 | import { StatisticsController } from "./statistics.controller";
10 |
11 | class MessagesController {
12 | private static instance: MessagesController;
13 |
14 | private constructor() {}
15 |
16 | public static getInstance(): MessagesController {
17 | if (!MessagesController.instance) {
18 | MessagesController.instance = new MessagesController();
19 | }
20 | return MessagesController.instance;
21 | }
22 |
23 | public async sendMessage(serverId: string, content: string, senderId: string, receiverId: string, attachmentUrl?: string, sent: boolean = true): Promise {
24 | if (!serverId || !senderId || !receiverId) {
25 | throw new AppError(400, 'Missing required fields');
26 | }
27 |
28 | if (!content && !attachmentUrl) {
29 | throw new AppError(400, 'Message content or attachment is required');
30 | }
31 |
32 | const server = await Server.findOne({ serverId });
33 | if (!server) {
34 | throw new AppError(404, 'Server not found');
35 | }
36 | const isSenderInServer = isUserInServer(serverId, senderId);
37 | if (!isSenderInServer) {
38 | throw new AppError(403, 'Sender is not a member of this server');
39 | }
40 | const isReceiverInServer = isUserInServer(serverId, receiverId);
41 | if (!isReceiverInServer) {
42 | throw new AppError(403, 'Receiver is not a member of this server');
43 | }
44 |
45 | const payload = {
46 | serverId,
47 | content,
48 | senderId,
49 | receiverId,
50 | attachmentUrl: attachmentUrl || null,
51 | readBySender: false,
52 | readByReceiver: false,
53 | sent: sent || false,
54 | createdAt: new Date()
55 | };
56 |
57 | const message = await new Message(payload).save();
58 |
59 | // Update statistics
60 | await StatisticsController.incrementTotalMessages();
61 | if (attachmentUrl) {
62 | await StatisticsController.incrementTotalFileUploads();
63 | const ext = attachmentUrl.toLowerCase();
64 | if (ext.match(/\.(jpg|jpeg|png|webp)$/)) await StatisticsController.incrementFileType('images');
65 | else if (ext.match(/\.(mp4|webm|mov)$/)) await StatisticsController.incrementFileType('videos');
66 | else if (ext.match(/\.gif$/)) await StatisticsController.incrementFileType('gifs');
67 | else if (ext.match(/\.pdf$/)) await StatisticsController.incrementFileType('pdfs');
68 | else await StatisticsController.incrementFileType('others');
69 | }
70 |
71 | const output = {
72 | serverId: message.serverId,
73 | content: payload.content,
74 | senderId: message.senderId,
75 | receiverId: message.receiverId,
76 | readBySender: message.readBySender,
77 | readByReceiver: message.readByReceiver,
78 | messageId: message.messageId,
79 | createdAt: message.createdAt,
80 | sent: message.sent,
81 | attachmentUrl: payload.attachmentUrl || undefined
82 | }
83 |
84 | const socketService = getSocketService();
85 | socketService.broadcastNewMessage(serverId, output);
86 | }
87 |
88 | public async getMessages(request: AuthRequest, response: Response, next: NextFunction): Promise {
89 | try {
90 | const { serverId } = request.params;
91 | const userId = request.user?.userId;
92 |
93 | if (!userId) {
94 | throw new AppError(400, "User ID is required");
95 | }
96 |
97 | if (!serverId) {
98 | throw new AppError(400, "Server ID is required");
99 | }
100 |
101 | const server = await Server.findOne({ serverId });
102 | if (!server) {
103 | throw new AppError(404, "Server not found");
104 | }
105 |
106 | const isServerUser = isUserInServer(serverId, userId);
107 | if (!isServerUser) {
108 | throw new AppError(403, "You are not a member of this server");
109 | }
110 |
111 | const messages = await Message.find({ serverId });
112 |
113 | response.status(200).json({
114 | message: "Messages found",
115 | data: messages.map(message => ({
116 | ...message.toObject(),
117 | content: decryptMessage(message.content, server.salt),
118 | attachmentUrl: message.attachmentUrl ? decryptMessage(message.attachmentUrl, server.salt) : null,
119 | }))
120 | });
121 | } catch (error) {
122 | next(error);
123 | }
124 | }
125 |
126 | public async markMessageRead(messageId: string): Promise {
127 | try {
128 | const message = await Message.findOne({ messageId });
129 | if (!message) {
130 | throw new AppError(404, "Message not found");
131 | }
132 | message.readByReceiver = true;
133 | await message.save();
134 |
135 | const socketService = getSocketService();
136 | socketService.broadcastMessageRead(messageId, message.serverId);
137 | } catch (error) {
138 | throw error;
139 | }
140 | }
141 | }
142 |
143 | const messagesController = MessagesController.getInstance();
144 |
145 | export default messagesController;
--------------------------------------------------------------------------------
/src/controllers/server.controller.ts:
--------------------------------------------------------------------------------
1 | import { NextFunction, Request, Response } from "express";
2 | import { Server } from "../models/server.model";
3 | import { generateId } from "../utils/index.util";
4 | import AppError from "../types/error.class";
5 | import {
6 | CreateServerRequest,
7 | ServerResponse,
8 | ServerData,
9 | IServerController
10 | } from "../types/server.interface";
11 | import { getSocketService } from "../sockets/index.socket";
12 | import { addUserToServer, getActiveUsers, clearServerUsers, isUserInServer } from "./users.controller";
13 | import { generateUsername } from "../utils/user.util";
14 | import { Message } from "../models/messages.model";
15 | import Invitation from "../models/invitation.model";
16 | import { generateToken } from "../middleware/auth";
17 | import { AuthRequest } from "../middleware/auth";
18 | import { ServerAction } from "../types/server-socket.interface";
19 | import { User } from "../models/user.model";
20 | import { StatisticsController } from "./statistics.controller";
21 | import messagesController from "./messages.controller";
22 |
23 | class ServerController implements IServerController {
24 | private static instance: ServerController;
25 | private readonly MIN_SERVER_NAME_LENGTH = 3;
26 | private readonly MAX_SERVER_NAME_LENGTH = 50;
27 | private readonly MIN_ENCRYPTION_KEY_LENGTH = 8;
28 | private readonly MIN_LIFESPAN = 1000 * 60 * 60;
29 | private readonly MAX_LIFESPAN = 1000 * 60 * 60 * 24;
30 |
31 | // Character sets for encryption key validation
32 | private readonly VALID_CHARS = {
33 | uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
34 | lowercase: 'abcdefghijklmnopqrstuvwxyz',
35 | numbers: '0123456789',
36 | specialChars: '!@#$%^&*()_+-=[]{}|;:,.<>?',
37 | hexChars: 'ABCDEF0123456789',
38 | base64Chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
39 | };
40 |
41 | public static getInstance(): ServerController {
42 | if (!ServerController.instance) {
43 | ServerController.instance = new ServerController();
44 | }
45 | return ServerController.instance;
46 | }
47 |
48 | public validateRequest(body: any): body is CreateServerRequest {
49 | const { serverName, encryptionKey, lifeSpan, fingerprint } = body;
50 |
51 | if (!serverName || !encryptionKey || !lifeSpan || !fingerprint) {
52 | throw new AppError(400, "All fields are required");
53 | }
54 |
55 | if (typeof serverName !== 'string' ||
56 | typeof encryptionKey !== 'string' ||
57 | typeof lifeSpan !== 'number' ||
58 | typeof fingerprint !== 'string') {
59 | throw new AppError(400, "Invalid field types. Expected: serverName (string), encryptionKey (string), lifeSpan (number), fingerprint (string)");
60 | }
61 |
62 | this.validateServerName(serverName);
63 | this.validateEncryptionKey(encryptionKey);
64 | this.validateLifeSpan(lifeSpan);
65 |
66 | return true;
67 | }
68 |
69 | private validateServerName(serverName: string): void {
70 | if (serverName.length < this.MIN_SERVER_NAME_LENGTH) {
71 | throw new AppError(400, `Server name must be at least ${this.MIN_SERVER_NAME_LENGTH} characters long`);
72 | }
73 | if (serverName.length > this.MAX_SERVER_NAME_LENGTH) {
74 | throw new AppError(400, `Server name cannot exceed ${this.MAX_SERVER_NAME_LENGTH} characters`);
75 | }
76 | if (!/^[a-zA-Z0-9-_\s]+$/.test(serverName)) {
77 | throw new AppError(400, "Server name can only contain letters, numbers, spaces, hyphens, and underscores");
78 | }
79 | }
80 |
81 | private validateEncryptionKey(encryptionKey: string): void {
82 | if (encryptionKey.length < this.MIN_ENCRYPTION_KEY_LENGTH) {
83 | throw new AppError(400, `Encryption key must be at least ${this.MIN_ENCRYPTION_KEY_LENGTH} characters long`);
84 | }
85 |
86 | const validChars = new Set([
87 | ...this.VALID_CHARS.uppercase,
88 | ...this.VALID_CHARS.lowercase,
89 | ...this.VALID_CHARS.numbers,
90 | ...this.VALID_CHARS.specialChars,
91 | ...this.VALID_CHARS.base64Chars
92 | ]);
93 |
94 | const invalidChars = [...encryptionKey].filter(char => !validChars.has(char));
95 | if (invalidChars.length > 0) {
96 | throw new AppError(400, `Invalid characters in encryption key: ${invalidChars.join(' ')}`);
97 | }
98 | }
99 |
100 | private validateLifeSpan(lifeSpan: number): void {
101 | if (lifeSpan < this.MIN_LIFESPAN) {
102 | throw new AppError(400, "Lifespan must be at least 1 hour");
103 | }
104 | if (lifeSpan > this.MAX_LIFESPAN) {
105 | throw new AppError(400, "Lifespan cannot exceed 1 day");
106 | }
107 | }
108 |
109 | public generateServerData(request: CreateServerRequest): ServerData {
110 | return {
111 | serverId: `server-${generateId()}`,
112 | serverName: request.serverName.trim(),
113 | salt: request.encryptionKey,
114 | expiresAt: new Date(Date.now() + request.lifeSpan)
115 | };
116 | }
117 |
118 | public formatResponse(server: ServerData): ServerResponse {
119 | return {
120 | server_name: server.serverName,
121 | server_id: server.serverId,
122 | expiration: server.expiresAt
123 | };
124 | }
125 |
126 | public async createServer(request: Request, response: Response, next: NextFunction): Promise {
127 | try {
128 | this.validateRequest(request.body);
129 |
130 | const createUserIdentity = {
131 | userId: request.body.fingerprint,
132 | }
133 |
134 | const serverData = this.generateServerData(request.body);
135 | const server = new Server({
136 | ...serverData,
137 | type: "Private",
138 | owner: createUserIdentity.userId,
139 | globalInvitationId: `global-${generateId()}`
140 | });
141 |
142 | await server.save();
143 |
144 | const formattedResponse = this.formatResponse(serverData);
145 |
146 | addUserToServer(server.serverId, {
147 | ...createUserIdentity,
148 | isOnline: true,
149 | lastSeen: new Date()
150 | });
151 |
152 | // Generate JWT token for the new user
153 | const token = generateToken({
154 | userId: createUserIdentity.userId,
155 | serverId: serverData.serverId
156 | });
157 |
158 | // Get socket service and emit server creation event
159 | const socketService = getSocketService();
160 | socketService.emitToServer(serverData.serverId, {
161 | type: 'status',
162 | content: 'Server created successfully',
163 | timestamp: Date.now()
164 | });
165 |
166 | await StatisticsController.incrementTotalServers();
167 | await StatisticsController.incrementActiveServers();
168 |
169 |
170 | response.status(201).json({
171 | message: "Server created successfully",
172 | data: {
173 | ...formattedResponse,
174 | global_invitation_id: server.globalInvitationId,
175 | owner: server.owner
176 | },
177 | user: createUserIdentity,
178 | token
179 | });
180 | } catch (error) {
181 | next(error);
182 | }
183 | }
184 |
185 | public async getServer(request: AuthRequest, response: Response, next: NextFunction): Promise {
186 | try {
187 | if (!request.user) {
188 | throw new AppError(401, "Unauthorized");
189 | }
190 |
191 | const { serverId } = request.params;
192 | const userId = request.user.userId;
193 | const refreshToken = request.header("X-Refresh-Token");
194 |
195 | if (!serverId) {
196 | throw new AppError(400, "Server ID is required");
197 | }
198 |
199 | if (!userId) {
200 | throw new AppError(400, "User ID is required");
201 | }
202 |
203 | const server = await Server.findOne({ serverId });
204 |
205 | if (!server) {
206 | throw new AppError(404, "Server not found");
207 | }
208 |
209 | // Check if user is in server
210 | const isActive = await isUserInServer(serverId, userId as string);
211 | const username = generateUsername(`${userId}-${serverId}`);
212 |
213 | if (!isActive) {
214 | // If user is not active but is the owner, add them back
215 | if (server.owner === userId) {
216 | addUserToServer(serverId, {
217 | userId: userId as string,
218 | isOnline: true,
219 | lastSeen: new Date()
220 | });
221 | } else {
222 | throw new AppError(403, "You are not a member of this server");
223 | }
224 | }
225 |
226 | // Connect to socket
227 | const socketService = getSocketService();
228 |
229 | socketService.emitServerActions(serverId, ServerAction.JOIN, username);
230 |
231 | response.status(200).json({
232 | message: "Server found",
233 | data: {
234 | server_name: server.serverName,
235 | server_id: server.serverId,
236 | expiration: server.expiresAt,
237 | global_invitation_id: server.globalInvitationId,
238 | owner: server.owner,
239 | username
240 | },
241 | refresh_token: refreshToken
242 | });
243 | } catch (error) {
244 | next(error);
245 | }
246 | }
247 |
248 | public async getServerActiveUsers(request: AuthRequest, response: Response, next: NextFunction): Promise {
249 | try {
250 | const { serverId } = request.params;
251 | const userId = request.user?.userId;
252 | const refreshToken = request.header("X-Refresh-Token");
253 |
254 | if (!serverId) {
255 | throw new AppError(400, "Server ID is required");
256 | }
257 |
258 | if (!userId) {
259 | throw new AppError(400, "User Fingerprint is required");
260 | }
261 |
262 | const isServerUser = await isUserInServer(serverId, userId);
263 | if (!isServerUser) {
264 | throw new AppError(403, "You are not a member of this server");
265 | }
266 |
267 | const server = await Server.findOne({ serverId });
268 | if (!server) {
269 | throw new AppError(404, "Server not found");
270 | }
271 |
272 | const activeUsers = await getActiveUsers(server.serverId);
273 |
274 | response.status(200).json({
275 | message: "Active users retrieved successfully",
276 | data: activeUsers,
277 | refresh_token: refreshToken
278 | });
279 | } catch (error) {
280 | next(error);
281 | }
282 | }
283 |
284 | public async globalInvitation(request: Request, response: Response, next: NextFunction): Promise {
285 | try {
286 | const { globalInvitationId } = request.params;
287 | const { fingerprint } = request.body;
288 |
289 | if (!globalInvitationId) {
290 | throw new AppError(400, "Global invitation ID is required");
291 | }
292 |
293 | if (!fingerprint) {
294 | throw new AppError(400, "A fingerprint ID is required");
295 | }
296 |
297 | // Find server by global invitation ID
298 | const server = await Server.findOne({ globalInvitationId });
299 |
300 |
301 | if (!server) {
302 | throw new AppError(404, "Invalid invitation link");
303 | }
304 |
305 | // Check if server has expired
306 | if (server.expiresAt < new Date() && server.type === "Private") {
307 | throw new AppError(411, "This server has expired 02");
308 | }
309 |
310 | // Check if user already exists in server
311 | const userIdentityExist = await isUserInServer(server.serverId, fingerprint) || false;
312 |
313 | const userIdentity = {
314 | userId: fingerprint,
315 | };
316 |
317 | // If user doesn't exist, create new identity
318 | if (userIdentityExist) {
319 | throw new AppError(409, `You are already in this server_${server.serverId}`);
320 | }
321 |
322 | addUserToServer(server.serverId, {
323 | ...userIdentity,
324 | isOnline: true,
325 | lastSeen: new Date()
326 | });
327 |
328 | const socketService = getSocketService();
329 | socketService.emitServerActions(server.serverId, ServerAction.JOIN, generateUsername(`${userIdentity.userId}-${server.serverId}`));
330 |
331 | // Generate JWT token for the user
332 | const token = generateToken({
333 | userId: userIdentity.userId,
334 | serverId: server.serverId
335 | });
336 |
337 | // send user a message if they joined a public server
338 | if (server.type === "Public") {
339 | const message = {
340 | content: "You’re anonymous now. What’s something you’ve been wanting to say out loud?",
341 | senderId: server.owner,
342 | receiverId: userIdentity.userId,
343 | attachmentUrl: undefined
344 | };
345 |
346 | const message_2 = {
347 | content: "You can send messages, share files, and much more.",
348 | senderId: server.owner,
349 | receiverId: userIdentity.userId,
350 | attachmentUrl: undefined
351 | };
352 |
353 | const message_3 = {
354 | content: "💀",
355 | senderId: server.owner,
356 | receiverId: userIdentity.userId,
357 | attachmentUrl: undefined
358 | };
359 |
360 | await messagesController.sendMessage(server.serverId, message.content, message.senderId, message.receiverId, message.attachmentUrl);
361 | await messagesController.sendMessage(server.serverId, message_2.content, message_2.senderId, message_2.receiverId, message_2.attachmentUrl);
362 | await messagesController.sendMessage(server.serverId, message_3.content, message_3.senderId, message_3.receiverId, message_3.attachmentUrl);
363 | }
364 |
365 | // Return server details needed for joining
366 | response.status(200).json({
367 | success: true,
368 | data: {
369 | serverId: server.serverId,
370 | serverName: server.serverName,
371 | expiresAt: server.expiresAt
372 | },
373 | user: userIdentity,
374 | token
375 | });
376 |
377 | } catch (error) {
378 | next(error);
379 | }
380 | }
381 |
382 | public async uniqueInvitation(request: Request, response: Response, next: NextFunction): Promise {
383 | try {
384 | const { inviteCode } = request.params;
385 | const { fingerprint } = request.body;
386 |
387 | if (!inviteCode) {
388 | throw new AppError(400, "Invite code is required");
389 | }
390 |
391 | if (!fingerprint) {
392 | throw new AppError(400, "Fingerprint is required");
393 | }
394 |
395 | const invitation = await Invitation.findOne({ inviteCode });
396 |
397 | if (!invitation) {
398 | throw new AppError(404, "Server invitation not found");
399 | }
400 |
401 | if (invitation.used) {
402 | throw new AppError(409, "Server invitation already used");
403 | }
404 |
405 | const server = await Server.findOne({ serverId: invitation.serverId });
406 |
407 | if (!server) {
408 | throw new AppError(404, "Server not found");
409 | }
410 |
411 | // Check if user already exists in server
412 | const userIdentityExist = await isUserInServer(server.serverId, fingerprint) || false;
413 |
414 | const userIdentity = {
415 | userId: fingerprint,
416 | };
417 |
418 | // If user doesn't exist, create new identity
419 | if (userIdentityExist) {
420 | throw new AppError(409, `You are already in this server_${server.serverId}`);
421 | }
422 |
423 | addUserToServer(server.serverId, {
424 | ...userIdentity,
425 | isOnline: true,
426 | lastSeen: new Date()
427 | });
428 |
429 | const socketService = getSocketService();
430 | const username = generateUsername(`${userIdentity.userId}-${server.serverId}`);
431 |
432 | socketService.emitServerActions(server.serverId, ServerAction.JOIN, username);
433 |
434 | // Generate JWT token for the user
435 | const token = generateToken({
436 | userId: userIdentity.userId,
437 | serverId: server.serverId
438 | });
439 |
440 | // Update invitation status
441 | await Invitation.updateOne({ inviteCode }, { used: true });
442 |
443 | // Return server details needed for joining
444 | response.status(200).json({
445 | success: true,
446 | data: {
447 | serverId: server.serverId,
448 | serverName: server.serverName,
449 | expiresAt: server.expiresAt
450 | },
451 | user: userIdentity,
452 | token
453 | });
454 |
455 | } catch (error) {
456 | next(error);
457 | }
458 | }
459 |
460 | public async generateUniqueServerInvitationId(request: Request, response: Response, next: NextFunction): Promise {
461 | try {
462 | const { serverId } = request.params;
463 |
464 | if (!serverId) {
465 | throw new AppError(400, "Server ID is required");
466 | }
467 |
468 | const server = await Server.findOne({ serverId });
469 | if (!server) {
470 | throw new AppError(404, "Server not found");
471 | }
472 |
473 | const uniqueInvitationId = `invite-${generateId()}`;
474 |
475 | const invitation = new Invitation({
476 | inviteCode: uniqueInvitationId,
477 | used: false,
478 | serverId,
479 | expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
480 | });
481 |
482 | await invitation.save();
483 |
484 | response.status(200).json({
485 | message: "Server invitation ID generated successfully",
486 | data: {
487 | inviteCode: uniqueInvitationId
488 | }
489 | });
490 | } catch (error) {
491 | next(error);
492 | }
493 | }
494 |
495 | // Leave server
496 | public async leaveServer(serverId: string, userId: string): Promise {
497 | try {
498 | if (!serverId) {
499 | throw new AppError(400, "Server ID is required");
500 | }
501 |
502 | const server = await Server.findOne({ serverId });
503 | const username = generateUsername(`${userId}-${serverId}`);
504 |
505 | if (!server) {
506 | throw new AppError(404, "Server not found");
507 | }
508 |
509 | if (!userId) {
510 | throw new AppError(400, "User ID is required");
511 | }
512 |
513 | const isServerUser = await isUserInServer(serverId, userId);
514 | if (!isServerUser) {
515 | throw new AppError(403, "You are not a member of this server");
516 | }
517 |
518 | // Remove user from server
519 | await Server.updateOne({ serverId }, { $pull: { approvedUsers: userId } });
520 |
521 | // Remove server from user
522 | await User.updateOne({ userId }, { $pull: { servers: serverId } });
523 |
524 | const socketService = getSocketService();
525 | socketService.emitServerActions(server.serverId, ServerAction.LEAVE, username);
526 | } catch (error) {
527 | console.error('Error during leave process:', error);
528 | }
529 | }
530 |
531 | public async deleteServer(request: AuthRequest, response: Response, next: NextFunction): Promise {
532 | try {
533 | const { serverId } = request.params;
534 | const userId = request.user?.userId;
535 |
536 | if (!serverId) {
537 | throw new AppError(400, "Server ID is required");
538 | }
539 |
540 | if (!userId) {
541 | throw new AppError(400, "User ID is required");
542 | }
543 |
544 | const isServerUser = await isUserInServer(serverId, userId);
545 | if (!isServerUser) {
546 | throw new AppError(403, "You are not a member of this server");
547 | }
548 |
549 | const server = await Server.findOne({ serverId: serverId });
550 |
551 | if (!server) {
552 | throw new AppError(404, "Server not found");
553 | }
554 |
555 | if (server.owner !== userId) {
556 | throw new AppError(403, "You are not the owner of this server");
557 | }
558 |
559 | await this.deleteServerById(serverId);
560 |
561 | const socketService = getSocketService();
562 | socketService.broadcastServerDeleted(serverId);
563 |
564 | await StatisticsController.increment('deletedServers');
565 |
566 | response.status(200).json({
567 | message: "Server deleted successfully"
568 | });
569 | } catch (error) {
570 | next(error);
571 | }
572 | }
573 |
574 | private async deleteServerById(serverId: string) {
575 | try {
576 | const server = await Server.findOne({ serverId });
577 | if (!server) {
578 | throw new AppError(404, "Server not found");
579 | }
580 |
581 | // Remove server from user
582 | await User.updateMany({ servers: serverId }, { $pull: { servers: serverId } });
583 |
584 | // Delete messages
585 | await Message.deleteMany({ serverId });
586 |
587 | // Delete invitations
588 | await Invitation.deleteMany({ serverId });
589 |
590 | // Delete server
591 | await Server.deleteOne({ serverId });
592 |
593 | await StatisticsController.decrementActiveServers(1);
594 |
595 | const socketService = getSocketService();
596 | socketService.broadcastServerDeleted(serverId);
597 | } catch (error) {
598 | throw error;
599 | }
600 | }
601 |
602 | public async checkAndDestroyExpiredServers() {
603 | let counter = 0;
604 | const publicServers = await Server.find({ type: "Public"});
605 | for (const server of publicServers) {
606 | server.expiresAt = new Date();
607 | await server.save();
608 | }
609 | const expiredServers = await Server.find({ type: "Private"});
610 | for (const server of expiredServers) {
611 | // Check if the server has expired
612 | const expirationDate = new Date(server.expiresAt).getTime();
613 |
614 | if (expirationDate < new Date().getTime()) {
615 | counter++;
616 | await this.deleteServerById(server.serverId);
617 | await StatisticsController.increment('expiredServers');
618 | }
619 | }
620 |
621 | await StatisticsController.updatePreviousStats();
622 |
623 | }
624 | }
625 |
626 | // Export singleton instance
627 | export const serverController = ServerController.getInstance();
628 |
629 | // Export controller method for route handler
630 | export const CreateServer = (req: Request, res: Response, next: NextFunction): Promise =>
631 | serverController.createServer(req, res, next);
632 |
633 | export const GetServer = (req: Request, res: Response, next: NextFunction): Promise =>
634 | serverController.getServer(req, res, next);
635 |
636 | export const GetServerActiveUsers = (req: Request, res: Response, next: NextFunction): Promise =>
637 | serverController.getServerActiveUsers(req, res, next);
638 |
639 | export const GlobalInvitation = (req: Request, res: Response, next: NextFunction): Promise =>
640 | serverController.globalInvitation(req, res, next);
641 |
642 | export const UniqueInvitation = (req: Request, res: Response, next: NextFunction): Promise =>
643 | serverController.uniqueInvitation(req, res, next);
644 |
645 | export const GenerateUniqueServerInvitationId = (req: Request, res: Response, next: NextFunction): Promise =>
646 | serverController.generateUniqueServerInvitationId(req, res, next);
647 |
648 | export const LeaveServer = (serverId: string, userId: string): Promise =>
649 | serverController.leaveServer(serverId, userId);
650 |
651 | export const DeleteServer = (req: AuthRequest, res: Response, next: NextFunction): Promise =>
652 | serverController.deleteServer(req, res, next);
653 |
654 | export const CheckAndDestroyExpiredServers = (): Promise =>
655 | serverController.checkAndDestroyExpiredServers();
--------------------------------------------------------------------------------
/src/controllers/statistics.controller.ts:
--------------------------------------------------------------------------------
1 | // controllers/statistics.controller.ts
2 | import { AppStatistics, IAppStatistics } from '../models/statistics.model';
3 | import { config } from '../config/dotenv.config';
4 | import { initialStats } from '../utils/constants';
5 | import { FilterQuery } from 'mongoose';
6 | import { Server } from '../models/server.model';
7 | import { User } from '../models/user.model';
8 | import { Message } from '../models/messages.model';
9 |
10 | const { STATS_ID } = config;
11 |
12 | type StatKey = 'totalServers' | 'totalMessages' | 'totalUsers' | 'activeServers' | 'activeUsers' | 'totalFileUploads' | 'fileTypes' | 'deletedServers' | 'expiredServers';
13 | type FileTypeKey = keyof IAppStatistics['fileTypes'];
14 | type UpdateResult = { success: boolean; data?: any; error?: Error };
15 |
16 | export class StatisticsController {
17 | public static async initialize() {
18 | try {
19 | const stats = await AppStatistics.findOne({ id: STATS_ID });
20 |
21 | if (stats) return;
22 |
23 | const servers = await Server.find();
24 | const users = await User.find();
25 | const messages = await Message.find();
26 |
27 | // Count file types from messages
28 | const initialStats = {
29 | totalServers: 0,
30 | totalMessages: 0,
31 | totalUsers: 0,
32 | activeServers: 0,
33 | activeUsers: 0,
34 | totalFileUploads: 0,
35 | deletedServers: 0,
36 | expiredServers: 0,
37 | fileTypes: {
38 | images: 0,
39 | videos: 0,
40 | gifs: 0,
41 | pdfs: 0,
42 | others: 0
43 | }
44 | };
45 |
46 | const messagesWithAttachments = await Message.find({ attachmentUrl: { $exists: true, $ne: null } });
47 | messagesWithAttachments.forEach(msg => {
48 | if (msg.attachmentUrl) {
49 | const ext = msg.attachmentUrl.toLowerCase();
50 | if (ext.match(/\.(jpg|jpeg|png|webp)$/)) {
51 | initialStats.fileTypes.images++;
52 | } else if (ext.match(/\.(mp4|webm|mov)$/)) {
53 | initialStats.fileTypes.videos++;
54 | } else if (ext.match(/\.gif$/)) {
55 | initialStats.fileTypes.gifs++;
56 | } else if (ext.match(/\.pdf$/)) {
57 | initialStats.fileTypes.pdfs++;
58 | } else {
59 | initialStats.fileTypes.others++;
60 | }
61 | }
62 | });
63 |
64 | const payload = {
65 | id: STATS_ID,
66 | ...initialStats,
67 | totalServers: servers.length,
68 | totalMessages: messages.length,
69 | totalUsers: users.length,
70 | totalFileUploads: messagesWithAttachments.length,
71 | activeUsers: users.filter((u) => u.isOnline === true).length,
72 | activeServers: servers.length,
73 | lastUpdated: new Date(),
74 | previousStats: []
75 | };
76 |
77 | await AppStatistics.create(payload);
78 | } catch (error) {
79 | console.error('Error initializing statistics:', error);
80 | }
81 | }
82 |
83 | /**
84 | * Increment total servers count
85 | * @param count Amount to increment (default: 1)
86 | */
87 | static async incrementTotalServers(count = 1): Promise {
88 | return this.increment('totalServers', count);
89 | }
90 |
91 | /**
92 | * Set total servers count
93 | * @param value New value to set
94 | */
95 | static async setTotalServers(value: number): Promise {
96 | return this.set('totalServers', value);
97 | }
98 |
99 | /**
100 | * Increment total messages count
101 | * @param count Amount to increment (default: 1)
102 | */
103 | static async incrementTotalMessages(count = 1): Promise {
104 | return this.increment('totalMessages', count);
105 | }
106 |
107 | /**
108 | * Set total messages count
109 | * @param value New value to set
110 | */
111 | static async setTotalMessages(value: number): Promise {
112 | return this.set('totalMessages', value);
113 | }
114 |
115 | /**
116 | * Increment total users count
117 | * @param count Amount to increment (default: 1)
118 | */
119 | static async incrementTotalUsers(count = 1): Promise {
120 | return this.increment('totalUsers', count);
121 | }
122 |
123 | /**
124 | * Set total users count
125 | * @param value New value to set
126 | */
127 | static async setTotalUsers(value: number): Promise {
128 | return this.set('totalUsers', value);
129 | }
130 |
131 | /**
132 | * Increment active servers count
133 | * @param count Amount to increment (default: 1)
134 | */
135 | static async incrementActiveServers(count = 1): Promise {
136 | return this.increment('activeServers', count);
137 | }
138 |
139 | static async decrementActiveServers(count = 1): Promise {
140 | return this.increment('activeServers', -count);
141 | }
142 |
143 | /**
144 | * Set active servers count
145 | * @param value New value to set
146 | */
147 | static async setActiveServers(value: number): Promise {
148 | return this.set('activeServers', value);
149 | }
150 |
151 | /**
152 | * Increment active users count
153 | * @param count Amount to increment (default: 1)
154 | */
155 | static async incrementActiveUsers(count = 1): Promise {
156 | return this.increment('activeUsers', count);
157 | }
158 |
159 | static async decrementActiveUsers(count = 1): Promise {
160 | return this.increment('activeUsers', -count);
161 | }
162 |
163 | /**
164 | * Set active users count
165 | * @param value New value to set
166 | */
167 | static async setActiveUsers(value: number): Promise {
168 | return this.set('activeUsers', value);
169 | }
170 |
171 | /**
172 | * Increment total file uploads count
173 | * @param count Amount to increment (default: 1)
174 | */
175 | static async incrementTotalFileUploads(count = 1): Promise {
176 | return this.increment('totalFileUploads', count);
177 | }
178 |
179 | /**
180 | * Set total file uploads count
181 | * @param value New value to set
182 | */
183 | static async setTotalFileUploads(value: number): Promise {
184 | return this.set('totalFileUploads', value);
185 | }
186 |
187 | /**
188 | * Increment image uploads count
189 | * @param count Amount to increment (default: 1)
190 | */
191 | static async incrementImageUploads(count = 1): Promise {
192 | return this.incrementFileType('images', count);
193 | }
194 |
195 | /**
196 | * Increment video uploads count
197 | * @param count Amount to increment (default: 1)
198 | */
199 | static async incrementVideoUploads(count = 1): Promise {
200 | return this.incrementFileType('videos', count);
201 | }
202 |
203 | /**
204 | * Increment gif uploads count
205 | * @param count Amount to increment (default: 1)
206 | */
207 | static async incrementGifUploads(count = 1): Promise {
208 | return this.incrementFileType('gifs', count);
209 | }
210 |
211 | /**
212 | * Increment PDF uploads count
213 | * @param count Amount to increment (default: 1)
214 | */
215 | static async incrementPdfUploads(count = 1): Promise {
216 | return this.incrementFileType('pdfs', count);
217 | }
218 |
219 | /**
220 | * Increment other file type uploads count
221 | * @param count Amount to increment (default: 1)
222 | */
223 | static async incrementOtherFileUploads(count = 1): Promise {
224 | return this.incrementFileType('others', count);
225 | }
226 |
227 | /**
228 | * Get total servers count
229 | */
230 | static async getTotalServers(): Promise {
231 | const result = await this.getStats();
232 | return result.success ? result.data?.totalServers ?? null : null;
233 | }
234 |
235 | /**
236 | * Get total messages count
237 | */
238 | static async getTotalMessages(): Promise {
239 | const result = await this.getStats();
240 | return result.success ? result.data?.totalMessages ?? null : null;
241 | }
242 |
243 | /**
244 | * Get total users count
245 | */
246 | static async getTotalUsers(): Promise {
247 | const result = await this.getStats();
248 | return result.success ? result.data?.totalUsers ?? null : null;
249 | }
250 |
251 | /**
252 | * Get active servers count
253 | */
254 | static async getActiveServers(): Promise {
255 | const result = await this.getStats();
256 | return result.success ? result.data?.activeServers ?? null : null;
257 | }
258 |
259 | /**
260 | * Get active users count
261 | */
262 | static async getActiveUsers(): Promise {
263 | const result = await this.getStats();
264 | return result.success ? result.data?.activeUsers ?? null : null;
265 | }
266 |
267 | /**
268 | * Get total file uploads count
269 | */
270 | static async getTotalFileUploads(): Promise {
271 | const result = await this.getStats();
272 | return result.success ? result.data?.totalFileUploads ?? null : null;
273 | }
274 |
275 | /**
276 | * Get file type statistics
277 | */
278 | static async getFileTypeStats(): Promise {
279 | const result = await this.getStats();
280 | return result.success ? result.data?.fileTypes ?? null : null;
281 | }
282 |
283 | /**
284 | * Get a specific file type count
285 | * @param fileType The file type to get count for
286 | */
287 | static async getFileTypeCount(fileType: FileTypeKey): Promise {
288 | const result = await this.getStats();
289 | return result.success ? result.data?.fileTypes?.[fileType] ?? null : null;
290 | }
291 |
292 | /**
293 | * Update statistics for active usage
294 | * Used for tracking currently active users and servers
295 | * @param activeUsers Number of currently active users
296 | * @param activeServers Number of currently active servers
297 | */
298 | static async updateActiveStats(activeUsers: number, activeServers: number): Promise {
299 | return this.batchUpdate({
300 | activeUsers,
301 | activeServers
302 | });
303 | }
304 |
305 | /**
306 | * Record a file upload with automatic type detection
307 | * @param fileType The type of file being uploaded
308 | */
309 | static async recordFileUpload(fileType: FileTypeKey): Promise {
310 | // Increment both the specific file type and total file uploads
311 | return this.incrementFileType(fileType);
312 | }
313 |
314 | /**
315 | * Get the last time statistics were updated
316 | */
317 | static async getLastUpdated(): Promise {
318 | const result = await this.getStats();
319 | return result.success ? result.data?.lastUpdated ?? null : null;
320 | }
321 | /**
322 | * Increment a top-level statistic value
323 | * @param key The statistic key to increment
324 | * @param count The amount to increment by (default: 1)
325 | * @returns Promise with the operation result
326 | */
327 | static async increment(key: StatKey, count = 1): Promise {
328 | try {
329 | // Validate increment amount
330 | if (isNaN(count) || count <= 0) {
331 | return {
332 | success: false,
333 | error: new Error(`Invalid increment value for "${key}": ${count}`)
334 | };
335 | }
336 |
337 | const stats = await AppStatistics.findOneAndUpdate(
338 | { id: STATS_ID },
339 | {
340 | $inc: { [key]: count },
341 | $set: { lastUpdated: new Date() }
342 | },
343 | { upsert: true, new: true }
344 | );
345 |
346 | return { success: true, data: stats };
347 | } catch (err) {
348 | console.error(`Failed to increment stat "${key}":`, err);
349 | return { success: false, error: err instanceof Error ? err : new Error(String(err)) };
350 | }
351 | }
352 |
353 | /**
354 | * Increment a file type statistic
355 | * @param fileType The file type to increment
356 | * @param count The amount to increment by (default: 1)
357 | * @returns Promise with the operation result
358 | */
359 | static async incrementFileType(fileType: FileTypeKey | string, count = 1): Promise {
360 | try {
361 | // Validate increment amount
362 | if (isNaN(count)) {
363 | return {
364 | success: false,
365 | error: new Error(`Invalid increment value for file type "${fileType}": ${count}`)
366 | };
367 | }
368 |
369 | const stats = await AppStatistics.findOneAndUpdate(
370 | { id: STATS_ID },
371 | {
372 | $inc: { [`fileTypes.${fileType}`]: count },
373 | $set: { lastUpdated: new Date() }
374 | },
375 | { upsert: true, new: true }
376 | );
377 |
378 | return { success: true, data: stats };
379 | } catch (err) {
380 | console.error(`Failed to increment file type "${fileType}":`, err);
381 | return { success: false, error: err instanceof Error ? err : new Error(String(err)) };
382 | }
383 | }
384 |
385 | /**
386 | * Set a statistic to a specific value
387 | * @param key The statistic key to set
388 | * @param value The value to set
389 | * @returns Promise with the operation result
390 | */
391 | static async set(key: StatKey, value: number): Promise {
392 | try {
393 | const stats = await AppStatistics.findOneAndUpdate(
394 | { id: STATS_ID },
395 | {
396 | $set: {
397 | [key]: value,
398 | lastUpdated: new Date()
399 | }
400 | },
401 | { upsert: true, new: true }
402 | );
403 |
404 | return { success: true, data: stats };
405 | } catch (err) {
406 | console.error(`Failed to set stat "${key}":`, err);
407 | return { success: false, error: err instanceof Error ? err : new Error(String(err)) };
408 | }
409 | }
410 |
411 | /**
412 | * Get all statistics or filter by specific keys
413 | * @param filter Optional filter to apply to the query
414 | * @returns Promise with the operation result containing stats
415 | */
416 | static async getStats(filter?: FilterQuery): Promise {
417 | try {
418 | const query = filter ? { id: STATS_ID, ...filter } : { id: STATS_ID };
419 | const stats = await AppStatistics.findOne(query).lean();
420 |
421 | return { success: true, data: stats };
422 | } catch (err) {
423 | console.error('Failed to fetch stats:', err);
424 | return { success: false, error: err instanceof Error ? err : new Error(String(err)) };
425 | }
426 | }
427 |
428 | /**
429 | * Reset statistics to initial values
430 | * @returns Promise with the operation result
431 | */
432 | static async resetStats(): Promise {
433 | try {
434 | const stats = await AppStatistics.findOneAndUpdate(
435 | { id: STATS_ID },
436 | {
437 | $set: {
438 | ...initialStats,
439 | lastUpdated: new Date()
440 | }
441 | },
442 | { upsert: true, new: true }
443 | );
444 |
445 | return { success: true, data: stats };
446 | } catch (err) {
447 | console.error('Failed to reset stats:', err);
448 | return { success: false, error: err instanceof Error ? err : new Error(String(err)) };
449 | }
450 | }
451 |
452 | /**
453 | * Batch update multiple statistics at once
454 | * @param updates Object containing stat keys and their new values
455 | * @returns Promise with the operation result
456 | */
457 | static async batchUpdate(updates: Partial>): Promise {
458 | try {
459 | const updateObj: Record = {};
460 |
461 | // Process each update
462 | Object.entries(updates).forEach(([key, value]) => {
463 | updateObj[key] = value;
464 | });
465 |
466 | const stats = await AppStatistics.findOneAndUpdate(
467 | { id: STATS_ID },
468 | {
469 | $set: {
470 | ...updateObj,
471 | lastUpdated: new Date()
472 | }
473 | },
474 | { upsert: true, new: true }
475 | );
476 |
477 | return { success: true, data: stats };
478 | } catch (err) {
479 | console.error('Failed to batch update stats:', err);
480 | return { success: false, error: err instanceof Error ? err : new Error(String(err)) };
481 | }
482 | }
483 |
484 | static async updatePreviousStats(): Promise {
485 | try {
486 | const currentStats = await AppStatistics.findOne({ id: STATS_ID });
487 |
488 | if (!currentStats) {
489 | const stats = await AppStatistics.findOneAndUpdate(
490 | { id: STATS_ID },
491 | {
492 | $set: {
493 | previousStats: [initialStats],
494 | lastUpdated: new Date()
495 | }
496 | },
497 | { upsert: true, new: true }
498 | );
499 | return { success: true, data: stats };
500 | }
501 |
502 | const today = new Date();
503 | const lastEntry = currentStats.previousStats[currentStats.previousStats.length - 1];
504 | const isNotToday = lastEntry.recordedOn.getDate() !== today.getDate() ||
505 | lastEntry.recordedOn.getMonth() !== today.getMonth() ||
506 | lastEntry.recordedOn.getFullYear() !== today.getFullYear();
507 |
508 |
509 | if (isNotToday) {
510 | const stats = await AppStatistics.findOneAndUpdate(
511 | { id: STATS_ID },
512 | {
513 | $set: {
514 | previousStats: [...currentStats.previousStats, {
515 | totalServers: currentStats.totalServers,
516 | totalMessages: currentStats.totalMessages,
517 | totalUsers: currentStats.totalUsers,
518 | activeServers: currentStats.activeServers,
519 | activeUsers: currentStats.activeUsers,
520 | totalFileUploads: currentStats.totalFileUploads,
521 | deletedServers: currentStats.deletedServers,
522 | expiredServers: currentStats.expiredServers,
523 | fileTypes: currentStats.fileTypes,
524 | }],
525 | lastUpdated: today
526 | }
527 | },
528 | { new: true }
529 | );
530 | return { success: true, data: stats };
531 | }
532 |
533 | return { success: true, data: currentStats };
534 | } catch (err) {
535 | console.error('Failed to update previous stats:', err);
536 | return { success: false, error: err instanceof Error ? err : new Error(String(err)) };
537 | }
538 | }
539 | }
540 |
541 | StatisticsController.initialize();
--------------------------------------------------------------------------------
/src/controllers/users.controller.ts:
--------------------------------------------------------------------------------
1 | import { Server } from '../models/server.model';
2 | import { User } from '../models/user.model';
3 | import { generateUsername } from '../utils/user.util';
4 | import { StatisticsController } from './statistics.controller';
5 |
6 | interface ServerUser {
7 | userId: string;
8 | isOnline: boolean;
9 | lastSeen: Date;
10 | bgColor: string;
11 | textColor: string;
12 | }
13 |
14 | class UserController {
15 | private static instance: UserController;
16 |
17 | private constructor() {}
18 |
19 | public static getInstance(): UserController {
20 | if (!UserController.instance) {
21 | UserController.instance = new UserController();
22 | }
23 | return UserController.instance;
24 | }
25 |
26 | public async addUserToServer(serverId: string, user: Omit): Promise {
27 | const server = await Server.findOne({ serverId });
28 | if (!server) {
29 | throw new Error(`Server ${serverId} not found`);
30 | }
31 |
32 | let dbUser = await User.findOne({ userId: user.userId });
33 | if (!dbUser) {
34 | dbUser = await User.create({
35 | userId: user.userId,
36 | isOnline: true,
37 | currentServer: serverId
38 | });
39 |
40 | await StatisticsController.incrementTotalUsers();
41 | await StatisticsController.incrementActiveUsers();
42 | } else {
43 | await StatisticsController.incrementActiveUsers();
44 | await User.updateOne(
45 | { userId: user.userId },
46 | {
47 | isOnline: true,
48 | currentServer: serverId,
49 | lastSeen: new Date()
50 | }
51 | );
52 | }
53 |
54 | if (!server.approvedUsers.includes(dbUser.userId)) {
55 | server.approvedUsers.push(dbUser.userId);
56 | await server.save();
57 | }
58 |
59 | const username = generateUsername(`${dbUser.userId}-${serverId}`);
60 |
61 | }
62 |
63 | public async removeUserFromServer(serverId: string, userId: string): Promise {
64 | const server = await Server.findOne({ serverId });
65 | if (!server) return;
66 |
67 | server.approvedUsers = server.approvedUsers.filter(id => id !== userId);
68 | await server.save();
69 |
70 | // Update user status
71 | await User.updateOne(
72 | { userId },
73 | {
74 | isOnline: false,
75 | currentServer: null,
76 | lastSeen: new Date()
77 | }
78 | );
79 |
80 | await StatisticsController.decrementActiveUsers();
81 | }
82 |
83 | public async getActiveUsers(serverId: string): Promise {
84 | const server = await Server.findOne({ serverId });
85 | if (!server) return [];
86 |
87 | const users = await User.find({ userId: { $in: server.allUsers } });
88 | const approvedUsers = await User.find({ userId: { $in: server.approvedUsers } });
89 | return users.map(user => {
90 | if(!approvedUsers.find(u => u.userId === user.userId)) return({
91 | userId: user.userId,
92 | username: generateUsername(`${user.userId}-${serverId}`),
93 | isOnline: false,
94 | lastSeen: user.lastSeen,
95 | bgColor: user.bgColor,
96 | textColor: user.textColor
97 | });
98 |
99 | return ({
100 | userId: user.userId,
101 | username: generateUsername(`${user.userId}-${serverId}`),
102 | isOnline: user.isOnline,
103 | lastSeen: user.lastSeen,
104 | bgColor: user.bgColor,
105 | textColor: user.textColor
106 | })
107 | });
108 | }
109 |
110 | public async clearServerUsers(serverId: string): Promise {
111 | const server = await Server.findOne({ serverId });
112 | if (!server) return;
113 |
114 | // Update all users in this server to offline
115 | await User.updateMany(
116 | { userId: { $in: server.approvedUsers } },
117 | {
118 | isOnline: false,
119 | currentServer: null,
120 | lastSeen: new Date()
121 | }
122 | );
123 |
124 | await StatisticsController.decrementActiveUsers(server.approvedUsers.length);
125 |
126 | server.approvedUsers = [];
127 | await server.save();
128 | }
129 |
130 | public async isUserInServer(serverId: string, userId: string): Promise {
131 | const server = await Server.findOne({ serverId });
132 | if (!server) return false;
133 |
134 | return server.approvedUsers.includes(userId);
135 | }
136 |
137 | public async setUserOnlineStatus(userId: string, isOnline: boolean, serverId?: string): Promise {
138 | await User.updateOne(
139 | { userId },
140 | {
141 | isOnline,
142 | currentServer: isOnline ? serverId : null,
143 | lastSeen: new Date()
144 | }
145 | );
146 | }
147 | }
148 |
149 | // Export singleton instance methods
150 | const userController = UserController.getInstance();
151 |
152 | export const addUserToServer = (serverId: string, user: Omit): Promise =>
153 | userController.addUserToServer(serverId, user);
154 |
155 | export const removeUserFromServer = (serverId: string, userId: string): Promise =>
156 | userController.removeUserFromServer(serverId, userId);
157 |
158 | export const getActiveUsers = (serverId: string): Promise =>
159 | userController.getActiveUsers(serverId);
160 |
161 | export const clearServerUsers = (serverId: string): Promise =>
162 | userController.clearServerUsers(serverId);
163 |
164 | export const isUserInServer = (serverId: string, userId: string): Promise =>
165 | userController.isUserInServer(serverId, userId);
166 |
167 | export const setUserOnlineStatus = (userId: string, isOnline: boolean, serverId?: string): Promise =>
168 | userController.setUserOnlineStatus(userId, isOnline, serverId);
169 |
--------------------------------------------------------------------------------
/src/middleware/auth.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import jwt from "jsonwebtoken";
3 | import { config } from "../config/dotenv.config";
4 | import AppError from "../types/error.class";
5 | import { Server } from "../models/server.model";
6 |
7 | export interface JWTPayload {
8 | userId: string;
9 | serverId: string;
10 | }
11 |
12 | export interface AuthRequest extends Request {
13 | apiKey?: string;
14 | user?: JWTPayload;
15 | }
16 |
17 | class AuthMiddleware {
18 | private static instance: AuthMiddleware;
19 | private readonly API_KEY_HEADER = "X-API-Key";
20 |
21 | private constructor() { }
22 |
23 | public static getInstance(): AuthMiddleware {
24 | if (!AuthMiddleware.instance) {
25 | AuthMiddleware.instance = new AuthMiddleware();
26 | }
27 | return AuthMiddleware.instance;
28 | }
29 |
30 | private validateApiKey(apiKey: string | undefined): string {
31 | if (!apiKey) {
32 | throw new AppError(401, "Access denied. No API key provided.");
33 | }
34 |
35 | if (apiKey !== config.API_KEY) {
36 | throw new AppError(401, "Invalid API key.");
37 | }
38 |
39 | return apiKey;
40 | }
41 |
42 | private extractToken(token: string | undefined): string {
43 | if (!token) {
44 | throw new AppError(401, "Access denied. No token provided.");
45 | }
46 |
47 | return token;
48 | }
49 |
50 | private verifyToken(token: string): JWTPayload {
51 | try {
52 | return jwt.verify(token, config.JWT_SECRET) as JWTPayload;
53 | } catch (error) {
54 | if (error instanceof jwt.TokenExpiredError) {
55 | throw new AppError(401, "Token has expired.");
56 | }
57 | if (error instanceof jwt.JsonWebTokenError) {
58 | throw new AppError(401, "Invalid token.");
59 | }
60 | throw new AppError(401, "Token verification failed.");
61 | }
62 | }
63 |
64 | private async validateServerAccess(payload: JWTPayload): Promise {
65 | const server = await Server.findOne({ serverId: payload.serverId });
66 | if (!server) {
67 | throw new AppError(404, "Server not found");
68 | }
69 |
70 | if (server.expiresAt < new Date() && server.type === "Private") {
71 | throw new AppError(410, "Server has expired 01");
72 | }
73 | }
74 |
75 | public shouldRefreshToken(decoded: JWTPayload): boolean {
76 | const exp = (decoded as any).exp;
77 | if (!exp) return false;
78 |
79 | const currentTime = Math.floor(Date.now() / 1000);
80 | const timeUntilExpiry = exp - currentTime;
81 |
82 | return timeUntilExpiry < 3600;
83 | }
84 |
85 | public authenticate = async (req: AuthRequest, res: Response, next: NextFunction): Promise => {
86 | try {
87 | // Always validate API key
88 | const apiKey = this.validateApiKey(req.header(this.API_KEY_HEADER));
89 | req.apiKey = apiKey;
90 | next();
91 | } catch (error) {
92 | if (error instanceof AppError) {
93 | res.status(error.statusCode).json({ message: error.message });
94 | } else {
95 | res.status(500).json({ message: "Internal server error during authentication." });
96 | }
97 | }
98 | }
99 |
100 | public authenticateWithJWT = async (req: AuthRequest, res: Response, next: NextFunction): Promise => {
101 | try {
102 | // First validate API key
103 | const apiKey = this.validateApiKey(req.header(this.API_KEY_HEADER));
104 |
105 | req.apiKey = apiKey;
106 |
107 | // Then validate JWT token
108 | const rawToken = req.header("Authorization");
109 | const token = this.extractToken(rawToken);
110 | const decoded = this.verifyToken(token);
111 |
112 |
113 | // Validate server access
114 | await this.validateServerAccess(decoded);
115 |
116 | // Check if token needs refresh
117 | if (this.shouldRefreshToken(decoded)) {
118 | const newToken = this.generateToken(decoded);
119 | res.setHeader("X-Refresh-Token", newToken);
120 | }
121 |
122 | req.user = decoded;
123 |
124 | next();
125 | } catch (error) {
126 |
127 | if (error instanceof AppError) {
128 | res.status(error.statusCode).json({ message: error.message });
129 | } else {
130 | res.status(500).json({ message: "Internal server error during authentication." });
131 | }
132 | }
133 | }
134 |
135 | public generateToken(payload: JWTPayload): string {
136 | return jwt.sign(payload, config.JWT_SECRET, {
137 | expiresIn: "24h" // Token expires in 24 hours
138 | });
139 | }
140 | }
141 |
142 | // Export singleton instance method
143 | export const authMiddleware = AuthMiddleware.getInstance();
144 |
145 | // Export authenticate middleware
146 | export const authenticateUser = authMiddleware.authenticate;
147 |
148 | // Export authenticateWithJWT middleware
149 | export const authenticateUserWithJWT = authMiddleware.authenticateWithJWT;
150 |
151 | // Export token generator
152 | export const generateToken = (payload: JWTPayload): string =>
153 | authMiddleware.generateToken(payload);
--------------------------------------------------------------------------------
/src/middleware/errorHandler.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from 'express';
2 | import AppError from '../types/error.class';
3 |
4 | const errorHandler = (err: AppError, req: Request, res: Response, next: NextFunction) => {
5 | console.error(err.stack);
6 | res.status(err.statusCode).json({
7 | message: err.message,
8 | statusCode: err.statusCode
9 | });
10 | };
11 |
12 | export default errorHandler;
--------------------------------------------------------------------------------
/src/models/invitation.model.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const invitationSchema = new mongoose.Schema({
4 | inviteCode: { type: String, required: true, unique: true },
5 | used: { type: Boolean, default: false },
6 | serverId: { type: String, required: true, ref: "Server" },
7 | createdAt: { type: Date, default: Date.now },
8 | expiresAt: { type: Date, required: true }
9 | });
10 |
11 | const Invitation = mongoose.model("Invitation", invitationSchema);
12 | export default Invitation;
--------------------------------------------------------------------------------
/src/models/messages.model.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import { Server } from "./server.model";
3 | import { encryptMessage } from "../utils/messages.utli";
4 | import { generateId } from "../utils/index.util";
5 |
6 | const messageSchema = new mongoose.Schema({
7 | serverId: { type: String, required: true, ref: "Server" },
8 | senderId: { type: String, required: true, ref: "User" },
9 | receiverId: { type: String, required: true, ref: "User" },
10 | messageId: { type: String, required: true, unique: true, default: () => `txt-${generateId()}` },
11 | content: { type: String, required: true },
12 | attachmentUrl: { type: String, default: null },
13 | sent: { type: Boolean, default: false },
14 | createdAt: { type: Date, default: Date.now },
15 | readBySender: { type: Boolean, default: true },
16 | readByReceiver: { type: Boolean, default: false }
17 | });
18 |
19 | // Encrypt message before saving
20 | messageSchema.pre("save", async function (next) {
21 | if (!this.serverId) return next(new Error("Server ID is required"));
22 | if (this.readByReceiver) return next();
23 |
24 | const message = this;
25 | const server = await Server.findOne({ serverId: message.serverId });
26 | if (!server) return next(new Error("Server not found"));
27 |
28 | const encryptedContent = encryptMessage(message.content, server.salt);
29 | message.content = encryptedContent;
30 |
31 | if (message.attachmentUrl) {
32 | const encryptedAttachmentUrl = encryptMessage(message.attachmentUrl, server.salt);
33 | message.attachmentUrl = encryptedAttachmentUrl;
34 | }
35 |
36 | next();
37 | });
38 |
39 | export const Message = mongoose.model("Message", messageSchema);
--------------------------------------------------------------------------------
/src/models/server.model.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const serverSchema = new mongoose.Schema({
4 | serverId: { type: String, required: true, unique: true },
5 | owner: { type: String, required: true },
6 | serverName: { type: String, required: true },
7 | salt: { type: String, required: true },
8 | globalInvitationId: { type: String, required: true, unique: true },
9 | expiresAt: { type: Date, required: true },
10 | createdAt: { type: Date, default: Date.now },
11 | approvedUsers: [{ type: String, ref: 'User' }],
12 | type: { type: String, enum: ['Public', 'Private'], default: 'Public' },
13 | allUsers: [{ type: String, ref: 'User' }]
14 | });
15 |
16 | // Add this before the model export
17 | serverSchema.pre('save', function (next) {
18 | // Get all approved users
19 | const approvedUsers = this.approvedUsers || [];
20 |
21 | // Initialize all users if it doesn't exist
22 | if (!this.allUsers) {
23 | this.allUsers = [];
24 | }
25 |
26 | // Add any approved users that aren't already in allUsers
27 | approvedUsers.forEach(userId => {
28 | if (!this.allUsers.includes(userId)) {
29 | this.allUsers.push(userId);
30 | }
31 | });
32 |
33 | next();
34 | });
35 |
36 | export const Server = mongoose.model("Server", serverSchema);
--------------------------------------------------------------------------------
/src/models/statistics.model.ts:
--------------------------------------------------------------------------------
1 | import { Schema, model, Document } from 'mongoose';
2 | import { config } from '../config/dotenv.config';
3 |
4 | interface previousStats extends Omit {
5 | recordedOn: Date;
6 | }
7 |
8 | export interface IAppStatistics extends Document {
9 | id: string;
10 | totalServers: number;
11 | totalMessages: number;
12 | totalUsers: number;
13 | activeServers: number;
14 | activeUsers: number;
15 | totalFileUploads: number;
16 | deletedServers: number;
17 | expiredServers: number;
18 | fileTypes: {
19 | images: number;
20 | videos: number;
21 | gifs: number;
22 | pdfs: number;
23 | others?: number;
24 | };
25 | lastUpdated: Date;
26 | previousStats: previousStats[];
27 | }
28 |
29 | const AppStatisticsSchema = new Schema({
30 | id: { type: String, default: config.STATS_ID },
31 | totalServers: { type: Number, default: 0 },
32 | totalMessages: { type: Number, default: 0 },
33 | totalUsers: { type: Number, default: 0 },
34 | activeServers: { type: Number, default: 0 },
35 | activeUsers: { type: Number, default: 0 },
36 | totalFileUploads: { type: Number, default: 0 },
37 | deletedServers: { type: Number, default: 0 },
38 | expiredServers: { type: Number, default: 0 },
39 | fileTypes: {
40 | images: { type: Number, default: 0 },
41 | videos: { type: Number, default: 0 },
42 | gifs: { type: Number, default: 0 },
43 | pdfs: { type: Number, default: 0 },
44 | others: { type: Number, default: 0 },
45 | },
46 | lastUpdated: { type: Date, default: Date.now },
47 | previousStats: [{
48 | totalServers: { type: Number, default: 0 },
49 | totalMessages: { type: Number, default: 0 },
50 | totalUsers: { type: Number, default: 0 },
51 | activeServers: { type: Number, default: 0 },
52 | activeUsers: { type: Number, default: 0 },
53 | totalFileUploads: { type: Number, default: 0 },
54 | deletedServers: { type: Number, default: 0 },
55 | expiredServers: { type: Number, default: 0 },
56 | fileTypes: {
57 | images: { type: Number, default: 0 },
58 | videos: { type: Number, default: 0 },
59 | gifs: { type: Number, default: 0 },
60 | pdfs: { type: Number, default: 0 },
61 | others: { type: Number, default: 0 },
62 | },
63 | recordedOn: { type: Date, default: Date.now }
64 | }]
65 | });
66 |
67 | export const AppStatistics = model('AppStatistics', AppStatisticsSchema);
--------------------------------------------------------------------------------
/src/models/user.model.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import { generateVibrantBgColorWithTextVisibility } from "../utils/index.util";
3 |
4 | const userSchema = new mongoose.Schema({
5 | userId: { type: String, required: true, unique: true, index: true },
6 | isOnline: { type: Boolean, default: false },
7 | lastSeen: { type: Date, default: Date.now },
8 | currentServer: { type: String, default: null, ref: "Server" },
9 | createdAt: { type: Date, default: Date.now },
10 | bgColor: { type: String, default: null },
11 | textColor: { type: String, default: null },
12 | typing: { type: Boolean, default: false },
13 | typingTo: { type: String, default: null },
14 | });
15 |
16 | // Pre-save hook to set background and text colors if not already set
17 | userSchema.pre('save', function (next) {
18 | // Only set colors for new documents (when they don't have an _id yet)
19 | if (this.isNew && (!this.bgColor || !this.textColor)) {
20 | const { backgroundColor, isWhiteTextVisible } = generateVibrantBgColorWithTextVisibility();
21 | this.bgColor = backgroundColor;
22 | this.textColor = isWhiteTextVisible ? '#FFFFFF' : '#000000';
23 | }
24 | next();
25 | });
26 |
27 | export const User = mongoose.model("User", userSchema);
28 |
--------------------------------------------------------------------------------
/src/routes/auth.routes.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { authenticateUser } from "../middleware/auth";
3 | import { refreshToken } from "../controllers/auth.controller";
4 |
5 | const authRouter = express.Router();
6 |
7 | // Add refresh token endpoint
8 | authRouter.post("/refresh-token", authenticateUser, refreshToken);
9 |
10 | export default authRouter;
11 |
--------------------------------------------------------------------------------
/src/routes/messages.route.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import messagesController from "../controllers/messages.controller";
3 | import { authenticateUserWithJWT } from "../middleware/auth";
4 |
5 | const messagesRouter = express.Router();
6 |
7 | // Private route with JWT
8 | messagesRouter.get("/:serverId", authenticateUserWithJWT, messagesController.getMessages);
9 |
10 | export default messagesRouter;
11 |
--------------------------------------------------------------------------------
/src/routes/server.routes.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { CreateServer, DeleteServer, GenerateUniqueServerInvitationId, GetServer, GetServerActiveUsers, GlobalInvitation, UniqueInvitation } from "../controllers/server.controller";
3 | import { authenticateUser, authenticateUserWithJWT } from "../middleware/auth";
4 | import messagesController from "../controllers/messages.controller";
5 |
6 | const router = express.Router();
7 |
8 | // Public route with API key only
9 | router.post("/", authenticateUser, CreateServer);
10 | router.post("/invitation/:globalInvitationId", authenticateUser, GlobalInvitation);
11 | router.post("/unique-invitation/:inviteCode", authenticateUser, UniqueInvitation);
12 |
13 | // Private route with JWT
14 | router.get("/:serverId", authenticateUserWithJWT, GetServer);
15 | router.get("/:serverId/active-users", authenticateUserWithJWT, GetServerActiveUsers);
16 | router.get("/:serverId/generate-unique-server-invitation-id", authenticateUserWithJWT, GenerateUniqueServerInvitationId);
17 | router.get("/:serverId/messages", authenticateUserWithJWT, messagesController.getMessages);
18 | router.delete("/:serverId", authenticateUserWithJWT, DeleteServer);
19 | export default router;
--------------------------------------------------------------------------------
/src/sockets/index.socket.ts:
--------------------------------------------------------------------------------
1 | import { Server as SocketServer, Socket } from 'socket.io';
2 | import { Server } from '../models/server.model';
3 | import { io } from '../../server';
4 | import { ServerJoinPayload, ServerMessage, ServerAction, UserStatusUpdate } from '../types/server-socket.interface';
5 | import { getActiveUsers, setUserOnlineStatus } from '../controllers/users.controller';
6 | import messagesController from '../controllers/messages.controller';
7 | import { Message } from '../types/message.interface';
8 | import { LeaveServer } from '../controllers/server.controller';
9 | import { User } from '../models/user.model';
10 |
11 | class ServerSocketService {
12 | private static instance: ServerSocketService;
13 | private io: SocketServer;
14 | private userSocketMap: Map>; // userId -> Set of socket IDs
15 | private serverUsersMap: Map>; // serverId -> Set of userIds
16 |
17 | private constructor() {
18 | this.io = io;
19 | this.userSocketMap = new Map();
20 | this.serverUsersMap = new Map();
21 | this.setupSocketHandlers();
22 | }
23 |
24 | public static getInstance(): ServerSocketService {
25 | if (!ServerSocketService.instance) {
26 | ServerSocketService.instance = new ServerSocketService();
27 | }
28 | return ServerSocketService.instance;
29 | }
30 |
31 | private async validateServerAccess(serverId: string): Promise {
32 | try {
33 | const server = await Server.findOne({ serverId });
34 | if (!server) return false;
35 | if (server.expiresAt < new Date() && server.type === "Private") return false;
36 | return true;
37 | } catch (error) {
38 | console.error('Error validating server access:', error);
39 | return false;
40 | }
41 | }
42 |
43 | private async broadcastActiveUsers(serverId: string): Promise {
44 | try {
45 | const activeUsers = await getActiveUsers(serverId);
46 | this.io.to(serverId).emit('active_users_updated', activeUsers);
47 | } catch (error) {
48 | console.error('Error broadcasting active users:', error);
49 | }
50 | }
51 |
52 | private async broadcastUserStatus(update: UserStatusUpdate): Promise {
53 | try {
54 | // First update the user's status in the database
55 | await setUserOnlineStatus(update.userId, update.isOnline, update.serverId);
56 |
57 | // Then broadcast the updated user list to all clients in the server
58 | await this.broadcastActiveUsers(update.serverId);
59 |
60 | // Also emit a specific status update event for real-time UI updates
61 | this.io.to(update.serverId).emit('user_status_changed', update);
62 |
63 | // Emit typing status
64 | await this.broadcastUserTypingStatus(update.serverId, update.userId, false, "");
65 | } catch (error) {
66 | console.error('Error broadcasting user status:', error);
67 | }
68 | }
69 |
70 | private trackUserSocket(userId: string, socketId: string, serverId: string): void {
71 | // Track socket for user
72 | if (!this.userSocketMap.has(userId)) {
73 | this.userSocketMap.set(userId, new Set());
74 | }
75 | this.userSocketMap.get(userId)!.add(socketId);
76 |
77 | // Track user for server
78 | if (!this.serverUsersMap.has(serverId)) {
79 | this.serverUsersMap.set(serverId, new Set());
80 | }
81 | this.serverUsersMap.get(serverId)!.add(userId);
82 | }
83 |
84 | private async untrackUserSocket(userId: string, socketId: string): Promise {
85 | const userSockets = this.userSocketMap.get(userId);
86 | if (userSockets) {
87 | userSockets.delete(socketId);
88 | if (userSockets.size === 0) {
89 | this.userSocketMap.delete(userId);
90 |
91 | // Find which server this user was in
92 | for (const [serverId, users] of this.serverUsersMap) {
93 | if (users.has(userId)) {
94 | users.delete(userId);
95 | // Broadcast status update
96 | await this.broadcastUserStatus({
97 | userId,
98 | isOnline: false,
99 | serverId
100 | });
101 | break;
102 | }
103 | }
104 | }
105 | }
106 | }
107 |
108 | private setupSocketHandlers(): void {
109 | this.io.on('connection', (socket: Socket) => {
110 | let currentServerId: string | null = null;
111 | let currentUserId: string | null = null;
112 |
113 | socket.on('join_server', async (payload: ServerJoinPayload) => {
114 | try {
115 | const { serverId, userId } = payload;
116 | currentServerId = serverId;
117 | currentUserId = userId;
118 |
119 | const hasAccess = await this.validateServerAccess(serverId);
120 | if (!hasAccess) {
121 | socket.emit('server_error', {
122 | type: 'error',
123 | content: 'Invalid server credentials or server has expired',
124 | timestamp: Date.now()
125 | });
126 | return;
127 | }
128 |
129 | // Leave all other rooms first
130 | socket.rooms.forEach(room => {
131 | if (room !== socket.id) {
132 | socket.leave(room);
133 | }
134 | });
135 |
136 | // Track user and join server room
137 | if (userId) {
138 | this.trackUserSocket(userId, socket.id, serverId);
139 | socket.join(serverId);
140 |
141 | // Broadcast user's online status
142 | await this.broadcastUserStatus({
143 | userId,
144 | isOnline: true,
145 | serverId
146 | });
147 | }
148 |
149 | socket.emit('server_joined', {
150 | type: 'status',
151 | content: 'Successfully joined the server',
152 | timestamp: Date.now()
153 | });
154 |
155 | } catch (error) {
156 | console.error('Error during join process:', error);
157 | socket.emit('server_error', {
158 | type: 'error',
159 | content: 'Failed to join server',
160 | timestamp: Date.now()
161 | });
162 | }
163 | });
164 |
165 | socket.on('new-message', async (serverId: string, message: Message) => {
166 | await messagesController.sendMessage(serverId, message.content, message.senderId, message.receiverId, message.attachmentUrl);
167 | });
168 |
169 | socket.on('mark_message_read', async (messageId: string) => {
170 | await messagesController.markMessageRead(messageId);
171 | });
172 |
173 | socket.on('leave_server', async (serverId: string) => {
174 | socket.leave(serverId);
175 | try {
176 | if (currentUserId) {
177 | await this.untrackUserSocket(currentUserId, socket.id);
178 | await LeaveServer(serverId, currentUserId);
179 | }
180 | } catch (error) {
181 | console.error('Error during leave process:', error);
182 | }
183 | currentServerId = null;
184 | currentUserId = null;
185 |
186 | // emit typing status
187 | await this.broadcastUserTypingStatus(serverId, serverId, false, "");
188 |
189 | socket.emit('server_left', {
190 | type: 'status',
191 | content: 'Left the server',
192 | timestamp: Date.now()
193 | });
194 | });
195 |
196 | // user is typing
197 | socket.on('typing', async (serverId: string, receiverId: string, userId: string) => {
198 | await User.updateOne({ userId }, { typing: true, typingTo: receiverId });
199 | await this.broadcastUserTypingStatus(serverId, userId, true, receiverId);
200 | });
201 |
202 | // user is not typing
203 | socket.on('not_typing', async (serverId: string, userId: string) => {
204 | const user = await User.findOne({ userId });
205 | if (!user) {
206 | return;
207 | }
208 |
209 | user.typing = false;
210 | const typingTo = user.typingTo;
211 | user.typingTo = "";
212 | await user.save();
213 | await this.broadcastUserTypingStatus(serverId, userId, false, typingTo);
214 | });
215 |
216 | // user disconnect
217 | socket.on('disconnect', async () => {
218 | if (currentUserId) {
219 | await this.untrackUserSocket(currentUserId, socket.id);
220 | if (!currentServerId) return;
221 | await setUserOnlineStatus(currentUserId, false, currentServerId);
222 | }
223 | });
224 | });
225 | }
226 |
227 | private async broadcastUserTypingStatus(serverId: string, userId: string, typing: boolean, typingTo: string | null): Promise {
228 | this.io.in(serverId).emit('user_typing', { userId, typing, typingTo });
229 | }
230 |
231 | public emitToServer(serverId: string, message: ServerMessage): void {
232 | this.io.to(serverId).emit('server_message', message);
233 | }
234 |
235 | // broadcast new message
236 | public broadcastNewMessage(serverId: string, message: Omit): void {
237 | this.io.in(serverId).emit('new-message', message);
238 | }
239 |
240 | // emit server actions
241 | public emitServerActions(serverId: string, action: ServerAction, username?: string): void {
242 | this.io.to(serverId).emit('server_action', {
243 | type: 'status',
244 | content: `${username || 'A user'} ${action === ServerAction.JOIN ? 'joined' : 'left'} the server`,
245 | action,
246 | username,
247 | timestamp: Date.now()
248 | });
249 | }
250 |
251 | public broadcastServerMessage(serverId: string, message: ServerMessage): void {
252 | this.io.in(serverId).emit('server_message', message);
253 | }
254 |
255 | public broadcastMessageRead(messageId: string, serverId: string): void {
256 | this.io.in(serverId).emit('message_read', messageId);
257 | }
258 |
259 | public broadcastServerDeleted(serverId: string) {
260 | this.io.in(serverId).emit("server_deleted");
261 | }
262 | }
263 |
264 | export const getSocketService = (): ServerSocketService => {
265 | return ServerSocketService.getInstance();
266 | };
267 |
--------------------------------------------------------------------------------
/src/types/error.class.ts:
--------------------------------------------------------------------------------
1 | class AppError extends Error {
2 | statusCode: number;
3 |
4 | constructor(statusCode: number = 500, message: string = "Internal Server Error") {
5 | super(message);
6 | this.statusCode = statusCode;
7 | }
8 | }
9 |
10 | export default AppError;
--------------------------------------------------------------------------------
/src/types/message.interface.ts:
--------------------------------------------------------------------------------
1 | export interface Message {
2 | serverId: string,
3 | senderId: string,
4 | receiverId: string,
5 | messageId: string,
6 | content: string,
7 | createdAt: Date,
8 | readBySender: boolean,
9 | readByReceiver: boolean,
10 | sent: boolean,
11 | attachmentUrl?: string,
12 | attachments?: string[]
13 | }
--------------------------------------------------------------------------------
/src/types/server-socket.interface.ts:
--------------------------------------------------------------------------------
1 | export interface ServerJoinPayload {
2 | serverId: string;
3 | userId: string;
4 | }
5 |
6 | export interface ServerMessage {
7 | type: 'status' | 'error';
8 | content: string;
9 | timestamp: number;
10 | }
11 |
12 | export interface UserStatusUpdate {
13 | userId: string;
14 | isOnline: boolean;
15 | serverId: string;
16 | }
17 |
18 | export enum ServerAction {
19 | JOIN = 'join',
20 | LEAVE = 'leave'
21 | }
--------------------------------------------------------------------------------
/src/types/server.interface.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from 'express';
2 |
3 | export interface CreateServerRequest {
4 | serverName: string;
5 | encryptionKey: string;
6 | lifeSpan: number;
7 | fingerprint: string;
8 | }
9 |
10 | export interface ServerResponse {
11 | server_name: string;
12 | server_id: string;
13 | expiration: Date;
14 | }
15 |
16 | export interface ServerData {
17 | serverId: string;
18 | serverName: string;
19 | salt: string;
20 | expiresAt: Date;
21 | isActive?: boolean;
22 | }
23 |
24 | export interface IServerController {
25 | createServer(req: Request, res: Response, next: NextFunction): Promise;
26 | validateRequest(body: any): body is CreateServerRequest;
27 | generateServerData(request: CreateServerRequest): ServerData;
28 | formatResponse(server: ServerData): ServerResponse;
29 | }
--------------------------------------------------------------------------------
/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Initial statistics values used for new instances and resets
3 | * These values match the structure of the IAppStatistics interface
4 | */
5 | export const initialStats = {
6 | totalServers: 0,
7 | totalMessages: 0,
8 | totalUsers: 0,
9 | activeServers: 0,
10 | activeUsers: 0,
11 | totalFileUploads: 0,
12 | fileTypes: {
13 | images: 0,
14 | videos: 0,
15 | gifs: 0,
16 | pdfs: 0,
17 | others: 0,
18 | },
19 | };
20 |
21 | /**
22 | * Statistics keys for type safety throughout the application
23 | */
24 | export const STAT_KEYS = {
25 | // Top-level stats
26 | TOTAL_SERVERS: 'totalServers' as const,
27 | TOTAL_MESSAGES: 'totalMessages' as const,
28 | TOTAL_USERS: 'totalUsers' as const,
29 | ACTIVE_SERVERS: 'activeServers' as const,
30 | ACTIVE_USERS: 'activeUsers' as const,
31 | TOTAL_FILE_UPLOADS: 'totalFileUploads' as const,
32 |
33 | // File type stats
34 | FILE_TYPES: {
35 | IMAGES: 'images' as const,
36 | VIDEOS: 'videos' as const,
37 | GIFS: 'gifs' as const,
38 | PDFS: 'pdfs' as const,
39 | OTHERS: 'others' as const
40 | }
41 | };
--------------------------------------------------------------------------------
/src/utils/index.util.ts:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from "uuid";
2 |
3 | export function generateId() {
4 | return uuidv4();
5 | }
6 |
7 | export function generateVibrantBgColorWithTextVisibility(): {
8 | backgroundColor: string;
9 | isWhiteTextVisible: boolean
10 | } {
11 | const hue = 220 + Math.floor(Math.random() * 60);
12 | const saturation = 70 + Math.floor(Math.random() * 30);
13 | const lightness = 40 + Math.floor(Math.random() * 30);
14 |
15 | const h = hue / 360;
16 | const s = saturation / 100;
17 | const l = lightness / 100;
18 |
19 | let r, g, b;
20 |
21 | if (s === 0) {
22 | r = g = b = l;
23 | } else {
24 | const hue2rgb = (p: number, q: number, t: number) => {
25 | if (t < 0) t += 1;
26 | if (t > 1) t -= 1;
27 | if (t < 1 / 6) return p + (q - p) * 6 * t;
28 | if (t < 1 / 2) return q;
29 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
30 | return p;
31 | };
32 |
33 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
34 | const p = 2 * l - q;
35 |
36 | r = hue2rgb(p, q, h + 1 / 3);
37 | g = hue2rgb(p, q, h);
38 | b = hue2rgb(p, q, h - 1 / 3);
39 | }
40 |
41 |
42 | const rInt = Math.round(r * 255);
43 | const gInt = Math.round(g * 255);
44 | const bInt = Math.round(b * 255);
45 |
46 |
47 | const toHex = (c: number) => {
48 | const hex = c.toString(16);
49 | return hex.length === 1 ? '0' + hex : hex;
50 | };
51 |
52 | const backgroundColor = `#${toHex(rInt)}${toHex(gInt)}${toHex(bInt)}`;
53 |
54 |
55 | const sRGB = [r, g, b].map(val => {
56 | return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
57 | });
58 |
59 | const luminance = 0.2126 * sRGB[0] + 0.7152 * sRGB[1] + 0.0722 * sRGB[2];
60 |
61 |
62 |
63 | const contrastRatio = (1.0 + 0.05) / (luminance + 0.05);
64 |
65 |
66 | const isWhiteTextVisible = contrastRatio >= 3;
67 |
68 | return { backgroundColor, isWhiteTextVisible };
69 | }
--------------------------------------------------------------------------------
/src/utils/messages.utli.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 |
3 | // Function to encrypt messages
4 | function encryptMessage(text: string, salt: string): string {
5 | const iv = crypto.randomBytes(16);
6 |
7 | const key = crypto.pbkdf2Sync(salt, 'fixed-salt', 100000, 32, 'sha256');
8 |
9 | const cipher = crypto.createCipheriv('aes-256-ctr', key, iv);
10 |
11 | let encrypted = cipher.update(text, 'utf8', 'hex');
12 | encrypted += cipher.final('hex');
13 |
14 | return iv.toString('hex') + encrypted;
15 | }
16 |
17 | // Function to decrypt messages
18 | function decryptMessage(encryptedText: string, salt: string): string {
19 | const iv = Buffer.from(encryptedText.slice(0, 32), 'hex');
20 | const encryptedContent = encryptedText.slice(32);
21 |
22 | const key = crypto.pbkdf2Sync(salt, 'fixed-salt', 100000, 32, 'sha256');
23 |
24 | const decipher = crypto.createDecipheriv('aes-256-ctr', key, iv);
25 |
26 | let decrypted = decipher.update(encryptedContent, 'hex', 'utf8');
27 | decrypted += decipher.final('utf8');
28 |
29 | return decrypted;
30 | }
31 |
32 | export { decryptMessage, encryptMessage }
--------------------------------------------------------------------------------
/src/utils/server.util.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fabiconcept/secret-room-backend/b781f85e513b4da479d98fbd90697ddc36ffc82e/src/utils/server.util.ts
--------------------------------------------------------------------------------
/src/utils/user.util.ts:
--------------------------------------------------------------------------------
1 | import { adjectives, nouns } from "../../constants/user.constant";
2 |
3 | const patterns = [
4 | // Pattern types
5 | { type: 'alpha', generate: () => generateAlphaPattern() },
6 | { type: 'alphaNum', generate: () => generateAlphaNumPattern() }
7 | ];
8 |
9 | function generateAlphaPattern(): string {
10 | const pattern = Math.random() < 0.5
11 | ? adjectives[Math.floor(Math.random() * adjectives.length)]
12 | : nouns[Math.floor(Math.random() * nouns.length)];
13 |
14 | // Capitalize first letter sometimes
15 | return Math.random() < 0.3 ? capitalize(pattern) : pattern;
16 | }
17 |
18 | function generateAlphaNumPattern(): string {
19 | const word = Math.random() < 0.5
20 | ? adjectives[Math.floor(Math.random() * adjectives.length)]
21 | : nouns[Math.floor(Math.random() * nouns.length)];
22 |
23 | const number = Math.floor(Math.random() * 99) + 1;
24 | return `${word}${number}`;
25 | }
26 |
27 | function capitalize(str: string): string {
28 | return str.charAt(0).toUpperCase() + str.slice(1);
29 | }
30 |
31 | export function generateUsername(salt: string): string {
32 | if (!salt || typeof salt !== 'string' || salt.length === 0) {
33 | throw new Error('A non-empty salt string must be provided');
34 | }
35 |
36 | // Use the salt to create a deterministic random value
37 | const hashCode = (str: string): number => {
38 | let hash = 0;
39 | for (let i = 0; i < str.length; i++) {
40 | const char = str.charCodeAt(i);
41 | hash = ((hash << 5) - hash) + char;
42 | hash = hash & hash; // Convert to 32bit integer
43 | }
44 | return Math.abs(hash);
45 | };
46 |
47 | const hash = hashCode(salt);
48 |
49 | // Define patterns for username generation
50 | const patterns = [
51 | // Pattern for non-alphanumeric (using letters only)
52 | {
53 | generate: () => {
54 | const consonants = 'bcdfghjklmnpqrstvwxyz';
55 | const vowels = 'aeiou';
56 | let result = '';
57 |
58 | // Use the hash to deterministically generate a username
59 | const seed = hash;
60 | const length = (seed % 6) + 3; // Length between 3 and 8
61 |
62 | for (let i = 0; i < length; i++) {
63 | // Alternate between consonants and vowels
64 | const charSet = i % 2 === 0 ? consonants : vowels;
65 | const charIndex = Math.floor((seed / (i + 1)) % charSet.length);
66 | result += charSet[charIndex];
67 | }
68 |
69 | return result;
70 | }
71 | },
72 | // Pattern for alphanumeric
73 | {
74 | generate: () => {
75 | const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
76 | let result = '';
77 |
78 | const seed = hash;
79 | const length = (seed % 6) + 3; // Length between 3 and 8
80 |
81 | for (let i = 0; i < length; i++) {
82 | const charIndex = Math.floor((seed / (i + 1)) % chars.length);
83 | result += chars[charIndex];
84 | }
85 |
86 | return result;
87 | }
88 | }
89 | ];
90 |
91 | // Use the first digit of the hash to determine whether to use alphanumeric
92 | const alphanumeric = (hash % 10) < 3; // 30% chance, just like the original
93 |
94 | // Generate username based on the selected pattern
95 | const pattern = alphanumeric ? patterns[1] : patterns[0];
96 | const username = pattern.generate();
97 |
98 | return username;
99 | }
100 |
101 | export function validateUsername(username: string): boolean {
102 | if (username.length < 3 || username.length > 8) return false;
103 | return /^[a-zA-Z0-9]+$/.test(username);
104 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 | /* Projects */
5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
11 | /* Language and Environment */
12 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
14 | // "jsx": "preserve", /* Specify what JSX code is generated. */
15 | // "libReplacement": true, /* Enable lib replacement. */
16 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
17 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
18 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
19 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
20 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
21 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
22 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
23 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
24 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
25 | /* Modules */
26 | "module": "commonjs", /* Specify what module code is generated. */
27 | "rootDir": "./", /* Specify the root folder within your source files. */
28 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
29 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
30 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
31 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
32 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
33 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
34 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
35 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
36 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
37 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
38 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
39 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
40 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
41 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */
42 | // "resolveJsonModule": true, /* Enable importing .json files. */
43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
45 | /* JavaScript Support */
46 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
47 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
48 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
49 | /* Emit */
50 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
51 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
52 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
53 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
54 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
55 | // "noEmit": true, /* Disable emitting files from a compilation. */
56 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
57 | "outDir": "./dist", /* Specify an output folder for all emitted files. */
58 | // "removeComments": true, /* Disable emitting comments. */
59 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
60 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
61 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
63 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
64 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
65 | // "newLine": "crlf", /* Set the newline character for emitting files. */
66 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
67 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
68 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
69 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
70 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
71 | /* Interop Constraints */
72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
73 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
74 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
75 | // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
76 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
77 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
78 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
79 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
80 | /* Type Checking */
81 | "strict": true, /* Enable all strict type-checking options. */
82 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
83 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
84 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
85 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
86 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
87 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
88 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
89 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
90 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
91 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
92 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
93 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
94 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
95 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
96 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
97 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
98 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
99 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
100 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
101 | /* Completeness */
102 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
103 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
104 | }
105 | }
--------------------------------------------------------------------------------