├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── package.json ├── src ├── config │ └── db.ts ├── controllers │ ├── auth.ts │ ├── channel.ts │ ├── conversations.ts │ ├── message.ts │ ├── organisation.ts │ ├── teammates.ts │ └── thread.ts ├── helpers │ ├── createTodaysFirstMessage.ts │ ├── sendEmail.ts │ ├── successResponse.ts │ └── updateConversationStatus.ts ├── html │ ├── confirmation-code-email.ts │ └── join-teammates-email.ts ├── index.ts ├── middleware │ ├── errorResponse.ts │ └── protect.ts ├── models │ ├── channel.ts │ ├── conversation.ts │ ├── message.ts │ ├── organisation.ts │ ├── thread.ts │ └── user.ts └── routes │ ├── auth.ts │ ├── channel.ts │ ├── conversations.ts │ ├── message.ts │ ├── organisation.ts │ ├── teammates.ts │ └── thread.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | node_modules/ 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | __DateNight 23 | old-teammates.text -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Adeola Adeoti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slack Clone Api Repository 2 | - Client repository https://github.com/adeolaadeoti/slack-clone-client 3 | 4 | ## Table of Contents 5 | 6 | - [Introduction](#introduction) 7 | - [Features](#features) 8 | - [Getting Started](#getting-started) 9 | - [Installation](#installation) 10 | - [Usage](#usage) 11 | - [User Registration](#user-registration) 12 | - [Creating Channels](#creating-channels) 13 | - [Sending Messages](#sending-messages) 14 | - [Message Replies (Threads)](#message-replies-threads) 15 | - [Huddle with Other Users](#huddle-with-other-users) 16 | - [Contributing](#contributing) 17 | - [License](#license) 18 | 19 | ## Introduction 20 | 21 | Slack clone is a powerful team collaboration platform that allows you to communicate and collaborate with your team members in real-time. Whether you're working remotely or in the same office, our application provides a seamless and efficient way to stay connected. 22 | 23 | ![Screenshot](https://res.cloudinary.com/adeolaadeoti/image/upload/v1695161023/screenshot_jsd2mf.png) 24 | 25 | ## Features 26 | 27 | - Real-time chat and messaging 28 | - Channel-based communication 29 | - Direct messaging between users 30 | - File and media sharing 31 | - Customizable notifications 32 | - **Message Replies (Threads)** 33 | - Start threads to reply to specific messages in a conversation. 34 | - Keep discussions organized and focused. 35 | - **Huddle with Other Users** 36 | - Create private huddles for group discussions. 37 | - Collaborate with select team members in a secure environment. 38 | 39 | ## Getting Started 40 | 41 | ### Installation 42 | 43 | 1. Clone the repository: 44 | 45 | ```bash 46 | git clone https://github.com/adeolaadeoti/slack-clone-api.git 47 | ``` 48 | 49 | 2. Change to the project directory: 50 | 51 | ```bash 52 | cd slack-clone-api 53 | ``` 54 | 55 | 3. Install dependencies: 56 | 57 | ```bash 58 | yarn 59 | ``` 60 | 61 | 4. Set up environment variables: 62 | 63 | Create a `.env` file in the root directory of the project and add the necessary environment variables, including your database connection details and any API keys. 64 | 65 | ```plaintext 66 | MONGODB_URI= 67 | JWT_SECRET= 68 | JWT_EXPIRE=30d 69 | SMTP_USERNAME= 70 | SMTP_PASSWORD= 71 | MONGODB_DB_NAME=slack-clone-dev 72 | NODE_ENV=development 73 | GOOGLE_CLIENT_ID= 74 | GOOGLE_CLIENT_SECRET= 75 | CLIENT_URL=http://localhost:3000 76 | API_URL=http://localhost:8080/api/v1 77 | ``` 78 | 79 | 5. Start the application: 80 | 81 | ```bash 82 | yarn start 83 | ``` 84 | 85 | 6. Access the application at `http://localhost:8080`. 86 | 87 | ## Usage 88 | 89 | ### User Registration 90 | 91 | 1. Visit the application's URL. 92 | 93 | 2. Click on the "Sign Up" or "Register" button to create a new user account. 94 | 95 | 3. Fill out the registration form with your details. 96 | 97 | 4. Once registered, you can log in with your credentials. 98 | 99 | ### Creating Channels 100 | 101 | 1. After logging in, you can create a new channel by clicking on the "Create Channel" button. 102 | 103 | 2. Choose a name and description for your channel. 104 | 105 | 3. Invite team members to join the channel. 106 | 107 | ### Sending Messages 108 | 109 | 1. To send a message in a channel, click on the channel's name in the left sidebar. 110 | 111 | 2. Type your message in the input field at the bottom of the chat window and press Enter to send. 112 | 113 | 3. You can also send direct messages to other users by clicking on their name in the user list. 114 | 115 | ### Message Replies (Threads) 116 | 117 | 1. To start a thread in response to a specific message: 118 | 119 | - Hover over the message you want to reply to. 120 | 121 | - Click on the "Reply" or "Start Thread" button. 122 | 123 | - Type your reply in the thread and send it. 124 | 125 | 2. Keep discussions organized by using threads to respond to messages. 126 | 127 | ### Huddle with Other Users 128 | 129 | 1. To create a private huddle: 130 | 131 | - Click on the "Huddle" button in the sidebar. 132 | 133 | - Select the users you want to include in the huddle. 134 | 135 | - Start your private conversation. 136 | 137 | 2. Huddles provide a secure environment for group discussions with select team members. 138 | 139 | ## Contributing 140 | 141 | We welcome contributions from the community. 142 | 143 | ## License 144 | 145 | This project is licensed under the [MIT License](LICENSE). 146 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slack-clone", 3 | "version": "1.0.0", 4 | "description": "an even better version of slack", 5 | "main": "index.ts", 6 | "author": "adeola adeoti", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "nodemon --exec ts-node src/index.ts", 10 | "build": "tsc -p .", 11 | "lint": "tslint -c tslint.json -p tsconfig.json --fix" 12 | }, 13 | "dependencies": { 14 | "@types/socket.io": "^3.0.2", 15 | "body-parser": "^1.20.2", 16 | "cookie-parser": "^1.4.6", 17 | "cookie-session": "^2.0.0", 18 | "cors": "^2.8.5", 19 | "dotenv": "^16.1.4", 20 | "express": "^4.18.2", 21 | "express-rate-limit": "^6.7.0", 22 | "express-session": "^1.17.3", 23 | "helmet": "^7.0.0", 24 | "hpp": "^0.2.3", 25 | "jsonwebtoken": "^9.0.0", 26 | "mongodb": "^5.6.0", 27 | "mongoose": "^7.2.4", 28 | "morgan": "^1.10.0", 29 | "nodemailer": "^6.9.3", 30 | "passport": "0.5", 31 | "passport-google-oauth20": "^2.0.0", 32 | "randomatic": "^3.1.1", 33 | "socket.io": "^4.7.2", 34 | "ts-node": "^10.9.1", 35 | "tslib": "^2.5.3", 36 | "uuid": "^9.0.0", 37 | "xss-clean": "^0.1.4" 38 | }, 39 | "devDependencies": { 40 | "nodemon": "^3.0.1", 41 | "typescript": "^5.1.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/config/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | export default async function connectDB() { 4 | try { 5 | const conn = await mongoose.connect(process.env.MONGODB_URI, { 6 | dbName: process.env.MONGODB_DB_NAME, 7 | }) 8 | console.log(`MongoDB connected: ${conn.connection.host}`) 9 | } catch (error) { 10 | console.error(`Error: ${(error as Error).message}`) 11 | process.exit(1) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/controllers/auth.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express' 2 | import User from '../models/user' 3 | import sendEmail from '../helpers/sendEmail' 4 | import crypto from 'crypto' 5 | import { verificationHtml } from '../html/confirmation-code-email' 6 | import passport from 'passport' 7 | import { Strategy as GoogleStrategy } from 'passport-google-oauth20' 8 | 9 | // Configure Google OAuth strategy 10 | passport.use( 11 | new GoogleStrategy( 12 | { 13 | clientID: process.env.GOOGLE_CLIENT_ID, 14 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 15 | callbackURL: `${process.env.API_URL}/auth/google/callback`, 16 | scope: ['profile', 'email'], 17 | }, 18 | async (accessToken, refreshToken, profile, done) => { 19 | try { 20 | // Find or create a user based on the Google profile information 21 | let user = await User.findOne({ googleId: profile.id }) 22 | if (!user) { 23 | user = new User({ 24 | googleId: profile.id, 25 | username: profile.displayName, 26 | email: profile.emails[0].value, 27 | }) 28 | await user.save() 29 | } 30 | 31 | return done(null, user) 32 | } catch (error) { 33 | return done(error, false) 34 | } 35 | } 36 | ) 37 | ) 38 | 39 | passport.serializeUser((user, done) => { 40 | done(null, user) 41 | }) 42 | 43 | passport.deserializeUser((user, done) => { 44 | done(null, user) 45 | }) 46 | 47 | // @desc Register user 48 | // @route POST /api/v1/auth/register 49 | // @access Public 50 | export const register = async ( 51 | req: Request, 52 | res: Response, 53 | next: NextFunction 54 | ) => { 55 | const { username, email } = req.body 56 | 57 | if (!email) { 58 | return res.status(400).json({ 59 | success: false, 60 | data: { 61 | name: 'Please provide your email address', 62 | }, 63 | }) 64 | } 65 | 66 | const emailExist = await User.findOne({ email }) 67 | 68 | if (emailExist) { 69 | return res.status(400).json({ 70 | success: false, 71 | data: { 72 | name: 'User already exists', 73 | }, 74 | }) 75 | } 76 | 77 | const user = await User.create({ 78 | username, 79 | email, 80 | }) 81 | try { 82 | const verificationToken = user.getVerificationCode() 83 | await user.save() 84 | sendEmail( 85 | email, 86 | 'Slack confirmation code', 87 | verificationHtml(verificationToken) 88 | ) 89 | 90 | res.status(201).json({ 91 | success: true, 92 | data: { 93 | name: 'Verification token sent to email', 94 | }, 95 | }) 96 | } catch (err) { 97 | user.loginVerificationCode = undefined 98 | user.loginVerificationCodeExpires = undefined 99 | await user.save({ validateBeforeSave: false }) 100 | next(err) 101 | } 102 | } 103 | 104 | // @desc Signin user 105 | // @route POST /api/v1/auth/signin 106 | // @access Public 107 | export const signin = async ( 108 | req: Request, 109 | res: Response, 110 | next: NextFunction 111 | ) => { 112 | const { email } = req.body 113 | 114 | if (!email) { 115 | return res.status(400).json({ 116 | success: false, 117 | data: { 118 | name: 'Please provide your email address', 119 | }, 120 | }) 121 | } 122 | 123 | const user = await User.findOne({ email }) 124 | 125 | if (!user) { 126 | return res.status(400).json({ 127 | success: false, 128 | data: { 129 | name: "User doesn't exist", 130 | }, 131 | }) 132 | } 133 | 134 | try { 135 | const verificationToken = user.getVerificationCode() 136 | await user.save() 137 | 138 | sendEmail( 139 | email, 140 | 'Slack confirmation code', 141 | verificationHtml(verificationToken) 142 | ) 143 | 144 | res.status(201).json({ 145 | success: true, 146 | data: { 147 | name: 'Verification token sent to email', 148 | }, 149 | }) 150 | } catch (err) { 151 | user.loginVerificationCode = undefined 152 | user.loginVerificationCodeExpires = undefined 153 | await user.save({ validateBeforeSave: false }) 154 | next(err) 155 | } 156 | } 157 | 158 | // @desc Verify user 159 | // @route POST /api/v1/auth/verify 160 | // @access Public 161 | export const verify = async ( 162 | req: Request, 163 | res: Response, 164 | next: NextFunction 165 | ) => { 166 | try { 167 | if (!req.body.loginVerificationCode) { 168 | return res.status(400).json({ 169 | success: false, 170 | data: { 171 | name: 'Please provide verification token', 172 | }, 173 | }) 174 | } 175 | // Get hashed token 176 | const loginVerificationCode = crypto 177 | .createHash('sha256') 178 | .update(req.body.loginVerificationCode) 179 | .digest('hex') 180 | 181 | const user = await User.findOne({ 182 | loginVerificationCode, 183 | loginVerificationCodeExpires: { $gt: Date.now() }, 184 | }) 185 | 186 | if (!user) { 187 | return res.status(400).json({ 188 | success: false, 189 | data: { 190 | name: 'Invalid verification token', 191 | }, 192 | }) 193 | } 194 | 195 | res.status(200).json({ 196 | success: true, 197 | data: { 198 | username: user.username, 199 | email: user.email, 200 | token: user.getSignedJwtToken(), 201 | }, 202 | }) 203 | 204 | user.loginVerificationCode = undefined 205 | user.loginVerificationCodeExpires = undefined 206 | await user.save({ validateBeforeSave: false }) 207 | } catch (err) { 208 | next(err) 209 | } 210 | } 211 | 212 | // @desc Google OAuth Callback 213 | // @route GET /auth/google/callback 214 | // @access Public 215 | export const googleCallback = async ( 216 | req: Request, 217 | res: Response, 218 | next: NextFunction 219 | ) => { 220 | passport.authenticate('google', (err, user) => { 221 | if (err) { 222 | // Handle error 223 | return next(err) 224 | } 225 | 226 | if (!user) { 227 | // Handle user not found 228 | return res.status(401).json({ message: 'Authentication failed' }) 229 | } 230 | 231 | const token = user.getSignedJwtToken() 232 | 233 | res.redirect( 234 | `${process.env.CLIENT_URL}?token=${token}&email=${user.email}&username=${user.username}` 235 | ) 236 | })(req, res, next) 237 | } 238 | -------------------------------------------------------------------------------- /src/controllers/channel.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express' 2 | import Channel from '../models/channel' 3 | import successResponse from '../helpers/successResponse' 4 | 5 | // @desc create channel 6 | // @route POST /api/v1/channel/create 7 | // @access Private 8 | export async function createChannel( 9 | req: Request, 10 | res: Response, 11 | next: NextFunction 12 | ) { 13 | try { 14 | const { name, organisationId } = req.body 15 | const channel = await Channel.create({ 16 | name, 17 | collaborators: [req.user.id], 18 | organisation: organisationId, 19 | }) 20 | successResponse(res, channel) 21 | } catch (error) { 22 | next(error) 23 | } 24 | } 25 | 26 | export async function getChannels( 27 | req: Request, 28 | res: Response, 29 | next: NextFunction 30 | ) { 31 | try { 32 | const id = req.params.id 33 | const channels = await Channel.find({ organisation: id }) 34 | .populate({ 35 | path: 'organisation', 36 | populate: [{ path: 'owner' }, { path: 'coWorkers' }], 37 | }) 38 | .populate('collaborators') 39 | .sort({ _id: -1 }) 40 | 41 | successResponse(res, channels) 42 | } catch (error) { 43 | next(error) 44 | } 45 | } 46 | 47 | export async function getChannel( 48 | req: Request, 49 | res: Response, 50 | next: NextFunction 51 | ) { 52 | try { 53 | const id = req.params.id 54 | const channel = await Channel.findById(id) 55 | .populate('collaborators') 56 | .sort({ _id: -1 }) 57 | 58 | if (!channel) { 59 | return res.status(400).json({ 60 | name: 'not found', 61 | }) 62 | } 63 | 64 | const updatedChannel = { 65 | ...channel.toObject(), 66 | isChannel: true, 67 | } 68 | 69 | successResponse(res, updatedChannel) 70 | } catch (error) { 71 | next(error) 72 | } 73 | } 74 | 75 | export async function updateChannel( 76 | req: Request, 77 | res: Response, 78 | next: NextFunction 79 | ) { 80 | try { 81 | const id = req.params.id 82 | const channel = await Channel.findById(id) 83 | if (!channel) { 84 | return res.status(400).json({ 85 | name: 'not found', 86 | }) 87 | } 88 | 89 | const updatedChannel = await Channel.findByIdAndUpdate( 90 | id, 91 | { $addToSet: { collaborators: req.body.userId } }, 92 | { 93 | new: true, 94 | } 95 | ) 96 | 97 | successResponse(res, updatedChannel) 98 | } catch (error) { 99 | next(error) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/controllers/conversations.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express' 2 | import Conversations from '../models/conversation' 3 | import successResponse from '../helpers/successResponse' 4 | import { UserSchemaType } from 'src/models/user' 5 | 6 | export async function getConversations( 7 | req: Request, 8 | res: Response, 9 | next: NextFunction 10 | ) { 11 | try { 12 | const { id } = req.body 13 | const conversations = await Conversations.find({ organisation: id }).sort({ 14 | _id: -1, 15 | }) 16 | successResponse(res, conversations) 17 | } catch (error) { 18 | next(error) 19 | } 20 | } 21 | 22 | export async function getConversation( 23 | req: Request, 24 | res: Response, 25 | next: NextFunction 26 | ) { 27 | try { 28 | const id = req.params.id 29 | const conversation = await Conversations.findById(id) 30 | .populate('collaborators') 31 | .sort({ _id: -1 }) 32 | 33 | if (!conversation) { 34 | return res.status(400).json({ 35 | name: 'not found', 36 | }) 37 | } 38 | 39 | // const conversations = await Conversations.findById(id).populate( 40 | // 'collaborators' 41 | // ) 42 | const collaborators = [...conversation.collaborators] 43 | // Find the index of the collaborator with the current user's ID 44 | const currentUserIndex = conversation.collaborators.findIndex( 45 | (coworker: UserSchemaType) => coworker._id.toString() === req.user.id 46 | ) 47 | 48 | // // Remove the current user collaborator from the array 49 | conversation.collaborators.splice(currentUserIndex, 1) 50 | 51 | // // Create the name field based on the other collaborator's username 52 | const name = conversation.collaborators[0]?.username || conversation.name 53 | 54 | successResponse(res, { 55 | ...conversation.toObject(), 56 | name, 57 | collaborators, 58 | }) 59 | } catch (error) { 60 | next(error) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/controllers/message.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express' 2 | import Message from '../models/message' 3 | import successResponse from '../helpers/successResponse' 4 | 5 | // @desc get messages 6 | // @route POST /api/v1/message 7 | // @access Private 8 | export async function getMessages( 9 | req: Request, 10 | res: Response, 11 | next: NextFunction 12 | ) { 13 | try { 14 | const channelId = req.query.channelId 15 | const conversationId = req.query.conversation 16 | const isSelf = req.query.isSelf 17 | const organisation = req.query.organisation 18 | 19 | if (channelId) { 20 | const channel = await Message.find({ 21 | channel: channelId, 22 | organisation, 23 | }).populate(['sender', 'reactions.reactedToBy', 'threadReplies']) 24 | successResponse(res, channel) 25 | } else if (conversationId) { 26 | let conversation 27 | 28 | if (isSelf) { 29 | conversation = await Message.find({ 30 | organisation, 31 | conversation: conversationId, 32 | isSelf, 33 | }).populate(['sender', 'reactions.reactedToBy', 'threadReplies']) 34 | } else { 35 | conversation = await Message.find({ 36 | organisation, 37 | conversation: conversationId, 38 | }).populate(['sender', 'reactions.reactedToBy', 'threadReplies']) 39 | } 40 | successResponse(res, conversation) 41 | } else { 42 | res.status(400).json({}) 43 | } 44 | } catch (error) { 45 | next(error) 46 | } 47 | } 48 | 49 | // @desc get message 50 | // @route POST /api/v1/message 51 | // @access Private 52 | export async function getMessage( 53 | req: Request, 54 | res: Response, 55 | next: NextFunction 56 | ) { 57 | try { 58 | const id = req.params.id 59 | 60 | if (id) { 61 | const message = await Message.findById(id).populate([ 62 | 'sender', 63 | 'threadReplies', 64 | 'reactions.reactedToBy', 65 | ]) 66 | successResponse(res, message) 67 | } else { 68 | res.status(400).json({ 69 | name: 'message not found', 70 | }) 71 | } 72 | } catch (error) { 73 | next(error) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/controllers/organisation.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express' 2 | import Organisation from '../models/organisation' 3 | import successResponse from '../helpers/successResponse' 4 | import Channel from '../models/channel' 5 | import Conversations from '../models/conversation' 6 | import { UserSchemaType } from 'src/models/user' 7 | 8 | // @desc get organisation 9 | // @route GET /api/v1/organisation/:id 10 | // @access Private 11 | export async function getOrganisation( 12 | req: Request, 13 | res: Response, 14 | next: NextFunction 15 | ) { 16 | try { 17 | const id = req.params.id 18 | if (id) { 19 | let organisation = await Organisation.findById(id).populate([ 20 | 'coWorkers', 21 | 'owner', 22 | ]) 23 | 24 | if (!organisation) { 25 | res.status(400, { 26 | name: 'no organisation found', 27 | }) 28 | } 29 | 30 | const channels = await Channel.find({ 31 | organisation: id, 32 | }).populate('collaborators') 33 | 34 | const conversations = await Conversations.find({ 35 | organisation: id, 36 | }).populate('collaborators') 37 | 38 | const conversationsWithCurrentUser = conversations.filter( 39 | (conversation) => 40 | conversation.collaborators.some( 41 | (collaborator: UserSchemaType) => 42 | collaborator._id.toString() === req.user.id 43 | ) 44 | ) 45 | 46 | const updatedConversations = conversationsWithCurrentUser.map((convo) => { 47 | // Find the index of the collaborator with the current user's ID 48 | const currentUserIndex = convo.collaborators.findIndex( 49 | (coworker: UserSchemaType) => coworker._id.toString() === req.user.id 50 | ) 51 | 52 | const collaborators = [...convo.collaborators] 53 | 54 | // Remove the current user collaborator from the array 55 | convo.collaborators.splice(currentUserIndex, 1) 56 | 57 | // Create the name field based on the other collaborator's username 58 | const name = convo.collaborators[0]?.username || convo.name 59 | 60 | return { 61 | ...convo.toObject(), 62 | name, 63 | createdBy: convo.collaborators[0]?._id, 64 | collaborators, 65 | } 66 | }) 67 | 68 | // Check if the authenticated user is a co-worker of the organisation 69 | const currentUserIsCoWorker = organisation.coWorkers.some( 70 | (coworker: UserSchemaType) => coworker._id.toString() === req.user.id 71 | ) 72 | 73 | // Replace the profile object with the corresponding co-worker's values 74 | let profile: UserSchemaType 75 | if (currentUserIsCoWorker) { 76 | const currentUser = organisation.coWorkers.find( 77 | (coworker: UserSchemaType) => coworker._id.toString() === req.user.id 78 | ) 79 | profile = currentUser 80 | } 81 | 82 | // Update the coWorkers array in the organisation object 83 | const updatedOrganisation = { 84 | ...organisation.toObject(), 85 | conversations: updatedConversations, 86 | channels, 87 | profile, 88 | } 89 | 90 | successResponse(res, updatedOrganisation) 91 | } 92 | } catch (error) { 93 | next(error) 94 | } 95 | } 96 | // @desc get organisation 97 | // @route POST /api/v1/organisation 98 | // @access Private 99 | export async function createOrganisation( 100 | req: Request, 101 | res: Response, 102 | next: NextFunction 103 | ) { 104 | try { 105 | const { name, id } = req.body 106 | 107 | if (!name && !id) { 108 | const organisation = await Organisation.create({ 109 | owner: req.user.id, 110 | coWorkers: [req.user.id], 111 | }) 112 | 113 | successResponse(res, organisation) 114 | } 115 | 116 | if (name && id) { 117 | const organisation = await Organisation.findOneAndUpdate( 118 | { _id: id }, 119 | { $set: { name } }, 120 | { new: true } 121 | ).populate(['coWorkers', 'owner']) 122 | 123 | organisation.generateJoinLink() 124 | await organisation.save() 125 | successResponse(res, organisation) 126 | } 127 | } catch (error) { 128 | next(error) 129 | } 130 | } 131 | 132 | // @desc get organisations associated with an email 133 | // @route POST /api/v1/organisation/workspaces 134 | // @access Private 135 | export async function getWorkspaces( 136 | req: Request, 137 | res: Response, 138 | next: NextFunction 139 | ) { 140 | try { 141 | const id = req.user.id 142 | // Find all organizations where the user is a co-worker 143 | const workspaces = await Organisation.find({ coWorkers: id }) 144 | // Fetch channels for each organization 145 | const workspacesWithChannels = await Promise.all( 146 | workspaces.map(async (workspace) => { 147 | const channels = await Channel.find({ organisation: workspace._id }) 148 | return { 149 | ...workspace.toObject(), 150 | channels, 151 | } 152 | }) 153 | ) 154 | 155 | successResponse(res, workspacesWithChannels) 156 | 157 | // successResponse(res, workspaces); 158 | } catch (error) { 159 | next(error) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/controllers/teammates.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express' 2 | import Organisation, { OrganisationSchemaType } from '../models/organisation' 3 | import Conversation from '../models/conversation' 4 | import successResponse from '../helpers/successResponse' 5 | import Channel, { ChannelSchemaType } from '../models/channel' 6 | import User from '../models/user' 7 | import sendEmail from '../helpers/sendEmail' 8 | import { joinTeammatesEmail } from '../html/join-teammates-email' 9 | 10 | // @desc add teammates to either organisation or a channel 11 | // @route POST /api/v1/teammates 12 | // @access Private 13 | export async function createTeammates( 14 | req: Request, 15 | res: Response, 16 | next: NextFunction 17 | ) { 18 | try { 19 | const { emails, channelId, organisationId, userIds } = req.body 20 | const channelExist = await Channel.findById(channelId) 21 | let organisation: OrganisationSchemaType 22 | 23 | const invitedBy = await User.findById(req.user.id) 24 | 25 | // add existed teammates to channel 26 | if (userIds?.length > 0 && channelExist) { 27 | let channel: ChannelSchemaType 28 | 29 | for (const id of userIds) { 30 | try { 31 | channel = await Channel.findOneAndUpdate( 32 | { _id: channelId }, 33 | { $addToSet: { collaborators: id } }, 34 | { new: true } 35 | ).populate('collaborators') 36 | 37 | const user = await User.findById(id) 38 | 39 | sendEmail( 40 | user.email, 41 | `${invitedBy.email} has invited you to work with them in Slack`, 42 | joinTeammatesEmail( 43 | invitedBy.username, 44 | invitedBy.email, 45 | organisation.name, 46 | req.user.id, 47 | organisation.joinLink, 48 | organisation.url 49 | ) 50 | ) 51 | } catch (error) { 52 | next(error) 53 | } 54 | } 55 | successResponse(res, channel) 56 | } else if (channelId && channelExist) { 57 | // add new teammates to channel 58 | let channel: ChannelSchemaType 59 | 60 | for (const email of emails) { 61 | try { 62 | const newUser = await User.create({ email }) 63 | channel = await Channel.findOneAndUpdate( 64 | { _id: channelId }, 65 | { $push: { collaborators: newUser._id } }, 66 | { new: true } 67 | ).populate('collaborators') 68 | await Organisation.findOneAndUpdate( 69 | { _id: organisationId }, 70 | { $push: { coWorkers: newUser._id } } 71 | ) 72 | // send email to the ids 73 | sendEmail( 74 | email, 75 | `${invitedBy.email} has invited you to work with them in Slack`, 76 | joinTeammatesEmail( 77 | invitedBy.username, 78 | invitedBy.email, 79 | organisation.name, 80 | req.user.id, 81 | organisation.joinLink, 82 | organisation.url 83 | ) 84 | ) 85 | } catch (error) { 86 | next(error) 87 | } 88 | } 89 | successResponse(res, channel) 90 | } else if (organisationId) { 91 | // add new teammates to organisation 92 | const organisationExist = await Organisation.findById(organisationId) 93 | 94 | if (organisationExist) { 95 | for (const email of emails) { 96 | try { 97 | const existingUser = await User.findOne({ email }) 98 | 99 | if (existingUser) { 100 | // Check if existingUser is not part of coWorkers field before pushing 101 | const isUserAlreadyInCoWorkers = 102 | organisationExist.coWorkers.includes(existingUser._id) 103 | 104 | if (!isUserAlreadyInCoWorkers) { 105 | organisation = await Organisation.findOneAndUpdate( 106 | { _id: organisationId }, 107 | { 108 | $addToSet: { coWorkers: existingUser._id }, 109 | }, 110 | { new: true } 111 | ).populate(['coWorkers', 'owner']) 112 | } 113 | } else { 114 | const newUser = await User.create({ email }) 115 | organisation = await Organisation.findOneAndUpdate( 116 | { _id: organisationId }, 117 | { 118 | $push: { 119 | coWorkers: newUser._id, 120 | }, 121 | }, 122 | { new: true } 123 | ).populate(['coWorkers', 'owner']) 124 | } 125 | 126 | // vibe and inshallah 127 | sendEmail( 128 | email, 129 | `${invitedBy.email} has invited you to work with them in Slack`, 130 | joinTeammatesEmail( 131 | invitedBy.username, 132 | invitedBy.email, 133 | organisation.name, 134 | req.user.id, 135 | organisation.joinLink, 136 | organisation.url 137 | ) 138 | ) 139 | } catch (error) { 140 | next(error) 141 | } 142 | } 143 | 144 | if (!channelId) { 145 | successResponse(res, organisation) 146 | } 147 | 148 | // Create separate conversations for each unique pair of coWorkers 149 | for (let i = 0; i < organisation.coWorkers.length; i++) { 150 | for (let j = i + 1; j < organisation.coWorkers.length; j++) { 151 | // Check if a conversation with these collaborators already exists 152 | const existingConversation = await Conversation.findOne({ 153 | collaborators: { 154 | $all: [ 155 | organisation.coWorkers[i]._id, 156 | organisation.coWorkers[j]._id, 157 | ], 158 | }, 159 | organisation: organisationId, 160 | }) 161 | 162 | // If no conversation exists, create a new one 163 | if (!existingConversation) { 164 | await Conversation.create({ 165 | name: `${organisation.coWorkers[i].username}, ${organisation.coWorkers[j].username}`, 166 | description: `This conversation is between ${organisation.coWorkers[i].username} and ${organisation.coWorkers[j].username}`, 167 | organisation: organisationId, 168 | isSelf: 169 | organisation.coWorkers[i]._id === 170 | organisation.coWorkers[j]._id, 171 | collaborators: [ 172 | organisation.coWorkers[i]._id, 173 | organisation.coWorkers[j]._id, 174 | ], 175 | }) 176 | } 177 | } 178 | } 179 | 180 | // Create self-conversations for each coworker 181 | for (let i = 0; i < organisation.coWorkers.length; i++) { 182 | const selfConversationExists = await Conversation.findOne({ 183 | collaborators: { 184 | $all: [organisation.coWorkers[i]._id], 185 | }, 186 | organisation: organisationId, 187 | isSelf: true, 188 | }) 189 | 190 | // If no self-conversation exists, create one 191 | if (!selfConversationExists) { 192 | await Conversation.create({ 193 | name: `${organisation.coWorkers[i].username}`, 194 | description: `This is a conversation with oneself (${organisation.coWorkers[i].username}).`, 195 | organisation: organisationId, 196 | isSelf: true, 197 | collaborators: [organisation.coWorkers[i]._id], 198 | }) 199 | } 200 | } 201 | } 202 | } 203 | } catch (error) { 204 | next(error) 205 | } 206 | } 207 | 208 | // @desc get a teammate of an organisation 209 | // @route GET /api/v1/teammates 210 | // @access Private 211 | export async function getTeammate( 212 | req: Request, 213 | res: Response, 214 | next: NextFunction 215 | ) { 216 | try { 217 | const coworkerId = req.params.id 218 | const coworker = await User.findById(coworkerId) 219 | // console.log(coworker); 220 | if (!coworker) { 221 | return res.status(400).json({ 222 | name: 'Coworker not found', 223 | }) 224 | } 225 | 226 | return successResponse(res, coworker) 227 | } catch (error) { 228 | next(error) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/controllers/thread.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express' 2 | import Thread from '../models/thread' 3 | import successResponse from '../helpers/successResponse' 4 | 5 | // @desc get threads 6 | // @route POST /api/v1/threads 7 | // @access Private 8 | export async function getThreads( 9 | req: Request, 10 | res: Response, 11 | next: NextFunction 12 | ) { 13 | try { 14 | const messageId = req.query.message 15 | 16 | if (messageId) { 17 | const threads = await Thread.find({ 18 | message: messageId, 19 | }) 20 | .populate('sender') 21 | .populate('reactions.reactedToBy') 22 | successResponse(res, threads) 23 | } else { 24 | res.status(400).json({}) 25 | } 26 | } catch (error) { 27 | next(error) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/helpers/createTodaysFirstMessage.ts: -------------------------------------------------------------------------------- 1 | import Message from '../models/message' 2 | 3 | function formatDate(date) { 4 | const options = { weekday: 'long', month: 'long', day: 'numeric' } 5 | const formattedDate = date.toLocaleDateString('en-US', options) 6 | return formattedDate.charAt(0).toUpperCase() + formattedDate.slice(1) // Capitalize the first letter 7 | } 8 | 9 | interface CreateTodaysFirstMessage { 10 | channelId?: string 11 | conversationId?: string 12 | organisation: string 13 | } 14 | 15 | export default async function createTodaysFirstMessage({ 16 | channelId, 17 | conversationId, 18 | organisation, 19 | }: CreateTodaysFirstMessage) { 20 | try { 21 | // Check if there are any messages for today in the channel 22 | const today = new Date() 23 | today.setHours(0, 0, 0, 0) 24 | let existingMessages 25 | 26 | if (channelId) { 27 | existingMessages = await Message.find({ 28 | channel: channelId, 29 | createdAt: { $gte: today }, 30 | }) 31 | } else if (conversationId) { 32 | existingMessages = await Message.find({ 33 | conversation: conversationId, 34 | createdAt: { $gte: today }, 35 | }) 36 | } 37 | 38 | if (existingMessages.length === 0) { 39 | await Message.create({ 40 | organisation, 41 | content: formatDate(today), 42 | ...(channelId 43 | ? { channel: channelId } 44 | : { conversation: conversationId }), 45 | hasRead: false, 46 | type: 'date', 47 | }) 48 | } 49 | } catch (error) {} 50 | } 51 | -------------------------------------------------------------------------------- /src/helpers/sendEmail.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer' 2 | 3 | export default async function sendEmail( 4 | to: string, 5 | subject: string, 6 | html?: string, 7 | text?: string 8 | ) { 9 | try { 10 | const transporter = nodemailer.createTransport({ 11 | service: 'hotmail', 12 | auth: { 13 | user: process.env.SMTP_USERNAME, 14 | pass: process.env.SMTP_PASSWORD, 15 | }, 16 | }) 17 | 18 | await transporter.sendMail({ 19 | from: process.env.SMTP_USERNAME, 20 | to, 21 | subject, 22 | html, 23 | text, 24 | }) 25 | } catch (error) { 26 | console.log(error) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/helpers/successResponse.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express' 2 | 3 | export default function successResponse(res: Response, data) { 4 | res.status(201).json({ 5 | data, 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /src/helpers/updateConversationStatus.ts: -------------------------------------------------------------------------------- 1 | import Conversations from '../models/conversation' 2 | import User from '../models/user' 3 | 4 | async function updateUserStatus(id, isOnline) { 5 | try { 6 | // Update the user's "isOnline" field 7 | const updatedUser = await User.findByIdAndUpdate( 8 | id, 9 | { isOnline }, 10 | { new: true } 11 | ) 12 | return updatedUser 13 | } catch (error) { 14 | console.error('Error updating user status:', error) 15 | throw error 16 | } 17 | } 18 | 19 | export default async function updateConversationStatus(id, isOnline) { 20 | try { 21 | await updateUserStatus(id, isOnline) 22 | 23 | // Find all conversations where the user is part of the collaborators 24 | const conversations = await Conversations.find({ 25 | collaborators: { $in: id }, 26 | }).populate('collaborators') 27 | 28 | // Check if all collaborators in each conversation are online 29 | for (const conversation of conversations) { 30 | let allCollaboratorsOnline = true 31 | 32 | for (const collaborator of conversation.collaborators) { 33 | const user = await User.findById(collaborator._id) 34 | if (!(user && user.isOnline)) { 35 | allCollaboratorsOnline = false 36 | break // No need to check further if one is offline 37 | } 38 | } 39 | 40 | if (allCollaboratorsOnline) { 41 | // Update the Conversation model's "isOnline" field to true 42 | await Conversations.findByIdAndUpdate( 43 | conversation._id, 44 | { isOnline: true }, 45 | { new: true } 46 | ) 47 | } else { 48 | await Conversations.findByIdAndUpdate( 49 | conversation._id, 50 | { isOnline: false }, 51 | { new: true } 52 | ) 53 | } 54 | } 55 | } catch (error) { 56 | // Handle any errors here 57 | console.error('Error updating conversation status:', error) 58 | throw error 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/html/confirmation-code-email.ts: -------------------------------------------------------------------------------- 1 | export function verificationHtml(code: string) { 2 | return `Slack confirmation code: ${code}
Confirm your email address. Your confirmation code is below — enter it in your open browser window and we'll help you get signed in.
slack logo

Confirm your email address

Your confirmation code is below — enter it in your open browser window and we'll help you get signed in.

${code}

If you didn’t request this email, there’s nothing to worry about — you can safely ignore it.

452 | ` 453 | } 454 | -------------------------------------------------------------------------------- /src/html/join-teammates-email.ts: -------------------------------------------------------------------------------- 1 | export function joinTeammatesEmail( 2 | name: string, 3 | email: string, 4 | organisationName: string, 5 | invitedBy: string, 6 | organisationLink: string, 7 | joinLink: string 8 | ) { 9 | return `${name} has invited you to work with them in Slack
Join your team on Slack. ${name} (${email}) has invited you to use Slack with them, in a workspace called ${organisationName}.
slack logo

Join your team on Slack

${name} (${email}) has invited you to use Slack with them, in a workspace called ${organisationName}.

M

${organisationName}

Join Now
Join the conversation with ${invitedBy}.

Once you join, you can always access the ${organisationName} workspace at ${organisationLink}

What is Slack?

Slack is a messaging app for teams, a place you can collaborate on projects and organize conversations — so you can work together, no matter where you are.

491 | ` 492 | } 493 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import dotenv from 'dotenv' 3 | import connectDB from './config/db' 4 | dotenv.config() 5 | import auth from './routes/auth' 6 | import cookieParser from 'cookie-parser' 7 | import morgan from 'morgan' 8 | import helmet from 'helmet' 9 | import xss from 'xss-clean' 10 | import rateLimit from 'express-rate-limit' 11 | import hpp from 'hpp' 12 | import cors from 'cors' 13 | import channel from './routes/channel' 14 | import message from './routes/message' 15 | import thread from './routes/thread' 16 | import teammates from './routes/teammates' 17 | import organisation from './routes/organisation' 18 | import errorResponse from './middleware/errorResponse' 19 | import Message from '../src/models/message' 20 | import Channels from '../src/models/channel' 21 | import Conversations from './models/conversation' 22 | import conversations from './routes/conversations' 23 | import { Server } from 'socket.io' 24 | import http from 'http' 25 | import updateConversationStatus from './helpers/updateConversationStatus' 26 | import Thread from './models/thread' 27 | import createTodaysFirstMessage from './helpers/createTodaysFirstMessage' 28 | import passport from 'passport' 29 | import cookieSession from 'cookie-session' 30 | 31 | const app = express() 32 | const server = http.createServer(app) 33 | const io = new Server(server, { 34 | cors: { 35 | origin: process.env.CLIENT_URL, 36 | methods: ['GET', 'POST'], 37 | }, 38 | }) 39 | 40 | // Connect to MongoDB 41 | connectDB() 42 | 43 | // Express configuration 44 | app.use( 45 | cookieSession({ 46 | name: 'session', 47 | keys: ['cyberwolve'], 48 | maxAge: 24 * 60 * 60 * 100, 49 | }) 50 | ) 51 | 52 | app.use(passport.initialize()) 53 | app.use(passport.session()) 54 | 55 | app.use(express.json()) 56 | app.use(express.urlencoded({ extended: true })) 57 | 58 | // cookie-parser configuration 59 | app.use(cookieParser()) 60 | 61 | // Dev logging middleware 62 | if (process.env.NODE_ENV === 'development') { 63 | app.use(morgan('dev')) 64 | } 65 | 66 | // Set security headers 67 | app.use(helmet()) 68 | 69 | // Prevent XSS attacks 70 | app.use(xss()) 71 | 72 | // Rate limiting 73 | const limiter = rateLimit({ 74 | windowMs: 10 * 60 * 1000, // 10 mins 75 | max: 1000, 76 | }) 77 | app.use(limiter) 78 | 79 | // Prevent http param pollution 80 | app.use(hpp()) 81 | 82 | // Enable CORS 83 | app.use(cors()) 84 | 85 | // Store users' sockets by their user IDs 86 | const users = {} 87 | 88 | // Set up WebSocket connections 89 | io.on('connection', (socket) => { 90 | socket.on('user-join', async ({ id, isOnline }) => { 91 | socket.join(id) 92 | await updateConversationStatus(id, isOnline) 93 | io.emit('user-join', { id, isOnline }) 94 | }) 95 | 96 | socket.on('user-leave', async ({ id, isOnline }) => { 97 | socket.leave(id) 98 | await updateConversationStatus(id, isOnline) 99 | io.emit('user-leave', { id, isOnline }) 100 | }) 101 | 102 | socket.on('channel-open', async ({ id, userId }) => { 103 | if (id) { 104 | socket.join(id) 105 | const updatedChannel = await Channels.findByIdAndUpdate( 106 | id, 107 | { $pull: { hasNotOpen: userId } }, 108 | { new: true } 109 | ) 110 | io.to(id).emit('channel-updated', updatedChannel) 111 | } 112 | }) 113 | socket.on('convo-open', async ({ id, userId }) => { 114 | if (id) { 115 | socket.join(id) 116 | const updatedConversation = await Conversations.findByIdAndUpdate( 117 | id, 118 | { $pull: { hasNotOpen: userId } }, 119 | { new: true } 120 | ) 121 | io.to(id).emit('convo-updated', updatedConversation) 122 | } 123 | }) 124 | 125 | socket.on('thread-message', async ({ userId, messageId, message }) => { 126 | try { 127 | socket.join(messageId) 128 | let newMessage = await Thread.create({ 129 | sender: message.sender, 130 | content: message.content, 131 | message: messageId, 132 | hasRead: false, 133 | }) 134 | newMessage = await newMessage.populate('sender') 135 | io.to(messageId).emit('thread-message', { newMessage }) 136 | const updatedMessage = await Message.findByIdAndUpdate( 137 | messageId, 138 | { 139 | threadLastReplyDate: newMessage.createdAt, 140 | $addToSet: { threadReplies: userId }, 141 | $inc: { threadRepliesCount: 1 }, 142 | }, 143 | { new: true } 144 | ).populate(['threadReplies', 'sender', 'reactions.reactedToBy']) 145 | 146 | io.to(messageId).emit('message-updated', { 147 | id: messageId, 148 | message: updatedMessage, 149 | }) 150 | 151 | // socket.emit("message-updated", { messageId, message: updatedMessage }); 152 | } catch (error) { 153 | console.log(error) 154 | } 155 | }) 156 | 157 | socket.on( 158 | 'message', 159 | async ({ 160 | channelId, 161 | channelName, 162 | conversationId, 163 | collaborators, 164 | isSelf, 165 | message, 166 | organisation, 167 | hasNotOpen, 168 | }) => { 169 | try { 170 | if (channelId) { 171 | socket.join(channelId) 172 | // Check if there are any messages for today in the channel 173 | await createTodaysFirstMessage({ channelId, organisation }) 174 | 175 | let newMessage = await Message.create({ 176 | organisation, 177 | sender: message.sender, 178 | content: message.content, 179 | channel: channelId, 180 | hasRead: false, 181 | }) 182 | 183 | newMessage = await newMessage.populate('sender') 184 | io.to(channelId).emit('message', { newMessage, organisation }) 185 | 186 | const updatedChannel = await Channels.findByIdAndUpdate( 187 | channelId, 188 | { hasNotOpen }, 189 | { new: true } 190 | ) 191 | 192 | io.to(channelId).emit('channel-updated', updatedChannel) 193 | socket.broadcast.emit('notification', { 194 | channelName, 195 | channelId, 196 | collaborators, 197 | newMessage, 198 | organisation, 199 | }) 200 | } else if (conversationId) { 201 | socket.join(conversationId) 202 | // Check if there are any messages for today in the channel 203 | await createTodaysFirstMessage({ conversationId, organisation }) 204 | let newMessage = await Message.create({ 205 | organisation, 206 | sender: message.sender, 207 | content: message.content, 208 | conversation: conversationId, 209 | collaborators, 210 | isSelf, 211 | hasRead: false, 212 | }) 213 | newMessage = await newMessage.populate('sender') 214 | 215 | io.to(conversationId).emit('message', { 216 | collaborators, 217 | organisation, 218 | newMessage, 219 | }) 220 | const updatedConversation = await Conversations.findByIdAndUpdate( 221 | conversationId, 222 | { hasNotOpen }, 223 | { new: true } 224 | ) 225 | io.to(conversationId).emit('convo-updated', updatedConversation) 226 | socket.broadcast.emit('notification', { 227 | collaborators, 228 | organisation, 229 | newMessage, 230 | conversationId, 231 | }) 232 | } 233 | } catch (error) { 234 | console.log(error) 235 | } 236 | } 237 | ) 238 | 239 | socket.on('message-view', async (messageId) => { 240 | await Message.findByIdAndUpdate(messageId, { 241 | hasRead: true, 242 | }) 243 | if (message) { 244 | io.emit('message-view', messageId) 245 | } else { 246 | console.log('message not found') 247 | } 248 | }) 249 | 250 | socket.on('reaction', async ({ emoji, id, isThread, userId }) => { 251 | // 1. Message.findbyid(id) 252 | let message 253 | if (isThread) { 254 | message = await Thread.findById(id) 255 | } else { 256 | message = await Message.findById(id) 257 | } 258 | 259 | if (!message) { 260 | // Handle the case where the model with the given id is not found 261 | return 262 | } 263 | // 2. check if emoji already exists in Message.reactions array 264 | if (message.reactions.some((r) => r.emoji === emoji)) { 265 | // 3. if it does, check if userId exists in reactedToBy array 266 | if ( 267 | message.reactions.some( 268 | (r) => 269 | r.emoji === emoji && 270 | r.reactedToBy.some((v) => v.toString() === userId) 271 | ) 272 | ) { 273 | // Find the reaction that matches the emoji and remove userId from its reactedToBy array 274 | const reactionToUpdate = message.reactions.find( 275 | (r) => r.emoji === emoji 276 | ) 277 | if (reactionToUpdate) { 278 | reactionToUpdate.reactedToBy = reactionToUpdate.reactedToBy.filter( 279 | (v) => v.toString() !== userId 280 | ) 281 | 282 | // If reactedToBy array is empty after removing userId, remove the reaction object 283 | if (reactionToUpdate.reactedToBy.length === 0) { 284 | message.reactions = message.reactions.filter( 285 | (r) => r !== reactionToUpdate 286 | ) 287 | } 288 | // await message.populate([ 289 | // "reactions.reactedToBy", 290 | // "sender", 291 | // // "threadReplies", 292 | // ]); 293 | if (isThread) { 294 | await message.populate(['reactions.reactedToBy', 'sender']) 295 | } else { 296 | await message.populate([ 297 | 'reactions.reactedToBy', 298 | 'sender', 299 | 'threadReplies', 300 | ]) 301 | } 302 | socket.emit('message-updated', { id, message, isThread }) 303 | await message.save() 304 | } 305 | } else { 306 | // Find the reaction that matches the emoji and push userId to its reactedToBy array 307 | const reactionToUpdate = message.reactions.find( 308 | (r) => r.emoji === emoji 309 | ) 310 | if (reactionToUpdate) { 311 | reactionToUpdate.reactedToBy.push(userId) 312 | // await message.populate([ 313 | // "reactions.reactedToBy", 314 | // "sender", 315 | // // isThread && "threadReplies", 316 | 317 | // // "threadReplies", 318 | // ]); 319 | if (isThread) { 320 | await message.populate(['reactions.reactedToBy', 'sender']) 321 | } else { 322 | await message.populate([ 323 | 'reactions.reactedToBy', 324 | 'sender', 325 | 'threadReplies', 326 | ]) 327 | } 328 | socket.emit('message-updated', { id, message, isThread }) 329 | await message.save() 330 | } 331 | } 332 | } else { 333 | // 4. if it doesn't exists, create a new reaction like this {emoji, reactedToBy: [userId]} 334 | message.reactions.push({ emoji, reactedToBy: [userId] }) 335 | // await message.populate([ 336 | // "reactions.reactedToBy", 337 | // "sender", 338 | // // isThread && "threadReplies", 339 | // // "threadReplies", 340 | // ]); 341 | if (isThread) { 342 | await message.populate(['reactions.reactedToBy', 'sender']) 343 | } else { 344 | await message.populate([ 345 | 'reactions.reactedToBy', 346 | 'sender', 347 | 'threadReplies', 348 | ]) 349 | } 350 | socket.emit('message-updated', { id, message, isThread }) 351 | await message.save() 352 | } 353 | }) 354 | // Event handler for joining a room 355 | socket.on('join-room', ({ roomId, userId }) => { 356 | // Join the specified room 357 | socket.join(roomId) 358 | // Store the user's socket by their user ID 359 | users[userId] = socket 360 | // Broadcast the "join-room" event to notify other users in the room 361 | socket.to(roomId).emit('join-room', { roomId, otherUserId: userId }) 362 | 363 | console.log(`User ${userId} joined room ${roomId}`) 364 | }) 365 | 366 | // Event handler for sending an SDP offer to another user 367 | socket.on('offer', ({ offer, targetUserId }) => { 368 | // Find the target user's socket by their user ID 369 | const targetSocket = users[targetUserId] 370 | if (targetSocket) { 371 | targetSocket.emit('offer', { offer, senderUserId: targetUserId }) 372 | } 373 | }) 374 | 375 | // Event handler for sending an SDP answer to another user 376 | socket.on('answer', ({ answer, senderUserId }) => { 377 | socket.broadcast.emit('answer', { answer, senderUserId }) 378 | }) 379 | 380 | // Event handler for sending ICE candidates to the appropriate user (the answerer) 381 | socket.on('ice-candidate', ({ candidate, senderUserId }) => { 382 | // Find the target user's socket by their user ID 383 | const targetSocket = users[senderUserId] 384 | if (targetSocket) { 385 | targetSocket.emit('ice-candidate', candidate, senderUserId) 386 | } 387 | }) 388 | 389 | // Event handler for leaving a room 390 | socket.on('room-leave', ({ roomId, userId }) => { 391 | socket.leave(roomId) 392 | // Remove the user's socket from the users object 393 | delete users[userId] 394 | // Broadcast the "room-leave" event to notify other users in the room 395 | socket.to(roomId).emit('room-leave', { roomId, leftUserId: userId }) 396 | console.log(`User ${userId} left room ${roomId}`) 397 | }) 398 | }) 399 | 400 | // Routes 401 | app.use('/api/v1/auth', auth) 402 | app.use('/api/v1/channel', channel) 403 | app.use('/api/v1/messages', message) 404 | app.use('/api/v1/threads', thread) 405 | app.use('/api/v1/teammates', teammates) 406 | app.use('/api/v1/organisation', organisation) 407 | app.use('/api/v1/conversations', conversations) 408 | 409 | // error handler 410 | app.use(errorResponse) 411 | 412 | // Start the server 413 | const port = process.env.PORT || 8080 414 | server.listen(port, () => { 415 | console.log(`Server is running on port ${port}`) 416 | }) 417 | -------------------------------------------------------------------------------- /src/middleware/errorResponse.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express' 2 | 3 | interface CustomError extends Error { 4 | name: string 5 | errors?: Record 6 | path?: string 7 | value?: string 8 | } 9 | 10 | function errorResponse(error: CustomError, res: Response) { 11 | if (error.name === 'ValidationError') { 12 | // Mongoose validation error: Required field missing 13 | const requiredFieldError = {} 14 | for (const field of Object.keys(error.errors)) { 15 | requiredFieldError[field] = error.errors[field].message 16 | } 17 | return res 18 | .status(400) 19 | .json({ name: 'Validation Failed', data: requiredFieldError }) 20 | } else if (error.name === 'CastError') { 21 | // Mongoose cast error (e.g., invalid ObjectId) 22 | res.status(400).json({ name: `Invalid ${error.path}: ${error.value}` }) 23 | } 24 | // Handle other types of errors if needed 25 | console.error(error) 26 | return res.status(500).json({ name: 'Internal Server Error' }) 27 | } 28 | 29 | export default errorResponse 30 | -------------------------------------------------------------------------------- /src/middleware/protect.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | 3 | export const protect = (req, res, next) => { 4 | let token 5 | 6 | if ( 7 | req.headers.authorization && 8 | req.headers.authorization.startsWith('Bearer') 9 | ) { 10 | // Set token from Bearer token in header 11 | token = req.headers.authorization.split(' ')[1] 12 | } 13 | 14 | // Make sure token exists 15 | if (!token) { 16 | return res.status(401).json({ 17 | success: false, 18 | data: { 19 | name: 'Not authorized to access this route', 20 | }, 21 | }) 22 | } 23 | 24 | try { 25 | // Verify token 26 | const decoded = jwt.verify(token, process.env.JWT_SECRET) 27 | req.user = decoded 28 | 29 | next() 30 | } catch (err) { 31 | return res.status(401).json({ 32 | success: false, 33 | data: { 34 | name: 'Not authorized to access this route', 35 | }, 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/models/channel.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | // import { v4 as uuidv4 } from "uuid"; 3 | 4 | export interface ChannelSchemaType { 5 | name: string 6 | collaborators: mongoose.Schema.Types.ObjectId[] 7 | title: string 8 | description: string 9 | organisation: mongoose.Schema.Types.ObjectId 10 | hasNotOpen: mongoose.Schema.Types.ObjectId[] 11 | isChannel: boolean 12 | // conversations: mongoose.Schema.Types.ObjectId[]; 13 | createdAt: Date 14 | updatedAt: Date 15 | } 16 | 17 | const channelSchema = new mongoose.Schema( 18 | { 19 | name: { 20 | type: String, 21 | required: [true, 'Please enter your channel name'], 22 | }, 23 | collaborators: [ 24 | { 25 | type: mongoose.Schema.Types.ObjectId, 26 | ref: 'User', 27 | }, 28 | ], 29 | title: { 30 | type: String, 31 | default() { 32 | return `This is the very first begining of the ${this.name} channel` 33 | }, 34 | }, 35 | description: { 36 | type: String, 37 | }, 38 | organisation: { 39 | type: mongoose.Schema.Types.ObjectId, 40 | ref: 'Organisation', 41 | }, 42 | hasNotOpen: [ 43 | { 44 | type: mongoose.Schema.Types.ObjectId, 45 | ref: 'User', 46 | }, 47 | ], 48 | isChannel: { 49 | type: Boolean, 50 | default: true, 51 | }, 52 | // conversations: [ 53 | // { 54 | // type: mongoose.Schema.Types.ObjectId, 55 | // ref: "Message", 56 | // }, 57 | // ], 58 | }, 59 | { 60 | timestamps: true, 61 | versionKey: false, 62 | } 63 | ) 64 | 65 | export default mongoose.model('Channel', channelSchema) 66 | -------------------------------------------------------------------------------- /src/models/conversation.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { UserSchemaType } from './user' 3 | 4 | export interface ConversationSchemaType { 5 | name: string 6 | collaborators: mongoose.Schema.Types.ObjectId[] & UserSchemaType[] 7 | description: string 8 | isSelf: boolean 9 | organisation: mongoose.Schema.Types.ObjectId 10 | createdBy: mongoose.Schema.Types.ObjectId 11 | hasNotOpen: mongoose.Schema.Types.ObjectId[] 12 | isConversation: boolean 13 | isOnline: boolean 14 | createdAt: Date 15 | updatedAt: Date 16 | } 17 | 18 | const conversationSchema = new mongoose.Schema( 19 | { 20 | name: { 21 | type: String, 22 | default() { 23 | if (this.createdBy) { 24 | return this.createdBy.username 25 | } 26 | return '' 27 | }, 28 | }, 29 | collaborators: [ 30 | { 31 | type: mongoose.Schema.Types.ObjectId, 32 | ref: 'User', 33 | }, 34 | ], 35 | description: { 36 | type: String, 37 | default() { 38 | return `This conversation is just between ${this.name} and you` 39 | }, 40 | }, 41 | isSelf: { 42 | type: Boolean, 43 | default: false, 44 | }, 45 | isConversation: { 46 | type: Boolean, 47 | default: true, 48 | }, 49 | organisation: { 50 | type: mongoose.Schema.Types.ObjectId, 51 | ref: 'Organisation', 52 | }, 53 | createdBy: { 54 | type: mongoose.Schema.Types.ObjectId, 55 | ref: 'User', 56 | }, 57 | isOnline: { 58 | type: Boolean, 59 | default: false, 60 | }, 61 | hasNotOpen: [ 62 | { 63 | type: mongoose.Schema.Types.ObjectId, 64 | ref: 'User', 65 | }, 66 | ], 67 | }, 68 | { 69 | timestamps: true, 70 | versionKey: false, 71 | } 72 | ) 73 | 74 | // Define a compound index on the collaborators field 75 | conversationSchema.index({ collaborators: 1 }) 76 | 77 | export default mongoose.model('Conversation', conversationSchema) 78 | -------------------------------------------------------------------------------- /src/models/message.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | interface MessageSchemaType { 4 | sender: mongoose.Schema.Types.ObjectId 5 | content: string 6 | channel: mongoose.Schema.Types.ObjectId 7 | organisation: mongoose.Schema.Types.ObjectId 8 | conversation: mongoose.Schema.Types.ObjectId 9 | collaborators: mongoose.Schema.Types.ObjectId[] 10 | reactions: { 11 | emoji: string 12 | reactedToBy: [mongoose.Schema.Types.ObjectId] 13 | }[] 14 | threadReplies: mongoose.Schema.Types.ObjectId[] 15 | threadRepliesCount: number 16 | threadLastReplyDate: Date 17 | isBookmarked: boolean 18 | isSelf: boolean 19 | hasRead: boolean 20 | type: string 21 | createdAt: Date 22 | updatedAt: Date 23 | } 24 | 25 | const messageSchema = new mongoose.Schema( 26 | { 27 | sender: { 28 | type: mongoose.Schema.Types.ObjectId, 29 | ref: 'User', 30 | }, 31 | content: String, 32 | channel: { 33 | type: mongoose.Schema.Types.ObjectId, 34 | ref: 'Channel', 35 | }, 36 | organisation: { 37 | type: mongoose.Schema.Types.ObjectId, 38 | ref: 'Organisation', 39 | }, 40 | conversation: { 41 | type: mongoose.Schema.Types.ObjectId, 42 | ref: 'Conversation', 43 | }, 44 | collaborators: [ 45 | { 46 | type: mongoose.Schema.Types.ObjectId, 47 | ref: 'User', 48 | }, 49 | ], 50 | reactions: [ 51 | { 52 | emoji: String, 53 | reactedToBy: [ 54 | { 55 | type: mongoose.Schema.Types.ObjectId, 56 | ref: 'User', 57 | }, 58 | ], 59 | }, 60 | ], 61 | threadReplies: [ 62 | { 63 | type: mongoose.Schema.Types.ObjectId, 64 | ref: 'User', 65 | }, 66 | ], 67 | threadRepliesCount: Number, 68 | threadLastReplyDate: Date, 69 | isBookmarked: { 70 | type: Boolean, 71 | default: false, 72 | }, 73 | isSelf: { 74 | type: Boolean, 75 | default: false, 76 | }, 77 | hasRead: { 78 | type: Boolean, 79 | default: false, 80 | }, 81 | type: String, 82 | }, 83 | { 84 | timestamps: true, 85 | versionKey: false, 86 | } 87 | ) 88 | 89 | export default mongoose.model('Message', messageSchema) 90 | -------------------------------------------------------------------------------- /src/models/organisation.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { UserSchemaType } from './user' 3 | 4 | export interface OrganisationSchemaType { 5 | owner: mongoose.Schema.Types.ObjectId 6 | name: string 7 | hobbies: string[] 8 | coWorkers: mongoose.Schema.Types.ObjectId[] & UserSchemaType[] 9 | generateJoinLink: () => string 10 | joinLink: string 11 | url: string 12 | } 13 | 14 | const organisationSchema = new mongoose.Schema( 15 | { 16 | owner: { 17 | type: mongoose.Schema.Types.ObjectId, 18 | ref: 'User', 19 | }, 20 | name: { 21 | type: String, 22 | }, 23 | coWorkers: [ 24 | { 25 | type: mongoose.Schema.Types.ObjectId, 26 | ref: 'User', 27 | }, 28 | ], 29 | joinLink: String, 30 | url: String, 31 | }, 32 | { 33 | timestamps: true, 34 | versionKey: false, 35 | } 36 | ) 37 | 38 | // Generate orgnaisation join link 39 | organisationSchema.methods.generateJoinLink = function () { 40 | const url = 41 | process.env.NODE_ENV === 'development' 42 | ? process.env.STAGING_URL 43 | : process.env.PRODUCTION_URL 44 | this.joinLink = `${url}/${this._id}` 45 | this.url = `${url}/${this.name}` 46 | } 47 | 48 | export default mongoose.model('Organisation', organisationSchema) 49 | -------------------------------------------------------------------------------- /src/models/thread.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | interface ThreadSchemaType { 4 | sender: mongoose.Schema.Types.ObjectId 5 | content: string 6 | message: mongoose.Schema.Types.ObjectId 7 | reactions: { 8 | emoji: string 9 | reactedToBy: [mongoose.Schema.Types.ObjectId] 10 | }[] 11 | isBookmarked: boolean 12 | hasRead: boolean 13 | createdAt: Date 14 | updatedAt: Date 15 | } 16 | 17 | const threadSchema = new mongoose.Schema( 18 | { 19 | sender: { 20 | type: mongoose.Schema.Types.ObjectId, 21 | ref: 'User', 22 | }, 23 | content: String, 24 | message: mongoose.Schema.Types.ObjectId, 25 | reactions: [ 26 | { 27 | emoji: String, 28 | reactedToBy: [ 29 | { 30 | type: mongoose.Schema.Types.ObjectId, 31 | ref: 'User', 32 | }, 33 | ], 34 | }, 35 | ], 36 | isBookmarked: { 37 | type: Boolean, 38 | default: false, 39 | }, 40 | hasRead: { 41 | type: Boolean, 42 | default: false, 43 | }, 44 | }, 45 | { 46 | timestamps: true, 47 | versionKey: false, 48 | } 49 | ) 50 | 51 | export default mongoose.model('Thread', threadSchema) 52 | -------------------------------------------------------------------------------- /src/models/user.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import jwt from 'jsonwebtoken' 3 | import crypto from 'crypto' 4 | import randomize from 'randomatic' 5 | 6 | export interface UserSchemaType { 7 | _id: mongoose.Types.ObjectId 8 | username?: string 9 | email?: string 10 | role?: string 11 | phone?: string 12 | profilePicture?: string 13 | isOnline?: boolean 14 | loginVerificationCode?: string 15 | loginVerificationCodeExpires?: Date 16 | googleId?: string 17 | getSignedJwtToken?: () => string 18 | getVerificationCode?: () => string 19 | } 20 | 21 | const userSchema = new mongoose.Schema( 22 | { 23 | username: { 24 | type: String, 25 | default() { 26 | return this.email.split('@')[0] 27 | }, 28 | }, 29 | email: { 30 | type: String, 31 | required: [true, 'Please enter your email'], 32 | unique: true, 33 | match: [/^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$/, 'Please enter a valid email'], 34 | }, 35 | googleId: String, 36 | isOnline: Boolean, 37 | role: String, 38 | phone: String, 39 | profilePicture: String, 40 | loginVerificationCode: String, 41 | loginVerificationCodeExpires: Date, 42 | }, 43 | { 44 | timestamps: true, 45 | versionKey: false, 46 | } 47 | ) 48 | 49 | // Sign JWT and return 50 | userSchema.methods.getSignedJwtToken = function () { 51 | return jwt.sign({ id: this._id }, process.env.JWT_SECRET, { 52 | expiresIn: process.env.JWT_EXPIRE, 53 | }) 54 | } 55 | 56 | // Generate login verification code 57 | userSchema.methods.getVerificationCode = function () { 58 | const verificationCode = randomize('Aa0', 6) 59 | 60 | this.loginVerificationCode = crypto 61 | .createHash('sha256') 62 | .update(verificationCode) 63 | .digest('hex') 64 | 65 | this.loginVerificationCodeExpires = Date.now() + 10 * 60 * 1000 // 10 minutes 66 | 67 | return verificationCode 68 | } 69 | 70 | export default mongoose.model('User', userSchema) 71 | -------------------------------------------------------------------------------- /src/routes/auth.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { googleCallback, register, signin, verify } from '../controllers/auth' 3 | import passport from 'passport' 4 | 5 | const router = express.Router() 6 | 7 | router.post('/register', register) 8 | router.post('/signin', signin) 9 | router.post('/verify', verify) 10 | router.get('/google', passport.authenticate('google', ['profile', 'email'])) 11 | 12 | router.get('/google/callback', googleCallback) 13 | 14 | export default router 15 | -------------------------------------------------------------------------------- /src/routes/channel.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { 3 | createChannel, 4 | getChannel, 5 | updateChannel, 6 | } from '../controllers/channel' 7 | import { protect } from '../middleware/protect' 8 | 9 | const router = express.Router() 10 | 11 | router.post('/', protect, createChannel) 12 | router.get('/:id', protect, getChannel) 13 | router.post('/:id', protect, updateChannel) 14 | 15 | export default router 16 | -------------------------------------------------------------------------------- /src/routes/conversations.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { getConversation, getConversations } from '../controllers/conversations' 3 | import { protect } from '../middleware/protect' 4 | 5 | const router = express.Router() 6 | 7 | router.get('/', protect, getConversations) 8 | router.get('/:id', protect, getConversation) 9 | 10 | export default router 11 | -------------------------------------------------------------------------------- /src/routes/message.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { protect } from '../middleware/protect' 3 | import { getMessage, getMessages } from '../controllers/message' 4 | 5 | const router = express.Router() 6 | 7 | router.get('/', protect, getMessages) 8 | router.get('/:id', protect, getMessage) 9 | 10 | export default router 11 | -------------------------------------------------------------------------------- /src/routes/organisation.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { protect } from '../middleware/protect' 3 | import { 4 | createOrganisation, 5 | getOrganisation, 6 | getWorkspaces, 7 | } from '../controllers/organisation' 8 | 9 | const router = express.Router() 10 | 11 | router.get('/workspaces', protect, getWorkspaces) 12 | router.get('/:id', protect, getOrganisation) 13 | router.post('/', protect, createOrganisation) 14 | 15 | export default router 16 | -------------------------------------------------------------------------------- /src/routes/teammates.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { createTeammates, getTeammate } from '../controllers/teammates' 3 | import { protect } from '../middleware/protect' 4 | 5 | const router = express.Router() 6 | 7 | router.get('/:id', protect, getTeammate) 8 | router.post('/', protect, createTeammates) 9 | 10 | export default router 11 | -------------------------------------------------------------------------------- /src/routes/thread.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { protect } from '../middleware/protect' 3 | import { getThreads } from '../controllers/thread' 4 | 5 | const router = express.Router() 6 | 7 | router.get('/', protect, getThreads) 8 | 9 | export default router 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "pretty": true, 6 | "sourceMap": true, 7 | "outDir": "dist", 8 | "importHelpers": true, 9 | "strict": true, 10 | "noImplicitAny": false, 11 | "strictNullChecks": false, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "moduleResolution": "node", 19 | "baseUrl": ".", 20 | "allowSyntheticDefaultImports": true, 21 | "experimentalDecorators": true, 22 | "emitDecoratorMetadata": true, 23 | "resolveJsonModule": true, 24 | "esModuleInterop": true, 25 | "lib": [ 26 | "es5", 27 | "es6", 28 | "dom", 29 | "es2015.core", 30 | "es2015.collection", 31 | "es2015.generator", 32 | "es2015.iterable", 33 | "es2015.promise", 34 | "es2015.proxy", 35 | "es2015.reflect", 36 | "es2015.symbol", 37 | "es2015.symbol.wellknown", 38 | "esnext.asynciterable" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "max-classes-per-file": false, 5 | "max-line-length": [true, 160], 6 | "no-unnecessary-initializer": false, 7 | "no-var-requires": true, 8 | "no-null-keyword": true, 9 | "no-consecutive-blank-lines": true, 10 | "quotemark": [true, "single", "avoid-escape"], 11 | "interface-name": false, 12 | "no-empty-interface": false, 13 | "no-namespace": false, 14 | "ordered-imports": false, 15 | "object-literal-sort-keys": false, 16 | "arrow-parens": false, 17 | "member-ordering": [ 18 | true, 19 | { 20 | "order": [ 21 | "public-static-field", 22 | "public-static-method", 23 | "protected-static-field", 24 | "protected-static-method", 25 | "private-static-field", 26 | "private-static-method", 27 | "public-instance-field", 28 | "protected-instance-field", 29 | "private-instance-field", 30 | "public-constructor", 31 | "protected-constructor", 32 | "private-constructor", 33 | "public-instance-method", 34 | "protected-instance-method", 35 | "private-instance-method" 36 | ] 37 | } 38 | ], 39 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 40 | "no-inferrable-types": [true, "ignore-params"], 41 | "no-switch-case-fall-through": true, 42 | "typedef": [true, "call-signature", "parameter"], 43 | "trailing-comma": [ 44 | true, 45 | { 46 | "multiline": { 47 | "objects": "always", 48 | "arrays": "always", 49 | "functions": "never", 50 | "typeLiterals": "ignore" 51 | }, 52 | "singleline": "never" 53 | } 54 | ], 55 | "align": [true, "parameters"], 56 | "class-name": true, 57 | "curly": true, 58 | "eofline": true, 59 | "jsdoc-format": true, 60 | "member-access": true, 61 | "no-arg": true, 62 | "no-construct": true, 63 | "no-duplicate-variable": true, 64 | "no-empty": true, 65 | "no-eval": true, 66 | "no-internal-module": true, 67 | "no-string-literal": true, 68 | "no-trailing-whitespace": true, 69 | "no-unused-expression": true, 70 | "no-var-keyword": true, 71 | "one-line": [ 72 | true, 73 | "check-open-brace", 74 | "check-catch", 75 | "check-else", 76 | "check-finally", 77 | "check-whitespace" 78 | ], 79 | "semicolon": true, 80 | "switch-default": true, 81 | "triple-equals": [true, "allow-null-check"], 82 | "typedef-whitespace": [ 83 | true, 84 | { 85 | "call-signature": "nospace", 86 | "index-signature": "nospace", 87 | "parameter": "nospace", 88 | "property-declaration": "nospace", 89 | "variable-declaration": "nospace" 90 | } 91 | ], 92 | "variable-name": false, 93 | "whitespace": [ 94 | true, 95 | "check-branch", 96 | "check-decl", 97 | "check-operator", 98 | "check-separator", 99 | "check-type" 100 | ] 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@cspotcode/source-map-support@^0.8.0": 6 | version "0.8.1" 7 | resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" 8 | integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== 9 | dependencies: 10 | "@jridgewell/trace-mapping" "0.3.9" 11 | 12 | "@jridgewell/resolve-uri@^3.0.3": 13 | version "3.1.1" 14 | resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" 15 | integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== 16 | 17 | "@jridgewell/sourcemap-codec@^1.4.10": 18 | version "1.4.15" 19 | resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" 20 | integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== 21 | 22 | "@jridgewell/trace-mapping@0.3.9": 23 | version "0.3.9" 24 | resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" 25 | integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== 26 | dependencies: 27 | "@jridgewell/resolve-uri" "^3.0.3" 28 | "@jridgewell/sourcemap-codec" "^1.4.10" 29 | 30 | "@socket.io/component-emitter@~3.1.0": 31 | version "3.1.0" 32 | resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" 33 | integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== 34 | 35 | "@tsconfig/node10@^1.0.7": 36 | version "1.0.9" 37 | resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" 38 | integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== 39 | 40 | "@tsconfig/node12@^1.0.7": 41 | version "1.0.11" 42 | resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" 43 | integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== 44 | 45 | "@tsconfig/node14@^1.0.0": 46 | version "1.0.3" 47 | resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" 48 | integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== 49 | 50 | "@tsconfig/node16@^1.0.2": 51 | version "1.0.4" 52 | resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" 53 | integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== 54 | 55 | "@types/cookie@^0.4.1": 56 | version "0.4.1" 57 | resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" 58 | integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== 59 | 60 | "@types/cors@^2.8.12": 61 | version "2.8.13" 62 | resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.13.tgz#b8ade22ba455a1b8cb3b5d3f35910fd204f84f94" 63 | integrity sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA== 64 | dependencies: 65 | "@types/node" "*" 66 | 67 | "@types/node@*": 68 | version "20.3.0" 69 | resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.0.tgz#719498898d5defab83c3560f45d8498f58d11938" 70 | integrity sha512-cumHmIAf6On83X7yP+LrsEyUOf/YlociZelmpRYaGFydoaPdxdt80MAbu6vWerQT2COCp2nPvHdsbD7tHn/YlQ== 71 | 72 | "@types/node@>=10.0.0": 73 | version "20.5.0" 74 | resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.0.tgz#7fc8636d5f1aaa3b21e6245e97d56b7f56702313" 75 | integrity sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q== 76 | 77 | "@types/socket.io@^3.0.2": 78 | version "3.0.2" 79 | resolved "https://registry.yarnpkg.com/@types/socket.io/-/socket.io-3.0.2.tgz#606c9639e3f93bb8454cba8f5f0a283d47917759" 80 | integrity sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ== 81 | dependencies: 82 | socket.io "*" 83 | 84 | "@types/webidl-conversions@*": 85 | version "7.0.0" 86 | resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz#2b8e60e33906459219aa587e9d1a612ae994cfe7" 87 | integrity sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog== 88 | 89 | "@types/whatwg-url@^8.2.1": 90 | version "8.2.2" 91 | resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-8.2.2.tgz#749d5b3873e845897ada99be4448041d4cc39e63" 92 | integrity sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA== 93 | dependencies: 94 | "@types/node" "*" 95 | "@types/webidl-conversions" "*" 96 | 97 | abbrev@1: 98 | version "1.1.1" 99 | resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" 100 | integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== 101 | 102 | accepts@~1.3.4, accepts@~1.3.8: 103 | version "1.3.8" 104 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" 105 | integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== 106 | dependencies: 107 | mime-types "~2.1.34" 108 | negotiator "0.6.3" 109 | 110 | acorn-walk@^8.1.1: 111 | version "8.2.0" 112 | resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" 113 | integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== 114 | 115 | acorn@^8.4.1: 116 | version "8.8.2" 117 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" 118 | integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== 119 | 120 | anymatch@~3.1.2: 121 | version "3.1.3" 122 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" 123 | integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== 124 | dependencies: 125 | normalize-path "^3.0.0" 126 | picomatch "^2.0.4" 127 | 128 | arg@^4.1.0: 129 | version "4.1.3" 130 | resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" 131 | integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== 132 | 133 | array-flatten@1.1.1: 134 | version "1.1.1" 135 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 136 | integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== 137 | 138 | balanced-match@^1.0.0: 139 | version "1.0.2" 140 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 141 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 142 | 143 | base64id@2.0.0, base64id@~2.0.0: 144 | version "2.0.0" 145 | resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" 146 | integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== 147 | 148 | base64url@3.x.x: 149 | version "3.0.1" 150 | resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" 151 | integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== 152 | 153 | basic-auth@~2.0.1: 154 | version "2.0.1" 155 | resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" 156 | integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== 157 | dependencies: 158 | safe-buffer "5.1.2" 159 | 160 | binary-extensions@^2.0.0: 161 | version "2.2.0" 162 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" 163 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 164 | 165 | body-parser@1.20.1: 166 | version "1.20.1" 167 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" 168 | integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== 169 | dependencies: 170 | bytes "3.1.2" 171 | content-type "~1.0.4" 172 | debug "2.6.9" 173 | depd "2.0.0" 174 | destroy "1.2.0" 175 | http-errors "2.0.0" 176 | iconv-lite "0.4.24" 177 | on-finished "2.4.1" 178 | qs "6.11.0" 179 | raw-body "2.5.1" 180 | type-is "~1.6.18" 181 | unpipe "1.0.0" 182 | 183 | body-parser@^1.20.2: 184 | version "1.20.2" 185 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" 186 | integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== 187 | dependencies: 188 | bytes "3.1.2" 189 | content-type "~1.0.5" 190 | debug "2.6.9" 191 | depd "2.0.0" 192 | destroy "1.2.0" 193 | http-errors "2.0.0" 194 | iconv-lite "0.4.24" 195 | on-finished "2.4.1" 196 | qs "6.11.0" 197 | raw-body "2.5.2" 198 | type-is "~1.6.18" 199 | unpipe "1.0.0" 200 | 201 | brace-expansion@^1.1.7: 202 | version "1.1.11" 203 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 204 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 205 | dependencies: 206 | balanced-match "^1.0.0" 207 | concat-map "0.0.1" 208 | 209 | braces@~3.0.2: 210 | version "3.0.2" 211 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 212 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 213 | dependencies: 214 | fill-range "^7.0.1" 215 | 216 | bson@^5.3.0: 217 | version "5.3.0" 218 | resolved "https://registry.yarnpkg.com/bson/-/bson-5.3.0.tgz#37b006df4cd91ed125cb686467c1dd6d4606b514" 219 | integrity sha512-ukmCZMneMlaC5ebPHXIkP8YJzNl5DC41N5MAIvKDqLggdao342t4McltoJBQfQya/nHBWAcSsYRqlXPoQkTJag== 220 | 221 | buffer-equal-constant-time@1.0.1: 222 | version "1.0.1" 223 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 224 | integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== 225 | 226 | bytes@3.1.2: 227 | version "3.1.2" 228 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" 229 | integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== 230 | 231 | call-bind@^1.0.0: 232 | version "1.0.2" 233 | resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" 234 | integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== 235 | dependencies: 236 | function-bind "^1.1.1" 237 | get-intrinsic "^1.0.2" 238 | 239 | chokidar@^3.5.2: 240 | version "3.5.3" 241 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" 242 | integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== 243 | dependencies: 244 | anymatch "~3.1.2" 245 | braces "~3.0.2" 246 | glob-parent "~5.1.2" 247 | is-binary-path "~2.1.0" 248 | is-glob "~4.0.1" 249 | normalize-path "~3.0.0" 250 | readdirp "~3.6.0" 251 | optionalDependencies: 252 | fsevents "~2.3.2" 253 | 254 | concat-map@0.0.1: 255 | version "0.0.1" 256 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 257 | integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 258 | 259 | content-disposition@0.5.4: 260 | version "0.5.4" 261 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" 262 | integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== 263 | dependencies: 264 | safe-buffer "5.2.1" 265 | 266 | content-type@~1.0.4, content-type@~1.0.5: 267 | version "1.0.5" 268 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" 269 | integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== 270 | 271 | cookie-parser@^1.4.6: 272 | version "1.4.6" 273 | resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594" 274 | integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA== 275 | dependencies: 276 | cookie "0.4.1" 277 | cookie-signature "1.0.6" 278 | 279 | cookie-session@^2.0.0: 280 | version "2.0.0" 281 | resolved "https://registry.yarnpkg.com/cookie-session/-/cookie-session-2.0.0.tgz#d07aa27822f43619e4342df1342268c849833089" 282 | integrity sha512-hKvgoThbw00zQOleSlUr2qpvuNweoqBtxrmx0UFosx6AGi9lYtLoA+RbsvknrEX8Pr6MDbdWAb2j6SnMn+lPsg== 283 | dependencies: 284 | cookies "0.8.0" 285 | debug "3.2.7" 286 | on-headers "~1.0.2" 287 | safe-buffer "5.2.1" 288 | 289 | cookie-signature@1.0.6: 290 | version "1.0.6" 291 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 292 | integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== 293 | 294 | cookie@0.4.1: 295 | version "0.4.1" 296 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" 297 | integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== 298 | 299 | cookie@0.4.2, cookie@~0.4.1: 300 | version "0.4.2" 301 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" 302 | integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== 303 | 304 | cookie@0.5.0: 305 | version "0.5.0" 306 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" 307 | integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== 308 | 309 | cookies@0.8.0: 310 | version "0.8.0" 311 | resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" 312 | integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow== 313 | dependencies: 314 | depd "~2.0.0" 315 | keygrip "~1.1.0" 316 | 317 | cors@^2.8.5, cors@~2.8.5: 318 | version "2.8.5" 319 | resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" 320 | integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== 321 | dependencies: 322 | object-assign "^4" 323 | vary "^1" 324 | 325 | create-require@^1.1.0: 326 | version "1.1.1" 327 | resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" 328 | integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== 329 | 330 | debug@2.6.9: 331 | version "2.6.9" 332 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 333 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 334 | dependencies: 335 | ms "2.0.0" 336 | 337 | debug@3.2.7, debug@^3.2.7: 338 | version "3.2.7" 339 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" 340 | integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== 341 | dependencies: 342 | ms "^2.1.1" 343 | 344 | debug@4.x, debug@~4.3.1, debug@~4.3.2: 345 | version "4.3.4" 346 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 347 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 348 | dependencies: 349 | ms "2.1.2" 350 | 351 | depd@2.0.0, depd@~2.0.0: 352 | version "2.0.0" 353 | resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" 354 | integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== 355 | 356 | destroy@1.2.0: 357 | version "1.2.0" 358 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" 359 | integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== 360 | 361 | diff@^4.0.1: 362 | version "4.0.2" 363 | resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" 364 | integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== 365 | 366 | dotenv@^16.1.4: 367 | version "16.1.4" 368 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.1.4.tgz#67ac1a10cd9c25f5ba604e4e08bc77c0ebe0ca8c" 369 | integrity sha512-m55RtE8AsPeJBpOIFKihEmqUcoVncQIwo7x9U8ZwLEZw9ZpXboz2c+rvog+jUaJvVrZ5kBOeYQBX5+8Aa/OZQw== 370 | 371 | ecdsa-sig-formatter@1.0.11: 372 | version "1.0.11" 373 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" 374 | integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== 375 | dependencies: 376 | safe-buffer "^5.0.1" 377 | 378 | ee-first@1.1.1: 379 | version "1.1.1" 380 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 381 | integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== 382 | 383 | encodeurl@~1.0.2: 384 | version "1.0.2" 385 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 386 | integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== 387 | 388 | engine.io-parser@~5.2.1: 389 | version "5.2.1" 390 | resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb" 391 | integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ== 392 | 393 | engine.io@~6.5.2: 394 | version "6.5.2" 395 | resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.2.tgz#769348ced9d56bd47bd83d308ec1c3375e85937c" 396 | integrity sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA== 397 | dependencies: 398 | "@types/cookie" "^0.4.1" 399 | "@types/cors" "^2.8.12" 400 | "@types/node" ">=10.0.0" 401 | accepts "~1.3.4" 402 | base64id "2.0.0" 403 | cookie "~0.4.1" 404 | cors "~2.8.5" 405 | debug "~4.3.1" 406 | engine.io-parser "~5.2.1" 407 | ws "~8.11.0" 408 | 409 | escape-html@~1.0.3: 410 | version "1.0.3" 411 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 412 | integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== 413 | 414 | etag@~1.8.1: 415 | version "1.8.1" 416 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 417 | integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== 418 | 419 | express-rate-limit@^6.7.0: 420 | version "6.7.0" 421 | resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-6.7.0.tgz#6aa8a1bd63dfe79702267b3af1161a93afc1d3c2" 422 | integrity sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA== 423 | 424 | express-session@^1.17.3: 425 | version "1.17.3" 426 | resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.3.tgz#14b997a15ed43e5949cb1d073725675dd2777f36" 427 | integrity sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw== 428 | dependencies: 429 | cookie "0.4.2" 430 | cookie-signature "1.0.6" 431 | debug "2.6.9" 432 | depd "~2.0.0" 433 | on-headers "~1.0.2" 434 | parseurl "~1.3.3" 435 | safe-buffer "5.2.1" 436 | uid-safe "~2.1.5" 437 | 438 | express@^4.18.2: 439 | version "4.18.2" 440 | resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" 441 | integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== 442 | dependencies: 443 | accepts "~1.3.8" 444 | array-flatten "1.1.1" 445 | body-parser "1.20.1" 446 | content-disposition "0.5.4" 447 | content-type "~1.0.4" 448 | cookie "0.5.0" 449 | cookie-signature "1.0.6" 450 | debug "2.6.9" 451 | depd "2.0.0" 452 | encodeurl "~1.0.2" 453 | escape-html "~1.0.3" 454 | etag "~1.8.1" 455 | finalhandler "1.2.0" 456 | fresh "0.5.2" 457 | http-errors "2.0.0" 458 | merge-descriptors "1.0.1" 459 | methods "~1.1.2" 460 | on-finished "2.4.1" 461 | parseurl "~1.3.3" 462 | path-to-regexp "0.1.7" 463 | proxy-addr "~2.0.7" 464 | qs "6.11.0" 465 | range-parser "~1.2.1" 466 | safe-buffer "5.2.1" 467 | send "0.18.0" 468 | serve-static "1.15.0" 469 | setprototypeof "1.2.0" 470 | statuses "2.0.1" 471 | type-is "~1.6.18" 472 | utils-merge "1.0.1" 473 | vary "~1.1.2" 474 | 475 | fill-range@^7.0.1: 476 | version "7.0.1" 477 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 478 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 479 | dependencies: 480 | to-regex-range "^5.0.1" 481 | 482 | finalhandler@1.2.0: 483 | version "1.2.0" 484 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" 485 | integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== 486 | dependencies: 487 | debug "2.6.9" 488 | encodeurl "~1.0.2" 489 | escape-html "~1.0.3" 490 | on-finished "2.4.1" 491 | parseurl "~1.3.3" 492 | statuses "2.0.1" 493 | unpipe "~1.0.0" 494 | 495 | forwarded@0.2.0: 496 | version "0.2.0" 497 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" 498 | integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== 499 | 500 | fresh@0.5.2: 501 | version "0.5.2" 502 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 503 | integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== 504 | 505 | fsevents@~2.3.2: 506 | version "2.3.3" 507 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" 508 | integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== 509 | 510 | function-bind@^1.1.1: 511 | version "1.1.1" 512 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 513 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 514 | 515 | get-intrinsic@^1.0.2: 516 | version "1.2.1" 517 | resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" 518 | integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== 519 | dependencies: 520 | function-bind "^1.1.1" 521 | has "^1.0.3" 522 | has-proto "^1.0.1" 523 | has-symbols "^1.0.3" 524 | 525 | glob-parent@~5.1.2: 526 | version "5.1.2" 527 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 528 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 529 | dependencies: 530 | is-glob "^4.0.1" 531 | 532 | has-flag@^3.0.0: 533 | version "3.0.0" 534 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 535 | integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== 536 | 537 | has-proto@^1.0.1: 538 | version "1.0.1" 539 | resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" 540 | integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== 541 | 542 | has-symbols@^1.0.3: 543 | version "1.0.3" 544 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" 545 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== 546 | 547 | has@^1.0.3: 548 | version "1.0.3" 549 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 550 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 551 | dependencies: 552 | function-bind "^1.1.1" 553 | 554 | helmet@^7.0.0: 555 | version "7.0.0" 556 | resolved "https://registry.yarnpkg.com/helmet/-/helmet-7.0.0.tgz#ac3011ba82fa2467f58075afa58a49427ba6212d" 557 | integrity sha512-MsIgYmdBh460ZZ8cJC81q4XJknjG567wzEmv46WOBblDb6TUd3z8/GhgmsM9pn8g2B80tAJ4m5/d3Bi1KrSUBQ== 558 | 559 | hpp@^0.2.3: 560 | version "0.2.3" 561 | resolved "https://registry.yarnpkg.com/hpp/-/hpp-0.2.3.tgz#33bcc5fda713d2a962173c84f79a915a739b57bc" 562 | integrity sha512-4zDZypjQcxK/8pfFNR7jaON7zEUpXZxz4viyFmqjb3kWNWAHsLEUmWXcdn25c5l76ISvnD6hbOGO97cXUI3Ryw== 563 | dependencies: 564 | lodash "^4.17.12" 565 | type-is "^1.6.12" 566 | 567 | http-errors@2.0.0: 568 | version "2.0.0" 569 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" 570 | integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== 571 | dependencies: 572 | depd "2.0.0" 573 | inherits "2.0.4" 574 | setprototypeof "1.2.0" 575 | statuses "2.0.1" 576 | toidentifier "1.0.1" 577 | 578 | iconv-lite@0.4.24: 579 | version "0.4.24" 580 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 581 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 582 | dependencies: 583 | safer-buffer ">= 2.1.2 < 3" 584 | 585 | ignore-by-default@^1.0.1: 586 | version "1.0.1" 587 | resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" 588 | integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== 589 | 590 | inherits@2.0.4: 591 | version "2.0.4" 592 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 593 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 594 | 595 | ip@^2.0.0: 596 | version "2.0.0" 597 | resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" 598 | integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== 599 | 600 | ipaddr.js@1.9.1: 601 | version "1.9.1" 602 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 603 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 604 | 605 | is-binary-path@~2.1.0: 606 | version "2.1.0" 607 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 608 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 609 | dependencies: 610 | binary-extensions "^2.0.0" 611 | 612 | is-extglob@^2.1.1: 613 | version "2.1.1" 614 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 615 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 616 | 617 | is-glob@^4.0.1, is-glob@~4.0.1: 618 | version "4.0.3" 619 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 620 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 621 | dependencies: 622 | is-extglob "^2.1.1" 623 | 624 | is-number@^4.0.0: 625 | version "4.0.0" 626 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" 627 | integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== 628 | 629 | is-number@^7.0.0: 630 | version "7.0.0" 631 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 632 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 633 | 634 | jsonwebtoken@^9.0.0: 635 | version "9.0.0" 636 | resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d" 637 | integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw== 638 | dependencies: 639 | jws "^3.2.2" 640 | lodash "^4.17.21" 641 | ms "^2.1.1" 642 | semver "^7.3.8" 643 | 644 | jwa@^1.4.1: 645 | version "1.4.1" 646 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" 647 | integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== 648 | dependencies: 649 | buffer-equal-constant-time "1.0.1" 650 | ecdsa-sig-formatter "1.0.11" 651 | safe-buffer "^5.0.1" 652 | 653 | jws@^3.2.2: 654 | version "3.2.2" 655 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" 656 | integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== 657 | dependencies: 658 | jwa "^1.4.1" 659 | safe-buffer "^5.0.1" 660 | 661 | kareem@2.5.1: 662 | version "2.5.1" 663 | resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.5.1.tgz#7b8203e11819a8e77a34b3517d3ead206764d15d" 664 | integrity sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA== 665 | 666 | keygrip@~1.1.0: 667 | version "1.1.0" 668 | resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" 669 | integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ== 670 | dependencies: 671 | tsscmp "1.0.6" 672 | 673 | kind-of@^6.0.0: 674 | version "6.0.3" 675 | resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" 676 | integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== 677 | 678 | lodash@^4.17.12, lodash@^4.17.21: 679 | version "4.17.21" 680 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 681 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 682 | 683 | lru-cache@^6.0.0: 684 | version "6.0.0" 685 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" 686 | integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== 687 | dependencies: 688 | yallist "^4.0.0" 689 | 690 | make-error@^1.1.1: 691 | version "1.3.6" 692 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" 693 | integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== 694 | 695 | math-random@^1.0.1: 696 | version "1.0.4" 697 | resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" 698 | integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== 699 | 700 | media-typer@0.3.0: 701 | version "0.3.0" 702 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 703 | integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== 704 | 705 | memory-pager@^1.0.2: 706 | version "1.5.0" 707 | resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" 708 | integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== 709 | 710 | merge-descriptors@1.0.1: 711 | version "1.0.1" 712 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 713 | integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== 714 | 715 | methods@~1.1.2: 716 | version "1.1.2" 717 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 718 | integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== 719 | 720 | mime-db@1.52.0: 721 | version "1.52.0" 722 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 723 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 724 | 725 | mime-types@~2.1.24, mime-types@~2.1.34: 726 | version "2.1.35" 727 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 728 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 729 | dependencies: 730 | mime-db "1.52.0" 731 | 732 | mime@1.6.0: 733 | version "1.6.0" 734 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 735 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 736 | 737 | minimatch@^3.1.2: 738 | version "3.1.2" 739 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 740 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 741 | dependencies: 742 | brace-expansion "^1.1.7" 743 | 744 | mongodb-connection-string-url@^2.6.0: 745 | version "2.6.0" 746 | resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz#57901bf352372abdde812c81be47b75c6b2ec5cf" 747 | integrity sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ== 748 | dependencies: 749 | "@types/whatwg-url" "^8.2.1" 750 | whatwg-url "^11.0.0" 751 | 752 | mongodb@5.5.0: 753 | version "5.5.0" 754 | resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.5.0.tgz#5d213ef68f6b48610909d98d537059f2d7f374a1" 755 | integrity sha512-XgrkUgAAdfnZKQfk5AsYL8j7O99WHd4YXPxYxnh8dZxD+ekYWFRA3JktUsBnfg+455Smf75/+asoU/YLwNGoQQ== 756 | dependencies: 757 | bson "^5.3.0" 758 | mongodb-connection-string-url "^2.6.0" 759 | socks "^2.7.1" 760 | optionalDependencies: 761 | saslprep "^1.0.3" 762 | 763 | mongodb@^5.6.0: 764 | version "5.6.0" 765 | resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.6.0.tgz#caff5278341bfc0f1ef6f394bb403d207de03d1e" 766 | integrity sha512-z8qVs9NfobHJm6uzK56XBZF8XwM9H294iRnB7wNjF0SnY93si5HPziIJn+qqvUR5QOff/4L0gCD6SShdR/GtVQ== 767 | dependencies: 768 | bson "^5.3.0" 769 | mongodb-connection-string-url "^2.6.0" 770 | socks "^2.7.1" 771 | optionalDependencies: 772 | saslprep "^1.0.3" 773 | 774 | mongoose@^7.2.4: 775 | version "7.2.4" 776 | resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.2.4.tgz#0cbccd23746c5bd82a4330e83ea2b2c52e03c044" 777 | integrity sha512-BWcgShV2WH1rspICiJKLPi7QssTebpGJ23Nyk7qG0TMEE/OEAlsQKEhI7VlrXg4ZnoOcHgG+N+upW9tj17TTQg== 778 | dependencies: 779 | bson "^5.3.0" 780 | kareem "2.5.1" 781 | mongodb "5.5.0" 782 | mpath "0.9.0" 783 | mquery "5.0.0" 784 | ms "2.1.3" 785 | sift "16.0.1" 786 | 787 | morgan@^1.10.0: 788 | version "1.10.0" 789 | resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" 790 | integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== 791 | dependencies: 792 | basic-auth "~2.0.1" 793 | debug "2.6.9" 794 | depd "~2.0.0" 795 | on-finished "~2.3.0" 796 | on-headers "~1.0.2" 797 | 798 | mpath@0.9.0: 799 | version "0.9.0" 800 | resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.9.0.tgz#0c122fe107846e31fc58c75b09c35514b3871904" 801 | integrity sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew== 802 | 803 | mquery@5.0.0: 804 | version "5.0.0" 805 | resolved "https://registry.yarnpkg.com/mquery/-/mquery-5.0.0.tgz#a95be5dfc610b23862df34a47d3e5d60e110695d" 806 | integrity sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg== 807 | dependencies: 808 | debug "4.x" 809 | 810 | ms@2.0.0: 811 | version "2.0.0" 812 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 813 | integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== 814 | 815 | ms@2.1.2: 816 | version "2.1.2" 817 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 818 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 819 | 820 | ms@2.1.3, ms@^2.1.1: 821 | version "2.1.3" 822 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 823 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 824 | 825 | negotiator@0.6.3: 826 | version "0.6.3" 827 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" 828 | integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== 829 | 830 | nodemailer@^6.9.3: 831 | version "6.9.3" 832 | resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.3.tgz#e4425b85f05d83c43c5cd81bf84ab968f8ef5cbe" 833 | integrity sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg== 834 | 835 | nodemon@^3.0.1: 836 | version "3.0.1" 837 | resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.0.1.tgz#affe822a2c5f21354466b2fc8ae83277d27dadc7" 838 | integrity sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw== 839 | dependencies: 840 | chokidar "^3.5.2" 841 | debug "^3.2.7" 842 | ignore-by-default "^1.0.1" 843 | minimatch "^3.1.2" 844 | pstree.remy "^1.1.8" 845 | semver "^7.5.3" 846 | simple-update-notifier "^2.0.0" 847 | supports-color "^5.5.0" 848 | touch "^3.1.0" 849 | undefsafe "^2.0.5" 850 | 851 | nopt@~1.0.10: 852 | version "1.0.10" 853 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" 854 | integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg== 855 | dependencies: 856 | abbrev "1" 857 | 858 | normalize-path@^3.0.0, normalize-path@~3.0.0: 859 | version "3.0.0" 860 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 861 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 862 | 863 | oauth@0.9.x: 864 | version "0.9.15" 865 | resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" 866 | integrity sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA== 867 | 868 | object-assign@^4: 869 | version "4.1.1" 870 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 871 | integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== 872 | 873 | object-inspect@^1.9.0: 874 | version "1.12.3" 875 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" 876 | integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== 877 | 878 | on-finished@2.4.1: 879 | version "2.4.1" 880 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" 881 | integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== 882 | dependencies: 883 | ee-first "1.1.1" 884 | 885 | on-finished@~2.3.0: 886 | version "2.3.0" 887 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 888 | integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== 889 | dependencies: 890 | ee-first "1.1.1" 891 | 892 | on-headers@~1.0.2: 893 | version "1.0.2" 894 | resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" 895 | integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== 896 | 897 | parseurl@~1.3.3: 898 | version "1.3.3" 899 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 900 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 901 | 902 | passport-google-oauth20@^2.0.0: 903 | version "2.0.0" 904 | resolved "https://registry.yarnpkg.com/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz#0d241b2d21ebd3dc7f2b60669ec4d587e3a674ef" 905 | integrity sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ== 906 | dependencies: 907 | passport-oauth2 "1.x.x" 908 | 909 | passport-oauth2@1.x.x: 910 | version "1.7.0" 911 | resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.7.0.tgz#5c4766c8531ac45ffe9ec2c09de9809e2c841fc4" 912 | integrity sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ== 913 | dependencies: 914 | base64url "3.x.x" 915 | oauth "0.9.x" 916 | passport-strategy "1.x.x" 917 | uid2 "0.0.x" 918 | utils-merge "1.x.x" 919 | 920 | passport-strategy@1.x.x: 921 | version "1.0.0" 922 | resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" 923 | integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== 924 | 925 | passport@0.5: 926 | version "0.5.3" 927 | resolved "https://registry.yarnpkg.com/passport/-/passport-0.5.3.tgz#e69b46c9bb3290660bc2b3299330d78710b198cc" 928 | integrity sha512-gGc+70h4gGdBWNsR3FuV3byLDY6KBTJAIExGFXTpQaYfbbcHCBlRRKx7RBQSpqEqc5Hh2qVzRs7ssvSfOpkUEA== 929 | dependencies: 930 | passport-strategy "1.x.x" 931 | pause "0.0.1" 932 | 933 | path-to-regexp@0.1.7: 934 | version "0.1.7" 935 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 936 | integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== 937 | 938 | pause@0.0.1: 939 | version "0.0.1" 940 | resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" 941 | integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== 942 | 943 | picomatch@^2.0.4, picomatch@^2.2.1: 944 | version "2.3.1" 945 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" 946 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 947 | 948 | proxy-addr@~2.0.7: 949 | version "2.0.7" 950 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" 951 | integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== 952 | dependencies: 953 | forwarded "0.2.0" 954 | ipaddr.js "1.9.1" 955 | 956 | pstree.remy@^1.1.8: 957 | version "1.1.8" 958 | resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" 959 | integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== 960 | 961 | punycode@^2.1.1: 962 | version "2.3.0" 963 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" 964 | integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== 965 | 966 | qs@6.11.0: 967 | version "6.11.0" 968 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" 969 | integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== 970 | dependencies: 971 | side-channel "^1.0.4" 972 | 973 | random-bytes@~1.0.0: 974 | version "1.0.0" 975 | resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" 976 | integrity sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ== 977 | 978 | randomatic@^3.1.1: 979 | version "3.1.1" 980 | resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" 981 | integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw== 982 | dependencies: 983 | is-number "^4.0.0" 984 | kind-of "^6.0.0" 985 | math-random "^1.0.1" 986 | 987 | range-parser@~1.2.1: 988 | version "1.2.1" 989 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 990 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 991 | 992 | raw-body@2.5.1: 993 | version "2.5.1" 994 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" 995 | integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== 996 | dependencies: 997 | bytes "3.1.2" 998 | http-errors "2.0.0" 999 | iconv-lite "0.4.24" 1000 | unpipe "1.0.0" 1001 | 1002 | raw-body@2.5.2: 1003 | version "2.5.2" 1004 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" 1005 | integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== 1006 | dependencies: 1007 | bytes "3.1.2" 1008 | http-errors "2.0.0" 1009 | iconv-lite "0.4.24" 1010 | unpipe "1.0.0" 1011 | 1012 | readdirp@~3.6.0: 1013 | version "3.6.0" 1014 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" 1015 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== 1016 | dependencies: 1017 | picomatch "^2.2.1" 1018 | 1019 | safe-buffer@5.1.2: 1020 | version "5.1.2" 1021 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 1022 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 1023 | 1024 | safe-buffer@5.2.1, safe-buffer@^5.0.1: 1025 | version "5.2.1" 1026 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 1027 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 1028 | 1029 | "safer-buffer@>= 2.1.2 < 3": 1030 | version "2.1.2" 1031 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 1032 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 1033 | 1034 | saslprep@^1.0.3: 1035 | version "1.0.3" 1036 | resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" 1037 | integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== 1038 | dependencies: 1039 | sparse-bitfield "^3.0.3" 1040 | 1041 | semver@^7.3.8: 1042 | version "7.5.1" 1043 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec" 1044 | integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw== 1045 | dependencies: 1046 | lru-cache "^6.0.0" 1047 | 1048 | semver@^7.5.3: 1049 | version "7.5.4" 1050 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" 1051 | integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== 1052 | dependencies: 1053 | lru-cache "^6.0.0" 1054 | 1055 | send@0.18.0: 1056 | version "0.18.0" 1057 | resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" 1058 | integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== 1059 | dependencies: 1060 | debug "2.6.9" 1061 | depd "2.0.0" 1062 | destroy "1.2.0" 1063 | encodeurl "~1.0.2" 1064 | escape-html "~1.0.3" 1065 | etag "~1.8.1" 1066 | fresh "0.5.2" 1067 | http-errors "2.0.0" 1068 | mime "1.6.0" 1069 | ms "2.1.3" 1070 | on-finished "2.4.1" 1071 | range-parser "~1.2.1" 1072 | statuses "2.0.1" 1073 | 1074 | serve-static@1.15.0: 1075 | version "1.15.0" 1076 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" 1077 | integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== 1078 | dependencies: 1079 | encodeurl "~1.0.2" 1080 | escape-html "~1.0.3" 1081 | parseurl "~1.3.3" 1082 | send "0.18.0" 1083 | 1084 | setprototypeof@1.2.0: 1085 | version "1.2.0" 1086 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" 1087 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 1088 | 1089 | side-channel@^1.0.4: 1090 | version "1.0.4" 1091 | resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" 1092 | integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== 1093 | dependencies: 1094 | call-bind "^1.0.0" 1095 | get-intrinsic "^1.0.2" 1096 | object-inspect "^1.9.0" 1097 | 1098 | sift@16.0.1: 1099 | version "16.0.1" 1100 | resolved "https://registry.yarnpkg.com/sift/-/sift-16.0.1.tgz#e9c2ccc72191585008cf3e36fc447b2d2633a053" 1101 | integrity sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ== 1102 | 1103 | simple-update-notifier@^2.0.0: 1104 | version "2.0.0" 1105 | resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" 1106 | integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== 1107 | dependencies: 1108 | semver "^7.5.3" 1109 | 1110 | smart-buffer@^4.2.0: 1111 | version "4.2.0" 1112 | resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" 1113 | integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== 1114 | 1115 | socket.io-adapter@~2.5.2: 1116 | version "2.5.2" 1117 | resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12" 1118 | integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA== 1119 | dependencies: 1120 | ws "~8.11.0" 1121 | 1122 | socket.io-parser@~4.2.4: 1123 | version "4.2.4" 1124 | resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" 1125 | integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== 1126 | dependencies: 1127 | "@socket.io/component-emitter" "~3.1.0" 1128 | debug "~4.3.1" 1129 | 1130 | socket.io@*, socket.io@^4.7.2: 1131 | version "4.7.2" 1132 | resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.2.tgz#22557d76c3f3ca48f82e73d68b7add36a22df002" 1133 | integrity sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw== 1134 | dependencies: 1135 | accepts "~1.3.4" 1136 | base64id "~2.0.0" 1137 | cors "~2.8.5" 1138 | debug "~4.3.2" 1139 | engine.io "~6.5.2" 1140 | socket.io-adapter "~2.5.2" 1141 | socket.io-parser "~4.2.4" 1142 | 1143 | socks@^2.7.1: 1144 | version "2.7.1" 1145 | resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" 1146 | integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== 1147 | dependencies: 1148 | ip "^2.0.0" 1149 | smart-buffer "^4.2.0" 1150 | 1151 | sparse-bitfield@^3.0.3: 1152 | version "3.0.3" 1153 | resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" 1154 | integrity sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ== 1155 | dependencies: 1156 | memory-pager "^1.0.2" 1157 | 1158 | statuses@2.0.1: 1159 | version "2.0.1" 1160 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" 1161 | integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== 1162 | 1163 | supports-color@^5.5.0: 1164 | version "5.5.0" 1165 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 1166 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 1167 | dependencies: 1168 | has-flag "^3.0.0" 1169 | 1170 | to-regex-range@^5.0.1: 1171 | version "5.0.1" 1172 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 1173 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 1174 | dependencies: 1175 | is-number "^7.0.0" 1176 | 1177 | toidentifier@1.0.1: 1178 | version "1.0.1" 1179 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" 1180 | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== 1181 | 1182 | touch@^3.1.0: 1183 | version "3.1.0" 1184 | resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" 1185 | integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== 1186 | dependencies: 1187 | nopt "~1.0.10" 1188 | 1189 | tr46@^3.0.0: 1190 | version "3.0.0" 1191 | resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" 1192 | integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== 1193 | dependencies: 1194 | punycode "^2.1.1" 1195 | 1196 | ts-node@^10.9.1: 1197 | version "10.9.1" 1198 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" 1199 | integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== 1200 | dependencies: 1201 | "@cspotcode/source-map-support" "^0.8.0" 1202 | "@tsconfig/node10" "^1.0.7" 1203 | "@tsconfig/node12" "^1.0.7" 1204 | "@tsconfig/node14" "^1.0.0" 1205 | "@tsconfig/node16" "^1.0.2" 1206 | acorn "^8.4.1" 1207 | acorn-walk "^8.1.1" 1208 | arg "^4.1.0" 1209 | create-require "^1.1.0" 1210 | diff "^4.0.1" 1211 | make-error "^1.1.1" 1212 | v8-compile-cache-lib "^3.0.1" 1213 | yn "3.1.1" 1214 | 1215 | tslib@^2.5.3: 1216 | version "2.5.3" 1217 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" 1218 | integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== 1219 | 1220 | tsscmp@1.0.6: 1221 | version "1.0.6" 1222 | resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" 1223 | integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== 1224 | 1225 | type-is@^1.6.12, type-is@~1.6.18: 1226 | version "1.6.18" 1227 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 1228 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 1229 | dependencies: 1230 | media-typer "0.3.0" 1231 | mime-types "~2.1.24" 1232 | 1233 | typescript@^5.1.3: 1234 | version "5.1.3" 1235 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.3.tgz#8d84219244a6b40b6fb2b33cc1c062f715b9e826" 1236 | integrity sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw== 1237 | 1238 | uid-safe@~2.1.5: 1239 | version "2.1.5" 1240 | resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" 1241 | integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== 1242 | dependencies: 1243 | random-bytes "~1.0.0" 1244 | 1245 | uid2@0.0.x: 1246 | version "0.0.4" 1247 | resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.4.tgz#033f3b1d5d32505f5ce5f888b9f3b667123c0a44" 1248 | integrity sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA== 1249 | 1250 | undefsafe@^2.0.5: 1251 | version "2.0.5" 1252 | resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" 1253 | integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== 1254 | 1255 | unpipe@1.0.0, unpipe@~1.0.0: 1256 | version "1.0.0" 1257 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 1258 | integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== 1259 | 1260 | utils-merge@1.0.1, utils-merge@1.x.x: 1261 | version "1.0.1" 1262 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 1263 | integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== 1264 | 1265 | uuid@^9.0.0: 1266 | version "9.0.0" 1267 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" 1268 | integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== 1269 | 1270 | v8-compile-cache-lib@^3.0.1: 1271 | version "3.0.1" 1272 | resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" 1273 | integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== 1274 | 1275 | vary@^1, vary@~1.1.2: 1276 | version "1.1.2" 1277 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 1278 | integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== 1279 | 1280 | webidl-conversions@^7.0.0: 1281 | version "7.0.0" 1282 | resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" 1283 | integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== 1284 | 1285 | whatwg-url@^11.0.0: 1286 | version "11.0.0" 1287 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" 1288 | integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== 1289 | dependencies: 1290 | tr46 "^3.0.0" 1291 | webidl-conversions "^7.0.0" 1292 | 1293 | ws@~8.11.0: 1294 | version "8.11.0" 1295 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" 1296 | integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== 1297 | 1298 | xss-clean@^0.1.4: 1299 | version "0.1.4" 1300 | resolved "https://registry.yarnpkg.com/xss-clean/-/xss-clean-0.1.4.tgz#592b7f59cc52d39d4b8c397787243817639fb73d" 1301 | integrity sha512-4hArTFHYxrifK9VXOu/zFvwjTXVjKByPi6woUHb1IqxlX0Z4xtFBRjOhTKuYV/uE1VswbYsIh5vUEYp7MmoISQ== 1302 | dependencies: 1303 | xss-filters "1.2.7" 1304 | 1305 | xss-filters@1.2.7: 1306 | version "1.2.7" 1307 | resolved "https://registry.yarnpkg.com/xss-filters/-/xss-filters-1.2.7.tgz#59fa1de201f36f2f3470dcac5f58ccc2830b0a9a" 1308 | integrity sha512-KzcmYT/f+YzcYrYRqw6mXxd25BEZCxBQnf+uXTopQDIhrmiaLwO+f+yLsIvvNlPhYvgff8g3igqrBxYh9k8NbQ== 1309 | 1310 | yallist@^4.0.0: 1311 | version "4.0.0" 1312 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" 1313 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 1314 | 1315 | yn@3.1.1: 1316 | version "3.1.1" 1317 | resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" 1318 | integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== 1319 | --------------------------------------------------------------------------------