From 2793e7d0c767c2074e877e47398da150c94fa8d3 Mon Sep 17 00:00:00 2001 From: chimpdev Date: Wed, 6 Nov 2024 15:07:55 +0300 Subject: [PATCH] chore: init --- .env.example | 4 + .github/CODEOWNERS | 4 + .github/CODE_OF_CONDUCT.md | 21 + .github/CONTRIBUTING.md | 32 + .github/FUNDING.yml | 3 + .github/ISSUE_TEMPLATE/issue-with-website.md | 27 + .github/actions/setup-node-env/action.yml | 24 + .github/dependabot.yml | 6 + .github/labels.yml | 27 + .github/workflows/build.yml | 23 + .../workflows/conventional-commits-check.yml | 17 + .github/workflows/eslint.yml | 34 + .github/workflows/label-sync.yml | 27 + .github/workflows/release-please.yml | 21 + .gitignore | 5 + .husky/commit-msg | 1 + LICENSE.md | 14 + README.md | 879 ++++ commitling.config.js | 3 + eslint.config.mjs | 139 + package.json | 97 + pnpm-lock.yaml | 4565 +++++++++++++++++ src/config.toml | 68 + src/example.config.toml | 68 + src/index.ts | 10 + src/lib/bot/commands/admin/eval.ts | 137 + src/lib/bot/commands/general/ping.ts | 21 + src/lib/bot/commands/kv/storage.ts | 263 + src/lib/bot/commands/utils/evaluateCode.ts | 29 + src/lib/bot/createClient.ts | 64 + src/lib/bot/crons/checkInactiveWebsockets.ts | 14 + .../bot/crons/updateClientActivityState.ts | 22 + src/lib/bot/events/guildMemberAdd.ts | 20 + src/lib/bot/events/guildMemberRemove.ts | 32 + src/lib/bot/events/interactionCreate.ts | 154 + src/lib/bot/events/presenceUpdate.ts | 26 + .../bot/handlers/commands/fetchCommands.ts | 33 + .../bot/handlers/commands/registerCommands.ts | 46 + src/lib/bot/handlers/crons/fetchCrons.ts | 34 + src/lib/bot/handlers/crons/listenCrons.ts | 18 + src/lib/bot/handlers/events/fetchEvents.ts | 32 + src/lib/bot/handlers/events/listenEvents.ts | 10 + src/lib/constants/badges.ts | 48 + src/lib/constants/status.ts | 20 + src/lib/express/createServer.ts | 65 + .../express/middlewares/addCustomMethods.ts | 14 + src/lib/express/middlewares/handleErrors.ts | 11 + .../middlewares/handleSimultaneousRequests.ts | 56 + .../express/middlewares/morganMiddleware.ts | 9 + .../express/middlewares/notFoundHandler.ts | 8 + .../express/middlewares/validateRequest.ts | 11 + .../api/v1/users/[user_id]/createSvg.ts | 453 ++ .../routes/api/v1/users/[user_id]/index.ts | 203 + .../api/v1/users/[user_id]/storage/[key].ts | 226 + .../api/v1/users/[user_id]/storage/index.ts | 77 + src/lib/express/routes/api/v1/users/index.ts | 41 + src/lib/express/routes/index.ts | 16 + src/lib/express/routes/socket/index.ts | 190 + src/lib/express/routes/socket/schemas.ts | 16 + src/lib/express/routes/socket/utils.ts | 51 + src/lib/models/EvaulateResult.ts | 28 + src/lib/models/Storage.ts | 82 + src/lib/models/User.ts | 16 + src/lib/utils/bot/createUserData.ts | 160 + .../utils/bot/getApplicationIdFromToken.ts | 14 + src/lib/utils/bot/getCommandName.ts | 34 + src/lib/utils/bot/syncUsers.ts | 39 + src/lib/utils/encryption.ts | 46 + src/lib/utils/generateUniqueId.ts | 13 + src/lib/utils/getValidationError.ts | 20 + src/lib/utils/getZodError.ts | 17 + src/lib/utils/sleep.ts | 11 + src/scripts/connectDatabase.ts | 23 + src/scripts/createMongoBackup.ts | 80 + src/scripts/handleUncaughtExceptions.ts | 14 + src/scripts/loadConfig.ts | 7 + src/scripts/loadLogger.ts | 54 + src/scripts/registerCommands.ts | 15 + src/scripts/unregisterCommands.ts | 13 + src/scripts/validateEnvironmentVariables.ts | 18 + src/types/global.d.ts | 125 + src/types/index.d.ts | 172 + swagger.json | 792 +++ tsconfig.json | 25 + 84 files changed, 10407 insertions(+) create mode 100644 .env.example create mode 100644 .github/CODEOWNERS create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/issue-with-website.md create mode 100644 .github/actions/setup-node-env/action.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/labels.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/conventional-commits-check.yml create mode 100644 .github/workflows/eslint.yml create mode 100644 .github/workflows/label-sync.yml create mode 100644 .github/workflows/release-please.yml create mode 100644 .gitignore create mode 100644 .husky/commit-msg create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 commitling.config.js create mode 100644 eslint.config.mjs create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 src/config.toml create mode 100644 src/example.config.toml create mode 100644 src/index.ts create mode 100644 src/lib/bot/commands/admin/eval.ts create mode 100644 src/lib/bot/commands/general/ping.ts create mode 100644 src/lib/bot/commands/kv/storage.ts create mode 100644 src/lib/bot/commands/utils/evaluateCode.ts create mode 100644 src/lib/bot/createClient.ts create mode 100644 src/lib/bot/crons/checkInactiveWebsockets.ts create mode 100644 src/lib/bot/crons/updateClientActivityState.ts create mode 100644 src/lib/bot/events/guildMemberAdd.ts create mode 100644 src/lib/bot/events/guildMemberRemove.ts create mode 100644 src/lib/bot/events/interactionCreate.ts create mode 100644 src/lib/bot/events/presenceUpdate.ts create mode 100644 src/lib/bot/handlers/commands/fetchCommands.ts create mode 100644 src/lib/bot/handlers/commands/registerCommands.ts create mode 100644 src/lib/bot/handlers/crons/fetchCrons.ts create mode 100644 src/lib/bot/handlers/crons/listenCrons.ts create mode 100644 src/lib/bot/handlers/events/fetchEvents.ts create mode 100644 src/lib/bot/handlers/events/listenEvents.ts create mode 100644 src/lib/constants/badges.ts create mode 100644 src/lib/constants/status.ts create mode 100644 src/lib/express/createServer.ts create mode 100644 src/lib/express/middlewares/addCustomMethods.ts create mode 100644 src/lib/express/middlewares/handleErrors.ts create mode 100644 src/lib/express/middlewares/handleSimultaneousRequests.ts create mode 100644 src/lib/express/middlewares/morganMiddleware.ts create mode 100644 src/lib/express/middlewares/notFoundHandler.ts create mode 100644 src/lib/express/middlewares/validateRequest.ts create mode 100644 src/lib/express/routes/api/v1/users/[user_id]/createSvg.ts create mode 100644 src/lib/express/routes/api/v1/users/[user_id]/index.ts create mode 100644 src/lib/express/routes/api/v1/users/[user_id]/storage/[key].ts create mode 100644 src/lib/express/routes/api/v1/users/[user_id]/storage/index.ts create mode 100644 src/lib/express/routes/api/v1/users/index.ts create mode 100644 src/lib/express/routes/index.ts create mode 100644 src/lib/express/routes/socket/index.ts create mode 100644 src/lib/express/routes/socket/schemas.ts create mode 100644 src/lib/express/routes/socket/utils.ts create mode 100644 src/lib/models/EvaulateResult.ts create mode 100644 src/lib/models/Storage.ts create mode 100644 src/lib/models/User.ts create mode 100644 src/lib/utils/bot/createUserData.ts create mode 100644 src/lib/utils/bot/getApplicationIdFromToken.ts create mode 100644 src/lib/utils/bot/getCommandName.ts create mode 100644 src/lib/utils/bot/syncUsers.ts create mode 100644 src/lib/utils/encryption.ts create mode 100644 src/lib/utils/generateUniqueId.ts create mode 100644 src/lib/utils/getValidationError.ts create mode 100644 src/lib/utils/getZodError.ts create mode 100644 src/lib/utils/sleep.ts create mode 100644 src/scripts/connectDatabase.ts create mode 100644 src/scripts/createMongoBackup.ts create mode 100644 src/scripts/handleUncaughtExceptions.ts create mode 100644 src/scripts/loadConfig.ts create mode 100644 src/scripts/loadLogger.ts create mode 100644 src/scripts/registerCommands.ts create mode 100644 src/scripts/unregisterCommands.ts create mode 100644 src/scripts/validateEnvironmentVariables.ts create mode 100644 src/types/global.d.ts create mode 100644 src/types/index.d.ts create mode 100644 swagger.json create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9994048 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +DISCORD_BOT_TOKEN='' +MONGODB_URI='' +MONGODB_NAME='' +KV_TOKEN_ENCRYPTION_SECRET='' \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..0a2bd8d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# Learn how to add code owners here: +# https://help.github.com/en/articles/about-code-owners + +* @chimpdev \ No newline at end of file diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..54f6714 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,21 @@ +# Code of Conduct + +As contributors and maintainers of the project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests, and any other activities. + +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery +- Personal attacks +- Trolling or insulting/derogatory comments +- Public or private harassment +- Publishing others' private information, such as physical or electronic addresses, without explicit permission +- Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. + +This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [insert contact information]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html), version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..6fd7a10 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing Guidelines + +We welcome contributions to the project! To ensure smooth collaboration, please follow these guidelines when contributing: + +## Reporting Issues + +If you encounter any bugs, unexpected behavior, or have suggestions for improvements, please open an issue on our [GitHub repository](https://github.com/discordplace/lantern/issues) with a clear description of the problem or feature request. Make sure to include any relevant details, such as error messages, screenshots, or steps to reproduce the issue. + +## Pull Requests + +We encourage you to submit pull requests for bug fixes, feature additions, or other improvements to the project. Before creating a pull request, please ensure the following: + +- Fork the repository and create a new branch for your changes. +- Follow the coding style and conventions used in the project. +- Write clear and concise commit messages that follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. +- Update documentation as necessary. + +Once your pull request is ready, submit it for review against the `main` branch. We'll review it as soon as possible and provide feedback or merge it if everything looks good. + +## Code Style + +Please adhere to the coding style and conventions used throughout the project. Consistent coding style helps maintain readability and makes collaboration easier for everyone. + +## Licensing + +By contributing to this project, you agree to license your contributions under the [GPL v3 License](LICENSE). This ensures that your contributions can be freely used, modified, and distributed by others. + +## Code of Conduct + +Please review and abide by our [Code of Conduct](CODE_OF_CONDUCT.md) when participating in this project. We are committed to fostering an inclusive and welcoming community for all. + +If you have any questions or need further assistance, feel free to reach out to the project maintainers. Thank you for your contributions! diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b8810e2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: chimpdev diff --git a/.github/ISSUE_TEMPLATE/issue-with-website.md b/.github/ISSUE_TEMPLATE/issue-with-website.md new file mode 100644 index 0000000..fedef65 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue-with-website.md @@ -0,0 +1,27 @@ +--- +name: Issue with Lantern +about: Issues that related to Lantern +title: '' +labels: '' +assignees: chimpdev + +--- + +**Describe the Bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected Behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional Context** +Add any other context about the problem here. diff --git a/.github/actions/setup-node-env/action.yml b/.github/actions/setup-node-env/action.yml new file mode 100644 index 0000000..99daef4 --- /dev/null +++ b/.github/actions/setup-node-env/action.yml @@ -0,0 +1,24 @@ +name: "Setup Node Environment" +description: "Setup Node, cache and clean install" +inputs: + node-version: + description: "the Node.js version to use" + required: true + cache-path: + description: "path to cache folders and files" + required: true + cache-key: + description: "the key for caching" + required: true +runs: + using: composite + steps: + - uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + - uses: actions/cache@v4 + with: + path: ${{ inputs.cache-path }} + key: ${{ inputs.cache-key }} + - run: pnpm install --frozen-lockfile + shell: bash \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..63dc312 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: 'pnpm' + directory: '/' + schedule: + interval: 'weekly' diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..b3b6216 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,27 @@ +- name: 'Bug' + color: 'FF6347' + description: Issues related to bugs specifically in the project. +- name: 'Feature' + color: '4682B4' + description: Requests or discussions related to new features in the project. +- name: 'Security' + color: 'FF0000' + description: Issues related to security vulnerabilities in the project. +- name: 'Performance' + color: '32CD32' + description: Discussions or problems regarding performance optimizations in the project. +- name: 'Database' + color: 'FFA500' + description: Issues related to database related problems or discussions. +- name: 'Documentation' + color: '8A2BE2' + description: Issues related to documentation or the lack of it. +- name: 'Question' + color: 'FFD700' + description: Issues that are questions or require clarification. +- name: 'Invalid' + color: '4682B4' + description: Issues that are invalid or do not belong to the project. +- name: 'Duplicate' + color: 'FFD700' + description: Issues that are duplicates of other issues. \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..26a4520 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,23 @@ +name: 'Build and Deploy' + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + build-and-deploy: + name: Build and Deploy + runs-on: ubuntu-latest + + steps: + - name: Connect to SSH and run commands + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + password: ${{ secrets.SSH_PASSWORD }} + port: ${{ secrets.SSH_PORT }} + script: | + ./build.sh \ No newline at end of file diff --git a/.github/workflows/conventional-commits-check.yml b/.github/workflows/conventional-commits-check.yml new file mode 100644 index 0000000..e1ce96f --- /dev/null +++ b/.github/workflows/conventional-commits-check.yml @@ -0,0 +1,17 @@ +name: Conventional Commits + +on: + pull_request: + branches: + - main + +jobs: + build: + name: Conventional Commits + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: webiny/action-conventional-commits@v1.3.0 + with: + allowed-commit-types: "feat,fix,docs,style,refactor,perf,test,chore,build,ci" \ No newline at end of file diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml new file mode 100644 index 0000000..8ab8175 --- /dev/null +++ b/.github/workflows/eslint.yml @@ -0,0 +1,34 @@ +name: "ESLint" + +on: + pull_request: + branches: + - main + +jobs: + lint: + name: "Lint" + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - uses: ./.github/actions/setup-node-env + with: + node-version: 18 + cache-path: /node_modules + cache-key: "${{hashFiles('pnpm-lock.yaml')}}" + + - name: Run lint script + run: | + npm run lint \ No newline at end of file diff --git a/.github/workflows/label-sync.yml b/.github/workflows/label-sync.yml new file mode 100644 index 0000000..7e29d5b --- /dev/null +++ b/.github/workflows/label-sync.yml @@ -0,0 +1,27 @@ +name: Label sync +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + push: + branches: + - main + paths: + - '.github/labels.yml' + +permissions: + contents: write + +jobs: + label-sync: + name: Label sync + runs-on: ubuntu-latest + if: github.repository_owner == 'discordplace' + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Label sync + uses: crazy-max/ghaction-github-labeler@v4 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..2abf161 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,21 @@ +name: Release Please + +on: + workflow_dispatch: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v4 + id: release + with: + token: ${{ secrets.GITHUB_TOKEN }} + release-type: node \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2095f49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +node_modules/ +logs/ +dist/ +database-backups/ \ No newline at end of file diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..990bd0b --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +npx --no -- commitlint --edit "$1" \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f43ade8 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,14 @@ +Copyright (C) 2024 Gökhan Bulut + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..eee2d98 --- /dev/null +++ b/README.md @@ -0,0 +1,879 @@ +# 🔦 Lantern: Illuminate Your Discord Presence with Real-Time API and WebSocket + +Lantern is a powerful service designed to effortlessly broadcast your active Discord status to both a RESTful API endpoint (`lantern.rest/api/v1/users/:your_id`) and a WebSocket connection. Want to showcase your current Spotify tracks on your personal site? Lantern has you covered. + +While Lantern is ready to use out-of-the-box without the need for any deployment, it also offers the flexibility for self-hosting with minimal setup. Enjoy seamless integration and real-time updates with Lantern. + +--- + +## Table of Contents + +- [API Docs](#api-docs) + - [**GET** `/api/v1/users`](#get-apiv1users) + - [**GET** `/api/v1/users/:id`](#get-apiv1usersid) + - [WebSocket](#websocket) +- [KV Storage](#kv-storage) + - [Use cases](#use-cases) + - [Limits](#limits) + - [WebSocket](#websocket-1) + - [RESTful API](#restful-api) + * [**GET** `/api/v1/users/:user_id/storage`](#get-apiv1usersuser_idstorage) + * [**DELETE** `/api/v1/users/:user_id/storage`](#delete-apiv1usersuser_idstorage-requires-authorization-header) + * [**GET** `/api/v1/users/:user_id/storage/:key`](#get-apiv1usersuser_idstoragekey) + * [**PUT** `/api/v1/users/:user_id/storage/:key`](#put-apiv1usersuser_idstoragekey-requires-authorization-header) + * [**PATCH** `/api/v1/users/:user_id/storage/:key`](#patch-apiv1usersuser_idstoragekey-requires-authorization-header) + * [**DELETE** `/api/v1/users/:user_id/storage/:key`](#delete-apiv1usersuser_idstoragekey-requires-authorization-header) +- [Self-Hosting](#self-hosting) +- [Contributing](#contributing) +- [Help](#help) +- [License](#license) + +--- + +## API Docs + +Lantern provides OpenAPI documentation for the RESTful API, which you can access at `https://lantern.rest/docs`. The API offers endpoints to retrieve user data and key-value storage. + +You can also refer to the swagger.json file in the repository for the API documentation. The API endpoints are as follows: + +#### GET `/api/v1/users` + +Retrieve the data of users with the specified IDs. + +##### Parameters +| Name | Type | Description | +| ---- | ---- | ----------- | +| `user_ids` | string | The IDs of the users to retrieve. | + +> [!NOTE] +> - The `user_ids` parameter should be an array of user IDs. You should include this parameter multiple times for each user ID you want to retrieve. +> - The maximum number of user IDs you can retrieve at once is 50. +> - Example: `?user_ids=123456789012345678&user_ids=234567890123456789` + +##### Response +
+ + Example Response + + + ```json + [ + { + // User metadata object + "metadata": { + "id": "123456789012345678", + "username": "example", + "discriminator": "0", + "global_name": "example", + "avatar": "abcdef1234567890", + "avatar_url": "https://cdn.discordapp.com/avatars/123456789012345678/123456789012345678.png", + "display_avatar_url": "https://cdn.discordapp.com/avatars/123456789012345678/123456789012345678.png", + "bot": false, + "flags": { + "human_readable": ["Staff"], + "bitfield": 1 + }, + "monitoring_since": { + "unix": 1620000000, + "raw": "2021-05-03T00:00:00.000Z" + } + }, + "status": "online", + // Active platforms object with current Spotify track + "active_platforms": { + "desktop": "online", + "mobile": "offline", + "web": "offline", + "spotify": { + "track_id": "abcdef1234567890", + "song": "example", + "artist": "example", + "album": "example", + "album_cover": "https://i.scdn.co/image/abcdef1234567890", + "start_time": { + "unix": 1620000000, + "raw": "2021-05-03T00:00:00.000Z" + }, + "end_time": { + "unix": 1620000000, + "raw": "2021-05-03T00:00:00.000Z" + }, + "time": { + "start_human_readable": "00:00", + "end_human_readable": "00:00" + } + } + }, + // Array of user activities + "activities": [ + { + "id": "abcdef1234567890", + "name": "example", + "type": "PLAYING", + "state": "example", + "details": "example", + "application_id": "123456789012345678", + "created_at": 1620000000, + "assets": { + "large_image": { + "hash": "abcdef1234567890", + "image_url": "https://cdn.discordapp.com/app-assets/123456789012345678/abcdef1234567890.png", + "text": "example" + }, + "small_image": { + "hash": "abcdef1234567890", + "image_url": "https://cdn.discordapp.com/app-assets/123456789012345678/abcdef1234567890.png", + "text": "example" + } + }, + "start_time": { + "unix": 1620000000, + "raw": "2021-05-03T00:00:00.000Z" + } + } + ], + // Key-value pairs for the user (if any) + "storage": { + "key": "value" + } + }, + // Additional user objects + ] +``` +
+ +##### GET `/api/v1/users/:id` + +Retrieve the data of a user with the specified ID. + +##### Parameters +| Name | Type | Description | +| ---- | ---- | ----------- | +| `id` | string | The ID of the user to retrieve. | +| `svg` | boolean | Whether to return the user's avatar as an SVG image. Defaults to `false`. | +| `theme` | string | The theme to use for the user's card. Must be either `light` or `dark`. | +| `borderRadius` | number | The border radius rem value for the user's card. Defaults to `2`. | +| `hideGlobalName` | number | Whether to hide the user's global name. Must be either `0` or `1`. | +| `hideStatus` | number | Whether to hide the user's status. Must be either `0` or `1`. | +| `hideBadges` | number | Whether to hide the user's badges. Must be either `0` or `1`. | +| `hideActivity` | number | Whether to hide the user's activity. Must be either `0` or `1`. | +| `noActivityTitle` | string | The title to display when the user has no activity. Default might be `No Activity`. Can't be greater than 64 characters. | +| `noActivityMessage` | string | The message to display when the user has no activity. Default might be `This user is not currently doing anything.`. Can't be greater than 256 characters. | + +##### Response +
+ + Example Response + + +```json +{ + // User metadata object + "metadata": { + "id": "123456789012345678", + "username": "example", + "discriminator": "0", + "global_name": "example", + "avatar": "abcdef1234567890", + "avatar_url": "https://cdn.discordapp.com/avatars/123456789012345678/123456789012345678.png", + "display_avatar_url": "https://cdn.discordapp.com/avatars/123456789012345678/123456789012345678.png", + "bot": false, + "flags": { + "human_readable": ["Staff"], + "bitfield": 1 + }, + "monitoring_since": { + "unix": 1620000000, + "raw": "2021-05-03T00:00:00.000Z" + } + }, + "status": "online", + // Active platforms object with current Spotify track + "active_platforms": { + "desktop": "online", + "mobile": "offline", + "web": "offline", + "spotify": { + "track_id": "abcdef1234567890", + "song": "example", + "artist": "example", + "album": "example", + "album_cover": "https://i.scdn.co/image/abcdef1234567890", + "start_time": { + "unix": 1620000000, + "raw": "2021-05-03T00:00:00.000Z" + }, + "end_time": { + "unix": 1620000000, + "raw": "2021-05-03T00:00:00.000Z" + }, + "time": { + "start_human_readable": "00:00", + "end_human_readable": "00:00" + } + } + }, + // Array of user activities + "activities": [ + { + "id": "abcdef1234567890", + "name": "example", + "type": "PLAYING", + "state": "example", + "details": "example", + "application_id": "123456789012345678", + "created_at": 1620000000, + "assets": { + "large_image": { + "hash": "abcdef1234567890", + "image_url": "https://cdn.discordapp.com/app-assets/123456789012345678/abcdef1234567890.png", + "text": "example" + }, + "small_image": { + "hash": "abcdef1234567890", + "image_url": "https://cdn.discordapp.com/app-assets/123456789012345678/abcdef1234567890.png", + "text": "example" + } + }, + "start_time": { + "unix": 1620000000, + "raw": "2021-05-03T00:00:00.000Z" + } + } + ], + // Key-value pairs for the user (if any) + "storage": { + "key": "value" + } +} +``` +
+ +## WebSocket + +The WebSocket connection is available at `wss://lantern.rest/socket`. + +Once connected, you will receive `Opcode 1: Hello` which will contain `heartbeat_interval` in the data field. + +You should set a repeating interval for the time specified in `heartbeat_interval` which should send Opcode 4: Heartbeat on the interval. + +You should send `Opcode 2: Initialize` immediately after receiving `Opcode 1: Hello` with the following payload: + +
+ + Subscribe to User + + + ```json + { + "op": 2, + "d": { + "user_id": "123456789012345678" + } + } + ``` +
+ +
+ + Subscribe to Multiple Users + + + ```json + { + "op": 2, + "d": { + "user_ids": ["123456789012345678", "234567890123456789"] + } + } + ``` +
+ +
+ + Subscribe to All Users + + + ```json + { + "op": 2, + "d": { + "user_id": "All" + } + } + ``` +
+
+ +You will receive `Opcode 3: Initialize Acknowledgement` once the server has acknowledged your subscription and this will contain the all data for the user(s) you have subscribed to in the `d` field. + +If any user you have subscribed to updates their presence, you will receive `Opcode 6: Presence Update` with the updated data in the `d` field. + +If any user you have subscribed leaves the Lantern server, you will receive `Opcode 7: User Left` with the user ID in the `d` field. (when subscribing to multiple users, you will receive this for each user that leaves, if no users are left, you will receive `Opcode 9: Disconnect`) + +Additionally, if you want to add or remove a user from your subscription, you can also send `Opcode 12: Subscribe` and `Opcode 14: Unsubscribe` respectively. Note that, if you remove all users from your subscription, you will receive `Opcode 9: Disconnect`. + +##### List of Opcodes +| Opcode | Description | Type | +| ------ | ----------- | ---- | +| 1 | HELLO | Server -> Client | +| 2 | INIT | Client -> Server | +| 3 | INIT_ACK | Server -> Client | +| 4 | HEARTBEAT | Client -> Server | +| 5 | HEARTBEAT_ACK | Server -> Client | +| 6 | PRESENCE_UPDATE | Server -> Client | +| 7 | USER_LEFT | Server -> Client | +| 8 | USER_JOINED | Server -> Client | +| 9 | DISCONNECT | Server -> Client | +| 10 | STORAGE_UPDATE | Server -> Client | +| 11 | ERROR | Server -> Client | +| 12 | SUBSCRIBE | Client -> Server | +| 13 | SUBSCRIBE_ACK | Server -> Client | +| 14 | UNSUBSCRIBE | Client -> Server | +| 15 | UNSUBSCRIBE_ACK | Server -> Client | + +##### Example Payloads + +
+ + Opcode 1: Hello + + + ```json + { + "t": "HELLO", + "op": 1, + "d": { + "heartbeat_interval": 10000 + } + } + ``` +
+ +
+ + Opcode 2: Initialize + + + ```json + { + "op": 2, + "d": { + "user_id": "123456789012345678" + } + } + ``` +
+ +
+ + Opcode 3: Initialize Acknowledgement + + + ```json + { + "t": "INIT_ACK", + "op": 3, + "d": { + // User data + // Can be array if multiple users are subscribed + } + } + ``` +
+ +
+ + Opcode 4: Heartbeat + + + ```json + { + "t": "HEARTBEAT", + "op": 4 + } + ``` +
+ +
+ + Opcode 5: Heartbeat Acknowledgement + + + ```json + { + "t": "HEARTBEAT_ACK", + "op": 5 + } + ``` +
+ +
+ + Opcode 6: Presence Update + + + ```json + { + "t": "PRESENCE_UPDATE", + "op": 6, + "d": { + // Updated user data + // Can be array if multiple users are subscribed + } + } + ``` +
+ +
+ + Opcode 7: User Left + + + ```json + { + "t": "USER_LEFT", + "op": 7, + "d": { + "user_id": "123456789012345678" + } + } + ``` +
+ +
+ + Opcode 8: User Joined + + + ```json + { + "t": "USER_JOINED", + "op": 8, + "d": { + // User data + } + } + ``` +
+ +
+ + Opcode 9: Disconnect + + + ```json + { + "t": "DISCONNECT", + "op": 9, + "d": { + "reason": "Connection timed out." + } + } + ``` +
+ +
+ + Opcode 10: Storage Update + + + ```json + { + "t": "STORAGE_UPDATE", + "op": 10, + "d": { + // All key-value pairs for the user + "key": "value" + } + } + ``` +
+ +
+ + Opcode 11: Error + + + ```json + { + "t": "ERROR", + "op": 11, + "d": "Something went wrong." + } + ``` +
+ +
+ + Opcode 12: Subscribe + + + ```json + { + "op": 12, + "d": { + "user_id": "123456789012345678" + } + } + ``` + + ```json + { + "op": 12, + "d": { + "user_ids": ["123456789012345678", "234567890123456789"] + } + } + ``` + + ```json + { + "op": 12, + "d": { + "user_id": "All" + } + } + ``` +
+ +
+ + Opcode 13: Subscribe Acknowledgement + + + ```json + { + "t": "SUBSCRIBE_ACK", + "op": 13 + } + ``` +
+ +
+ + Opcode 14: Unsubscribe + + + ```json + { + "op": 14, + "d": { + "user_id": "123456789012345678" + } + } + ``` + + ```json + { + "op": 14, + "d": { + "user_ids": ["123456789012345678", "234567890123456789"] + } + } + ``` + + ```json + { + "op": 14, + "d": { + "user_id": "All" + } + } + ``` +
+ +
+ + Opcode 15: Unsubscribe Acknowledgement + + + ```json + { + "t": "UNSUBSCRIBE_ACK", + "op": 15 + } + ``` +
+ +--- + +## KV Storage + +Lantern also offers a simple key-value storage system that can be accessed through the RESTful API, WebSocket or the Lantern bot itself. + +#### Use cases +- Configuration values for your website +- Dynamic data for your website/profile (e.g. current location) +- User-specific data (e.g. user preferences) +- Temporary data storage + +#### Limits +1. Keys and values can only be strings (You can store JSON objects as strings) +2. Values can be 30,000 characters maximum +3. Keys must be alphanumeric (a-Z, A-Z, 0-9) and 255 characters max length +4. You can only store up to 512 key-value pairs linked + +#### WebSocket + +You can access the KV storage through the WebSocket connection. The following opcodes are available for the KV storage: + +##### Opcode 10: Storage Update + +This opcode is sent when a key-value pair is created, updated or deleted. + +##### Payload + +```json +{ + "t": "STORAGE_UPDATE", + "op": 10, + "d": { + // All key-value pairs for the user + "key": "value" + } +} +``` + +#### RESTful API + +For requests to the KV storage API, you will need to include the `Authorization` header with the value you get from the Lantern bot (use `/storage create token` command). The bot will provide you with a unique token that you can use to access the KV storage. + +Without the `Authorization` header, you can only access the data you have stored with the bot itself. + +##### GET `/api/v1/users/:user_id/storage` + +Retrieve all key-value pairs for a specific user. + +##### Parameters +| Name | Type | Description | +| ---- | ---- | ----------- | +| `user_id` | string | The ID of the user to retrieve the key-value pairs from. | + +##### Response + +
+ + Example Response + + + ```json + { + "key1": "value1", + "key2": "value2" + } + ``` +
+ +##### DELETE `/api/v1/users/:user_id/storage` (Requires `Authorization` header) + +Delete all key-value pairs for a specific user. + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `user_id` | string | The ID of the user to delete the key-value pairs from. | + +##### Response + +
+ + Example Response + + + ```json + { + "success": true + } + ``` +
+ +##### GET `/api/v1/users/:user_id/storage/:key` + +Retrieve the value of a key for a specific user. + +##### Parameters +| Name | Type | Description | +| ---- | ---- | ----------- | +| `user_id` | string | The ID of the user to retrieve the key from. | +| `key` | string | The key to retrieve the value for. | + +##### Response +
+ + Example Response + + + ```json + { + "value": "example_value" + } + ``` +
+ +##### PUT `/api/v1/users/:user_id/storage/:key` (Requires `Authorization` header) + +Create a new key-value pair for a specific user. + +##### Parameters +| Name | Type | Description | +| ---- | ---- | ----------- | +| `user_id` | string | The ID of the user to create the key-value pair for. | +| `key` | string | The key to create. | +| `value` | string | The value to assign to the key. | + +##### Response +
+ + Example Response + + + ```json + { + "success": true + } + ``` +
+ +##### PATCH `/api/v1/users/:user_id/storage/:key` (Requires `Authorization` header) + +Update the value of a key for a specific user. + +##### Parameters +| Name | Type | Description | +| ---- | ---- | ----------- | +| `user_id` | string | The ID of the user to update the key-value pair for. | +| `key` | string | The key to update. | +| `value` | string | The new value to assign to the key. | + +##### Response +
+ + Example Response + + + ```json + { + "success": true + } + ``` +
+ +##### DELETE `/api/v1/users/:user_id/storage/:key` (Requires `Authorization` header) + +Delete a key-value pair for a specific user. + +##### Parameters +| Name | Type | Description | +| ---- | ---- | ----------- | +| `user_id` | string | The ID of the user to delete the key-value pair from. | +| `key` | string | The key to delete. | + +##### Response +
+ + Example Response + + + ```json + { + "success": true + } + ``` +
+ +--- + +## Self-Hosting + +To self-host Lantern, you will need to have the following prerequisites installed on your system: + +- [Node.js](https://nodejs.org/en/download/) +- [pnpm](https://pnpm.io/installation) +- [Git](https://git-scm.com/downloads) +- [MongoDB](https://www.mongodb.com/try/download/community) + +Once you have the prerequisites installed, follow these steps to self-host Lantern: + +1. Clone the repository to your local machine: + +```bash +git clone https://github.com/discordplace/lantern.git +``` + +2. Navigate to the cloned repository: + +```bash +cd lantern +``` + +3. Install the required dependencies: + +```bash +pnpm install +``` + +4. Rename the `.env.example` file to `.env` and fill in the configuration values: + +```env +DISCORD_BOT_TOKEN=your_discord_bot_token +MONGODB_URI=your_mongodb_uri +MONGODB_NAME=your_mongodb_name +KV_TOKEN_ENCRYPTION_SECRET=your_256_bit_encryption_secret +``` + +> [!NOTE] +> - `KV_TOKEN_ENCRYPTION_SECRET` should be a 256-bit encryption secret that you generate. You can use a tool like [this](https://asecuritysite.com/encryption/plain). This secret is used to encrypt the KV storage token. So make sure to keep it secure. + +5. Fill these configuration values in the `config.toml` file with your own values: + +```toml +base_guild_id = 'your_base_guild_id' + +[server] +port = 8000 +``` + +6. Start the server. This builds the client and starts the server in the production environment: + +```bash +pnpm start +``` + +7. The server should now be running on `http://localhost:8000`. You can access the API from this URL. The WebSocket connection is available at `ws://localhost:8000/socket`. + +8. (Optional) To register/unregister bot commands, run the following command: + +```bash +npm run bot:registerCommands +``` + +```bash +npm run bot:unregisterCommands +``` + +> [!NOTE] +> __**Make sure you enable**__ these settings in your bot's application settings: +> - `PRESENCE INTENT` +> - `GUILD MEMBERS INTENT` + +## Contributing + +We welcome contributions from the community! If you'd like to contribute to the project, please follow these guidelines: + +1. Fork the repository and clone it locally. +2. Create a new branch for your feature or bug fix. +3. Make your changes and ensure the code passes any existing tests. +4. Commit your changes with descriptive commit messages that follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard. +5. Push your changes to your fork and submit a pull request to the `main` branch of the original repository. + +Please make sure to follow the [Code of Conduct](.github/CODE_OF_CONDUCT.md) and [Contributing Guidelines](.github/CONTRIBUTING.md) when contributing to this project. + +## Help + +If you encounter any issues with the Lantern or have any questions, feel free to [open an issue](https://github.com/discordplace/lantern/issues) on this repository. We'll do our best to assist you! + +## License + +This project is licensed under [The GNU General Public License v3.0](LICENSE). \ No newline at end of file diff --git a/commitling.config.js b/commitling.config.js new file mode 100644 index 0000000..1e26001 --- /dev/null +++ b/commitling.config.js @@ -0,0 +1,3 @@ +export default { + extends: ['@commitlint/config-conventional'] +}; \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..0c23e0b --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,139 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; +import pluginSecurity from "eslint-plugin-security"; + +export default [ + { + ignores: [ + 'dist/**/*.ts', + 'dist/**', + "**/*.mjs", + "eslint.config.mjs", + "**/*.js" + ] + }, + { + files: ["./src/**/*.ts"] + }, + { + languageOptions: { + globals: globals.node + } + }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + pluginSecurity.configs.recommended, + { + rules: { + indent: [ + 'error', + 2, + { SwitchCase: 1 } + ], + 'linebreak-style': [ + 'error', + process.platform === 'win32' ? 'windows' : 'unix' + ], + quotes: [ + 'error', + 'single' + ], + semi: [ + 'error', + 'always' + ], + 'comma-dangle': [ + 'error', + 'never' + ], + 'no-trailing-spaces': 'error', + 'no-multiple-empty-lines': [ + 'error', + { max: 1 } + ], + 'arrow-spacing': [ + 'error', + { + before: true, + after: true + } + ], + 'object-curly-spacing': [ + 'error', + 'always' + ], + 'key-spacing': [ + 'error', + { + beforeColon: false, + afterColon: true + } + ], + 'space-in-parens': [ + 'error', + 'never' + ], + 'brace-style': [ + 'error', + '1tbs', + { allowSingleLine: true } + ], + 'no-empty-function': 'error', + 'no-lonely-if': 'error', + 'no-useless-return': 'error', + 'spaced-comment': [ + 'error', + 'always', + { markers: ['/'] } + ], + 'func-call-spacing': [ + 'error', + 'never' + ], + 'template-curly-spacing': [ + 'error', + 'never' + ], + 'default-param-last': 'error', + 'newline-before-return': 'error', + 'no-duplicate-imports': [ + 'error', + { includeExports: true } + ], + 'prefer-template': 'error', + 'prefer-arrow-callback': 'error', + 'arrow-parens': [ + 'error', + 'as-needed' + ], + 'no-return-assign': 'error', + 'object-shorthand': 'error', + 'func-style': [ + 'error', + 'declaration', + { allowArrowFunctions: true } + ], + 'array-bracket-spacing': ['error', 'never'], + 'space-infix-ops': 'error', + 'keyword-spacing': [ + 'error', + { before: true, after: true } + ], + 'no-unneeded-ternary': 'error', + 'no-multi-spaces': 'error', + 'security/detect-object-injection': 'off', + 'security/detect-non-literal-fs-filename': 'off', + 'security/detect-unsafe-regex': 'off', + 'security/detect-non-literal-require': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + 'no-var': 'off' + }, + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + } + } + } +]; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..837b4dd --- /dev/null +++ b/package.json @@ -0,0 +1,97 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "lantern", + "version": "1.1.2", + "private": true, + "type": "module", + "scripts": { + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "dev": "cross-env NODE_ENV=development tsx ./src/index.ts", + "build": "rimraf dist && tsc --project tsconfig.json && tsc-alias -p tsconfig.json && npm run copyfiles", + "start": "npm run build && cd dist && cross-env NODE_ENV=production node src/index.js", + "copyfiles": "copyfiles src/config.toml dist/ && copyfiles .env dist/", + "register:commands": "npm run build && cross-env NODE_ENV=development tsx ./src/scripts/registerCommands.ts", + "unregister:commands": "npm run build && cross-env NODE_ENV=development tsx ./src/scripts/unregisterCommands.ts", + "prepare": "husky" + }, + "repository": { + "type": "git", + "url": "https://github.com/discordplace/lantern/tree/main/apps/server" + }, + "bugs": { + "url": "https://github.com/discordplace/lantern/issues" + }, + "contributors": [ + "chimpdev ", + "emirhan " + ], + "license": "GPL-3.0-only", + "devDependencies": { + "@commitlint/cli": "^19.5.0", + "@commitlint/config-conventional": "^19.5.0", + "@eslint/js": "^9.12.0", + "@types/archiver": "^6.0.2", + "@types/async-lock": "^1.4.2", + "@types/compression": "^1.7.5", + "@types/eslint__js": "^8.42.3", + "@types/express": "^5.0.0", + "@types/express-ws": "^3.0.5", + "@types/morgan": "^1.9.9", + "@types/ms": "^0.7.34", + "@types/node": "^22.7.4", + "@types/ws": "^8.5.12", + "@typescript-eslint/eslint-plugin": "^8.11.0", + "@typescript-eslint/parser": "^8.11.0", + "copyfiles": "^2.4.1", + "cross-env": "^7.0.3", + "eslint": "^9.12.0", + "eslint-plugin-security": "^3.0.1", + "globals": "^15.10.0", + "husky": "^9.1.6", + "rimraf": "^5.0.10", + "tsc-alias": "^1.8.10", + "tsx": "^4.19.1", + "typescript": "^5.6.3", + "typescript-eslint": "^8.8.0" + }, + "dependencies": { + "@iarna/toml": "^2.2.5", + "@types/swagger-ui-express": "^4.1.7", + "archiver": "^7.0.1", + "async-lock": "^1.4.1", + "axios": "^1.7.7", + "body-parser": "^1.20.2", + "chalk": "^4.1.0", + "chrono-node": "^2.7.6", + "compression": "^1.7.4", + "cron": "^3.1.7", + "cronstrue": "^2.50.0", + "date-fns": "^4.1.0", + "dedent": "^1.5.3", + "discord-verify": "^1.2.0", + "discord.js": "^14.16.3", + "dotenv": "^16.4.5", + "express": "^5.0.0", + "express-file-routing": "^3.0.3", + "express-validator": "^7.2.0", + "express-ws": "^5.0.2", + "he": "^1.2.0", + "imgur-anonymous-uploader": "^1.1.6", + "moment": "^2.30.1", + "mongodb": "^6.10.0", + "mongoose": "^8.4.3", + "morgan": "^1.10.0", + "ms": "^2.1.3", + "swagger-ui-express": "^5.0.1", + "winston": "^3.13.0", + "winston-daily-rotate-file": "^5.0.0", + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..6f88b78 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4565 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@iarna/toml': + specifier: ^2.2.5 + version: 2.2.5 + '@types/swagger-ui-express': + specifier: ^4.1.7 + version: 4.1.7 + archiver: + specifier: ^7.0.1 + version: 7.0.1 + async-lock: + specifier: ^1.4.1 + version: 1.4.1 + axios: + specifier: ^1.7.7 + version: 1.7.7 + body-parser: + specifier: ^1.20.2 + version: 1.20.3 + chalk: + specifier: ^4.1.0 + version: 4.1.2 + chrono-node: + specifier: ^2.7.6 + version: 2.7.7 + compression: + specifier: ^1.7.4 + version: 1.7.5 + cron: + specifier: ^3.1.7 + version: 3.1.8 + cronstrue: + specifier: ^2.50.0 + version: 2.51.0 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + dedent: + specifier: ^1.5.3 + version: 1.5.3 + discord-verify: + specifier: ^1.2.0 + version: 1.2.0 + discord.js: + specifier: ^14.16.3 + version: 14.16.3 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + express: + specifier: ^5.0.0 + version: 5.0.1 + express-file-routing: + specifier: ^3.0.3 + version: 3.0.3(express@5.0.1) + express-validator: + specifier: ^7.2.0 + version: 7.2.0 + express-ws: + specifier: ^5.0.2 + version: 5.0.2(express@5.0.1) + he: + specifier: ^1.2.0 + version: 1.2.0 + imgur-anonymous-uploader: + specifier: ^1.1.6 + version: 1.1.6 + moment: + specifier: ^2.30.1 + version: 2.30.1 + mongodb: + specifier: ^6.10.0 + version: 6.10.0 + mongoose: + specifier: ^8.4.3 + version: 8.8.0 + morgan: + specifier: ^1.10.0 + version: 1.10.0 + ms: + specifier: ^2.1.3 + version: 2.1.3 + swagger-ui-express: + specifier: ^5.0.1 + version: 5.0.1(express@5.0.1) + winston: + specifier: ^3.13.0 + version: 3.16.0 + winston-daily-rotate-file: + specifier: ^5.0.0 + version: 5.0.0(winston@3.16.0) + ws: + specifier: ^8.18.0 + version: 8.18.0 + zod: + specifier: ^3.23.8 + version: 3.23.8 + devDependencies: + '@commitlint/cli': + specifier: ^19.5.0 + version: 19.5.0(@types/node@22.8.7)(typescript@5.6.3) + '@commitlint/config-conventional': + specifier: ^19.5.0 + version: 19.5.0 + '@eslint/js': + specifier: ^9.12.0 + version: 9.14.0 + '@types/archiver': + specifier: ^6.0.2 + version: 6.0.3 + '@types/async-lock': + specifier: ^1.4.2 + version: 1.4.2 + '@types/compression': + specifier: ^1.7.5 + version: 1.7.5 + '@types/eslint__js': + specifier: ^8.42.3 + version: 8.42.3 + '@types/express': + specifier: ^5.0.0 + version: 5.0.0 + '@types/express-ws': + specifier: ^3.0.5 + version: 3.0.5 + '@types/morgan': + specifier: ^1.9.9 + version: 1.9.9 + '@types/ms': + specifier: ^0.7.34 + version: 0.7.34 + '@types/node': + specifier: ^22.7.4 + version: 22.8.7 + '@types/ws': + specifier: ^8.5.12 + version: 8.5.13 + '@typescript-eslint/eslint-plugin': + specifier: ^8.11.0 + version: 8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) + '@typescript-eslint/parser': + specifier: ^8.11.0 + version: 8.12.2(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) + copyfiles: + specifier: ^2.4.1 + version: 2.4.1 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + eslint: + specifier: ^9.12.0 + version: 9.14.0(jiti@1.21.6) + eslint-plugin-security: + specifier: ^3.0.1 + version: 3.0.1 + globals: + specifier: ^15.10.0 + version: 15.11.0 + husky: + specifier: ^9.1.6 + version: 9.1.6 + rimraf: + specifier: ^5.0.10 + version: 5.0.10 + tsc-alias: + specifier: ^1.8.10 + version: 1.8.10 + tsx: + specifier: ^4.19.1 + version: 4.19.2 + typescript: + specifier: ^5.6.3 + version: 5.6.3 + typescript-eslint: + specifier: ^8.8.0 + version: 8.12.2(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) + +packages: + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@commitlint/cli@19.5.0': + resolution: {integrity: sha512-gaGqSliGwB86MDmAAKAtV9SV1SHdmN8pnGq4EJU4+hLisQ7IFfx4jvU4s+pk6tl0+9bv6yT+CaZkufOinkSJIQ==} + engines: {node: '>=v18'} + hasBin: true + + '@commitlint/config-conventional@19.5.0': + resolution: {integrity: sha512-OBhdtJyHNPryZKg0fFpZNOBM1ZDbntMvqMuSmpfyP86XSfwzGw4CaoYRG4RutUPg0BTK07VMRIkNJT6wi2zthg==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@19.5.0': + resolution: {integrity: sha512-CHtj92H5rdhKt17RmgALhfQt95VayrUo2tSqY9g2w+laAXyk7K/Ef6uPm9tn5qSIwSmrLjKaXK9eiNuxmQrDBw==} + engines: {node: '>=v18'} + + '@commitlint/ensure@19.5.0': + resolution: {integrity: sha512-Kv0pYZeMrdg48bHFEU5KKcccRfKmISSm9MvgIgkpI6m+ohFTB55qZlBW6eYqh/XDfRuIO0x4zSmvBjmOwWTwkg==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@19.5.0': + resolution: {integrity: sha512-aqyGgytXhl2ejlk+/rfgtwpPexYyri4t8/n4ku6rRJoRhGZpLFMqrZ+YaubeGysCP6oz4mMA34YSTaSOKEeNrg==} + engines: {node: '>=v18'} + + '@commitlint/format@19.5.0': + resolution: {integrity: sha512-yNy088miE52stCI3dhG/vvxFo9e4jFkU1Mj3xECfzp/bIS/JUay4491huAlVcffOoMK1cd296q0W92NlER6r3A==} + engines: {node: '>=v18'} + + '@commitlint/is-ignored@19.5.0': + resolution: {integrity: sha512-0XQ7Llsf9iL/ANtwyZ6G0NGp5Y3EQ8eDQSxv/SRcfJ0awlBY4tHFAvwWbw66FVUaWICH7iE5en+FD9TQsokZ5w==} + engines: {node: '>=v18'} + + '@commitlint/lint@19.5.0': + resolution: {integrity: sha512-cAAQwJcRtiBxQWO0eprrAbOurtJz8U6MgYqLz+p9kLElirzSCc0vGMcyCaA1O7AqBuxo11l1XsY3FhOFowLAAg==} + engines: {node: '>=v18'} + + '@commitlint/load@19.5.0': + resolution: {integrity: sha512-INOUhkL/qaKqwcTUvCE8iIUf5XHsEPCLY9looJ/ipzi7jtGhgmtH7OOFiNvwYgH7mA8osUWOUDV8t4E2HAi4xA==} + engines: {node: '>=v18'} + + '@commitlint/message@19.5.0': + resolution: {integrity: sha512-R7AM4YnbxN1Joj1tMfCyBryOC5aNJBdxadTZkuqtWi3Xj0kMdutq16XQwuoGbIzL2Pk62TALV1fZDCv36+JhTQ==} + engines: {node: '>=v18'} + + '@commitlint/parse@19.5.0': + resolution: {integrity: sha512-cZ/IxfAlfWYhAQV0TwcbdR1Oc0/r0Ik1GEessDJ3Lbuma/MRO8FRQX76eurcXtmhJC//rj52ZSZuXUg0oIX0Fw==} + engines: {node: '>=v18'} + + '@commitlint/read@19.5.0': + resolution: {integrity: sha512-TjS3HLPsLsxFPQj6jou8/CZFAmOP2y+6V4PGYt3ihbQKTY1Jnv0QG28WRKl/d1ha6zLODPZqsxLEov52dhR9BQ==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@19.5.0': + resolution: {integrity: sha512-CU/GscZhCUsJwcKTJS9Ndh3AKGZTNFIOoQB2n8CmFnizE0VnEuJoum+COW+C1lNABEeqk6ssfc1Kkalm4bDklA==} + engines: {node: '>=v18'} + + '@commitlint/rules@19.5.0': + resolution: {integrity: sha512-hDW5TPyf/h1/EufSHEKSp6Hs+YVsDMHazfJ2azIk9tHPXS6UqSz1dIRs1gpqS3eMXgtkT7JH6TW4IShdqOwhAw==} + engines: {node: '>=v18'} + + '@commitlint/to-lines@19.5.0': + resolution: {integrity: sha512-R772oj3NHPkodOSRZ9bBVNq224DOxQtNef5Pl8l2M8ZnkkzQfeSTr4uxawV2Sd3ui05dUVzvLNnzenDBO1KBeQ==} + engines: {node: '>=v18'} + + '@commitlint/top-level@19.5.0': + resolution: {integrity: sha512-IP1YLmGAk0yWrImPRRc578I3dDUI5A2UBJx9FbSOjxe9sTlzFiwVJ+zeMLgAtHMtGZsC8LUnzmW1qRemkFU4ng==} + engines: {node: '>=v18'} + + '@commitlint/types@19.5.0': + resolution: {integrity: sha512-DSHae2obMSMkAtTBSOulg5X7/z+rGLxcXQIkg3OmWvY6wifojge5uVMydfhUvs7yQj+V7jNmRZ2Xzl8GJyqRgg==} + engines: {node: '>=v18'} + + '@dabh/diagnostics@2.0.3': + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + + '@discordjs/builders@1.9.0': + resolution: {integrity: sha512-0zx8DePNVvQibh5ly5kCEei5wtPBIUbSoE9n+91Rlladz4tgtFbJ36PZMxxZrTEOQ7AHMZ/b0crT/0fCy6FTKg==} + engines: {node: '>=18'} + + '@discordjs/collection@1.5.3': + resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@2.1.1': + resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} + engines: {node: '>=18'} + + '@discordjs/formatters@0.5.0': + resolution: {integrity: sha512-98b3i+Y19RFq1Xke4NkVY46x8KjJQjldHUuEbCqMvp1F5Iq9HgnGpu91jOi/Ufazhty32eRsKnnzS8n4c+L93g==} + engines: {node: '>=18'} + + '@discordjs/rest@2.4.0': + resolution: {integrity: sha512-Xb2irDqNcq+O8F0/k/NaDp7+t091p+acb51iA4bCKfIn+WFWd6HrNvcsSbMMxIR9NjcMZS6NReTKygqiQN+ntw==} + engines: {node: '>=18'} + + '@discordjs/util@1.1.1': + resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==} + engines: {node: '>=18'} + + '@discordjs/ws@1.1.1': + resolution: {integrity: sha512-PZ+vLpxGCRtmr2RMkqh8Zp+BenUaJqlS6xhgWKEZcgC/vfHLEzpHtKkB0sl3nZWpwtcKk6YWy+pU3okL2I97FA==} + engines: {node: '>=16.11.0'} + + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.18.0': + resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.7.0': + resolution: {integrity: sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.1.0': + resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.14.0': + resolution: {integrity: sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.4': + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.2': + resolution: {integrity: sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.0': + resolution: {integrity: sha512-xnRgu9DxZbkWak/te3fcytNyp8MTbuiZIaueg2rgEvBuN55n04nwLYLU9TX/VVlusc9L2ZNXi99nUFNkHXtr5g==} + engines: {node: '>=18.18'} + + '@iarna/toml@2.2.5': + resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@mongodb-js/saslprep@1.1.9': + resolution: {integrity: sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@sapphire/async-queue@1.5.4': + resolution: {integrity: sha512-id65RxAx34DCk8KAVTPWwcephJSkStiS9M15F87+zvK2gK47wf7yeRIo8WiuKeXQS6bsyo/uQ/t0QW1cLmSb+A==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@sapphire/shapeshift@4.0.0': + resolution: {integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==} + engines: {node: '>=v16'} + + '@sapphire/snowflake@3.5.3': + resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@types/archiver@6.0.3': + resolution: {integrity: sha512-a6wUll6k3zX6qs5KlxIggs1P1JcYJaTCx2gnlr+f0S1yd2DoaEwoIK10HmBaLnZwWneBz+JBm0dwcZu0zECBcQ==} + + '@types/async-lock@1.4.2': + resolution: {integrity: sha512-HlZ6Dcr205BmNhwkdXqrg2vkFMN2PluI7Lgr8In3B3wE5PiQHhjRqtW/lGdVU9gw+sM0JcIDx2AN+cW8oSWIcw==} + + '@types/body-parser@1.19.5': + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + + '@types/compression@1.7.5': + resolution: {integrity: sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/conventional-commits-parser@5.0.0': + resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/eslint__js@8.42.3': + resolution: {integrity: sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/express-serve-static-core@4.19.6': + resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + + '@types/express-serve-static-core@5.0.1': + resolution: {integrity: sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==} + + '@types/express-ws@3.0.5': + resolution: {integrity: sha512-lbWMjoHrm/v85j81UCmb/GNZFO3genxRYBW1Ob7rjRI+zxUBR+4tcFuOpKKsYQ1LYTYiy3356epLeYi/5zxUwA==} + + '@types/express@4.17.21': + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + + '@types/express@5.0.0': + resolution: {integrity: sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==} + + '@types/http-errors@2.0.4': + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/morgan@1.9.9': + resolution: {integrity: sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==} + + '@types/ms@0.7.34': + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + + '@types/node@22.8.7': + resolution: {integrity: sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==} + + '@types/qs@6.9.16': + resolution: {integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/readdir-glob@1.1.5': + resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} + + '@types/send@0.17.4': + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + + '@types/serve-static@1.15.7': + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + + '@types/swagger-ui-express@4.1.7': + resolution: {integrity: sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/webidl-conversions@7.0.3': + resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + + '@types/whatwg-url@11.0.5': + resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} + + '@types/ws@8.5.13': + resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} + + '@typescript-eslint/eslint-plugin@8.12.2': + resolution: {integrity: sha512-gQxbxM8mcxBwaEmWdtLCIGLfixBMHhQjBqR8sVWNTPpcj45WlYL2IObS/DNMLH1DBP0n8qz+aiiLTGfopPEebw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@8.12.2': + resolution: {integrity: sha512-MrvlXNfGPLH3Z+r7Tk+Z5moZAc0dzdVjTgUgwsdGweH7lydysQsnSww3nAmsq8blFuRD5VRlAr9YdEFw3e6PBw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@8.12.2': + resolution: {integrity: sha512-gPLpLtrj9aMHOvxJkSbDBmbRuYdtiEbnvO25bCMza3DhMjTQw0u7Y1M+YR5JPbMsXXnSPuCf5hfq0nEkQDL/JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.12.2': + resolution: {integrity: sha512-bwuU4TAogPI+1q/IJSKuD4shBLc/d2vGcRT588q+jzayQyjVK2X6v/fbR4InY2U2sgf8MEvVCqEWUzYzgBNcGQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@8.12.2': + resolution: {integrity: sha512-VwDwMF1SZ7wPBUZwmMdnDJ6sIFk4K4s+ALKLP6aIQsISkPv8jhiw65sAK6SuWODN/ix+m+HgbYDkH+zLjrzvOA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.12.2': + resolution: {integrity: sha512-mME5MDwGe30Pq9zKPvyduyU86PH7aixwqYR2grTglAdB+AN8xXQ1vFGpYaUSJ5o5P/5znsSBeNcs5g5/2aQwow==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@8.12.2': + resolution: {integrity: sha512-UTTuDIX3fkfAz6iSVa5rTuSfWIYZ6ATtEocQ/umkRSyC9O919lbZ8dcH7mysshrCdrAM03skJOEYaBugxN+M6A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + '@typescript-eslint/visitor-keys@8.12.2': + resolution: {integrity: sha512-PChz8UaKQAVNHghsHcPyx1OMHoFRUEA7rJSK/mDhdq85bk+PLsUHUBqTQTFt18VJZbmxBovM65fezlheQRsSDA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vladfrangu/async_event_emitter@2.4.6': + resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-flatten@3.0.0: + resolution: {integrity: sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==} + + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + + b4a@1.6.7: + resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bare-events@2.5.0: + resolution: {integrity: sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + body-parser@2.0.2: + resolution: {integrity: sha512-SNMk0OONlQ01uk8EPeiBvTW7W4ovpL5b1O3t1sjpPgfxOQ6BqQJ6XjxinDPR79Z6HdcD5zBBwr5ssiTlgdNztQ==} + engines: {node: '>=18'} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + bson@6.9.0: + resolution: {integrity: sha512-X9hJeyeM0//Fus+0pc5dSUMhhrrmWwQUtdavaQeF3Ta6m69matZkGWV/MrBcnwUeLC8W9kwwc2hfkZgUuCX3Ig==} + engines: {node: '>=16.20.1'} + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chrono-node@2.7.7: + resolution: {integrity: sha512-p3S7gotuTPu5oqhRL2p1fLwQXGgdQaRTtWR3e8Di9P1Pa9mzkK5DWR5AWBieMUh2ZdOnPgrK+zCrbbtyuA+D/Q==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + + colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.7.5: + resolution: {integrity: sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==} + engines: {node: '>= 0.8.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + conventional-changelog-angular@7.0.0: + resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} + engines: {node: '>=16'} + + conventional-changelog-conventionalcommits@7.0.2: + resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} + engines: {node: '>=16'} + + conventional-commits-parser@5.0.0: + resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} + engines: {node: '>=16'} + hasBin: true + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + copyfiles@2.4.1: + resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==} + hasBin: true + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cosmiconfig-typescript-loader@5.1.0: + resolution: {integrity: sha512-7PtBB+6FdsOvZyJtlF3hEPpACq7RQX6BVGsgC7/lfVXnKMvNCu/XY3ykreqG5w/rBNdu2z8LCIKoF3kpHHdHlA==} + engines: {node: '>=v16'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=8.2' + typescript: '>=4' + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + + cron@3.1.8: + resolution: {integrity: sha512-45bqmAOSd/XB5JJWfV1W59fFEzqgNNWmOYQZVcw0sfyQqU35HFdVfTsr2xzlqWoTAfspRrvK0lSSLj8Pj9YmpQ==} + + cronstrue@2.51.0: + resolution: {integrity: sha512-7EG9VaZZ5SRbZ7m25dmP6xaS0qe9ay6wywMskFOU/lMDKa+3gZr2oeT5OUfXwRP/Bcj8wxdYJ65AHU70CI3tsw==} + hasBin: true + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + dargs@8.1.0: + resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} + engines: {node: '>=12'} + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.1.0: + resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.6: + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + discord-api-types@0.37.100: + resolution: {integrity: sha512-a8zvUI0GYYwDtScfRd/TtaNBDTXwP5DiDVX7K5OmE+DRT57gBqKnwtOC5Ol8z0mRW8KQfETIgiB8U0YZ9NXiCA==} + + discord-api-types@0.37.83: + resolution: {integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==} + + discord-api-types@0.37.97: + resolution: {integrity: sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==} + + discord-verify@1.2.0: + resolution: {integrity: sha512-8qlrMROW8DhpzWWzgNq9kpeLDxKanWa4EDVoj/ASVv2nr+dSr4JPmu2tFSydf3hAGI/OIJTnZyD0JulMYIxx4w==} + engines: {node: '>=16'} + + discord.js@14.16.3: + resolution: {integrity: sha512-EPCWE9OkA9DnFFNrO7Kl1WHHDYFXu3CNVFJg63bfU7hVtjZGyhShwZtSBImINQRWxWP2tgo2XI+QhdXx28r0aA==} + engines: {node: '>=18'} + + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-security@3.0.1: + resolution: {integrity: sha512-XjVGBhtDZJfyuhIxnQ/WMm385RbX3DBu7H1J7HNNhmB2tnGxMeqVSnYv79oAj992ayvIBZghsymwkYFS6cGH4Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.14.0: + resolution: {integrity: sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + express-file-routing@3.0.3: + resolution: {integrity: sha512-0936IhPc64moJHAG1Fz3SLCi0Upt6ahsbocrC6hdPUgmHY2018wjVrbln0HP67JoWFUdmmzypYiIHjF898s0hA==} + peerDependencies: + express: '>4.1.2' + + express-validator@7.2.0: + resolution: {integrity: sha512-I2ByKD8panjtr8Y05l21Wph9xk7kk64UMyvJCl/fFM/3CTJq8isXYPLeKW/aZBCdb/LYNv63PwhY8khw8VWocA==} + engines: {node: '>= 8.0.0'} + + express-ws@5.0.2: + resolution: {integrity: sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==} + engines: {node: '>=4.5.0'} + peerDependencies: + express: ^4.0.0 || ^5.0.0-alpha.1 + + express@5.0.1: + resolution: {integrity: sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.0.3: + resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-stream-rotator@0.6.1: + resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} + + file-type@16.5.4: + resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} + engines: {node: '>=10'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.0.0: + resolution: {integrity: sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ==} + engines: {node: '>= 0.8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + + git-raw-commits@4.0.0: + resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} + engines: {node: '>=16'} + hasBin: true + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.11.0: + resolution: {integrity: sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==} + engines: {node: '>=18'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + husky@9.1.6: + resolution: {integrity: sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==} + engines: {node: '>=18'} + hasBin: true + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.5.2: + resolution: {integrity: sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + imgur-anonymous-uploader@1.1.6: + resolution: {integrity: sha512-/DVy38yu9s2n9q9E3XVGUZGtNFA5jEpM8ryHABnyyJxqi7hasbHoJE9iPlQJR7v9l60ZPPRr1ICqVIKusgSZIQ==} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-text-path@2.0.0: + resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} + engines: {node: '>=8'} + + isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + kareem@2.6.3: + resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==} + engines: {node: '>=12.0.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + logform@2.6.1: + resolution: {integrity: sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==} + engines: {node: '>= 12.0.0'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + + magic-bytes.js@1.10.0: + resolution: {integrity: sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + + meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.53.0: + resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.0: + resolution: {integrity: sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==} + engines: {node: '>= 0.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + mongodb-connection-string-url@3.0.1: + resolution: {integrity: sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==} + + mongodb@6.10.0: + resolution: {integrity: sha512-gP9vduuYWb9ZkDM546M+MP2qKVk5ZG2wPF63OvSRuUbqCR+11ZCAE1mOfllhlAG0wcoJY5yDL/rV3OmYEwXIzg==} + engines: {node: '>=16.20.1'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.188.0 + '@mongodb-js/zstd': ^1.1.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' + snappy: ^7.2.2 + socks: ^2.7.1 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + + mongoose@8.8.0: + resolution: {integrity: sha512-KluvgwnQB1GPOYZZXUHJRjS1TW6xxwTlf/YgjWExuuNanIe3W7VcR7dDXQVCIRk8L7NYge8EnoTcu2grWtN+XQ==} + engines: {node: '>=16.20.1'} + + morgan@1.10.0: + resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} + engines: {node: '>= 0.8.0'} + + mpath@0.9.0: + resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==} + engines: {node: '>=4.0.0'} + + mquery@5.0.0: + resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} + engines: {node: '>=14.0.0'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mylas@2.1.13: + resolution: {integrity: sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==} + engines: {node: '>=12.0.0'} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + noms@0.0.0: + resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.2: + resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} + engines: {node: '>= 0.4'} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + peek-readable@4.1.0: + resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + + readable-stream@1.0.34: + resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.5.2: + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readable-web-to-node-stream@3.0.2: + resolution: {integrity: sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==} + engines: {node: '>=8'} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + + router@2.0.0: + resolution: {integrity: sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ==} + engines: {node: '>= 0.10'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex@2.1.1: + resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + send@1.1.0: + resolution: {integrity: sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==} + engines: {node: '>= 18'} + + serve-static@2.1.0: + resolution: {integrity: sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==} + engines: {node: '>= 18'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + + sift@17.1.3: + resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + sparse-bitfield@3.0.3: + resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + streamx@2.20.1: + resolution: {integrity: sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strtok3@6.3.0: + resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} + engines: {node: '>=10'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + swagger-ui-dist@5.17.14: + resolution: {integrity: sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==} + + swagger-ui-express@5.0.1: + resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==} + engines: {node: '>= v0.10.32'} + peerDependencies: + express: '>=4.0.0 || >=5.0.0-beta' + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + + text-decoder@1.2.1: + resolution: {integrity: sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==} + + text-extensions@2.4.0: + resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} + engines: {node: '>=8'} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tinyexec@0.3.1: + resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@4.2.1: + resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} + engines: {node: '>=10'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@4.1.1: + resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} + engines: {node: '>=14'} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + ts-api-utils@1.4.0: + resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-mixer@6.0.4: + resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} + + tsc-alias@1.8.10: + resolution: {integrity: sha512-Ibv4KAWfFkFdKJxnWfVtdOmB0Zi1RJVxcbPGiCDsFpCQSsmpWyuzHG3rQyI5YkobWwxFPEyQfu1hdo4qLG2zPw==} + hasBin: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.19.2: + resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.0.0: + resolution: {integrity: sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==} + engines: {node: '>= 0.6'} + + typescript-eslint@8.12.2: + resolution: {integrity: sha512-UbuVUWSrHVR03q9CWx+JDHeO6B/Hr9p4U5lRH++5tq/EbFq1faYZe50ZSBePptgfIKLEti0aPQ3hFgnPVcd8ZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + undici@6.19.8: + resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==} + engines: {node: '>=18.17'} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-url@13.0.0: + resolution: {integrity: sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==} + engines: {node: '>=16'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + winston-daily-rotate-file@5.0.0: + resolution: {integrity: sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==} + engines: {node: '>=8'} + peerDependencies: + winston: ^3 + + winston-transport@4.8.0: + resolution: {integrity: sha512-qxSTKswC6llEMZKgCQdaWgDuMJQnhuvF5f2Nk3SNXc4byfQ+voo2mX1Px9dkNOuR8p0KAjfPG29PuYUSIb+vSA==} + engines: {node: '>= 12.0.0'} + + winston@3.16.0: + resolution: {integrity: sha512-xz7+cyGN5M+4CmmD4Npq1/4T+UZaz7HaeTlAruFUTjk79CNMq+P6H30vlE4z0qfqJ01VHYQwd7OZo03nYm/+lg==} + engines: {node: '>= 12.0.0'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.1.1: + resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} + engines: {node: '>=12.20'} + + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + +snapshots: + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.25.9': {} + + '@colors/colors@1.6.0': {} + + '@commitlint/cli@19.5.0(@types/node@22.8.7)(typescript@5.6.3)': + dependencies: + '@commitlint/format': 19.5.0 + '@commitlint/lint': 19.5.0 + '@commitlint/load': 19.5.0(@types/node@22.8.7)(typescript@5.6.3) + '@commitlint/read': 19.5.0 + '@commitlint/types': 19.5.0 + tinyexec: 0.3.1 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/config-conventional@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + conventional-changelog-conventionalcommits: 7.0.2 + + '@commitlint/config-validator@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + ajv: 8.17.1 + + '@commitlint/ensure@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + + '@commitlint/execute-rule@19.5.0': {} + + '@commitlint/format@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + chalk: 5.3.0 + + '@commitlint/is-ignored@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + semver: 7.6.3 + + '@commitlint/lint@19.5.0': + dependencies: + '@commitlint/is-ignored': 19.5.0 + '@commitlint/parse': 19.5.0 + '@commitlint/rules': 19.5.0 + '@commitlint/types': 19.5.0 + + '@commitlint/load@19.5.0(@types/node@22.8.7)(typescript@5.6.3)': + dependencies: + '@commitlint/config-validator': 19.5.0 + '@commitlint/execute-rule': 19.5.0 + '@commitlint/resolve-extends': 19.5.0 + '@commitlint/types': 19.5.0 + chalk: 5.3.0 + cosmiconfig: 9.0.0(typescript@5.6.3) + cosmiconfig-typescript-loader: 5.1.0(@types/node@22.8.7)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/message@19.5.0': {} + + '@commitlint/parse@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + conventional-changelog-angular: 7.0.0 + conventional-commits-parser: 5.0.0 + + '@commitlint/read@19.5.0': + dependencies: + '@commitlint/top-level': 19.5.0 + '@commitlint/types': 19.5.0 + git-raw-commits: 4.0.0 + minimist: 1.2.8 + tinyexec: 0.3.1 + + '@commitlint/resolve-extends@19.5.0': + dependencies: + '@commitlint/config-validator': 19.5.0 + '@commitlint/types': 19.5.0 + global-directory: 4.0.1 + import-meta-resolve: 4.1.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + + '@commitlint/rules@19.5.0': + dependencies: + '@commitlint/ensure': 19.5.0 + '@commitlint/message': 19.5.0 + '@commitlint/to-lines': 19.5.0 + '@commitlint/types': 19.5.0 + + '@commitlint/to-lines@19.5.0': {} + + '@commitlint/top-level@19.5.0': + dependencies: + find-up: 7.0.0 + + '@commitlint/types@19.5.0': + dependencies: + '@types/conventional-commits-parser': 5.0.0 + chalk: 5.3.0 + + '@dabh/diagnostics@2.0.3': + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + + '@discordjs/builders@1.9.0': + dependencies: + '@discordjs/formatters': 0.5.0 + '@discordjs/util': 1.1.1 + '@sapphire/shapeshift': 4.0.0 + discord-api-types: 0.37.97 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.4 + tslib: 2.8.1 + + '@discordjs/collection@1.5.3': {} + + '@discordjs/collection@2.1.1': {} + + '@discordjs/formatters@0.5.0': + dependencies: + discord-api-types: 0.37.97 + + '@discordjs/rest@2.4.0': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/util': 1.1.1 + '@sapphire/async-queue': 1.5.4 + '@sapphire/snowflake': 3.5.3 + '@vladfrangu/async_event_emitter': 2.4.6 + discord-api-types: 0.37.97 + magic-bytes.js: 1.10.0 + tslib: 2.8.1 + undici: 6.19.8 + + '@discordjs/util@1.1.1': {} + + '@discordjs/ws@1.1.1': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/rest': 2.4.0 + '@discordjs/util': 1.1.1 + '@sapphire/async-queue': 1.5.4 + '@types/ws': 8.5.13 + '@vladfrangu/async_event_emitter': 2.4.6 + discord-api-types: 0.37.83 + tslib: 2.8.1 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@esbuild/aix-ppc64@0.23.1': + optional: true + + '@esbuild/android-arm64@0.23.1': + optional: true + + '@esbuild/android-arm@0.23.1': + optional: true + + '@esbuild/android-x64@0.23.1': + optional: true + + '@esbuild/darwin-arm64@0.23.1': + optional: true + + '@esbuild/darwin-x64@0.23.1': + optional: true + + '@esbuild/freebsd-arm64@0.23.1': + optional: true + + '@esbuild/freebsd-x64@0.23.1': + optional: true + + '@esbuild/linux-arm64@0.23.1': + optional: true + + '@esbuild/linux-arm@0.23.1': + optional: true + + '@esbuild/linux-ia32@0.23.1': + optional: true + + '@esbuild/linux-loong64@0.23.1': + optional: true + + '@esbuild/linux-mips64el@0.23.1': + optional: true + + '@esbuild/linux-ppc64@0.23.1': + optional: true + + '@esbuild/linux-riscv64@0.23.1': + optional: true + + '@esbuild/linux-s390x@0.23.1': + optional: true + + '@esbuild/linux-x64@0.23.1': + optional: true + + '@esbuild/netbsd-x64@0.23.1': + optional: true + + '@esbuild/openbsd-arm64@0.23.1': + optional: true + + '@esbuild/openbsd-x64@0.23.1': + optional: true + + '@esbuild/sunos-x64@0.23.1': + optional: true + + '@esbuild/win32-arm64@0.23.1': + optional: true + + '@esbuild/win32-ia32@0.23.1': + optional: true + + '@esbuild/win32-x64@0.23.1': + optional: true + + '@eslint-community/eslint-utils@4.4.1(eslint@9.14.0(jiti@1.21.6))': + dependencies: + eslint: 9.14.0(jiti@1.21.6) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.18.0': + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/core@0.7.0': {} + + '@eslint/eslintrc@3.1.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.14.0': {} + + '@eslint/object-schema@2.1.4': {} + + '@eslint/plugin-kit@0.2.2': + dependencies: + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.0': {} + + '@iarna/toml@2.2.5': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@mongodb-js/saslprep@1.1.9': + dependencies: + sparse-bitfield: 3.0.3 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@sapphire/async-queue@1.5.4': {} + + '@sapphire/shapeshift@4.0.0': + dependencies: + fast-deep-equal: 3.1.3 + lodash: 4.17.21 + + '@sapphire/snowflake@3.5.3': {} + + '@tokenizer/token@0.3.0': {} + + '@types/archiver@6.0.3': + dependencies: + '@types/readdir-glob': 1.1.5 + + '@types/async-lock@1.4.2': {} + + '@types/body-parser@1.19.5': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.8.7 + + '@types/compression@1.7.5': + dependencies: + '@types/express': 5.0.0 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.8.7 + + '@types/conventional-commits-parser@5.0.0': + dependencies: + '@types/node': 22.8.7 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + + '@types/eslint__js@8.42.3': + dependencies: + '@types/eslint': 9.6.1 + + '@types/estree@1.0.6': {} + + '@types/express-serve-static-core@4.19.6': + dependencies: + '@types/node': 22.8.7 + '@types/qs': 6.9.16 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + '@types/express-serve-static-core@5.0.1': + dependencies: + '@types/node': 22.8.7 + '@types/qs': 6.9.16 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + '@types/express-ws@3.0.5': + dependencies: + '@types/express': 5.0.0 + '@types/express-serve-static-core': 5.0.1 + '@types/ws': 8.5.13 + + '@types/express@4.17.21': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.19.6 + '@types/qs': 6.9.16 + '@types/serve-static': 1.15.7 + + '@types/express@5.0.0': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 5.0.1 + '@types/qs': 6.9.16 + '@types/serve-static': 1.15.7 + + '@types/http-errors@2.0.4': {} + + '@types/json-schema@7.0.15': {} + + '@types/luxon@3.4.2': {} + + '@types/mime@1.3.5': {} + + '@types/morgan@1.9.9': + dependencies: + '@types/node': 22.8.7 + + '@types/ms@0.7.34': {} + + '@types/node@22.8.7': + dependencies: + undici-types: 6.19.8 + + '@types/qs@6.9.16': {} + + '@types/range-parser@1.2.7': {} + + '@types/readdir-glob@1.1.5': + dependencies: + '@types/node': 22.8.7 + + '@types/send@0.17.4': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.8.7 + + '@types/serve-static@1.15.7': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 22.8.7 + '@types/send': 0.17.4 + + '@types/swagger-ui-express@4.1.7': + dependencies: + '@types/express': 5.0.0 + '@types/serve-static': 1.15.7 + + '@types/triple-beam@1.3.5': {} + + '@types/webidl-conversions@7.0.3': {} + + '@types/whatwg-url@11.0.5': + dependencies: + '@types/webidl-conversions': 7.0.3 + + '@types/ws@8.5.13': + dependencies: + '@types/node': 22.8.7 + + '@typescript-eslint/eslint-plugin@8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.12.2(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.12.2 + '@typescript-eslint/type-utils': 8.12.2(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) + '@typescript-eslint/utils': 8.12.2(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.12.2 + eslint: 9.14.0(jiti@1.21.6) + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.12.2(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.12.2 + '@typescript-eslint/types': 8.12.2 + '@typescript-eslint/typescript-estree': 8.12.2(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.12.2 + debug: 4.3.7 + eslint: 9.14.0(jiti@1.21.6) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.12.2': + dependencies: + '@typescript-eslint/types': 8.12.2 + '@typescript-eslint/visitor-keys': 8.12.2 + + '@typescript-eslint/type-utils@8.12.2(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.12.2(typescript@5.6.3) + '@typescript-eslint/utils': 8.12.2(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) + debug: 4.3.7 + ts-api-utils: 1.4.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - eslint + - supports-color + + '@typescript-eslint/types@8.12.2': {} + + '@typescript-eslint/typescript-estree@8.12.2(typescript@5.6.3)': + dependencies: + '@typescript-eslint/types': 8.12.2 + '@typescript-eslint/visitor-keys': 8.12.2 + debug: 4.3.7 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.4.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.12.2(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0(jiti@1.21.6)) + '@typescript-eslint/scope-manager': 8.12.2 + '@typescript-eslint/types': 8.12.2 + '@typescript-eslint/typescript-estree': 8.12.2(typescript@5.6.3) + eslint: 9.14.0(jiti@1.21.6) + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@8.12.2': + dependencies: + '@typescript-eslint/types': 8.12.2 + eslint-visitor-keys: 3.4.3 + + '@vladfrangu/async_event_emitter@2.4.6': {} + + JSONStream@1.3.5: + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.0 + negotiator: 1.0.0 + + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + archiver-utils@5.0.2: + dependencies: + glob: 10.4.5 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.21 + normalize-path: 3.0.0 + readable-stream: 4.5.2 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.5.2 + readdir-glob: 1.1.3 + tar-stream: 3.1.7 + zip-stream: 6.0.1 + + argparse@2.0.1: {} + + array-flatten@3.0.0: {} + + array-ify@1.0.0: {} + + array-union@2.1.0: {} + + async-lock@1.4.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + axios@1.7.7: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + b4a@1.6.7: {} + + balanced-match@1.0.2: {} + + bare-events@2.5.0: + optional: true + + base64-js@1.5.1: {} + + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + + binary-extensions@2.3.0: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + body-parser@2.0.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 3.1.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.5.2 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 3.0.0 + type-is: 1.6.18 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + bson@6.9.0: {} + + buffer-crc32@1.0.0: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bytes@3.1.2: {} + + call-bind@1.0.7: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + + callsites@3.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.3.0: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chrono-node@2.7.7: + dependencies: + dayjs: 1.11.13 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + + colorspace@1.1.4: + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@9.5.0: {} + + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.5.2 + + compressible@2.0.18: + dependencies: + mime-db: 1.53.0 + + compression@1.7.5: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.0.2 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + concat-map@0.0.1: {} + + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + conventional-changelog-angular@7.0.0: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-conventionalcommits@7.0.2: + dependencies: + compare-func: 2.0.0 + + conventional-commits-parser@5.0.0: + dependencies: + JSONStream: 1.3.5 + is-text-path: 2.0.0 + meow: 12.1.1 + split2: 4.2.0 + + cookie-signature@1.2.2: {} + + cookie@0.7.1: {} + + copyfiles@2.4.1: + dependencies: + glob: 7.2.3 + minimatch: 3.1.2 + mkdirp: 1.0.4 + noms: 0.0.0 + through2: 2.0.5 + untildify: 4.0.0 + yargs: 16.2.0 + + core-util-is@1.0.3: {} + + cosmiconfig-typescript-loader@5.1.0(@types/node@22.8.7)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3): + dependencies: + '@types/node': 22.8.7 + cosmiconfig: 9.0.0(typescript@5.6.3) + jiti: 1.21.6 + typescript: 5.6.3 + + cosmiconfig@9.0.0(typescript@5.6.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.6.3 + + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.5.2 + + cron@3.1.8: + dependencies: + '@types/luxon': 3.4.2 + luxon: 3.5.0 + + cronstrue@2.51.0: {} + + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.3 + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + dargs@8.1.0: {} + + date-fns@4.1.0: {} + + dayjs@1.11.13: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.1.0: + dependencies: + ms: 2.0.0 + + debug@4.3.6: + dependencies: + ms: 2.1.2 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + dedent@1.5.3: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + destroy@1.2.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + discord-api-types@0.37.100: {} + + discord-api-types@0.37.83: {} + + discord-api-types@0.37.97: {} + + discord-verify@1.2.0: + dependencies: + '@types/express': 4.17.21 + + discord.js@14.16.3: + dependencies: + '@discordjs/builders': 1.9.0 + '@discordjs/collection': 1.5.3 + '@discordjs/formatters': 0.5.0 + '@discordjs/rest': 2.4.0 + '@discordjs/util': 1.1.1 + '@discordjs/ws': 1.1.1 + '@sapphire/snowflake': 3.5.3 + discord-api-types: 0.37.100 + fast-deep-equal: 3.1.3 + lodash.snakecase: 4.1.1 + tslib: 2.8.1 + undici: 6.19.8 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + dotenv@16.4.5: {} + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + enabled@2.0.0: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + env-paths@2.2.1: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 + + es-errors@1.3.0: {} + + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-security@3.0.1: + dependencies: + safe-regex: 2.1.1 + + eslint-scope@8.2.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.14.0(jiti@1.21.6): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0(jiti@1.21.6)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.18.0 + '@eslint/core': 0.7.0 + '@eslint/eslintrc': 3.1.0 + '@eslint/js': 9.14.0 + '@eslint/plugin-kit': 0.2.2 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.0 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.7 + escape-string-regexp: 4.0.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + text-table: 0.2.0 + optionalDependencies: + jiti: 1.21.6 + transitivePeerDependencies: + - supports-color + + espree@10.3.0: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + events@3.3.0: {} + + express-file-routing@3.0.3(express@5.0.1): + dependencies: + express: 5.0.1 + + express-validator@7.2.0: + dependencies: + lodash: 4.17.21 + validator: 13.12.0 + + express-ws@5.0.2(express@5.0.1): + dependencies: + express: 5.0.1 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + express@5.0.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.0.2 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.2.2 + debug: 4.3.6 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.0.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + methods: 1.1.2 + mime-types: 3.0.0 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + router: 2.0.0 + safe-buffer: 5.2.1 + send: 1.1.0 + serve-static: 2.1.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 2.0.0 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-fifo@1.3.2: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.0.3: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fecha@4.2.3: {} + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-stream-rotator@0.6.1: + dependencies: + moment: 2.30.1 + + file-type@16.5.4: + dependencies: + readable-web-to-node-stream: 3.0.2 + strtok3: 6.3.0 + token-types: 4.2.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.0.0: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + + flatted@3.3.1: {} + + fn.name@1.1.0: {} + + follow-redirects@1.15.9: {} + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + fresh@2.0.0: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.2.4: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + git-raw-commits@4.0.0: + dependencies: + dargs: 8.1.0 + meow: 12.1.1 + split2: 4.2.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + + globals@14.0.0: {} + + globals@15.11.0: {} + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.0.1: + dependencies: + get-intrinsic: 1.2.4 + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.0 + + has-proto@1.0.3: {} + + has-symbols@1.0.3: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + husky@9.1.6: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.5.2: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + imgur-anonymous-uploader@1.1.6: + dependencies: + file-type: 16.5.4 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-meta-resolve@4.1.0: {} + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@4.1.1: {} + + ipaddr.js@1.9.1: {} + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.2: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-obj@2.0.0: {} + + is-promise@4.0.0: {} + + is-stream@2.0.1: {} + + is-text-path@2.0.0: + dependencies: + text-extensions: 2.4.0 + + isarray@0.0.1: {} + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@1.21.6: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jsonparse@1.3.1: {} + + kareem@2.6.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kuler@2.0.0: {} + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lines-and-columns@1.2.4: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash.camelcase@4.3.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.kebabcase@4.1.1: {} + + lodash.merge@4.6.2: {} + + lodash.mergewith@4.6.2: {} + + lodash.snakecase@4.1.1: {} + + lodash.startcase@4.4.0: {} + + lodash.uniq@4.5.0: {} + + lodash.upperfirst@4.3.1: {} + + lodash@4.17.21: {} + + logform@2.6.1: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + lru-cache@10.4.3: {} + + luxon@3.5.0: {} + + magic-bytes.js@1.10.0: {} + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + memory-pager@1.5.0: {} + + meow@12.1.1: {} + + merge-descriptors@2.0.0: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-db@1.53.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.0: + dependencies: + mime-db: 1.53.0 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + mkdirp@1.0.4: {} + + moment@2.30.1: {} + + mongodb-connection-string-url@3.0.1: + dependencies: + '@types/whatwg-url': 11.0.5 + whatwg-url: 13.0.0 + + mongodb@6.10.0: + dependencies: + '@mongodb-js/saslprep': 1.1.9 + bson: 6.9.0 + mongodb-connection-string-url: 3.0.1 + + mongoose@8.8.0: + dependencies: + bson: 6.9.0 + kareem: 2.6.3 + mongodb: 6.10.0 + mpath: 0.9.0 + mquery: 5.0.0 + ms: 2.1.3 + sift: 17.1.3 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - gcp-metadata + - kerberos + - mongodb-client-encryption + - snappy + - socks + - supports-color + + morgan@1.10.0: + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.0.2 + transitivePeerDependencies: + - supports-color + + mpath@0.9.0: {} + + mquery@5.0.0: + dependencies: + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + ms@2.0.0: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + mylas@2.1.13: {} + + natural-compare@1.4.0: {} + + negotiator@0.6.4: {} + + negotiator@1.0.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + noms@0.0.0: + dependencies: + inherits: 2.0.4 + readable-stream: 1.0.34 + + normalize-path@3.0.0: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.2: {} + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.0.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.1.1 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-to-regexp@8.2.0: {} + + path-type@4.0.0: {} + + peek-readable@4.1.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + plimit-lit@1.6.1: + dependencies: + queue-lit: 1.5.2 + + prelude-ls@1.2.1: {} + + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + qs@6.13.0: + dependencies: + side-channel: 1.0.6 + + queue-lit@1.5.2: {} + + queue-microtask@1.2.3: {} + + queue-tick@1.0.1: {} + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + + readable-stream@1.0.34: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.5.2: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readable-web-to-node-stream@3.0.2: + dependencies: + readable-stream: 3.6.2 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + regexp-tree@0.1.27: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + reusify@1.0.4: {} + + rimraf@5.0.10: + dependencies: + glob: 10.4.5 + + router@2.0.0: + dependencies: + array-flatten: 3.0.0 + is-promise: 4.0.0 + methods: 1.1.2 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + setprototypeof: 1.2.0 + utils-merge: 1.0.1 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-regex@2.1.1: + dependencies: + regexp-tree: 0.1.27 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + semver@7.6.3: {} + + send@1.1.0: + dependencies: + debug: 4.3.6 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime-types: 2.1.35 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@2.1.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.1.0 + transitivePeerDependencies: + - supports-color + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel@1.0.6: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.2 + + sift@17.1.3: {} + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + slash@3.0.0: {} + + sparse-bitfield@3.0.3: + dependencies: + memory-pager: 1.5.0 + + split2@4.2.0: {} + + stack-trace@0.0.10: {} + + statuses@2.0.1: {} + + streamx@2.20.1: + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + text-decoder: 1.2.1 + optionalDependencies: + bare-events: 2.5.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string_decoder@0.10.31: {} + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-json-comments@3.1.1: {} + + strtok3@6.3.0: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 4.1.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + swagger-ui-dist@5.17.14: {} + + swagger-ui-express@5.0.1(express@5.0.1): + dependencies: + express: 5.0.1 + swagger-ui-dist: 5.17.14 + + tar-stream@3.1.7: + dependencies: + b4a: 1.6.7 + fast-fifo: 1.3.2 + streamx: 2.20.1 + + text-decoder@1.2.1: {} + + text-extensions@2.4.0: {} + + text-hex@1.0.0: {} + + text-table@0.2.0: {} + + through2@2.0.5: + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + + through@2.3.8: {} + + tinyexec@0.3.1: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + token-types@4.2.1: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + tr46@0.0.3: {} + + tr46@4.1.1: + dependencies: + punycode: 2.3.1 + + triple-beam@1.4.1: {} + + ts-api-utils@1.4.0(typescript@5.6.3): + dependencies: + typescript: 5.6.3 + + ts-mixer@6.0.4: {} + + tsc-alias@1.8.10: + dependencies: + chokidar: 3.6.0 + commander: 9.5.0 + globby: 11.1.0 + mylas: 2.1.13 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 + + tslib@2.8.1: {} + + tsx@4.19.2: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.0.0: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.0 + + typescript-eslint@8.12.2(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) + '@typescript-eslint/parser': 8.12.2(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) + '@typescript-eslint/utils': 8.12.2(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - eslint + - supports-color + + typescript@5.6.3: {} + + undici-types@6.19.8: {} + + undici@6.19.8: {} + + unicorn-magic@0.1.0: {} + + unpipe@1.0.0: {} + + untildify@4.0.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + validator@13.12.0: {} + + vary@1.1.2: {} + + webidl-conversions@3.0.1: {} + + webidl-conversions@7.0.0: {} + + whatwg-url@13.0.0: + dependencies: + tr46: 4.1.1 + webidl-conversions: 7.0.0 + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + winston-daily-rotate-file@5.0.0(winston@3.16.0): + dependencies: + file-stream-rotator: 0.6.1 + object-hash: 3.0.0 + triple-beam: 1.4.1 + winston: 3.16.0 + winston-transport: 4.8.0 + + winston-transport@4.8.0: + dependencies: + logform: 2.6.1 + readable-stream: 4.5.2 + triple-beam: 1.4.1 + + winston@3.16.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.6.1 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.8.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + ws@7.5.10: {} + + ws@8.18.0: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yargs-parser@20.2.9: {} + + yargs-parser@21.1.1: {} + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yocto-queue@1.1.1: {} + + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.5.2 + + zod@3.23.8: {} diff --git a/src/config.toml b/src/config.toml new file mode 100644 index 0000000..96536a1 --- /dev/null +++ b/src/config.toml @@ -0,0 +1,68 @@ +bypass_command_permissions_check = ['957840712404193290'] + +base_guild_id = '1253092552404631582' +max_bulk_get_users_size = 50 + +[server] +port = 3003 + +[server.socket] +heartbeat_interval = 30000 +maxpayload = 1024 +clienttracking = true +keepalive = true +# Opcodes that the server can receive from the client +# INIT | HEARTBEAT | SUBSCRIBE | UNSUBSCRIBE +client_allowed_opcodes = [2, 4, 12, 14] + +[server.socket.opcodes] +HELLO = 1 +INIT = 2 +INIT_ACK = 3 +HEARTBEAT = 4 +HEARTBEAT_ACK = 5 +PRESENCE_UPDATE = 6 +USER_LEFT = 7 +USER_JOINED = 8 +DISCONNECT = 9 +STORAGE_UPDATE = 10 +ERROR = 11 +SUBSCRIBE = 12 +SUBSCRIBE_ACK = 13 +UNSUBSCRIBE = 14 +UNSUBSCRIBE_ACK = 15 + +[logger.levels] +error = 'red' +warn = 'yellow' +info = 'blue' +debug = 'grey' +database = 'magenta' +bot = 'green' +http = 'yellow' +socket = 'cyan' + +[database.backup] +output_dir = './database-backups' +enabled = false +discord_channel = '' +cron_pattern = '0 0 * * *' +exclude_collections = ['evaluateresults', 'messages'] + +[user_svg_card.colors.dark] + background = '#17171c' + background_secondary = '#1e1e24' + card = '#1e1e24' + +[user_svg_card.colors.dark.text] + primary = '#ffffff' + secondary = '#999999' + +[user_svg_card.colors.light] + background = '#e6e6e6' + background_secondary = '#dcdcdc' + card = '#eeeeee' + +[user_svg_card.colors.light.text] + primary = '#242a31' + secondary = '#6c757d' \ No newline at end of file diff --git a/src/example.config.toml b/src/example.config.toml new file mode 100644 index 0000000..ead086f --- /dev/null +++ b/src/example.config.toml @@ -0,0 +1,68 @@ +bypass_command_permissions_check = ['user_id'] + +base_guild_id = 'guild_id' +max_bulk_get_users_size = 50 + +[server] +port = 8000 + +[server.socket] +heartbeat_interval = 30000 +maxpayload = 1024 +clienttracking = true +keepalive = true +# Opcodes that the server can receive from the client +# INIT | HEARTBEAT | SUBSCRIBE | UNSUBSCRIBE +client_allowed_opcodes = [2, 4, 12, 14] + +[server.socket.opcodes] +HELLO = 1 +INIT = 2 +INIT_ACK = 3 +HEARTBEAT = 4 +HEARTBEAT_ACK = 5 +PRESENCE_UPDATE = 6 +USER_LEFT = 7 +USER_JOINED = 8 +DISCONNECT = 9 +STORAGE_UPDATE = 10 +ERROR = 11 +SUBSCRIBE = 12 +SUBSCRIBE_ACK = 13 +UNSUBSCRIBE = 14 +UNSUBSCRIBE_ACK = 15 + +[logger.levels] +error = 'red' +warn = 'yellow' +info = 'blue' +debug = 'grey' +database = 'magenta' +bot = 'green' +http = 'yellow' +socket = 'cyan' + +[database.backup] +output_dir = './database-backups' +enabled = false +discord_channel = '' +cron_pattern = '0 0 * * *' +exclude_collections = ['evaluateresults'] + +[user_svg_card.colors.dark] + background = '#17171c' + background_secondary = '#1e1e24' + card = '#1e1e24' + +[user_svg_card.colors.dark.text] + primary = '#ffffff' + secondary = '#999999' + +[user_svg_card.colors.light] + background = '#e6e6e6' + background_secondary = '#dcdcdc' + card = '#eeeeee' + +[user_svg_card.colors.light.text] + primary = '#242a31' + secondary = '#6c757d' \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..965137e --- /dev/null +++ b/src/index.ts @@ -0,0 +1,10 @@ +import 'dotenv/config'; + +import '@/scripts/loadConfig'; +import '@/scripts/loadLogger'; +import '@/scripts/validateEnvironmentVariables'; +import '@/scripts/connectDatabase'; +import '@/scripts/handleUncaughtExceptions'; + +import createClient from '@/bot/createClient'; +createClient(); \ No newline at end of file diff --git a/src/lib/bot/commands/admin/eval.ts b/src/lib/bot/commands/admin/eval.ts new file mode 100644 index 0000000..88f0d3e --- /dev/null +++ b/src/lib/bot/commands/admin/eval.ts @@ -0,0 +1,137 @@ +import * as Discord from 'discord.js'; +import type { CommandType } from '@/src/types'; +import EvaluateResult from '@/models/EvaulateResult'; +import generateUniqueId from '@/src/lib/utils/generateUniqueId'; +import evaluateCode from '@/bot/commands/utils/evaluateCode'; + +export default { + metadata: { + global: false + }, + json: new Discord.SlashCommandBuilder() + .setName('eval') + .setDescription('Runs a code snippet for the bot developer.'), + data: { + 'eval': { + restrictions: { + baseGuildOnly: true, + users: { + allow: ['957840712404193290'] + } + }, + execute: { + command: async interaction => { + await interaction.deferReply(); + + await interaction.followUp({ content: 'Write the code snippet to run.' }); + + const code = await collectCode(interaction); + if (!code) return interaction.editReply({ content: 'Code snippet to run not found.', components: [] }); + + const { result, hasError, id } = await executeCode(code); + const embeds = createResultEmbed(code, result, hasError); + const components = createResultComponents(id, interaction.user.id); + + await saveResult({ id, result, hasError, code }); + + return interaction.editReply({ + embeds, + components, + content: null + }); + }, + component: { + 'deleteEvalResultMessage': handleDelete, + 'repeatEval': handleRepeat + } + } + } + } +} satisfies CommandType; + +async function collectCode(interaction: Discord.CommandInteraction) { + const filter = (message: Discord.Message) => message.author.id === interaction.user.id; + + if (!interaction.channel?.isSendable()) return; + + const collected = await interaction.channel.awaitMessages({ + filter, + time: 60000, + max: 1 + }).catch(() => null); + + return collected?.first()?.content; +} + +async function executeCode(code: string) { + const id = generateUniqueId(); + const { result, hasError } = await evaluateCode(code); + + return { + result, + hasError, + id + }; +} + +function createResultEmbed(code: string, result: string, hasError: boolean) { + return [ + new Discord.EmbedBuilder() + .setColor(hasError ? '#ff0040' : '#0063ff') + .setFields([{ name: 'Executed Code', value: `\`\`\`js\n${code.slice(0, 1000)}\n\`\`\`` }]) + .setDescription(`### ${hasError ? 'Error' : 'Successful'}\n\`\`\`js\n${String(result).slice(0, 4000)}\n\`\`\``) + ]; +} + +function createResultComponents(id: string, userId: string) { + return [ + new Discord.ActionRowBuilder() + .addComponents( + new Discord.ButtonBuilder() + .setCustomId(`deleteEvalResultMessage:${id}:${userId}`) + .setLabel('Delete') + .setStyle(Discord.ButtonStyle.Secondary), + new Discord.ButtonBuilder() + .setCustomId(`repeatEval:${id}:${userId}`) + .setLabel('Run Again') + .setStyle(Discord.ButtonStyle.Secondary) + ) + ]; +} + +async function saveResult({ id, result, hasError, code }: { id: string, result: string, hasError: boolean, code: string }) { + await new EvaluateResult({ + id, + result, + hasError, + executedCode: code + }).save(); +} + +async function handleDelete(interaction: Discord.MessageComponentInteraction, { args }: { args: string[] }) { + const [id, userId] = args; + if (interaction.user.id !== userId) return; + + await EvaluateResult.deleteOne({ id }); + + return interaction.message.delete(); +} + +async function handleRepeat(interaction: Discord.MessageComponentInteraction, { args }: { args: string[] }) { + const [id, userId] = args; + if (interaction.user.id !== userId) return; + + const data = await EvaluateResult.findOne({ id }); + if (!data) return interaction.reply({ content: 'Kod parçası bulunamadı.' }); + + const { result, hasError } = await evaluateCode(data.executedCode); + + const embeds = createResultEmbed(data.executedCode, result, hasError); + + embeds[0]!.addFields({ + name: 'Tekrar Çalıştırıldı', + value: new Date().toLocaleDateString('tr-TR', { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }) + }); + + return interaction.update({ embeds }); +} \ No newline at end of file diff --git a/src/lib/bot/commands/general/ping.ts b/src/lib/bot/commands/general/ping.ts new file mode 100644 index 0000000..c5bf53f --- /dev/null +++ b/src/lib/bot/commands/general/ping.ts @@ -0,0 +1,21 @@ +import * as Discord from 'discord.js'; +import type { CommandType } from '@/src/types'; + +export default { + metadata: { + global: true + }, + json: new Discord.SlashCommandBuilder() + .setName('ping') + .setDescription('Pong!'), + data: { + 'ping': { + restrictions: {}, + execute: { + command: async interaction => { + interaction.success(`Pong! ${client.ws.ping} ms.`); + } + } + } + } +} satisfies CommandType; \ No newline at end of file diff --git a/src/lib/bot/commands/kv/storage.ts b/src/lib/bot/commands/kv/storage.ts new file mode 100644 index 0000000..372ca10 --- /dev/null +++ b/src/lib/bot/commands/kv/storage.ts @@ -0,0 +1,263 @@ +import * as Discord from 'discord.js'; +import crypto from 'node:crypto'; +import User from '@/models/User'; +import Storage from '@/models/Storage'; +import { decrypt, encrypt } from '@/utils/encryption'; +import dedent from 'dedent'; +import type { CommandType } from '@/src/types'; +import getValidationError from '@/utils/getValidationError'; + +export default { + metadata: { + global: true + }, + json: new Discord.SlashCommandBuilder() + .setName('storage') + .setDescription('Key-value storage commands.') + + .addSubcommandGroup(group => group.setName('create').setDescription('Key-value storage create commands.') + .addSubcommand(subcommand => subcommand.setName('token').setDescription('Create a new token for your key-value storage.')) + .addSubcommand(subcommand => subcommand.setName('data').setDescription('Create a new data for your key-value storage.') + .addStringOption(option => option.setName('key').setDescription('The key for the data.').setRequired(true)) + .addStringOption(option => option.setName('value').setDescription('The value for the data.').setRequired(true)))) + + .addSubcommandGroup(group => group.setName('get').setDescription('Key-value storage get commands.') + .addSubcommand(subcommand => subcommand.setName('token').setDescription('Get the token for your key-value storage.')) + .addSubcommand(subcommand => subcommand.setName('data').setDescription('Get the data for your key-value storage.') + .addStringOption(option => option.setName('key').setDescription('The key for the data.').setRequired(true).setAutocomplete(true)))) + + .addSubcommandGroup(group => group.setName('update').setDescription('Key-value storage update commands.') + .addSubcommand(subcommand => subcommand.setName('data').setDescription('Update the data for your key-value storage.') + .addStringOption(option => option.setName('key').setDescription('The key for the data.').setRequired(true).setAutocomplete(true)) + .addStringOption(option => option.setName('value').setDescription('The new value for the data.').setRequired(true)))) + + .addSubcommandGroup(group => group.setName('delete').setDescription('Key-value storage delete commands.') + .addSubcommand(subcommand => subcommand.setName('data').setDescription('Delete the data for your key-value storage.') + .addStringOption(option => option.setName('key').setDescription('The key for the data.').setRequired(true).setAutocomplete(true)))) + + .addSubcommand(subcommand => subcommand.setName('list').setDescription('List all the data in your key-value storage.')) + + .addSubcommand(subcommand => subcommand.setName('reset').setDescription('Reset your key-value storage.')), + data: { + 'storage create token': { + restrictions: {}, + execute: { + command: async interaction => { + await interaction.deferReply({ ephemeral: !!interaction.guild }); + + const user = await User.findOne({ id: interaction.user.id }); + if (!user) return interaction.error('We could not find your user data. This generally means you are not in our Discord server.'); + + const cryptoToken = crypto.randomBytes(16).toString('hex'); + const token = Buffer.from(`${interaction.user.id}:${cryptoToken}`).toString('base64'); + + const encryptedToken = encrypt(token, process.env.KV_TOKEN_ENCRYPTION_SECRET); + + await Storage.findOneAndUpdate( + { userId: interaction.user.id }, + { token: encryptedToken }, + { upsert: true } + ); + + return interaction.success(dedent` + We have created a new token for your key-value storage. + \`\`\`${token}\`\`\` + -# Please keep this token safe. + `); + } + } + }, + 'storage create data': { + restrictions: {}, + execute: { + command: async interaction => { + await interaction.deferReply({ ephemeral: !!interaction.guild }); + + const storage = await Storage.findOne({ userId: interaction.user.id }); + if (!storage) return interaction.error('We could not find your key-value storage data. Please create a storage first using `/storage create token`.'); + + const key = interaction.options.getString('key', true); + const value = interaction.options.getString('value', true); + + if (!storage.kv) storage.kv = new Map(); + + storage.kv.set(key, value); + + const validationError = getValidationError(storage); + if (validationError) return interaction.error(validationError); + + await storage.save(); + + return interaction.success(`New key-value pair added to your storage: \`${key}\` -> \`${value}\``); + } + } + }, + 'storage get token': { + restrictions: {}, + execute: { + command: async interaction => { + await interaction.deferReply({ ephemeral: !!interaction.guild }); + + const storage = await Storage.findOne({ userId: interaction.user.id }); + if (!storage) return interaction.error('We could not find your key-value storage data. Please create a storage first using `/storage create token`.'); + + if (!storage.token) return interaction.error('We could not find your token. Please create a new token using `/storage create token`.'); + + const decryptedToken = decrypt(storage.token, process.env.KV_TOKEN_ENCRYPTION_SECRET); + + return interaction.success(dedent` + Here is your token for your key-value storage. + \`\`\`${decryptedToken}\`\`\` + -# Please keep this token safe. + `); + } + } + }, + 'storage get data': { + restrictions: {}, + execute: { + command: async interaction => { + await interaction.deferReply({ ephemeral: !!interaction.guild }); + + const storage = await Storage.findOne({ userId: interaction.user.id }); + if (!storage) return interaction.error('We could not find your key-value storage data. Please create a storage first using `/storage create token`.'); + + const key = interaction.options.getString('key', true); + const value = storage.kv?.get(key); + + if (!value) return interaction.error(`We could not find the data for the key \`${key}\`.`); + + return interaction.success(`Here is the data for the key \`${key}\`: \`${value}\``); + }, + autocomplete: async interaction => { + const storage = await Storage.findOne({ userId: interaction.user.id }); + if (!storage || !storage.kv) return []; + + const keys = [...storage.kv.keys()]; + + return keys.map(key => ({ name: key, value: key })); + } + } + }, + 'storage update data': { + restrictions: {}, + execute: { + command: async interaction => { + await interaction.deferReply({ ephemeral: !!interaction.guild }); + + const storage = await Storage.findOne({ userId: interaction.user.id }); + if (!storage) return interaction.error('We could not find your key-value storage data. Please create a storage first using `/storage create token`.'); + + const key = interaction.options.getString('key', true); + const value = interaction.options.getString('value', true); + + storage.kv?.set(key, value); + + const validationError = getValidationError(storage); + if (validationError) return interaction.error(validationError); + + await storage.save(); + + return interaction.success(`Data for the key \`${key}\` updated to \`${value}\`.`); + } + } + }, + 'storage delete data': { + restrictions: {}, + execute: { + command: async interaction => { + await interaction.deferReply({ ephemeral: !!interaction.guild }); + + const storage = await Storage.findOne({ userId: interaction.user.id }); + if (!storage) return interaction.error('We could not find your key-value storage data. Please create a storage first using `/storage create token`.'); + + const key = interaction.options.getString('key', true); + + storage.kv?.delete(key); + + if (!storage.kv?.size) delete storage.kv; + + await storage.save(); + + return interaction.success(`Data for the key \`${key}\` deleted.`); + }, + autocomplete: async interaction => { + const storage = await Storage.findOne({ userId: interaction.user.id }); + if (!storage?.kv) return []; + + const keys = [...storage.kv.keys()]; + + return keys.map(key => ({ name: key, value: key })); + } + } + }, + 'storage list': { + restrictions: {}, + execute: { + command: async interaction => { + await interaction.deferReply({ ephemeral: !!interaction.guild }); + + const storage = await Storage.findOne({ userId: interaction.user.id }); + if (!storage) return interaction.error('We could not find your key-value storage data. Please create a storage first using `/storage create token`.'); + + if (!storage.kv?.size) return interaction.error('There is no data in your key-value storage.'); + + const data = [...storage.kv.entries()] + .map(([key, value]) => `"${key}": "${value.replace(/"/g, '\\"')}"`) + .join(',\n '); + + // If the data is too long, send it as a file + if (data.length > 1800) { + const buffer = Buffer.from(`{\n ${data}\n}`, 'utf-8'); + + const attachment = new Discord.AttachmentBuilder(buffer, { name: 'kv-storage.json' }); + + return interaction.followUp({ content: 'Here is the list of all the data in your key-value storage:', files: [attachment] }); + } else { + return interaction.success(`Here is the list of all the data in your key-value storage:\n\`\`\`json\n{\n ${data}\n}\n\`\`\``); + } + } + } + }, + 'storage reset': { + restrictions: {}, + execute: { + command: async interaction => { + await interaction.deferReply({ ephemeral: !!interaction.guild }); + + const storage = await Storage.findOne({ userId: interaction.user.id }); + if (!storage) return interaction.error('We could not find your key-value storage data. Please create a storage first using `/storage create token`.'); + + const components = [ + new Discord.ActionRowBuilder() + .addComponents( + new Discord.ButtonBuilder() + .setCustomId('confirmResetStorage') + .setLabel('Confirm') + .setStyle(Discord.ButtonStyle.Success), + new Discord.ButtonBuilder() + .setCustomId('cancelResetStorage') + .setLabel('Cancel') + .setStyle(Discord.ButtonStyle.Danger) + ) + ]; + + return interaction.followUp({ content: 'Are you sure you want to reset your key-value storage?', components }); + }, + component: { + 'confirmResetStorage': async interaction => { + const storage = await Storage.findOne({ userId: interaction.user.id }); + if (!storage) return interaction.error('We could not find your key-value storage data. Please create a storage first using `/storage create token`.'); + + delete storage.kv; + + await storage.save(); + + return interaction.success('Your key-value storage has been reset.'); + }, + 'cancelResetStorage': async interaction => interaction.update({ content: 'Your key-value storage reset has been cancelled.', components: [] }) + } + } + } + } +} satisfies CommandType; \ No newline at end of file diff --git a/src/lib/bot/commands/utils/evaluateCode.ts b/src/lib/bot/commands/utils/evaluateCode.ts new file mode 100644 index 0000000..a0b533f --- /dev/null +++ b/src/lib/bot/commands/utils/evaluateCode.ts @@ -0,0 +1,29 @@ +import { inspect } from 'node:util'; + +async function evaluateCode(code: string) { + const isAsync = code.includes('async') || code.includes('await'); + + let result; + let hasError = false; + + try { + // eslint-disable-next-line security/detect-eval-with-expression + result = await eval(isAsync ? `(async () => { ${code} })()` : code); + if (typeof result !== 'string') result = formatResult(result); + } catch (error) { + hasError = true; + result = error.stack; + } + + return { + result, + hasError + }; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function formatResult(result: any) { + return inspect(result, { depth: 4 }); +} + +export default evaluateCode; \ No newline at end of file diff --git a/src/lib/bot/createClient.ts b/src/lib/bot/createClient.ts new file mode 100644 index 0000000..b1d8d0c --- /dev/null +++ b/src/lib/bot/createClient.ts @@ -0,0 +1,64 @@ +import * as Discord from 'discord.js'; + +import fetchCommands from '@/bot/handlers/commands/fetchCommands'; +import fetchEvents from '@/bot/handlers/events/fetchEvents'; +import listenEvents from '@/bot/handlers/events/listenEvents'; +import fetchCrons from '@/bot/handlers/crons/fetchCrons'; +import listenCrons from '@/bot/handlers/crons/listenCrons'; +import createServer from '@/express/createServer'; +import syncUsers from '@/src/lib/utils/bot/syncUsers'; + +async function createClient() { + const client = new Discord.Client({ + intents: [ + Discord.GatewayIntentBits.Guilds, + Discord.GatewayIntentBits.GuildMembers, + Discord.GatewayIntentBits.GuildPresences + ] + }); + + client.login(process.env.DISCORD_BOT_TOKEN) + .catch(error => { + logger.error('Failed to login to Discord:'); + logger.error(error); + + process.exit(1); + }); + + client.once('ready', () => { + const level = process.env.NODE_ENV === 'development' ? 'info' : 'warn'; + logger[level](`Project is running in ${process.env.NODE_ENV} mode.`); + + logger.log('bot', `Client logged in as ${client.user!.tag}`); + + global.client = client; + + syncUsers() + .then(async () => { + // Start the Express server + createServer(); + + const commands = await fetchCommands(); + client.commands = commands; + + logger.log('bot', `Fetched ${commands.size} commands.`); + + const events = await fetchEvents(); + client.events = events; + + listenEvents(events); + + logger.log('bot', `Fetched and listened to ${events.size} events.`); + + const crons = await fetchCrons(); + client.crons = crons; + + listenCrons(crons); + + logger.log('bot', `Fetched and listened to ${crons.size} crons.`); + }) + .catch(error => logger.error(error)); + }); +} + +export default createClient; \ No newline at end of file diff --git a/src/lib/bot/crons/checkInactiveWebsockets.ts b/src/lib/bot/crons/checkInactiveWebsockets.ts new file mode 100644 index 0000000..a2f2539 --- /dev/null +++ b/src/lib/bot/crons/checkInactiveWebsockets.ts @@ -0,0 +1,14 @@ +import { disconnect } from '@/express/routes/socket/utils'; +import type { CronType } from '@/src/types'; + +export default { + pattern: '*/10 * * * * *', + execute: async () => { + for (const [id, { lastHeartbeat, instance }] of ActiveSockets.entries()) { + if (Date.now() - lastHeartbeat > config.server.socket.heartbeat_interval) { + disconnect(instance, id, 'Connection timed out.'); + } + } + }, + name: 'checkInactiveWebsockets' +} satisfies CronType; \ No newline at end of file diff --git a/src/lib/bot/crons/updateClientActivityState.ts b/src/lib/bot/crons/updateClientActivityState.ts new file mode 100644 index 0000000..28d380f --- /dev/null +++ b/src/lib/bot/crons/updateClientActivityState.ts @@ -0,0 +1,22 @@ +import * as Discord from 'discord.js'; +import User from '@/models/User'; +import type { CronType } from '@/src/types'; + +export default { + pattern: '0 */3 * * *', + execute: async () => { + const currentlyMonitoringUsers = await User.countDocuments(); + + const state = `Monitoring ${currentlyMonitoringUsers} users`; + + client.user!.setActivity({ + type: Discord.ActivityType.Custom, + name: state, + state + }); + + logger.log('bot', `Updated activity to "${state}".`); + }, + executeOnStart: true, + name: 'updateClientActivityState' +} satisfies CronType; \ No newline at end of file diff --git a/src/lib/bot/events/guildMemberAdd.ts b/src/lib/bot/events/guildMemberAdd.ts new file mode 100644 index 0000000..e919ac4 --- /dev/null +++ b/src/lib/bot/events/guildMemberAdd.ts @@ -0,0 +1,20 @@ +import syncUsers from '@/utils/bot/syncUsers'; +import { send as socket_send } from '@/express/routes/socket/utils'; +import createUserData from '@/utils/bot/createUserData'; +import type { EventType } from '@/src/types'; + +export default { + name: 'guildMemberAdd', + execute: async member => { + logger.info(`User ${member.user.id} has joined the server and is now monitored.`); + + syncUsers(); + + // Send a message to all active sockets that the user has joined the server + for (const [, data] of ActiveSockets) { + if (data.subscribed === 'ALL') { + socket_send(data.instance, config.server.socket.opcodes.USER_JOINED, createUserData(member.user.id, {})); + } + } + } +} satisfies EventType; \ No newline at end of file diff --git a/src/lib/bot/events/guildMemberRemove.ts b/src/lib/bot/events/guildMemberRemove.ts new file mode 100644 index 0000000..eeefb0c --- /dev/null +++ b/src/lib/bot/events/guildMemberRemove.ts @@ -0,0 +1,32 @@ +import syncUsers from '@/utils/bot/syncUsers'; +import { send as socket_send, disconnect as socket_disconnect } from '@/express/routes/socket/utils'; +import type { EventType } from '@/src/types'; + +export default { + name: 'guildMemberRemove', + execute: async member => { + syncUsers(); + + logger.info(`User ${member.user.id} has left the server and is no longer monitored.`); + + // Send a message to all active sockets that the user has left the server + for (const [id, data] of ActiveSockets) { + if (data.subscribed === member.user.id) { + socket_disconnect(data.instance, id, `User ${member.user.id} is not monitored anymore.`); + } + + if (Array.isArray(data.subscribed) && data.subscribed.includes(member.user.id)) { + socket_send(data.instance, config.server.socket.opcodes.USER_LEFT, { user_id: member.user.id }); + + if (data.subscribed.length === 1) { + socket_disconnect(data.instance, id, 'There is no user to monitor anymore.'); + } else { + ActiveSockets.set(id, { + ...data, + subscribed: data.subscribed.filter(id => id !== member.user.id) + }); + } + } + } + } +} satisfies EventType; \ No newline at end of file diff --git a/src/lib/bot/events/interactionCreate.ts b/src/lib/bot/events/interactionCreate.ts new file mode 100644 index 0000000..1bddda2 --- /dev/null +++ b/src/lib/bot/events/interactionCreate.ts @@ -0,0 +1,154 @@ +import * as Discord from 'discord.js'; +import getCommandName from '@/src/lib/utils/bot/getCommandName'; +import Storage from '@/models/Storage'; +import type { CommandType, EventType } from '@/src/types'; +import createUserData from '@/utils/bot/createUserData'; + +export default { + name: 'interactionCreate', + execute: async interaction => { + addPrototypes(interaction); + + if (interaction.isChatInputCommand()) { + const command = client.commands.get(interaction.commandName); + if (!command) return interaction.error('Unknown command.'); + + const commandNameData = getCommandName(interaction); + if (!commandNameData) return interaction.error('Failed to get the command name.'); + + const commandData = command.data[commandNameData.name]; + if (!commandData) return interaction.error('Failed to get the command data.'); + + const canUseThisCommand = permissionCheck(interaction, commandData); + if (canUseThisCommand !== true) return interaction.error('Missing permissions.'); + + await commandData.execute.command(interaction, { subcommand: commandNameData.subcommand, group: commandNameData.group }); + + if (interaction.guild) logger.log('bot', `User "${interaction.user.username}" (${interaction.user.id}) executed command "${commandNameData.name}" in guild "${interaction.guild.name}" (${interaction.guild.id}) which takes ${Date.now() - interaction.createdTimestamp} ms to execute.`); + else logger.log('bot', `User "${interaction.user.username}" (${interaction.user.id}) executed command "${commandNameData.name}" in DMs which takes ${Date.now() - interaction.createdTimestamp} ms to execute.`); + } + + if (interaction.isAutocomplete()) { + const command = client.commands.get(interaction.commandName); + if (!command) return; + + const commandNameData = getCommandName(interaction); + if (!commandNameData) return; + + const commandData = command.data[commandNameData.name]; + if (!commandData?.execute?.autocomplete) return; + + return interaction.respond( + await commandData.execute.autocomplete(interaction, { subcommand: commandNameData.subcommand, group: commandNameData.group }) + ); + } + + if (interaction.isMessageComponent()) { + const commandId = interaction.customId.includes(':') ? interaction.customId.split(':')[0] : interaction.customId; + if (!commandId) return; + + const commandFound = client.commands.find((command: CommandType) => Object.keys(command.data).some(commandName => command.data?.[commandName]?.execute?.component?.[commandId])) as CommandType; + if (commandFound) { + const execute = Object.entries(commandFound.data).find(([, { execute }]) => execute.component?.[commandId])?.[1].execute.component?.[commandId]; + if (!execute) return; + + const args = interaction.customId.split(':').slice(1); + + return execute(interaction, { args }); + } + + // "My User Data" Button + if (interaction.customId === 'my-user-data') { + const storage = await Storage.findOne({ userId: interaction.user.id }); + const data = createUserData(interaction.user.id, storage?.kv || {}); + + const embed = new Discord.EmbedBuilder() + .setFooter({ text: `${interaction.user.displayName}'s data`, iconURL: interaction.user.displayAvatarURL() }) + .setColor('#adadad') + .setDescription(`\`\`\`json\n${JSON.stringify(data, null, 2)}\`\`\``); + + interaction.reply({ embeds: [embed], ephemeral: true }); + + return; + } + } + + if (interaction.isModalSubmit()) { + const commandId = interaction.customId.includes(':') ? interaction.customId.split(':')[0] : interaction.customId; + if (!commandId) return; + + const commandFound = client.commands.find((command: CommandType) => Object.keys(command.data).some(commandName => command.data?.[commandName]?.execute?.modal?.[commandId])) as CommandType; + if (!commandFound) return; + + const execute = Object.entries(commandFound.data).find(([, { execute }]) => execute.modal?.[commandId])?.[1].execute.modal?.[commandId]; + if (!execute) return; + + const args = interaction.customId.split(':').slice(1); + + return execute(interaction, { args }); + } + } +} satisfies EventType; + +function addPrototypes(interaction: Discord.BaseInteraction) { + /* + We're adding the following prototypes to the interaction object: + - interaction.success(content: string, options: Discord.InteractionReplyOptions): Promise + - interaction.error(content: string, options: Discord.InteractionReplyOptions): Promise + + These prototypes will allow us to easily send success and error responses in commands + without having to repeat the same code over and over again in each command file. + */ + + if (interaction.isCommand()) { + interaction.success = async (content: string, options: Discord.InteractionReplyOptions) => { + if (interaction.deferred || interaction.replied) interaction.followUp({ content, ...options }); + else interaction.reply({ content, ...options }); + }; + + interaction.error = async (content: string, options: Discord.InteractionReplyOptions) => { + if (interaction.deferred || interaction.replied) interaction.followUp({ content, ...options }); + else interaction.reply({ content, ...options }); + }; + } +} + +function permissionCheck(interaction: Discord.BaseInteraction, commandData: CommandType['data'][string]) { + if (config.bypass_command_permissions_check.includes(interaction.user.id)) return true; + + if (!commandData.restrictions || !Object.keys(commandData.restrictions).length) return true; + + // Check if the command should only be used in a guild + if (commandData.restrictions.guildOnly && !interaction.guild) return false; + + // Check if the command should only be used in the base guild + if (commandData.restrictions.baseGuildOnly && interaction.guild?.id !== config.base_guild_id) return false; + + // Check if the command can only be used by the owner of the guild + if (commandData.restrictions.ownerOnly && interaction.user.id !== interaction.guild?.ownerId) return false; + + const userRestrictions = commandData.restrictions.users; + if (userRestrictions?.deny?.includes(interaction.user.id)) return false; + + const member = interaction.member as Discord.GuildMember; + + const findRole = (role: string | number) => { + if (typeof role === 'number') return member.roles.cache.has(role as unknown as string); + + return member.roles.cache.some(({ name }) => name.toLowerCase() === role.toLowerCase()); + }; + + const roleRestrictions = commandData.restrictions.roles; + if (roleRestrictions?.deny?.some(findRole)) return false; + + const permissionsRestrictions = commandData.restrictions.permissions; + if (permissionsRestrictions?.deny?.some(permission => member.permissions.has(permission))) return false; + + const allowedById = userRestrictions?.allow?.includes(interaction.user.id); + const allowedByRole = roleRestrictions?.allow?.some(findRole); + const allowedByPermission = permissionsRestrictions?.allow?.some(permission => member.permissions.has(permission)); + + if (!allowedById && !allowedByRole && !allowedByPermission) return false; + + return true; +} \ No newline at end of file diff --git a/src/lib/bot/events/presenceUpdate.ts b/src/lib/bot/events/presenceUpdate.ts new file mode 100644 index 0000000..477e739 --- /dev/null +++ b/src/lib/bot/events/presenceUpdate.ts @@ -0,0 +1,26 @@ +import createUserData from '@/utils/bot/createUserData'; +import { send as socket_send } from '@/express/routes/socket/utils'; +import Storage from '@/models/Storage'; +import type { EventType } from '@/src/types'; + +export default { + name: 'presenceUpdate', + execute: async (_, newPresence) => { + if (!newPresence.user) return; + + // find if any socket is monitoring the user + const anySocketMonitoring = [...ActiveSockets.values()] + .some(data => data.subscribed === 'ALL' || data.subscribed.includes(newPresence.user!.id)); + + if (!anySocketMonitoring) return; + + const user_storage = await Storage.findOne({ userId: newPresence.user.id }); + + // Send a message to all active sockets that the user's presence has changed + for (const [, data] of ActiveSockets) { + if (data.subscribed === 'ALL' || data.subscribed.includes(newPresence.user.id)) { + socket_send(data.instance, config.server.socket.opcodes.PRESENCE_UPDATE, createUserData(newPresence.user.id, user_storage?.kv || {})); + } + } + } +} satisfies EventType; \ No newline at end of file diff --git a/src/lib/bot/handlers/commands/fetchCommands.ts b/src/lib/bot/handlers/commands/fetchCommands.ts new file mode 100644 index 0000000..d49edd0 --- /dev/null +++ b/src/lib/bot/handlers/commands/fetchCommands.ts @@ -0,0 +1,33 @@ +import { lstatSync, readdirSync, existsSync } from 'node:fs'; +import * as Discord from 'discord.js'; + +import type { CommandType } from '@/src/types'; + +async function fetchCommands(): Promise> { + if (!existsSync('./src/lib/bot/commands/')) return new Discord.Collection(); + + const commandsFolders = readdirSync('./src/lib/bot/commands'); + if (!commandsFolders.length) return new Discord.Collection(); + + const commandsCollection = new Discord.Collection(); + + async function readRecursive(folderOrFile: string) { + if (lstatSync(`./src/lib/bot/commands/${folderOrFile}`).isDirectory()) { + const files = readdirSync(`./src/lib/bot/commands/${folderOrFile}`); + for (const file of files) await readRecursive(`${folderOrFile}/${file}`); + } else { + const commandModule = await import(`../../commands/${folderOrFile}`); + const command: CommandType = commandModule.default; + + if (!command.json) return; + + commandsCollection.set(command.json.name, command); + } + } + + await Promise.all(commandsFolders.map(folderOrFile => readRecursive(folderOrFile))); + + return commandsCollection; +} + +export default fetchCommands; \ No newline at end of file diff --git a/src/lib/bot/handlers/commands/registerCommands.ts b/src/lib/bot/handlers/commands/registerCommands.ts new file mode 100644 index 0000000..a8aeda8 --- /dev/null +++ b/src/lib/bot/handlers/commands/registerCommands.ts @@ -0,0 +1,46 @@ +import * as Discord from 'discord.js'; + +import type { CommandType } from '@/src/types'; + +async function registerCommands({ token, commands, application_id, base_guild_id }: { token: string, commands: Discord.Collection, application_id: string, base_guild_id: string }) { + const rest = new Discord.REST({ version: '10' }).setToken(token); + + try { + logger.info(`Started reloading application (/) commands. (${commands.size} commands)`); + + // Register global commands + await rest.put( + Discord.Routes.applicationCommands(application_id), + { + body: commands + .filter(command => command.metadata?.global) + .map(command => { + if (command instanceof Discord.SlashCommandBuilder) return command.toJSON(); + + return command.json; + }) + } + ); + + // Register guild commands + await rest.put( + Discord.Routes.applicationGuildCommands(application_id, base_guild_id), + { + body: commands + .filter(command => !command.metadata?.global) + .map(command => { + if (command instanceof Discord.SlashCommandBuilder) return command.toJSON(); + + return command.json; + }) + } + ); + + logger.info('Successfully reloaded application (/) commands.'); + } catch (error) { + logger.error('Failed to reload application (/) commands:'); + logger.error(error); + } +} + +export default registerCommands; \ No newline at end of file diff --git a/src/lib/bot/handlers/crons/fetchCrons.ts b/src/lib/bot/handlers/crons/fetchCrons.ts new file mode 100644 index 0000000..8a62e77 --- /dev/null +++ b/src/lib/bot/handlers/crons/fetchCrons.ts @@ -0,0 +1,34 @@ +import { lstatSync, readdirSync, existsSync } from 'node:fs'; +import * as Discord from 'discord.js'; + +import type { CronType } from '@/src/types'; + +async function fetchCrons(): Promise> { + if (!existsSync('./src/lib/bot/crons/')) return new Discord.Collection(); + + const cronsFolders = readdirSync('./src/lib/bot/crons/'); + if (!cronsFolders.length) return new Discord.Collection(); + + const cronsCollection = new Discord.Collection(); + + async function readRecursive(folderOrFile: string) { + if (lstatSync(`./src/lib/bot/crons/${folderOrFile}`).isDirectory()) { + const files = readdirSync(`./src/lib/bot/crons/${folderOrFile}`); + for (const file of files) await readRecursive(`${folderOrFile}/${file}`); + } else { + const cronModule = await import(`../../crons/${folderOrFile}`); + const cron: CronType = cronModule.default; + + const cronName = folderOrFile.split('.')?.[0]; + if (!cronName) return; + + cronsCollection.set(cronName, { ...cron, name: cronName }); + } + } + + await Promise.all(cronsFolders.map(folderOrFile => readRecursive(folderOrFile))); + + return cronsCollection; +} + +export default fetchCrons; \ No newline at end of file diff --git a/src/lib/bot/handlers/crons/listenCrons.ts b/src/lib/bot/handlers/crons/listenCrons.ts new file mode 100644 index 0000000..8b702d0 --- /dev/null +++ b/src/lib/bot/handlers/crons/listenCrons.ts @@ -0,0 +1,18 @@ +import * as Discord from 'discord.js'; +import { CronJob } from 'cron'; +import { toString } from 'cronstrue'; +import type { CronType } from '@/src/types'; + +function listenCrons(crons: Discord.Collection) { + crons.forEach(cron => { + new CronJob(cron.pattern, cron.execute, null, true); + + if (cron.executeOnStart) { + cron.execute(); + }; + + logger.info(`Cron ${cron.name} scheduled. ${toString(cron.pattern)}.`); + }); +} + +export default listenCrons; \ No newline at end of file diff --git a/src/lib/bot/handlers/events/fetchEvents.ts b/src/lib/bot/handlers/events/fetchEvents.ts new file mode 100644 index 0000000..f6ee25e --- /dev/null +++ b/src/lib/bot/handlers/events/fetchEvents.ts @@ -0,0 +1,32 @@ +import { lstatSync, readdirSync, existsSync } from 'node:fs'; +import * as Discord from 'discord.js'; +import type { EventType } from '@/src/types'; + +async function fetchEvents(): Promise> { + if (!existsSync('./src/lib/bot/events/')) return new Discord.Collection(); + + const eventsFolders = readdirSync('./src/lib/bot/events'); + if (!eventsFolders.length) return new Discord.Collection(); + + const eventsCollection = new Discord.Collection(); + + async function readRecursive(folderOrFile: string) { + if (lstatSync(`./src/lib/bot/events/${folderOrFile}`).isDirectory()) { + const files = readdirSync(`./src/lib/bot/events/${folderOrFile}`); + for (const file of files) await readRecursive(`${folderOrFile}/${file}`); + } else { + const eventModule = await import(`../../events/${folderOrFile}`); + const event = eventModule.default as EventType; + + if (!event.name || !event.execute) return; + + eventsCollection.set(event.name, event); + } + } + + await Promise.all(eventsFolders.map(folderOrFile => readRecursive(folderOrFile))); + + return eventsCollection; +} + +export default fetchEvents; diff --git a/src/lib/bot/handlers/events/listenEvents.ts b/src/lib/bot/handlers/events/listenEvents.ts new file mode 100644 index 0000000..b9ff162 --- /dev/null +++ b/src/lib/bot/handlers/events/listenEvents.ts @@ -0,0 +1,10 @@ +import * as Discord from 'discord.js'; +import type { EventType } from '@/src/types'; + +function listenEvents(events: Discord.Collection) { + for (const [, event] of events) { + client.on(event.name, event.execute); + } +} + +export default listenEvents; \ No newline at end of file diff --git a/src/lib/constants/badges.ts b/src/lib/constants/badges.ts new file mode 100644 index 0000000..46225c8 --- /dev/null +++ b/src/lib/constants/badges.ts @@ -0,0 +1,48 @@ +const badges = [ + { + id: 'ActiveDeveloper', + base64: 'PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjQxNjQgNi4wMDAxMkw2IDEzLjQxNjVWNDYuNTgzOEwxMy40MTY1IDU0LjAwMDFINDYuNTgzNEw1NCA0Ni41ODM4VjEzLjQxNjVMNDYuNTgzNyA2LjAwMDEySDEzLjQxNjRaTTI2LjU3NDYgNDMuMjc2NkgxOS45MzY1QzE5LjkzNjUgMzcuNzg1NyAxNS40NzAxIDMzLjMxOTMgOS45NzkyNiAzMy4zMTkzVjI2LjY4MDlDMTUuNDcwMSAyNi42ODA5IDE5LjkzNjUgMjIuMjE0NiAxOS45MzY1IDE2LjcyMzdIMjYuNTc0NkMyNi41NzQ2IDIyLjE3MTQgMjMuOTAwNSAyNi45NzI1IDE5LjgzMzkgMzAuMDAwMUMyMy45MDA1IDMzLjAyOCAyNi41NzQ2IDM3LjgyODkgMjYuNTc0NiA0My4yNzY2Wk01MC4wMDAxIDMzLjMxOTNDNDQuNTA5MiAzMy4zMTkzIDQwLjA0MjggMzcuNzg1NyA0MC4wNDI4IDQzLjI3NjZIMzMuNDA0NEMzMy40MDQ0IDM3LjgyODkgMzYuMDc4OSAzMy4wMjggNDAuMTQ1NCAzMC4wMDAxQzM2LjA3ODkgMjYuOTcyNSAzMy40MDQ0IDIyLjE3MTQgMzMuNDA0NCAxNi43MjM3SDQwLjA0MjhDNDAuMDQyOCAyMi4yMTQ2IDQ0LjUwOTIgMjYuNjgwOSA1MC4wMDAxIDI2LjY4MDlWMzMuMzE5M1oiIGZpbGw9IiMyRUE5NjciLz4KPC9zdmc+Cg==' + }, + { + id: 'BugHunterLevel1', + base64: 'PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQyLjEwOSAyNS44NjI3QzM3LjI4MzYgMzIuMDA0MSAzMC40NzQ3IDM1LjA3NDggMjUuMjEwNiAzMy45Njg2TDI0LjUzMDIgMzQuODM1NUw2Ljg0MzU2IDU3LjM3MDhDNi41MzQ0NiA1Ny43NjU2IDYuMTUwNTYgNTguMDk1NiA1LjcxMzgxIDU4LjM0MTlDNS4yNzcwNyA1OC41ODgyIDQuNzk2MDYgNTguNzQ1OSA0LjI5ODI4IDU4LjgwNjJDMy44MDA1MSA1OC44NjY0IDMuMjk1NzUgNTguODI3OSAyLjgxMjg3IDU4LjY5MjlDMi4zMjk5OSA1OC41NTc4IDEuODc4NDYgNTguMzI5IDEuNDg0MTIgNTguMDE5M0MxLjA4NTE2IDU3LjcxMTQgMC43NTExNjIgNTcuMzI3NSAwLjUwMTQxMSA1Ni44ODk4QzAuMjUxNjYxIDU2LjQ1MjEgMC4wOTExMTQzIDU1Ljk2OTMgMC4wMjkwNDUgNTUuNDY5MkMtMC4wMzMwMjQzIDU0Ljk2OTEgMC4wMDQ2MTQ1IDU0LjQ2MTYgMC4xMzk3ODcgNTMuOTc2MkMwLjI3NDk2IDUzLjQ5MDcgMC41MDQ5ODcgNTMuMDM2OCAwLjgxNjU3OSA1Mi42NDA4TDE5LjA4ODIgMjkuMjc2N0MxNi42NjYgMjQuNDMyMiAxOC4wMDExIDE3LjAxMjkgMjIuOTAyOCAxMC44MTQzQzI4Ljk4NyAzLjEwODkyIDM4LjE2MDkgMC4yMjkxOTQgNDMuNDgyMiA0LjM4NzA1QzQ4LjgwMzUgOC41NDQ5IDQ4LjE1NTEgMTguMTU3MyA0Mi4xMDkgMjUuODYyN1oiIGZpbGw9IiNCNEUxQ0UiLz4KPHBhdGggZD0iTTU3LjY5MTMgNDEuOTQxMkM2Ni4zODg1IDIwLjAwNzYgNDMuNDgyMiA0LjM4NzA1IDQzLjQ4MjIgNC4zODcwNUM0OC44MDM1IDguNTQ0OSA0OC4xNTUxIDE4LjE1NzMgNDIuMTA5IDI1Ljg2MjdDMzcuMjgzNiAzMi4wMDQxIDMwLjQ3NDcgMzUuMDc0OCAyNS4yMTA2IDMzLjk2ODZMMjQuNTMwMiAzNC44MzU1QzMzLjI5NzQgNDEuNTczNSA0My45Mzg5IDQ1LjY1NTQgMzguMTYwOSA1MS4zODIyQzMxLjY5NTIgNTcuNzkwNyA0OC45OTQyIDYzLjg3NDkgNTcuNjkxMyA0MS45NDEyWiIgZmlsbD0iIzNCQTU2MSIvPgo8L3N2Zz4K' + }, + { + id: 'BugHunterLevel2', + base64: 'PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQzLjUyMDggNC4yNzc4M0M0My41MjA4IDQuMjc3ODMgNjYuNDYzMSAxOS45Njc3IDU3LjY0NjUgNDEuOTYxOUM0OC44Mjk4IDYzLjk1NjIgMzEuNjIzMSA1Ny44NDE0IDM4LjE2NDUgNTEuMzQ3NEM0NC43MDU5IDQ0Ljg1MzQgMzAuNTMyOSA0MC42MzQ3IDIxLjUyNjYgMzIuMTk3Mkw0My41MjA4IDQuMjc3ODNaIiBmaWxsPSIjRkZFQUMwIi8+CjxtYXNrIGlkPSJtYXNrMF81MDJfNzEzOCIgc3R5bGU9Im1hc2stdHlwZTphbHBoYSIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeD0iMCIgeT0iMiIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjU3Ij4KPHBhdGggZD0iTTQyLjE5MzMgMjUuODkzMkMzNy40MDU3IDMyLjAwNzkgMzAuNTMyNSAzNS4wODkgMjUuMTc2MiAzMy45NTE0TDYuODMxODMgNTcuNDYyNUM1LjUwNDYgNTkuMTY4OSAzLjE4MTkzIDU5LjM1ODUgMS40NzU0OCA1OC4wMzEzQy0wLjIzMDk3MSA1Ni43MDQgLTAuNDIwNTc3IDU0LjM4MTQgMC43MTcwNTcgNTIuNjc0OUwxOS4wNjE0IDI5LjM1MzVDMTYuNTk2NSAyNC41NjU5IDE3LjkyMzggMTcuMTIzOSAyMi45MDA5IDEwLjgxOTVDMjkuMDE1NyAzLjE0MDQ5IDM4LjE2NDEgMC4yOTY0MDggNDMuNTIwNSA0LjI3ODEyQzQ4Ljg3NjkgOC4yNTk4NCA0OC4xMTg0IDE4LjA3MTkgNDIuMTkzMyAyNS44OTMyWiIgZmlsbD0iI0ZGRDU2QyIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazBfNTAyXzcxMzgpIj4KPHBhdGggZD0iTTAgMi40ODMyNkg0Ny4yMzM0VjU5LjY2MDVIMFYyLjQ4MzI2WiIgZmlsbD0iI0ZGRDU2QyIvPgo8cGF0aCBkPSJNMzEuODM1NiAxLjA0NDc1TDM0LjA0NDQgMS4zMzU3M0wyOS41OTk2IDM1LjA3NDZMMjcuMzkwOCAzNC43ODM2TDMxLjgzNTYgMS4wNDQ3NVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0zNS40NjYzIDAuODkxMTEzTDQwLjMwNjkgMS41Mjg4MUwzNS45NDMgMzQuNjUzNkwzMS4xMDI0IDM0LjAxNTlMMzUuNDY2MyAwLjg5MTExM1oiIGZpbGw9IndoaXRlIi8+CjwvZz4KPC9zdmc+Cg==' + }, + { + id: 'CertifiedModerator', + base64: 'PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQ1LjcyODQgMC40ODMyNzZIMTQuMjgzM0MxMy43OTE5IDYuODcwNTcgOC4zODczIDExLjc4MzkgMiAxMS43ODM5VjE0LjczMTlDMiAyOS4yMjYxIDguODc4NjIgNDIuNzM3NyAyMC45MTYyIDUyLjMxODZMMzAuMDA1OCA1OS40NDI5TDM5LjA5NTQgNTIuMzE4NkM1MS4xMzMgNDIuOTgzNCA1OC4wMTE3IDI5LjIyNjEgNTguMDExNyAxNC43MzE5VjExLjc4MzlDNTEuNjI0NCAxMS43ODM5IDQ2LjQ2NTQgNi44NzA1NyA0NS43Mjg0IDAuNDgzMjc2Wk0yNC4zNTU1IDQyLjk4MzRDMTYuOTg1NiAzNy4wODc0IDEyLjU2MzYgMjguNzM0OCAxMi41NjM2IDE5LjY0NTJWMTcuOTI1NUMxNi40OTQyIDE3LjkyNTUgMTkuOTMzNiAxNC45Nzc1IDIwLjE3OTIgMTEuMDQ2OUgzMC4wMDU4VjQ3LjY1MUwyNC4zNTU1IDQyLjk4MzRaIiBmaWxsPSIjRkY4QzE5Ii8+Cjwvc3ZnPgo=' + }, + { + id: 'Hypesquad', + base64: 'PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTU4Ljc3MzcgMTYuNzkwOEwzNS4xNjU1IDMyLjI4NzJDMzQuNzYwMiAzMi41NTE0IDM0LjQzMjEgMzIuOTE4MSAzNC4yMTQ0IDMzLjM1MDFMMzAuNjE1NCA0MC41NjY5QzMwLjU2MzIgNDAuNjg2NiAzMC40NzcxIDQwLjc4ODYgMzAuMzY3OCA0MC44NjAyQzMwLjI1ODUgNDAuOTMxOCAzMC4xMzA3IDQwLjk2OTkgMzAgNDAuOTY5OUMyOS44NjkzIDQwLjk2OTkgMjkuNzQxNSA0MC45MzE4IDI5LjYzMjIgNDAuODYwMkMyOS41MjI5IDQwLjc4ODYgMjkuNDM2OCA0MC42ODY2IDI5LjM4NDYgNDAuNTY2OUwyNS43ODU2IDMzLjM1MDFDMjUuNTY3OSAzMi45MTgxIDI1LjIzOTggMzIuNTUxNCAyNC44MzQ1IDMyLjI4NzJMMS4yMjYyNyAxNi43OTA4QzEuMTA3NTggMTYuNjQ5OCAwLjkzNzc0NyAxNi41NjE4IDAuNzU0MTQ2IDE2LjU0NjFDMC41NzA1NDUgMTYuNTMwMyAwLjM4ODIxMiAxNi41ODgyIDAuMjQ3MjU5IDE2LjcwNjlDMC4xMDYzMDUgMTYuODI1NiAwLjAxODI3NzQgMTYuOTk1NCAwLjAwMjU0MDE5IDE3LjE3OUMtMC4wMTMxOTcgMTcuMzYyNiAwLjA0NDY0NTUgMTcuNTQ0OSAwLjE2MzM0MyAxNy42ODU5TDEwLjAyODEgMzYuODc0NkMxMC4wNzYgMzYuOTc1IDEwLjA5OTcgMzcuMDg1NCAxMC4wOTcgMzcuMTk2N0MxMC4wOTQ0IDM3LjMwNzkgMTAuMDY1NiAzNy40MTcgMTAuMDEyOSAzNy41MTUxQzkuOTYwMjIgMzcuNjEzMiA5Ljg4NTE5IDM3LjY5NzUgOS43OTM4NyAzNy43NjExQzkuNzAyNTYgMzcuODI0OCA5LjU5NzUzIDM3Ljg2NjEgOS40ODczIDM3Ljg4MTZIMy41NTcyNkMzLjQxMDgzIDM3Ljg4MDIgMy4yNjc3NSAzNy45MjU0IDMuMTQ4NzQgMzguMDEwOEMzLjAyOTczIDM4LjA5NjEgMi45NDA5NiAzOC4yMTcxIDIuODk1MjggMzguMzU2M0MyLjg0OTU5IDM4LjQ5NTQgMi44NDkzNiAzOC42NDU1IDIuODk0NjEgMzguNzg0N0MyLjkzOTg3IDM4LjkyNCAzLjAyODI2IDM5LjA0NTMgMy4xNDcwMSAzOS4xMzFMMjkuNTg5NyA1OC4wNzczQzI5LjcxMDYgNTguMTU5OCAyOS44NTM2IDU4LjIwNCAzMCA1OC4yMDRDMzAuMTQ2NCA1OC4yMDQgMzAuMjg5NCA1OC4xNTk4IDMwLjQxMDMgNTguMDc3M0w1Ni44NTMgMzkuMTMxQzU2Ljk3MTcgMzkuMDQ1MyA1Ny4wNjAxIDM4LjkyNCA1Ny4xMDU0IDM4Ljc4NDdDNTcuMTUwNiAzOC42NDU1IDU3LjE1MDQgMzguNDk1NCA1Ny4xMDQ3IDM4LjM1NjNDNTcuMDU5IDM4LjIxNzEgNTYuOTcwMyAzOC4wOTYxIDU2Ljg1MTMgMzguMDEwOEM1Ni43MzIyIDM3LjkyNTQgNTYuNTg5MiAzNy44ODAyIDU2LjQ0MjcgMzcuODgxNkg1MC41MTI3QzUwLjM5NiAzNy44NzcxIDUwLjI4MjIgMzcuODQzOCA1MC4xODE0IDM3Ljc4NDhDNTAuMDgwNyAzNy43MjU3IDQ5Ljk5NjEgMzcuNjQyNiA0OS45MzUyIDM3LjU0M0M0OS44NzQzIDM3LjQ0MzMgNDkuODM4OSAzNy4zMzAxIDQ5LjgzMjMgMzcuMjEzNUM0OS44MjU3IDM3LjA5NjkgNDkuODQ4MSAzNi45ODA1IDQ5Ljg5NzMgMzYuODc0Nkw1OS44MzY3IDE3LjY4NTlDNTkuOTU1NCAxNy41NDQ5IDYwLjAxMzIgMTcuMzYyNiA1OS45OTc1IDE3LjE3OUM1OS45ODE3IDE2Ljk5NTQgNTkuODkzNyAxNi44MjU2IDU5Ljc1MjcgMTYuNzA2OUM1OS42MTE4IDE2LjU4ODIgNTkuNDI5NSAxNi41MzAzIDU5LjI0NTggMTYuNTQ2MUM1OS4wNjIyIDE2LjU2MTggNTguODkyNCAxNi42NDk4IDU4Ljc3MzcgMTYuNzkwOFoiIGZpbGw9IiNGQkI4NDgiLz4KPHBhdGggZD0iTTMwLjUyMjMgMi41ODA4OUwzMi45NDY1IDcuNTIyNTlDMzIuOTg5NiA3LjYwNDIyIDMzLjA1MDYgNy42NzUxIDMzLjEyNDggNy43Mjk5N0MzMy4xOTkxIDcuNzg0ODUgMzMuMjg0NyA3LjgyMjMyIDMzLjM3NTQgNy44Mzk2TDM4LjgzOTIgOC42MjI4MUMzOC45NDYyIDguNjM4NTIgMzkuMDQ2NiA4LjY4Mzg5IDM5LjEyOSA4Ljc1Mzc4QzM5LjIxMTUgOC44MjM2NyAzOS4yNzI3IDguOTE1MjggMzkuMzA1NyA5LjAxODJDMzkuMzM4NyA5LjEyMTEzIDM5LjM0MjIgOS4yMzEyNSAzOS4zMTU4IDkuMzM2MDZDMzkuMjg5NCA5LjQ0MDg3IDM5LjIzNDEgOS41MzYxNyAzOS4xNTYzIDkuNjExMTVMMzUuMjAyOSAxMy40NTI2QzM1LjEzOTEgMTMuNTE4NyAzNS4wOTEgMTMuNTk4MiAzNS4wNjE5IDEzLjY4NTNDMzUuMDMyOSAxMy43NzI1IDM1LjAyMzcgMTMuODY1IDM1LjAzNTEgMTMuOTU2MUwzNS45Njc1IDE5LjM4MjdDMzUuOTg4IDE5LjQ4NzMgMzUuOTc4MiAxOS41OTU2IDM1LjkzOTIgMTkuNjk0OEMzNS45MDAyIDE5Ljc5NCAzNS44MzM3IDE5Ljg4IDM1Ljc0NzUgMTkuOTQyN0MzNS42NjEzIDIwLjAwNTQgMzUuNTU5IDIwLjA0MjIgMzUuNDUyNiAyMC4wNDg3QzM1LjM0NjIgMjAuMDU1MiAzNS4yNDAyIDIwLjAzMTEgMzUuMTQ3IDE5Ljk3OTRMMzAuMjYxMiAxNy40MjQ2QzMwLjE4MTggMTcuMzc4OCAzMC4wOTE4IDE3LjM1NDcgMzAuMDAwMSAxNy4zNTQ3QzI5LjkwODUgMTcuMzU0NyAyOS44MTg0IDE3LjM3ODggMjkuNzM5MSAxNy40MjQ2TDI0Ljg1MzMgMTkuOTc5NEMyNC43NjAxIDIwLjAzMTEgMjQuNjU0MSAyMC4wNTUyIDI0LjU0NzcgMjAuMDQ4N0MyNC40NDEzIDIwLjA0MjIgMjQuMzM5IDIwLjAwNTQgMjQuMjUyNyAxOS45NDI3QzI0LjE2NjUgMTkuODggMjQuMSAxOS43OTQgMjQuMDYxMSAxOS42OTQ4QzI0LjAyMjEgMTkuNTk1NiAyNC4wMTIzIDE5LjQ4NzMgMjQuMDMyOCAxOS4zODI3TDI0Ljk2NTIgMTMuOTU2MUMyNC45NzY2IDEzLjg2NSAyNC45Njc0IDEzLjc3MjUgMjQuOTM4MyAxMy42ODUzQzI0LjkwOTMgMTMuNTk4MiAyNC44NjExIDEzLjUxODcgMjQuNzk3NCAxMy40NTI2TDIwLjg0NCA5LjYxMTE1QzIwLjc2NjEgOS41MzYxNyAyMC43MTA5IDkuNDQwODcgMjAuNjg0NSA5LjMzNjA2QzIwLjY1OCA5LjIzMTI1IDIwLjY2MTUgOS4xMjExMyAyMC42OTQ1IDkuMDE4MkMyMC43Mjc2IDguOTE1MjggMjAuNzg4OCA4LjgyMzY3IDIwLjg3MTIgOC43NTM3OEMyMC45NTM3IDguNjgzODkgMjEuMDU0MSA4LjYzODUyIDIxLjE2MSA4LjYyMjgxTDI2LjYyNDkgNy44NTgyNUMyNi43MTU2IDcuODQwOTcgMjYuODAxMiA3LjgwMzUgMjYuODc1NCA3Ljc0ODYyQzI2Ljk0OTcgNy42OTM3NSAyNy4wMTA2IDcuNjIyODcgMjcuMDUzOCA3LjU0MTI0TDI5LjQ3OCAyLjU5OTU0QzI5LjUyMzEgMi40OTk5MiAyOS41OTU2IDIuNDE1MTUgMjkuNjg3IDIuMzU1MDdDMjkuNzc4MyAyLjI5NDk4IDI5Ljg4NDkgMi4yNjIwNSAyOS45OTQyIDIuMjYwMUMzMC4xMDM2IDIuMjU4MTUgMzAuMjExMiAyLjI4NzI1IDMwLjMwNDcgMi4zNDQwNEMzMC4zOTgyIDIuNDAwODIgMzAuNDczNiAyLjQ4Mjk1IDMwLjUyMjMgMi41ODA4OVoiIGZpbGw9IiNGQkI4NDgiLz4KPC9zdmc+Cg==' + }, + { + id: 'HypeSquadOnlineHouse1', + base64: 'PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTU1LjIzMTUgNC4wMDAxMkg0Ljc4MDI3QzQuNjc3OCA0LjAwMDEyIDQuNTc2MzQgNC4wMTg5IDQuNDgxNjcgNC4wNTUzOEM0LjM4NyA0LjA5MTg2IDQuMzAwOTkgNC4xNDUzMyA0LjIyODUzIDQuMjEyNzNDNC4wODIyMSA0LjM0ODg3IDQgNC41MzM1IDQgNC43MjYwMlYzNy4xNDQyQzQuMDAyMTkgMzcuMjUxIDQuMDI4NiAzNy4zNTYyIDQuMDc3NDkgMzcuNDUyOEM0LjEyNjM5IDM3LjU0OTUgNC4xOTY2NyAzNy42MzU1IDQuMjgzNzMgMzcuNzA1MUwyOS41MTgyIDU1Ljg1MjdDMjkuNjU2NiA1NS45NTU4IDI5LjgyODYgNTYuMDExOSAzMC4wMDU5IDU2LjAxMTlDMzAuMTgzMiA1Ni4wMTE5IDMwLjM1NTIgNTUuOTU1OCAzMC40OTM2IDU1Ljg1MjdMNTUuNzEwMyAzNy43MDUxQzU1LjgwMjUgMzcuNjM4NSA1NS44Nzc0IDM3LjU1MzYgNTUuOTI5NSAzNy40NTY1QzU1Ljk4MTcgMzcuMzU5NCA1Ni4wMDk4IDM3LjI1MjcgNTYuMDExOCAzNy4xNDQyVjQuNzI2MDJDNTYuMDExOCA0LjUzMzUgNTUuOTI5NiA0LjM0ODg3IDU1Ljc4MzMgNC4yMTI3M0M1NS42MzY5IDQuMDc2NiA1NS40Mzg1IDQuMDAwMTIgNTUuMjMxNSA0LjAwMDEyWk00OC4xMzgyIDE4LjczMjZMNDIuMjE1MyAyOS42NTQyQzQyLjE2MzUgMjkuNzQ0MyA0Mi4xNTEyIDI5Ljg0OTYgNDIuMTgwOSAyOS45NDhDNDIuMjEwNyAzMC4wNDY0IDQyLjI4MDIgMzAuMTMwMyA0Mi4zNzQ5IDMwLjE4MjFINDYuMDk4OUM0Ni4xNDU0IDMwLjE2NjUgNDYuMTk0OCAzMC4xNTk3IDQ2LjI0NDMgMzAuMTYyMUM0Ni4yOTM3IDMwLjE2NDQgNDYuMzQyMSAzMC4xNzU5IDQ2LjM4NjYgMzAuMTk1OUM0Ni40MzEyIDMwLjIxNTkgNDYuNDcxMSAzMC4yNDM5IDQ2LjUwMzkgMzAuMjc4NEM0Ni41MzY3IDMwLjMxMjggNDYuNTYxOCAzMC4zNTMgNDYuNTc3NyAzMC4zOTY2QzQ2LjYxMjggMzAuNDg4NiA0Ni42MDg1IDMwLjU4OTcgNDYuNTY1NSAzMC42Nzg4QzQ2LjUyMjYgMzAuNzY3OSA0Ni40NDQ0IDMwLjgzODIgNDYuMzQ3MSAzMC44NzVMMzAuMjQ1MyA0MS43MTQxQzMwLjE3MjkgNDEuNzYyNCAzMC4wODYxIDQxLjc4ODQgMjkuOTk3IDQxLjc4ODRDMjkuOTA4IDQxLjc4ODQgMjkuODIxMSA0MS43NjI0IDI5Ljc0ODggNDEuNzE0MUwxMy40NTE5IDMwLjk0MUMxMy40MDI4IDMwLjkyNjEgMTMuMzU3MyAzMC45MDIzIDEzLjMxODIgMzAuODcwOUMxMy4yNzkxIDMwLjgzOTUgMTMuMjQ3MSAzMC44MDEzIDEzLjIyNCAzMC43NTgzQzEzLjIwMDkgMzAuNzE1MyAxMy4xODcyIDMwLjY2ODUgMTMuMTgzNyAzMC42MjA2QzEzLjE4MDIgMzAuNTcyNyAxMy4xODcgMzAuNTI0NiAxMy4yMDM2IDMwLjQ3OTFDMTMuMjE3NyAzMC40MzQgMTMuMjQxNCAzMC4zOTIxIDEzLjI3MzQgMzAuMzU1OEMxMy4zMDU0IDMwLjMxOTYgMTMuMzQ1IDMwLjI4OTcgMTMuMzg5OCAzMC4yNjgxQzEzLjQzNDYgMzAuMjQ2NSAxMy40ODM2IDMwLjIzMzYgMTMuNTMzOSAzMC4yMzAyQzEzLjU4NDIgMzAuMjI2NyAxMy42MzQ3IDMwLjIzMjggMTMuNjgyNCAzMC4yNDgxSDE3LjIyOTFDMTcuMzQwNCAzMC4yMzk4IDE3LjQ0NDIgMzAuMTkyNyAxNy41MTk4IDMwLjExNjJDMTcuNTk1NSAzMC4wMzk4IDE3LjYzNzMgMjkuOTM5NiAxNy42MzY5IDI5LjgzNTdDMTcuNjU1MSAyOS43NzY0IDE3LjY1NTEgMjkuNzEzNSAxNy42MzY5IDI5LjY1NDJMMTEuODAyNyAxOC43MzI2QzExLjc2MzggMTguNjQ2NiAxMS43NjA0IDE4LjU1MDIgMTEuNzkzMSAxOC40NjE5QzExLjgyNTggMTguMzczNiAxMS44OTIzIDE4LjI5OTcgMTEuOTggMTguMjU0MkMxMi4wNDg3IDE4LjIxMyAxMi4xMjg4IDE4LjE5MTEgMTIuMjEwNSAxOC4xOTExQzEyLjI5MjMgMTguMTkxMSAxMi4zNzIzIDE4LjIxMyAxMi40NDExIDE4LjI1NDJMMjYuODQwNSAyNy4wNDc1QzI3LjA5MDYgMjcuMjEwNiAyNy4yOTY5IDI3LjQyNTEgMjcuNDQzNCAyNy42NzQ0TDI5LjYyNDYgMzEuNzgyNEMyOS42NTU0IDMxLjgyOTYgMjkuNjk2MSAzMS44NzA2IDI5Ljc0NDMgMzEuOTAzQzI5Ljc5MjQgMzEuOTM1MyAyOS44NDcgMzEuOTU4MyAyOS45MDQ5IDMxLjk3MDZDMjkuOTYyOCAzMS45ODI5IDMwLjAyMjggMzEuOTg0MyAzMC4wODEzIDMxLjk3NDZDMzAuMTM5NyAzMS45NjUgMzAuMTk1NSAzMS45NDQ1IDMwLjI0NTMgMzEuOTE0NEMzMC4zMDEyIDMxLjg3OTIgMzAuMzQ5MyAzMS44MzQ0IDMwLjM4NzIgMzEuNzgyNEwzMi41Njg0IDI3LjY3NDRDMzIuNzA4NyAyNy40MjYyIDMyLjkwOSAyNy4yMTE2IDMzLjE1MzYgMjcuMDQ3NUw0Ny41NzA3IDE4LjI1NDJDNDcuNjUyNyAxOC4xODM1IDQ3Ljc2MDQgMTguMTQ0MiA0Ny44NzIyIDE4LjE0NDJDNDcuOTg0IDE4LjE0NDIgNDguMDkxNyAxOC4xODM1IDQ4LjE3MzcgMTguMjU0MkM0OC4yMDggMTguMzE4MyA0OC4yMjU5IDE4LjM4ODkgNDguMjI1OSAxOC40NjA0QzQ4LjIyNTkgMTguNTMyIDQ4LjIwOCAxOC42MDI2IDQ4LjE3MzcgMTguNjY2N0w0OC4xMzgyIDE4LjczMjZaIiBmaWxsPSIjOUM4NEVGIi8+Cjwvc3ZnPgo=' + }, + { + id: 'HypeSquadOnlineHouse2', + base64: 'PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTI5LjkwMjcgMy4wMDA1N0MyNi4zODk5IDIuOTgwMzIgMjIuOTA3NiAzLjY1MjE5IDE5LjY1NDUgNC45Nzc4MUMxNi40MDE0IDYuMzAzNDMgMTMuNDQxMyA4LjI1Njg0IDEwLjk0MzIgMTAuNzI2NUM4LjQ0NSAxMy4xOTYyIDYuNDU3NzQgMTYuMTMzNyA1LjA5NDg1IDE5LjM3MTRDMy43MzE5NiAyMi42MDkxIDMuMDIwMTMgMjYuMDgzNiAzIDI5LjU5NjRWMjkuOTAzOUMzLjAwMDM1IDM1LjIxMTcgNC41NzA3MSA0MC40MDA4IDcuNTEzNDQgNDQuODE4MUMxMC40NTYyIDQ5LjIzNTQgMTQuNjM5OCA1Mi42ODM1IDE5LjUzNzcgNTQuNzI4NUMyNC40MzU2IDU2Ljc3MzUgMjkuODI5IDU3LjMyMzkgMzUuMDM5IDU2LjMxMDVDNDAuMjQ5MSA1NS4yOTcxIDQ1LjA0MjkgNTIuNzY1MiA0OC44MTczIDQ5LjAzMzRDNTIuNTkxNyA0NS4zMDE2IDU1LjE3NzggNDAuNTM2NyA1Ni4yNTAzIDM1LjMzODRDNTcuMzIyOSAzMC4xNDAxIDU2LjgzMzggMjQuNzQwNyA1NC44NDQ2IDE5LjgxOTdDNTIuODU1NSAxNC44OTg3IDQ5LjQ1NTIgMTAuNjc2MSA0NS4wNzE3IDcuNjgzM0M0MC42ODgyIDQuNjkwNDkgMzUuNTE3NCAzLjA2MTIzIDMwLjIxMDEgMy4wMDA1N0gyOS45MDI3Wk00Ny4yNTcgMTkuNjU1TDQxLjQ2NjUgMzAuNTg3MUM0MS40NjY1IDMwLjkyODggNDEuNDY2NSAzMC45Mjg4IDQxLjgwODEgMzEuMjUzM0g0NS4yMjQzQzQ1LjU4MyAzMS4yNTMzIDQ1LjU4MzEgMzEuMjUzMyA0NS41ODMxIDMxLjYxMlYzMS45NTM2TDMwLjIxMDEgNDIuODUxNkgyOS45MDI3TDE0LjI3MzUgMzEuOTUzNlYzMS4yNTMzSDE4LjAzMTNDMTguMDc0NSAzMS4yNTgxIDE4LjExODIgMzEuMjUzOCAxOC4xNTk1IDMxLjI0MDVDMTguMjAwOCAzMS4yMjcyIDE4LjIzODkgMzEuMjA1MyAxOC4yNzEyIDMxLjE3NjNDMTguMzAzNCAzMS4xNDcyIDE4LjMyOTIgMzEuMTExNyAxOC4zNDY3IDMxLjA3MkMxOC4zNjQzIDMxLjAzMjIgMTguMzczMiAzMC45ODkzIDE4LjM3MjkgMzAuOTQ1OEwxMi41NDgzIDE5LjY1NVYxOC45NzE3SDEzLjI0ODZMMjYuOTEzNSAyNy44MTk5QzI3LjI1NTEgMjcuODE5OSAyNy4yNTUxIDI4LjE2MTYgMjcuNjEzOCAyOC41MjAzTDI5LjY0NjQgMzIuOTQ0NEMyOS45ODgxIDMyLjk0NDQgMjkuOTg4MSAzMy4yNjg5IDMwLjMyOTcgMzIuOTQ0NEwzMC42NzEzIDMyLjYwMjdMMzIuNzIxIDI4LjUyMDNDMzIuNzIxIDI4LjE2MTYgMzMuMDYyNiAyOC4xNjE2IDMzLjM4NzIgMjcuODE5OUw0Ny4wNTIgMTguNjQ3MkM0Ny4zOTM2IDE4LjY0NzIgNDcuMzkzNiAxOC42NDcyIDQ3LjM5MzYgMTguOTcxN1YxOS42NTVINDcuMjU3WiIgZmlsbD0iI0Y0N0I2NyIvPgo8L3N2Zz4K' + }, + { + id: 'HypeSquadOnlineHouse3', + base64: 'PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTU2LjUwMTggMjkuMzgxNUwzMC4zMDI1IDMuMTgyMjFDMzAuMTg0NCAzLjA2NTU0IDMwLjAyNSAzLjAwMDEyIDI5Ljg1OSAzLjAwMDEyQzI5LjY5MyAzLjAwMDEyIDI5LjUzMzYgMy4wNjU1NCAyOS40MTU1IDMuMTgyMjFMMy4xODIwOCAyOS4zODE1QzMuMDY1NDIgMjkuNDk5NyAzIDI5LjY1OSAzIDI5LjgyNUMzIDI5Ljk5MSAzLjA2NTQyIDMwLjE1MDQgMy4xODIwOCAzMC4yNjg1TDI5LjM4MTQgNTYuNTAxOUMyOS40OTk1IDU2LjYxODYgMjkuNjU4OSA1Ni42ODQgMjkuODI0OSA1Ni42ODRDMjkuOTkwOSA1Ni42ODQgMzAuMTUwMiA1Ni42MTg2IDMwLjI2ODQgNTYuNTAxOUw1Ni41MDE4IDMwLjMwMjZDNTYuNjE4NSAzMC4xODQ1IDU2LjY4MzkgMzAuMDI1MSA1Ni42ODM5IDI5Ljg1OTFDNTYuNjgzOSAyOS42OTMxIDU2LjYxODUgMjkuNTMzOCA1Ni41MDE4IDI5LjQxNTZWMjkuMzgxNVpNNDUuMTkzMSAyMy41MTRMNDAuMDc2MSAzMy4zODk5QzQwLjA0MiAzMy40ODA1IDQwLjA0NDIgMzMuNTgwOCA0MC4wODI0IDMzLjY2OThDNDAuMTIwNSAzMy43NTg5IDQwLjE5MTYgMzMuODI5NyA0MC4yODA3IDMzLjg2NzVINDMuNTA0NUM0My41NTEgMzMuODcxNiA0My41OTYyIDMzLjg4NTIgNDMuNjM3MiAzMy45MDc1QzQzLjY3ODMgMzMuOTI5OCA0My43MTQzIDMzLjk2MDMgNDMuNzQzMSAzMy45OTcxQzQzLjc3MTkgMzQuMDMzOCA0My43OTI4IDM0LjA3NjEgNDMuODA0NiAzNC4xMjEzQzQzLjgxNjMgMzQuMTY2NSA0My44MTg3IDM0LjIxMzYgNDMuODExNSAzNC4yNTk4QzQzLjgxMjIgMzQuMzA2MyA0My44MDE3IDM0LjM1MjIgNDMuNzgwOSAzNC4zOTM4QzQzLjc2MDIgMzQuNDM1MyA0My43Mjk3IDM0LjQ3MTMgNDMuNjkyMSAzNC40OTg2TDMwLjE4MzEgNDQuMjU1MUMzMC4xMTk5IDQ0LjI4OCAzMC4wNDk3IDQ0LjMwNTIgMjkuOTc4NCA0NC4zMDUyQzI5LjkwNzEgNDQuMzA1MiAyOS44MzY5IDQ0LjI4OCAyOS43NzM3IDQ0LjI1NTFMMTYuMTk2NSAzNC41MTU2QzE2LjEzMTcgMzQuNDQ2MiAxNi4wOTU3IDM0LjM1NDggMTYuMDk1NyAzNC4yNTk4QzE2LjA5NTcgMzQuMTY0OCAxNi4xMzE3IDM0LjA3MzQgMTYuMTk2NSAzNC4wMDM5QzE2LjI1NiAzMy45NDE4IDE2LjMzMzYgMzMuOTAwMSAxNi40MTgyIDMzLjg4NDVIMTkuNDM3M0MxOS41MzY4IDMzLjg4NDUgMTkuNjMyMiAzMy44NDUgMTkuNzAyNiAzMy43NzQ2QzE5Ljc3MyAzMy43MDQzIDE5LjgxMjUgMzMuNjA4OCAxOS44MTI1IDMzLjUwOTNWMzMuMzg5OUwxNC42OTU1IDIzLjUxNEMxNC42NTM5IDIzLjQyNTUgMTQuNjQ4NSAyMy4zMjQ0IDE0LjY4MDMgMjMuMjMyQzE0LjcxMjIgMjMuMTM5NiAxNC43Nzg4IDIzLjA2MzMgMTQuODY2IDIzLjAxOTNDMTQuOTAzNSAyMy4wMDI0IDE0Ljk0NDMgMjIuOTkzNiAxNC45ODU0IDIyLjk5MzZDMTUuMDI2NiAyMi45OTM2IDE1LjA2NzMgMjMuMDAyNCAxNS4xMDQ4IDIzLjAxOTNMMjcuMjQ5MyAzMC45ODQ5QzI3LjQ3MTYgMzEuMTA1MSAyNy42NDYxIDMxLjI5NzcgMjcuNzQ0IDMxLjUzMDdMMjkuNjAzMSAzNS4yNDkxQzI5LjYyMTQgMzUuMjg5IDI5LjY0NzUgMzUuMzI0OCAyOS42Nzk5IDM1LjM1NDRDMjkuNzEyNCAzNS4zODM5IDI5Ljc1MDUgMzUuNDA2NSAyOS43OTIgMzUuNDIwOUMyOS44MzM1IDM1LjQzNTIgMjkuODc3NCAzNS40NDEgMjkuOTIxMiAzNS40Mzc5QzI5Ljk2NSAzNS40MzQ3IDMwLjAwNzYgMzUuNDIyNyAzMC4wNDY2IDM1LjQwMjZDMzAuMTE4MSAzNS4zNzA1IDMwLjE3NzggMzUuMzE2OCAzMC4yMTcyIDM1LjI0OTFMMzIuMDA4MiAzMS41NDc4QzMyLjEzMTMgMzEuMzMyIDMyLjMwMDEgMzEuMTQ1NyAzMi41MDI4IDMxLjAwMTlMNDQuNjQ3MyAyMy4wMTkzQzQ0LjY4OTggMjIuOTk5MSA0NC43MzYgMjIuOTg3NSA0NC43ODMgMjIuOTg1MUM0NC44MzAxIDIyLjk4MjggNDQuODc3MSAyMi45ODk3IDQ0LjkyMTUgMjMuMDA1NUM0NC45NjU4IDIzLjAyMTQgNDUuMDA2NiAyMy4wNDU4IDQ1LjA0MTUgMjMuMDc3NUM0NS4wNzY0IDIzLjEwOTEgNDUuMTA0OCAyMy4xNDczIDQ1LjEyNDkgMjMuMTg5OUM0NS4xNjI4IDIzLjIzMzcgNDUuMTg5IDIzLjI4NjQgNDUuMjAxIDIzLjM0MzFDNDUuMjEyOSAyMy4zOTk4IDQ1LjIxMDIgMjMuNDU4NiA0NS4xOTMxIDIzLjUxNFoiIGZpbGw9IiM0NUREQzAiLz4KPC9zdmc+Cg==' + }, + { + id: 'Partner', + base64: 'PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQ0LjM2ODUgMjEuMTIwNkwzNi43MTE5IDI2LjAwMjVDMzUuNjE4MiAyNi41NDUgMzUuMDcxMyAyNi41NDUgMzMuOTc3NCAyNi4wMDI1QzMzLjQzMDUgMjUuNDYwMSAzMi4zMzY4IDI0LjkxNzYgMzEuNzg5OSAyNC4zNzUyQzI5LjYwMjMgMjMuODMyOCAyNy45NjE3IDI0LjM3NTIgMjYuMzIwOSAyNS40NjAxTDIzLjU4NjQgMjcuMDg3NEw5LjkxNDA2IDM2LjMwOUM2LjYzMjcgMzcuOTM2MyAyLjgwNDQyIDM3LjM5MzkgMS4xNjM3MyAzNC4xMzkxQy0xLjAyMzg1IDMxLjQyNjkgMC4wNjk5NDMyIDI3LjYyOTggMi44MDQ0MiAyNS40NjAxTDE5LjIxMTIgMTQuNjExMkMyMy41ODY0IDExLjg5OSAyOS4wNTU0IDEwLjgxNDEgMzMuOTc3NCAxMS44OTlDMzguMzUyNSAxMi45ODM5IDQxLjYzNCAxNS4xNTM3IDQ0LjM2ODUgMTguNDA4M0M0NS40NjIzIDE5LjQ5MzMgNDQuOTE1NCAyMC41NzgxIDQ0LjM2ODUgMjEuMTIwNloiIGZpbGw9IiM1ODY1RjIiLz4KPHBhdGggZD0iTTYwIDMwLjY0MjNDNjAgMzIuNzk5MiA1OC45MDggMzQuOTU2MiA1Ny4yNjk4IDM2LjAzNDdMNDAuMzQzMyA0Ni44MTkxQzM3LjA2NzEgNDguOTc1OSAzMy43OTA5IDUwLjA1NDQgMjkuOTY4NyA1MC4wNTQ0QzI4LjMzMDcgNTAuMDU0NCAyNy4yMzg3IDUwLjA1NDQgMjUuNjAwNSA0OS41MTUxQzIxLjIzMjUgNDguNDM2OCAxNy45NTYzIDQ2LjI3OTggMTUuMjI2MiA0My4wNDQ1QzE0LjY4MDIgNDEuOTY2IDE1LjIyNjIgNDAuODg3NSAxNS43NzIyIDQwLjM0ODVMMjMuNDE2NSAzNS40OTU0QzIzLjk2MjUgMzQuOTU2MiAyNS4wNTQ1IDM0Ljk1NjIgMjUuNjAwNSAzNS40OTU0QzI2LjY5MjcgMzYuMDM0NyAyNy43ODQ3IDM2LjU3MzkgMjguMzMwNyAzNy4xMTNDMzAuNTE0OSAzNy4xMTMgMzIuMTUyOSAzNy4xMTMgMzMuNzkwOSAzNi4wMzQ3TDM3LjYxMzEgMzMuODc3N0w0OC41MzM0IDI2LjMyODVMNTAuMTcxNiAyNS4yNUM1Mi45MDE2IDIzLjYzMjUgNTcuMjY5OCAyNC4xNzE3IDU4LjkwOCAyNi44Njc4QzYwIDI4LjQ4NTUgNjAgMjkuNTYzOCA2MCAzMC42NDIzWiIgZmlsbD0iIzU4NjVGMiIvPgo8L3N2Zz4K' + }, + { + id: 'PremiumEarlySupporter', + base64: 'PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTExLjE3MjUgMTIuNTk5M0MxMS4xNzI1IDEyLjU5OTMgMTEuODY2OCAxMS42MDU2IDEyLjE1NDggMTEuMTY1NEMxMi40NDI4IDEwLjcyNTIgMTEuNDA5MyA5LjY4NTE1IDExLjAwODMgOS4zNTU2NEMxMC42MDczIDkuMDI2MTMgOS45NzQ4MiA5LjE5MDg4IDkuOTc0ODIgOS4xOTA4OEM0LjkyODU4IDExLjMzMDEgMi40MDQxMSAxNS43MTkzIDIuMDU5NjIgMTYuNTk3MkMxLjcxNTEzIDE3LjQ3NSAyLjkzNjk5IDE4LjQzMjcgMy44Mzg1OSAxOC45MDEyQzQuMjUzMDUgMTkuMTE3NCA0LjkxMjQzIDE4LjYzODYgNS4zMTg4MiAxOC4yODM0TDUuNDU2MDggMTguMTQ5NUw1LjY2ODY5IDE3Ljk0MzZMNS42NzEzOCAxNy45NDFMMTkuMzk5OCAzMS4wMjExTDI0LjcyMzMgMjUuOTI5MUwxMS4xNTM2IDEyLjYxNzNMMTEuMTcyNSAxMi41OTkzWiIgZmlsbD0iIzUyNjZGRiIvPgo8cGF0aCBkPSJNNDMuNjA0NyAyNi4zOTcyTDQ0LjMyODYgMjUuNzM4Mkw1MC44MzM2IDMxLjkwMTFDNTAuODc5MyAzMS45Mzk3IDUwLjkyNzggMzEuOTU1MiA1MC45NzA4IDMxLjk1NTJDNTEuMDk0NiAzMS45NTUyIDUxLjE5NjkgMzEuODQ3MSA1MS4xOTY5IDMxLjg0NzFDNTEuMTk2OSAzMS44NDcxIDU4LjM5ODkgMjUuMDEyMyA1OC40MjMxIDI0Ljk4OTFDNTguNjM1NyAyNC43ODgzIDU4LjQyMzEgMjQuNjc3NiA1OC40MjMxIDI0LjY3NzZMNTIuODY4MiAxOS4zMjgyTDUyLjg2MDIgMTkuMzM1OUw1Mi4wOTg1IDE4LjYyMjhMNTIuNTIzNyAxOC4yMzE1TDUzLjQzMzQgMTguMzMxOUw1My4yNjEyIDE3LjE3ODZMNTMuNDg5OSAxNi45NTcyTDUzLjE0ODEgMTUuMjk0MkM1MS45NjEzIDEzLjY4NTMgNDguNzEwMSAxMS4xMDg0IDQ4LjcxMDEgMTEuMTA4NEw0Ny4wMDkyIDEwLjgxNDlMNDYuODM3IDExLjAzMzdMNDUuNTU1OSAxMC44NTFMNDUuNjc5NyAxMS44OTYxTDQ1Ljc2MDQgMTEuOTczNEw0NS4zODEgMTIuMzM4OUw0My4yODE3IDEwLjM3NDdDNDMuMjgxNyAxMC4zNzQ3IDMxLjEwMDggNC4wNDcwNSAzMC40NTIyIDMuNzUzNThDMzAuMDg2MSAzLjU5MTQgMjkuODE3IDMuNDgzMjggMjkuNTU4NiAzLjQ4MzI4QzI5LjM1OTUgMy40ODMyOCAyOS4xNjU3IDMuNTQ3NjMgMjguOTQyMyAzLjY5OTUyQzI4LjQyNTYgNC4wNDcwNSAyOC43Mjk3IDQuNzQ3MjYgMjguNzI5NyA0Ljc0NzI2TDM2LjMwMzEgMTguMTMzN0wzNy44MTgzIDE5LjU3MDJMMzcuMTY5NyAyMC4xOTA2TDM1Ljk1ODYgMjAuMDMxTDM2LjE0NDMgMjEuMTc5MUwzNS43OTQ0IDIxLjUxMzhMMzUuNzMyNSAyMS40NTQ2QzM1LjY3NiAyMS40MDA1IDM1LjU5OCAyMS4zNzIyIDM1LjUyMjYgMjEuMzcyMkMzNS40NDczIDIxLjM3MjIgMzUuMzcxOSAyMS40MDA1IDM1LjMxMjcgMjEuNDU0NkMzNS4xOTcgMjEuNTY1MyAzNS4xOTcgMjEuNzQ1NSAzNS4zMTI3IDIxLjg1NjJMMzUuMzc0NiAyMS45MTU0TDM1LjIwNzcgMjIuMDc3NUwzNS4xNjQ3IDIyLjAzMzhDMzUuMTA1NSAyMS45Nzk3IDM1LjAzMDEgMjEuOTUxNCAzNC45NTQ3IDIxLjk1MTRDMzQuODc2NyAyMS45NTE0IDM0LjgwMTMgMjEuOTc5NyAzNC43NDQ4IDIyLjAzMzhDMzQuNjI5MSAyMi4xNDQ1IDM0LjYyOTEgMjIuMzI0NyAzNC43NDQ4IDIyLjQzNTRMMzQuNzkwNiAyMi40NzkxTDMxLjYwNCAyNS41NDI2TDMxLjUzMTQgMjUuNDczMUMzMS40NzIyIDI1LjQxOSAzMS4zOTY4IDI1LjM5MDcgMzEuMzIxNSAyNS4zOTA3QzMxLjI0NjEgMjUuMzkwNyAzMS4xNzA3IDI1LjQxOSAzMS4xMTE1IDI1LjQ3MzFDMzAuOTk1OCAyNS41ODM4IDMwLjk5NTggMjUuNzY0IDMxLjExMTUgMjUuODc0N0wzMS4xODY5IDI1Ljk0NDJMMzEuMDE3MyAyNi4xMDYzTDMwLjk2MDggMjYuMDU0OUMzMC45MDQzIDI1Ljk5ODIgMzAuODI2MiAyNS45Njk5IDMwLjc1MzYgMjUuOTY5OUMzMC42NzU1IDI1Ljk2OTkgMzAuNjAwMiAyNS45OTgyIDMwLjU0MSAyNi4wNTQ5QzMwLjQyNzkgMjYuMTY1NiAzMC40Mjc5IDI2LjM0MzIgMzAuNTQxIDI2LjQ1MzlMMzAuNTk3NSAyNi41MDc5TDMwLjE4MyAyNi45MDk1TDMwLjEyNjUgMjcuNDE5MkwzMC4zNDQ1IDI3LjYzMjlMMzAuMzM5MSAyNy42MzU1TDMwLjA3NTQgMjcuODkyOUwxMi44ODA1IDQ0LjMzNzZMMTIuNjU3MSA0NC4xNkwxMS45NDM5IDQ0LjI1NzhMMTEuNTQwMiA0NC42NDkxTDExLjUxMzMgNDQuNjIzNEMxMS40NTQxIDQ0LjU2NjggMTEuMzc4NyA0NC41Mzg0IDExLjMwMzQgNDQuNTM4NEMxMS4yMjggNDQuNTM4NCAxMS4xNDk5IDQ0LjU2NjggMTEuMDkzNCA0NC42MjM0QzEwLjk3NzcgNDQuNzM0MSAxMC45Nzc3IDQ0LjkxMTcgMTEuMDkzNCA0NS4wMjI0TDExLjEyNTcgNDUuMDUzM0wxMC45NTg5IDQ1LjIxODFMMTAuOTQyNyA0NS4yMDI2QzEwLjg4NjIgNDUuMTQ2IDEwLjgwODEgNDUuMTE3NyAxMC43MzI4IDQ1LjExNzdDMTAuNjU3NCA0NS4xMTc3IDEwLjU4MjEgNDUuMTQ2IDEwLjUyMjkgNDUuMjAyNkMxMC40MDk4IDQ1LjMxMzMgMTAuNDA5OCA0NS40OTA5IDEwLjUyMjkgNDUuNjAxNkwxMC41NDE3IDQ1LjYyMjJMNy4zOTU1NCA0OC42ODMxTDcuMzMwOTUgNDguNjIxM0M3LjI3MTc0IDQ4LjU2NDcgNy4xOTkwNyA0OC41Mzg5IDcuMTIxMDMgNDguNTM4OUM3LjA0NTY3IDQ4LjUzODkgNi45NzAzMSA0OC41NjQ3IDYuOTExMSA0OC42MjEzQzYuNzk1MzcgNDguNzMyIDYuNzk1MzcgNDguOTEyMiA2LjkxMTEgNDkuMDIyOUw2Ljk4MTA4IDQ5LjA4NzJMNi44MTQyMSA0OS4yNDk0TDYuNzYzMDggNDkuMjAwNUM2LjcwMzg3IDQ5LjE0NjUgNi42Mjg1MSA0OS4xMTgxIDYuNTUwNDYgNDkuMTE4MUM2LjQ3NzggNDkuMTE4MSA2LjM5OTc1IDQ5LjE0NjUgNi4zNDMyMyA0OS4yMDA1QzYuMjI3NSA0OS4zMTEyIDYuMjI3NSA0OS40OTE0IDYuMzQzMjMgNDkuNjAyMUw2LjM5NzA2IDQ5LjY1NjJMNi4xNjI5MSA0OS44ODUzTDQuOTQzNzQgNDkuNjc2OEw1LjAzNTI1IDUwLjk3OTRMNC42NTAzOSA1MS4zNTUyTDQuOTg0MTEgNTMuMTEwOUM0Ljk4NDExIDUzLjExMDkgNS40OTU0NiA1NC42NCA2LjU0Nzc3IDU1LjY1NDNDNy41Njc3OSA1Ni42Mzc3IDkuMTIzMzcgNTcuMTA4OCA5LjE5MDY2IDU3LjE1NzdMMTAuOTQ4MSA1Ny40NTEyTDExLjM4MTQgNTcuMDQ0NUwxMi41OTI1IDU3LjIzMjRMMTIuMzkzMyA1Ni4wODY4TDEyLjc2MiA1NS43NDE5TDEyLjkzMTYgNTUuOTA0QzEyLjk4ODEgNTUuOTU4MSAxMy4wNjM1IDU1Ljk4NjQgMTMuMTM4OCA1NS45ODY0QzEzLjIxNDIgNTUuOTg2NCAxMy4yOTIyIDU1Ljk1ODEgMTMuMzQ4OCA1NS45MDRDMTMuNDY0NSA1NS43OTA4IDEzLjQ2NDUgNTUuNjEzMSAxMy4zNDg4IDU1LjUwMjRMMTMuMTgxOSA1NS4zNDI4TDEzLjM1NDEgNTUuMTgzMkwxMy40OTk1IDU1LjMyMjJDMTMuNTU2IDU1LjM3ODkgMTMuNjM0IDU1LjQwNzIgMTMuNzA5NCA1NS40MDcyQzEzLjc4NDggNTUuNDA3MiAxMy44NjAxIDU1LjM3ODkgMTMuOTE5MyA1NS4zMjIyQzE0LjAzNSA1NS4yMTE1IDE0LjAzNSA1NS4wMzM5IDEzLjkxOTMgNTQuOTIzMkwxMy43NzY3IDU0Ljc4NDJMMTYuOTcxMyA1MS43Njk3TDE3LjExMTIgNTEuOTAzNUMxNy4xNjc4IDUxLjk2MDIgMTcuMjQ1OCA1MS45ODU5IDE3LjMyMTIgNTEuOTg1OUMxNy4zOTY1IDUxLjk4NTkgMTcuNDcxOSA1MS45NjAyIDE3LjUzMTEgNTEuOTAzNUMxNy42NDQxIDUxLjc5MjkgMTcuNjQ0MSA1MS42MTI3IDE3LjUzMTEgNTEuNTAyTDE3LjM5MzggNTEuMzczMkwxNy41NjM0IDUxLjIxMTFMMTcuNjgxOCA1MS4zMjQzQzE3LjczODMgNTEuMzc4NCAxNy44MTM3IDUxLjQwNjcgMTcuODg5IDUxLjQwNjdDMTcuOTY3MSA1MS40MDY3IDE4LjA0MjQgNTEuMzc4NCAxOC4wOTkgNTEuMzI0M0MxOC4yMTQ3IDUxLjIxMzYgMTguMjE0NyA1MS4wMzM0IDE4LjA5OSA1MC45MjI3TDE3Ljk4MzIgNTAuODEyTDE4LjM0MTIgNTAuNDc3NEwxOC40NzA0IDQ5Ljc0MzdMMTguMjc5MyA0OS41NjA5TDE4LjI4MiA0OS41NTgzTDE4LjYyOTEgNDkuMjI4OEwzNS43MDgzIDMyLjg4NzFMMzUuNzAwMiAzMi44NzE2TDM2LjAzNCAzMy4yMDEyTDM2LjU2OTUgMzMuMTQ0NUwzNy4wNTEzIDMyLjY4MTFMMzcuMTI5MyAzMi43NTU4QzM3LjE4ODUgMzIuODA5OSAzNy4yNjM5IDMyLjgzODIgMzcuMzM5MyAzMi44MzgyQzM3LjQxNDYgMzIuODM4MiAzNy40OTI3IDMyLjgwOTkgMzcuNTQ5MiAzMi43NTU4QzM3LjY2NDkgMzIuNjQ1MSAzNy42NjQ5IDMyLjQ2NDkgMzcuNTQ5MiAzMi4zNTQyTDM3LjQ3MTEgMzIuMjc5NkwzNy42NDA3IDMyLjExOTlMMzcuNjk5OSAzMi4xNzY2QzM3Ljc1NjQgMzIuMjMwNiAzNy44MzE4IDMyLjI1OSAzNy45MDk4IDMyLjI1OUMzNy45ODUyIDMyLjI1OSAzOC4wNjA1IDMyLjIzMDYgMzguMTE3MSAzMi4xNzY2QzM4LjIzMjggMzIuMDY1OSAzOC4yMzI4IDMxLjg4NTcgMzguMTE3MSAzMS43NzVMMzguMDU3OCAzMS43MTU4TDQxLjI0OTggMjguNjU3NUw0MS4zMzMyIDI4LjczNzNDNDEuMzg5NyAyOC43OTE0IDQxLjQ2NTEgMjguODE5NyA0MS41NDA0IDI4LjgxOTdDNDEuNjE4NSAyOC44MTk3IDQxLjY5MzggMjguNzkxNCA0MS43NTAzIDI4LjczNzNDNDEuODY2MSAyOC42MjY2IDQxLjg2NjEgMjguNDQ2NCA0MS43NTAzIDI4LjMzNTdMNDEuNjY2OSAyOC4yNTU5TDQxLjgzNjUgMjguMDkzN0w0MS45MDExIDI4LjE1ODFDNDEuOTYwMyAyOC4yMTIxIDQyLjAzNTYgMjguMjQwNSA0Mi4xMTEgMjguMjQwNUM0Mi4xODYzIDI4LjI0MDUgNDIuMjYxNyAyOC4yMTIxIDQyLjMyMDkgMjguMTU4MUM0Mi40MzY2IDI4LjA0NDggNDIuNDM2NiAyNy44NjcyIDQyLjMyMDkgMjcuNzU2NUw0Mi4yNTYzIDI3LjY5MjFMNDIuNjExNiAyNy4zNTIzTDQzLjc5ODUgMjcuNTI5OUw0My42MDQ3IDI2LjM5NzJaIiBmaWxsPSIjNTI2NkZGIi8+CjxwYXRoIGQ9Ik00NS45NzY3IDQ0Ljg2NzhMMzguNDA2IDM5LjM3OTRMMzcuMDU3NiAzOC4wNTFMMzEuODMzNyA0My4wNDc3TDMzLjE0NzEgNDQuMjQ3NEwzOC4wODAzIDUxLjMyNDJMNDQuNzMzMyA1OC4yOTI4TDUyLjQwMDkgNTEuNDUwM0w0NS45NzY3IDQ0Ljg2NzhaIiBmaWxsPSIjNTI2NkZGIi8+Cjwvc3ZnPgo=' + }, + { + id: 'VerifiedDeveloper', + base64: 'PHN2ZyB3aWR0aD0iNjEiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MSA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTU1LjE1OTMgMjguODEzNUw0NC4yNjkgMTAuMDFMNDMuMzc4NCA4LjQ4MzI4SDE4LjE2MjhMMTcuMjcyMyAxMC4wMUw2LjQwNzQxIDI4LjgxMzVMNS41MTY4NSAzMC4zNDAyTDYuNDA3NDEgMzEuODY2OUwxNy4yNzIzIDUwLjY3MDVMMTguMTYyOCA1Mi4xOTcxSDQzLjQwMzlMNDQuMjk0NSA1MC42NzA1TDU1LjE4NDggMzEuODY2OUw1Ni4wNzUzIDMwLjM0MDJMNTUuMTU5MyAyOC44MTM1Wk0yMS45MDMyIDI2LjE5MjdMMTcuNzU1NyAzMC4zNDAyTDIxLjkwMzIgMzQuNDg3N1Y0MS40MzQxTDEwLjgwOTMgMzAuMzQwMkwyMS45Mjg2IDE5LjIyMDlWMjYuMTkyN0gyMS45MDMyWk0yOS44NDE5IDQzLjgyNTlMMjQuNjUxMiA0Mi4yMjI5TDMyLjUzOSAxNi44MjkxTDM3LjcyOTcgMTguNDU3NkwyOS44NDE5IDQzLjgyNTlaTTM5LjY2MzUgNDEuNDU5NVYzNC41MTMxTDQzLjgxMSAzMC4zNDAyTDM5LjY2MzUgMjYuMTkyN1YxOS4yMjA5TDUwLjc1NzQgMzAuMzQwMkwzOS42NjM1IDQxLjQ1OTVaIiBmaWxsPSIjM0U3MEREIi8+Cjwvc3ZnPgo=' + } +]; + +export default badges; \ No newline at end of file diff --git a/src/lib/constants/status.ts b/src/lib/constants/status.ts new file mode 100644 index 0000000..edb2cd8 --- /dev/null +++ b/src/lib/constants/status.ts @@ -0,0 +1,20 @@ +const status = [ + { + id: 'online', + base64: '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' + }, + { + id: 'dnd', + base64: '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' + }, + { + id: 'idle', + base64: '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' + }, + { + id: 'offline', + base64: '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' + } +]; + +export default status; \ No newline at end of file diff --git a/src/lib/express/createServer.ts b/src/lib/express/createServer.ts new file mode 100644 index 0000000..542d638 --- /dev/null +++ b/src/lib/express/createServer.ts @@ -0,0 +1,65 @@ +import express from 'express'; +import { createRouter } from 'express-file-routing'; +import compression from 'compression'; +import morganMiddleware from '@/express/middlewares/morganMiddleware'; +import handleSimultaneousRequests from '@/express/middlewares/handleSimultaneousRequests'; +import notFoundHandler from '@/express/middlewares/notFoundHandler'; +import addCustomMethods from '@/express/middlewares/addCustomMethods'; +import handleErrors from '@/express/middlewares/handleErrors'; +import ws from 'express-ws'; +import path from 'node:path'; +import fs from 'node:fs'; +import swaggerUi from 'swagger-ui-express'; + +async function createServer() { + const options = { + wsOptions: { + maxPayload: config.server.socket.maxpayload, + clientTracking: config.server.socket.clienttracking, + keepAlive: config.server.socket.keepalive + } + }; + const { app, getWss } = ws(express(), undefined, options); + + global.getWss = getWss; + + // Configure the server + app.set('trust proxy', true); + app.set('x-powered-by', false); + app.set('etag', false); + + // Add middlewares + app.use(morganMiddleware); + app.use(compression()); + app.use(handleSimultaneousRequests as express.RequestHandler); + app.use(addCustomMethods as express.RequestHandler); + + function corsMiddleware(request: express.Request, response: express.Response, next: express.NextFunction) { + response.header('Access-Control-Allow-Origin', '*'); + next(); + } + + // Add 'Access-Control-Allow-Origin' header to the response to allow CORS + app.use(corsMiddleware as express.RequestHandler); + + /* + Configure the express-file-routing package + to automatically load routes from the 'routes' directory + */ + await createRouter(app, { + directory: path.join(process.cwd(), 'src/lib/express/routes'), + additionalMethods: ['ws'] + }); + + const swaggerSpec = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'swagger.json'), 'utf-8')); + app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + + app.use(notFoundHandler as express.RequestHandler); + app.use(handleErrors as express.ErrorRequestHandler); + + app.listen(config.server.port, () => { + logger.log('http', `Server is listening on port ${config.server.port}.`); + }); +} + +export default createServer; \ No newline at end of file diff --git a/src/lib/express/middlewares/addCustomMethods.ts b/src/lib/express/middlewares/addCustomMethods.ts new file mode 100644 index 0000000..8a40a01 --- /dev/null +++ b/src/lib/express/middlewares/addCustomMethods.ts @@ -0,0 +1,14 @@ +import type { Request, Response, NextFunction } from 'express'; + +function addCustomMethods(_request: Request, response: Response, next: NextFunction): void { + response.sendError = (message: string, statusCode: number = 500): void => { + response.status(statusCode).json({ + success: false, + error: message + }); + }; + + next(); +} + +export default addCustomMethods; \ No newline at end of file diff --git a/src/lib/express/middlewares/handleErrors.ts b/src/lib/express/middlewares/handleErrors.ts new file mode 100644 index 0000000..865c14e --- /dev/null +++ b/src/lib/express/middlewares/handleErrors.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { Request, Response, NextFunction } from 'express'; + +function handleErrors(error: Error, _request: Request, response: Response, next: NextFunction): void { + logger.error(error); + + response.sendError('Internal Server Error', 500); +} + +export default handleErrors; \ No newline at end of file diff --git a/src/lib/express/middlewares/handleSimultaneousRequests.ts b/src/lib/express/middlewares/handleSimultaneousRequests.ts new file mode 100644 index 0000000..4b96966 --- /dev/null +++ b/src/lib/express/middlewares/handleSimultaneousRequests.ts @@ -0,0 +1,56 @@ +import crypto from 'node:crypto'; +import AsyncLock from 'async-lock'; +import type { Request, Response, NextFunction } from 'express'; + +const lock = new AsyncLock(); + +// Function to generate a unique request key +const generateRequestKey = (request: Request) => { + const { method, originalUrl, body } = request; + const bodyString = JSON.stringify(body); + const requestIp = request.ip; // Use request.ip instead of request.clientIp + + // Create an MD5 hash from the method, URL, body, and IP address + return crypto.createHash('md5').update(`${method}${originalUrl}${bodyString}${requestIp}`).digest('hex'); +}; + +async function handleSimultaneousRequests(request: Request, response: Response, next: NextFunction): Promise { + const { method } = request; + + // Only handle specific HTTP methods + if (!['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) return next(); + + const requestKey = generateRequestKey(request); + + try { + await new Promise((resolve, reject) => { + lock.acquire(requestKey, async done => { + try { + response.on('finish', () => { + done(); + resolve(); + }); + + response.on('error', error => { + done(); + reject(error); + }); + + next(); + } catch (error) { + done(); + reject(error); + } + }); + }); + } catch (error) { + const errorMessage = (error as Error).message; + + response.status(500).json({ error: `Failed to process the request: ${errorMessage}` }); + + logger.error('There was an error while processing the request:'); + logger.error(error); + } +}; + +export default handleSimultaneousRequests; \ No newline at end of file diff --git a/src/lib/express/middlewares/morganMiddleware.ts b/src/lib/express/middlewares/morganMiddleware.ts new file mode 100644 index 0000000..f041728 --- /dev/null +++ b/src/lib/express/middlewares/morganMiddleware.ts @@ -0,0 +1,9 @@ +import morgan from 'morgan'; + +const morganMiddleware = morgan(':method :url from :remote-addr | Status: :status | Response Time: :response-time ms', { + stream: { + write: (message: string) => logger.log('http', message.trim()) + } +}); + +export default morganMiddleware; \ No newline at end of file diff --git a/src/lib/express/middlewares/notFoundHandler.ts b/src/lib/express/middlewares/notFoundHandler.ts new file mode 100644 index 0000000..2acd4c4 --- /dev/null +++ b/src/lib/express/middlewares/notFoundHandler.ts @@ -0,0 +1,8 @@ +import type { Request, Response, NextFunction } from 'express'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function handleNotFound(_request: Request, response: Response, _next: NextFunction): void { + response.status(404).json({ error: 'Resource not found.' }); +} + +export default handleNotFound; \ No newline at end of file diff --git a/src/lib/express/middlewares/validateRequest.ts b/src/lib/express/middlewares/validateRequest.ts new file mode 100644 index 0000000..9e88c61 --- /dev/null +++ b/src/lib/express/middlewares/validateRequest.ts @@ -0,0 +1,11 @@ +import type { Request, Response, NextFunction } from 'express'; +import { validationResult } from 'express-validator'; + +const validateRequest = (request: Request, response: Response, next: NextFunction) => { + const errors = validationResult(request); + if (!errors.isEmpty()) return response.status(400).json({ errors: errors.array()[0].msg }); + + next(); +}; + +export default validateRequest; \ No newline at end of file diff --git a/src/lib/express/routes/api/v1/users/[user_id]/createSvg.ts b/src/lib/express/routes/api/v1/users/[user_id]/createSvg.ts new file mode 100644 index 0000000..b97f9ea --- /dev/null +++ b/src/lib/express/routes/api/v1/users/[user_id]/createSvg.ts @@ -0,0 +1,453 @@ +/* + my brain is completely fired + I can't think of anything to write here :( +*/ + +import * as dateFns from 'date-fns'; +import axios from 'axios'; +import badges from '@/constants/badges'; +import status from '@/constants/status'; +import * as Discord from 'discord.js'; +import he from 'he'; +import type { UserData, CustomStatusActivity, OtherActivity, CreateSvgOptions } from '@/src/types'; + +function activityElapsedTime(activity: CustomStatusActivity | OtherActivity) { + const startTime = new Date(activity.type === 4 ? activity.start_time.raw : activity.timestamps.start_time.raw); + const elapsedTime = dateFns.differenceInSeconds(new Date(), startTime); + const humanReadable = dateFns.format(new Date(elapsedTime * 1000), 'mm:ss'); + + return `${humanReadable} elapsed`; +} + +async function createSvg(userData: UserData, options: CreateSvgOptions = {}) { + const defaultOptions = { + theme: 'dark', + borderRadius: 2, + hideGlobalName: false, + hideStatus: false, + hideBadges: false, + hideActivity: false, + noActivityTitle: 'No Activity', + noActivityMessage: 'User is not currently doing anything.' + }; + + // Assign default options if not provided + options = Object.keys(defaultOptions).reduce((acc, key) => { + acc[key] = options[key] || defaultOptions[key]; + + return acc; + }, {}); + + const variables = { + colors: options.theme === 'dark' ? config.user_svg_card.colors.dark : config.user_svg_card.colors.light, + statusColors: { + online: '#43b581', + idle: '#faa61a', + dnd: '#f04747', + offline: '#747f8d' + } + }; + + const icons = { + FaImage: (width, height, fill) => `` + }; + + const currentStatus = Object.values(userData.active_platforms).find(platform => platform !== 'offline') || 'offline'; + + const div_height = 225; + const svg_height = 300; + + // Remove custom status activity from activities array + userData.activities = userData.activities.filter(activity => activity.type !== Discord.ActivityType.Custom); + + const firstActivity = userData.activities[0] as OtherActivity; + + const images = [ + { + id: 'display_avatar', + url: userData.metadata.display_avatar_url, + base64: null + }, + { + id: 'large_image', + url: firstActivity?.assets?.large_image?.image_url, + base64: null + }, + { + id: 'small_image', + url: firstActivity?.assets?.small_image?.image_url, + base64: null + } + ]; + + await Promise.all(images + .filter(image => image.url) + .map(async image => { + const response = await axios.get(image.url, { responseType: 'arraybuffer' }) + .catch(() => null); + + if (!response?.data) return; + + const base64 = Buffer.from(response.data, 'binary').toString('base64'); + + image.base64 = base64; + + return image; + })); + + const svgCard = ` + + +
+
+ Powered by lantern.rest +
+ +
+
+ ${he.escape(userData.metadata.username)}'s avatar + + ${!options.hideStatus ? ` +
+ Status +
+ ` : ''} +
+
+ +
+
+
+
+ @${he.escape(userData.metadata.username)} +
+ + ${!options.hideBadges ? ` +
+ ${userData.metadata.flags.human_readable.map(flag => ` + Badge ${flag} + `).join('')} +
+ ` : ''} +
+ ${!options.hideGlobalName ? ` +
+ ${he.escape(userData.metadata.global_name)} +
+ ` : ''} +
+
+ + ${!options.hideActivity && userData.activities.length > 0 ? ` +
+ +
+
+
+
+
+
+ ${(firstActivity?.name && firstActivity?.assets?.large_image?.image_url) ? ` + Large image + ` : ` + ${icons.FaImage(50, 50, variables.colors.text.secondary)} + `} + + ${firstActivity?.assets?.small_image?.image_url ? ` + Small image + ` : ''} +
+
+ +
+
+ ${he.escape(firstActivity?.name)} +
+ + ${firstActivity?.details ? ` +
+ ${he.escape(firstActivity?.details)} +
+ ` : ''} + + ${firstActivity?.state ? ` +
+ ${he.escape(firstActivity?.state)} +
+ ` : ''} + +
+ ${activityElapsedTime(userData.activities[0])} +
+
+
+
+
+
+ ` : ` +
+ +
+
+ ${he.escape(options.noActivityTitle)} +
+ +
+ ${he.escape(options.noActivityMessage)} +
+
+ `} +
+ + + `; + + console.log(svgCard); + + return svgCard; +} + +export default createSvg; \ No newline at end of file diff --git a/src/lib/express/routes/api/v1/users/[user_id]/index.ts b/src/lib/express/routes/api/v1/users/[user_id]/index.ts new file mode 100644 index 0000000..14e544d --- /dev/null +++ b/src/lib/express/routes/api/v1/users/[user_id]/index.ts @@ -0,0 +1,203 @@ +import User from '@/models/User'; +import { param, query } from 'express-validator'; +import validateRequest from '@/express/middlewares/validateRequest'; +import createUserData from '@/utils/bot/createUserData'; +import createSvg from '@/express/routes/api/v1/users/[user_id]/createSvg'; +import Storage from '@/models/Storage'; +import type { Request, Response } from 'express'; +import type { APIUsersGETRequestQuery } from '@/src/types'; + +interface RequestParams { + user_id: string; +} + +export const get = [ + param('user_id') + .exists().withMessage('user_id is required.') + .isNumeric().withMessage('user_id must be a number.') + .isLength({ min: 17, max: 19 }).withMessage('user_id must be 17-19 characters long.'), + query('svg') + .optional() + .isNumeric().withMessage('svg must be a number.') + .isIn([0, 1]).withMessage('svg must be either 0 or 1.') + .toInt(), + query('theme') + .optional() + .isString().withMessage('Theme must be a string.') + .isIn(['light', 'dark']).withMessage('Theme must be either "light" or "dark".'), + query('borderRadius') + .optional() + .isNumeric().withMessage('borderRadius must be a number.') + .toInt(), + query('hideGlobalName') + .optional() + .isNumeric().withMessage('hideGlobalName must be a number.') + .isIn([0, 1]).withMessage('hideGlobalName must be either 0 or 1.') + .toInt(), + query('hideStatus') + .optional() + .isNumeric().withMessage('hideStatus must be a number.') + .isIn([0, 1]).withMessage('hideStatus must be either 0 or 1.'), + query('hideBadges') + .optional() + .isNumeric().withMessage('hideBadges must be a number.') + .isIn([0, 1]).withMessage('hideBadges must be either 0 or 1.') + .toInt(), + query('hideActivity') + .optional() + .isNumeric().withMessage('hideActivity must be a number.') + .isIn([0, 1]).withMessage('hideActivity must be either 0 or 1.') + .toInt(), + query('noActivityTitle') + .optional() + .isString().withMessage('noActivityTitle must be a string.') + .isLength({ min: 1, max: 64 }).withMessage('noActivityTitle must be 1-64 characters long.'), + query('noActivityMessage') + .optional() + .isString().withMessage('noActivityMessage must be a string.') + .isLength({ min: 1, max: 256 }).withMessage('noActivityMessage must be 1-256 characters long.'), + validateRequest, + async (request: Request, response: Response) => { + const { user_id } = request.params; + const { svg } = request.query; + + const guild = client.guilds.cache.get(config.base_guild_id); + const member = guild.members.cache.get(user_id); + + if (!member) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + + const user = await User.findOne({ id: user_id }).lean(); + if (!user) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + + const user_storage = await Storage.findOne({ userId: user_id }); + + const createdUserData = createUserData(user_id, user_storage?.kv || {}); + + if (svg == 1) { + const options = {}; + + for (const key in request.query) { + if (key !== 'svg') { + options[key] = [0, 1].includes(parseInt(request.query[key])) ? parseInt(request.query[key]) : request.query[key]; + } + } + + try { + const svg = await createSvg( + createdUserData, + options + ); + + response.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); + response.setHeader('Content-Type', 'image/svg+xml'); + response.setHeader('content-security-policy', 'default-src \'none\'; img-src * data:; style-src \'unsafe-inline\''); + + return response.send(svg); + } catch (error) { + logger.error(`There was an error creating the SVG for user ${user_id}:`); + logger.error(error); + + return response.status(500).json({ error: 'An error occurred while creating the SVG.' }); + } + } + + return response.json(createdUserData); + } +]; + +// module.exports = { +// get: [ +// param('user_id') +// .exists().withMessage('user_id is required.') +// .isNumeric().withMessage('user_id must be a number.') +// .isLength({ min: 17, max: 19 }).withMessage('user_id must be 17-19 characters long.'), +// query('svg') +// .optional() +// .isNumeric().withMessage('svg must be a number.') +// .isIn([0, 1]).withMessage('svg must be either 0 or 1.'), +// query('theme') +// .optional() +// .isString().withMessage('Theme must be a string.') +// .isIn(['light', 'dark']).withMessage('Theme must be either "light" or "dark".'), +// query('borderRadius') +// .optional() +// .isNumeric().withMessage('borderRadius must be a number.'), +// query('hideGlobalName') +// .optional() +// .isNumeric().withMessage('hideGlobalName must be a number.') +// .isIn([0, 1]).withMessage('hideGlobalName must be either 0 or 1.'), +// query('hideStatus') +// .optional() +// .isNumeric().withMessage('hideStatus must be a number.') +// .isIn([0, 1]).withMessage('hideStatus must be either 0 or 1.'), +// query('hideBadges') +// .optional() +// .isNumeric().withMessage('hideBadges must be a number.') +// .isIn([0, 1]).withMessage('hideBadges must be either 0 or 1.'), +// query('hideActivity') +// .optional() +// .isNumeric().withMessage('hideActivity must be a number.') +// .isIn([0, 1]).withMessage('hideActivity must be either 0 or 1.'), +// query('noActivityTitle') +// .optional() +// .isString().withMessage('noActivityTitle must be a string.') +// .isLength({ min: 1, max: 64 }).withMessage('noActivityTitle must be 1-64 characters long.'), +// query('noActivityMessage') +// .optional() +// .isString().withMessage('noActivityMessage must be a string.') +// .isLength({ min: 1, max: 256 }).withMessage('noActivityMessage must be 1-256 characters long.'), +// validateRequest, +// async (request, response) => { +// const { user_id } = request.params; +// const { svg } = request.query; + +// const guild = client.guilds.cache.get(config.base_guild_id); +// const member = guild.members.cache.get(user_id); + +// if (!member) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + +// const user = await User.findOne({ id: user_id }).lean(); +// if (!user) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + +// const user_storage = await Storage.findOne({ userId: user_id }); + +// const createdUserData = createUserData(user_id, user_storage?.kv || {}); + +// if (svg == 1) { +// response.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); +// response.setHeader('Content-Type', 'image/svg+xml'); +// response.setHeader('content-security-policy', 'default-src \'none\'; img-src * data:; style-src \'unsafe-inline\''); + +// const defaultOptions = { +// theme: 'dark', +// borderRadius: 2, +// hideGlobalName: false, +// hideStatus: false, +// hideBadges: false, +// hideActivity: false, +// noActivityTitle: 'No Activity', +// noActivityMessage: 'User is not currently doing anything.' +// }; + +// const svg = await createSvg( +// createdUserData, +// { +// ...defaultOptions, +// ...Object.keys(request.query).reduce((acc, key) => { +// if (key !== 'svg') return { +// ...acc, +// // If the value is 0 or 1, convert it to a number +// // Otherwise, keep it as a string (for theme or borderRadius for example) +// [key]: [0, 1].includes(parseInt(request.query[key])) ? parseInt(request.query[key]) : request.query[key] +// }; +// }, {}) +// } +// ); + +// return response.send(svg); +// } + +// return response.json(createdUserData); +// } +// ] +// }; \ No newline at end of file diff --git a/src/lib/express/routes/api/v1/users/[user_id]/storage/[key].ts b/src/lib/express/routes/api/v1/users/[user_id]/storage/[key].ts new file mode 100644 index 0000000..6716834 --- /dev/null +++ b/src/lib/express/routes/api/v1/users/[user_id]/storage/[key].ts @@ -0,0 +1,226 @@ +import User from '@/models/User'; +import Storage from '@/models/Storage'; +import { param, body, matchedData } from 'express-validator'; +import validateRequest from '@/express/middlewares/validateRequest'; +import { decrypt } from '@/utils/encryption'; +import getValidationError from '@/utils/getValidationError'; +import bodyParser from 'body-parser'; +import type { Request, Response } from 'express'; +import type { IncomingHttpHeaders } from 'node:http'; + +interface PutRequestParams { + user_id: string; + key: string; +} + +interface PutRequestBody { + value: string; +} + +interface RequestHeaders extends IncomingHttpHeaders { + authorization?: string; +} + +export const put = [ + bodyParser.json(), + param('user_id') + .exists().withMessage('user_id is required.') + .isNumeric().withMessage('user_id must be a number.') + .isLength({ min: 17, max: 19 }).withMessage('user_id must be 17-19 characters long.'), + param('key') + .exists().withMessage('key is required.') + .isString().withMessage('key must be a string.') + .isLength({ min: 1, max: 255 }).withMessage('key must be 1-255 characters long.') + .custom(value => /^[a-zA-Z0-9]+$/.test(value)).withMessage('key must be alphanumeric.'), + body('value') + .exists().withMessage('value is required.') + .isString().withMessage('value must be a string.') + .isLength({ max: 30000 }).withMessage('value must not exceed 30,000 characters.'), + validateRequest, + async (request: Request, response: Response) => { + const { user_id, key, value } = matchedData(request); + + const guild = client.guilds.cache.get(config.base_guild_id); + const member = guild.members.cache.get(user_id); + + if (!member) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + + const user = await User.findOne({ id: user_id }).lean(); + if (!user) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + + const storage = await Storage.findOne({ userId: user_id }); + if (!storage) return response.status(404).json({ error: `User ${user_id} does not have any storage.` }); + + const headers = request.headers as RequestHeaders; + const authorizationHeader = headers.authorization; + + if (!authorizationHeader) return response.status(401).json({ error: 'Unauthorized.' }); + + const decryptedToken = decrypt(storage.token, process.env.KV_TOKEN_ENCRYPTION_SECRET); + if (authorizationHeader !== decryptedToken) return response.status(401).json({ error: 'Unauthorized.' }); + + if (!storage.kv) storage.kv = new Map(); + + storage.kv.set(key, value); + + const validationError = getValidationError(storage); + if (validationError) return response.status(400).json({ error: validationError }); + + await storage.save(); + + return response.json({ success: true }); + } +]; + +interface GetRequestParams { + user_id: string; + key: string; +} + +export const get = [ + param('user_id') + .exists().withMessage('user_id is required.') + .isNumeric().withMessage('user_id must be a number.') + .isLength({ min: 17, max: 19 }).withMessage('user_id must be 17-19 characters long.'), + param('key') + .exists().withMessage('key is required.') + .isString().withMessage('key must be a string.') + .isLength({ min: 1, max: 255 }).withMessage('key must be 1-255 characters long.') + .custom(value => /^[a-zA-Z0-9]+$/.test(value)).withMessage('key must be alphanumeric.'), + validateRequest, + async (request: Request, response: Response) => { + const { user_id, key } = matchedData(request); + + const guild = client.guilds.cache.get(config.base_guild_id); + const member = guild.members.cache.get(user_id); + + if (!member) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + + const user = await User.findOne({ id: user_id }).lean(); + if (!user) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + + const storage = await Storage.findOne({ userId: user_id }); + if (!storage) return response.status(404).json({ error: `User ${user_id} does not have any storage.` }); + + if (!storage.kv) storage.kv = new Map(); + + const value = storage.kv.get(key); + if (!value) return response.status(404).json({ error: `Key ${key} does not exist in the storage.` }); + + return response.json({ value }); + } +]; + +interface PatchRequestParams { + user_id: string; + key: string; +} + +interface PatchRequestBody { + value: string; +} + +export const patch = [ + bodyParser.json(), + param('user_id') + .exists().withMessage('user_id is required.') + .isNumeric().withMessage('user_id must be a number.') + .isLength({ min: 17, max: 19 }).withMessage('user_id must be 17-19 characters long.'), + param('key') + .exists().withMessage('key is required.') + .isString().withMessage('key must be a string.') + .isLength({ min: 1, max: 255 }).withMessage('key must be 1-255 characters long.') + .custom(value => /^[a-zA-Z0-9]+$/.test(value)).withMessage('key must be alphanumeric.'), + body('value') + .exists().withMessage('value is required.') + .isString().withMessage('value must be a string.') + .isLength({ max: 30000 }).withMessage('value must not exceed 30,000 characters.'), + validateRequest, + async (request: Request, response: Response) => { + const { user_id, key, value } = matchedData(request); + + const guild = client.guilds.cache.get(config.base_guild_id); + const member = guild.members.cache.get(user_id); + + if (!member) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + + const user = await User.findOne({ id: user_id }).lean(); + if (!user) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + + const storage = await Storage.findOne({ userId: user_id }); + if (!storage) return response.status(404).json({ error: `User ${user_id} does not have any storage.` }); + + const headers = request.headers as RequestHeaders; + const authorizationHeader = headers.authorization; + + if (!authorizationHeader) return response.status(401).json({ error: 'Unauthorized.' }); + + const decryptedToken = decrypt(storage.token, process.env.KV_TOKEN_ENCRYPTION_SECRET); + if (authorizationHeader !== decryptedToken) return response.status(401).json({ error: 'Unauthorized.' }); + + if (!storage.kv) storage.kv = new Map(); + + if (!storage.kv.has(key)) return response.status(404).json({ error: `Key ${key} does not exist in the storage.` }); + + storage.kv.set(key, value); + + const validationError = getValidationError(storage); + if (validationError) return response.status(400).json({ error: validationError }); + + await storage.save(); + + return response.json({ success: true }); + } +]; + +interface DeleteRequestParams { + user_id: string; + key: string; +} + +export const del = [ + param('user_id') + .exists().withMessage('user_id is required.') + .isNumeric().withMessage('user_id must be a number.') + .isLength({ min: 17, max: 19 }).withMessage('user_id must be 17-19 characters long.'), + param('key') + .exists().withMessage('key is required.') + .isString().withMessage('key must be a string.') + .isLength({ min: 1, max: 255 }).withMessage('key must be 1-255 characters long.') + .custom(value => /^[a-zA-Z0-9]+$/.test(value)).withMessage('key must be alphanumeric.'), + validateRequest, + async (request: Request, response: Response) => { + const { user_id, key } = matchedData(request); + + const guild = client.guilds.cache.get(config.base_guild_id); + const member = guild.members.cache.get(user_id); + + if (!member) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + + const user = await User.findOne({ id: user_id }).lean(); + if (!user) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + + const storage = await Storage.findOne({ userId: user_id }); + if (!storage) return response.status(404).json({ error: `User ${user_id} does not have any storage.` }); + + const headers = request.headers as RequestHeaders; + const authorizationHeader = headers.authorization; + + if (!authorizationHeader) return response.status(401).json({ error: 'Unauthorized.' }); + + const decryptedToken = decrypt(storage.token, process.env.KV_TOKEN_ENCRYPTION_SECRET); + if (authorizationHeader !== decryptedToken) return response.status(401).json({ error: 'Unauthorized.' }); + + if (!storage.kv) storage.kv = new Map(); + + if (!storage.kv.has(key)) return response.status(404).json({ error: `Key ${key} does not exist in the storage.` }); + + storage.kv.delete(key); + + if (storage.kv.size === 0) delete storage.kv; + + await storage.save(); + + return response.json({ success: true }); + } +]; \ No newline at end of file diff --git a/src/lib/express/routes/api/v1/users/[user_id]/storage/index.ts b/src/lib/express/routes/api/v1/users/[user_id]/storage/index.ts new file mode 100644 index 0000000..6ecab37 --- /dev/null +++ b/src/lib/express/routes/api/v1/users/[user_id]/storage/index.ts @@ -0,0 +1,77 @@ +import { param } from 'express-validator'; +import bodyParser from 'body-parser'; +import User from '@/models/User'; +import Storage from '@/models/Storage'; +import validateRequest from '@/express/middlewares/validateRequest'; +import { decrypt } from '@/utils/encryption'; +import type { Request, Response } from 'express'; +import { IncomingHttpHeaders } from 'node:http'; + +interface RequestQuery { + user_id: string; +} + +interface RequestHeaders extends IncomingHttpHeaders { + authorization?: string; +} + +export const get = [ + param('user_id') + .exists().withMessage('user_id is required.') + .isNumeric().withMessage('user_id must be a number.') + .isLength({ min: 17, max: 19 }).withMessage('user_id must be 17-19 characters long.'), + validateRequest, + async (request: Request, response: Response) => { + const { user_id } = request.query; + + const guild = client.guilds.cache.get(config.base_guild_id); + const member = guild.members.cache.get(user_id); + + if (!member) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + + const user = await User.findOne({ id: user_id }).lean(); + if (!user) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + + const storage = await Storage.findOne({ userId: user_id }); + if (!storage) return response.status(404).json({ error: `User ${user_id} does not have any storage.` }); + + return response.json(storage.kv || {}); + } +]; + +export const del = [ + bodyParser.json(), + param('user_id') + .exists().withMessage('user_id is required.') + .isNumeric().withMessage('user_id must be a number.') + .isLength({ min: 17, max: 19 }).withMessage('user_id must be 17-19 characters long.'), + validateRequest, + async (request: Request, response: Response) => { + const { user_id } = request.query; + + const guild = client.guilds.cache.get(config.base_guild_id); + const member = guild.members.cache.get(user_id); + + if (!member) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + + const user = await User.findOne({ id: user_id }).lean(); + if (!user) return response.status(404).json({ error: `User ${user_id} is not being monitored by Lantern.` }); + + const storage = await Storage.findOne({ userId: user_id }); + if (!storage) return response.status(404).json({ error: `User ${user_id} does not have any storage.` }); + + const headers = request.headers as RequestHeaders; + const authorizationHeader = headers.authorization; + + if (!authorizationHeader) return response.status(401).json({ error: 'Unauthorized.' }); + + const decryptedToken = decrypt(storage.token, process.env.KV_TOKEN_ENCRYPTION_SECRET); + if (authorizationHeader !== decryptedToken) return response.status(401).json({ error: 'Unauthorized.' }); + + delete storage.kv; + + await storage.save(); + + return response.json({ success: true }); + } +]; \ No newline at end of file diff --git a/src/lib/express/routes/api/v1/users/index.ts b/src/lib/express/routes/api/v1/users/index.ts new file mode 100644 index 0000000..ed5c5a4 --- /dev/null +++ b/src/lib/express/routes/api/v1/users/index.ts @@ -0,0 +1,41 @@ +import User from '@/models/User'; +import { query } from 'express-validator'; +import validateRequest from '@/express/middlewares/validateRequest'; +import createUserData from '@/utils/bot/createUserData'; +import Storage from '@/models/Storage'; +import bodyParser from 'body-parser'; +import type { Request, Response } from 'express'; + +interface RequestQuery { + user_ids: string[]; +} + +export const get = [ + bodyParser.json(), + query('user_ids') + .exists().withMessage('user_ids is required.') + .isArray().withMessage('user_ids must be an array.') + .custom(value => value.every(id => typeof id === 'string' && id.length >= 17 && id.length <= 19)).withMessage('user_ids must be an array of strings with 17-19 characters long.') + .custom(value => value.length === new Set(value).size).withMessage('user_ids must not contain duplicates.'), + validateRequest, + async (request: Request, response: Response) => { + const { user_ids } = request.query; + + if (user_ids.length === config.max_bulk_get_users_size) return response.status(400).json({ error: `You can only request up to ${config.max_bulk_get_users_size} users at once.` }); + + const users = await User.find({ id: { $in: user_ids } }).lean(); + + const notMonitoredUsers = user_ids.filter(id => !users.some(({ id: user_id }) => user_id === id)); + if (notMonitoredUsers.length === user_ids.length) return response.status(404).json({ error: 'Users you requested are not monitored by Lantern.' }); + + const usersStorages = await Storage.find({ userId: { $in: user_ids } }); + + const createdUsersData = users.map(user => { + const user_storage = usersStorages.find(({ userId }) => userId === user.id); + + return createUserData(user.id, user_storage?.kv || {}); + }); + + return response.json(createdUsersData); + } +]; \ No newline at end of file diff --git a/src/lib/express/routes/index.ts b/src/lib/express/routes/index.ts new file mode 100644 index 0000000..f5e90b7 --- /dev/null +++ b/src/lib/express/routes/index.ts @@ -0,0 +1,16 @@ +import type { Response } from 'express'; +import User from '@/models/User'; + +export const get = [ + async (_, response: Response) => { + const currentlyMonitoringUsers = await User.countDocuments(); + + return response.json({ + data: { + info: 'Lantern provides Discord presences as an API and WebSocket. Find out more here: https://github.com/discordplace/lantern', + discord_invite: 'https://invite.lantern.rest', + currently_monitoring_users: currentlyMonitoringUsers + } + }); + } +]; \ No newline at end of file diff --git a/src/lib/express/routes/socket/index.ts b/src/lib/express/routes/socket/index.ts new file mode 100644 index 0000000..9aaf356 --- /dev/null +++ b/src/lib/express/routes/socket/index.ts @@ -0,0 +1,190 @@ +import * as Discord from 'discord.js'; +import User from '@/models/User'; +import crypto from 'crypto'; +import getZodError from '@/utils/getZodError'; +import createUserData from '@/utils/bot/createUserData'; +import { send, disconnect } from '@/express/routes/socket/utils'; +import Storage from '@/models/Storage'; +import type { WebSocket } from 'ws'; +import { InitSchema } from '@/express/routes/socket/schemas'; + +global.ActiveSockets = new Discord.Collection(); + +const Opcodes = config.server.socket.opcodes; + +async function subscribeToUsers(socket: WebSocket, socketId: string, { user_id, user_ids }: { user_id?: string, user_ids?: string[] }) { + const subscribed_to_all = user_id === 'ALL'; + + if (user_id && !subscribed_to_all) { + const user = await User.findOne({ id: user_id }); + if (!user) return send(socket, Opcodes.ERROR, `User ${user_id} not found.`); + } + + if (user_ids) { + const users = await User.find({ id: { $in: user_ids } }); + if (users.length !== user_ids.length) return send(socket, Opcodes.ERROR, `User(s) ${user_ids.filter(id => !users.map(user => user.id).includes(id)).join(', ')} not found.`); + } + + // Update or create new entry in ActiveSockets collection. + if (ActiveSockets.has(socketId)) { + // check if the user is already subscribed to all/one user. + const { subscribed } = ActiveSockets.get(socketId); + if (subscribed === 'ALL') return send(socket, Opcodes.ERROR, 'You are already subscribed to all users.'); + + if (typeof subscribed === 'string') { + if (subscribed.includes(user_id)) return send(socket, Opcodes.ERROR, `You are already subscribed to user ${user_id}.`); + } + + if (typeof subscribed === 'object') { + const already_subscribed = subscribed.filter((subscribedUserId: string) => user_ids.includes(subscribedUserId)); + if (already_subscribed.length) return send(socket, Opcodes.ERROR, `You are already subscribed to user(s) ${already_subscribed.join(', ')}.`); + } + + ActiveSockets.set(socketId, { + instance: socket, + lastHeartbeat: Date.now(), + subscribed: subscribed_to_all ? 'ALL' : [...subscribed, ...(user_ids || [user_id])] + }); + + send(socket, Opcodes.SUBSCRIBE_ACK); + + logger.log('socket', `Websocket connection ${socketId} subscribed to ${subscribed_to_all ? 'all users' : (user_ids || [user_id]).join(', ')}.`); + } else { + ActiveSockets.set(socketId, { + instance: socket, + lastHeartbeat: Date.now(), + subscribed: subscribed_to_all ? 'ALL' : (user_ids || [user_id || '']) + }); + } +} + +export const ws = [ + async (websocket: WebSocket) => { + send(websocket, Opcodes.HELLO, { heartbeat_interval: config.server.socket.heartbeat_interval }); + + // wait for INIT message, if not received, close the connection. + + const timeout = setTimeout(() => websocket.close(), 5000); + const id = crypto.randomBytes(16).toString('hex'); + + websocket.on('message', async message => { + const { op, d: data } = JSON.parse(message.toString()); + + if (!Object.values(Opcodes).includes(op)) return disconnect(websocket, null, 'Invalid opcode.'); + + // Check if the opcode is allowed to be sent to the server + if (!config.server.socket.client_allowed_opcodes.includes(op)) return disconnect(websocket, null, 'You are not allowed to send this opcode to the server.'); + + switch (op) { + case Opcodes.INIT: + // Clear timeout since the connection is now established. + clearTimeout(timeout); + + var error = getZodError(data, InitSchema); + if (error) return disconnect(websocket, null, error); + + var subscribed_to_all = data.user_id === 'ALL'; + + await subscribeToUsers(websocket, id, { user_id: data.user_id, user_ids: data.user_ids }); + + logger.log('socket', `New websocket connection: ${id}`); + + // Acknowledge the connection. + if (subscribed_to_all) { + const users = await User.find(); + const storages = await Storage.find({ userId: { $in: users.map(user => user.id) } }); + + const userData = users.map(user => createUserData(user.id, storages.find(storage => storage.userId === user.id)?.kv || {})); + + send(websocket, Opcodes.INIT_ACK, userData); + + logger.log('socket', `Websocket connection ${id} subscribed to all users.`); + } else if (data.user_id) { + const user_storage = await Storage.findOne({ userId: data.user_id }); + + send(websocket, Opcodes.INIT_ACK, createUserData(data.user_id, user_storage?.kv || {})); + + logger.log('socket', `Websocket connection ${id} subscribed to user ${data.user_id}.`); + } else { + const users = await User.find({ id: { $in: data.user_ids } }); + const storages = await Storage.find({ userId: { $in: users.map(user => user.id) } }); + + const userData = users.map(user => createUserData(user.id, storages.find(storage => storage.userId === user.id)?.kv || {})); + + send(websocket, Opcodes.INIT_ACK, userData); + + logger.log('socket', `Websocket connection ${id} subscribed to users ${data.user_ids.join(', ')}.`); + } + + break; + case Opcodes.HEARTBEAT: + if (!ActiveSockets.has(id)) return disconnect(websocket, null, 'Invalid websocket connection.'); + + ActiveSockets.set(id, { + instance: websocket, + lastHeartbeat: Date.now(), + subscribed: ActiveSockets.get(id).subscribed + }); + + send(websocket, Opcodes.HEARTBEAT_ACK); + + break; + // Allow to adding user(s) after initial connection. + case Opcodes.SUBSCRIBE: + if (!ActiveSockets.has(id)) return disconnect(websocket, null, 'Invalid websocket connection.'); + + var error = getZodError(data, InitSchema); + if (error) return send(websocket, Opcodes.ERROR, error); + + // if already subscribed to all users, return an error. + if (ActiveSockets.get(id).subscribed === 'ALL') return send(websocket, Opcodes.ERROR, 'You are already subscribed to all users.'); + + await subscribeToUsers(websocket, id, { user_id: data.user_id, user_ids: data.user_ids }); + + break; + // Allow to remove user(s) after initial connection. + case Opcodes.UNSUBSCRIBE: + if (!ActiveSockets.has(id)) return disconnect(websocket, null, 'Invalid websocket connection.'); + + var error = getZodError(data, InitSchema); + if (error) return send(websocket, Opcodes.ERROR, error); + + var { user_id, user_ids } = data; + var { subscribed } = ActiveSockets.get(id); + + // if already subscribed to all users, return an error. + // If it's a string, it means the user is subscribed to all users. + if (typeof subscribed === 'string') return send(websocket, Opcodes.ERROR, 'You are subscribed to all users.'); + + if (user_id) { + if (!subscribed.includes(user_id)) return send(websocket, Opcodes.ERROR, `You are not subscribed to user ${user_id}.`); + } + + if (user_ids) { + const not_subscribed = user_ids.filter((userId: string) => !subscribed.includes(userId)); + if (not_subscribed.length) return send(websocket, Opcodes.ERROR, `You are not subscribed to user(s) ${not_subscribed.join(', ')}.`); + } + + // remove the user(s) from the subscribed list. + var new_subscribed = subscribed.filter((subscribedUserId: string) => { + if (user_id) return subscribedUserId !== user_id; + if (user_ids) return !user_ids.includes(subscribedUserId); + }); + + if (!new_subscribed.length) return disconnect(websocket, id, 'You require at least one user to be subscribed. Connection closed.'); + + ActiveSockets.set(id, { + instance: websocket, + lastHeartbeat: Date.now(), + subscribed: new_subscribed + }); + + send(websocket, Opcodes.UNSUBSCRIBE_ACK); + + logger.log('socket', `Websocket connection ${id} unsubscribed from ${user_id ? `user ${user_id}` : user_ids.join(', ')}.`); + + break; + } + }); + } +]; \ No newline at end of file diff --git a/src/lib/express/routes/socket/schemas.ts b/src/lib/express/routes/socket/schemas.ts new file mode 100644 index 0000000..88c2e14 --- /dev/null +++ b/src/lib/express/routes/socket/schemas.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +const InitSchema = z.object({ + user_ids: z.array(z.string().min(3).max(19)).optional(), + user_id: z.string().min(3).max(19).optional() +}) + .refine(data => data.user_ids || data.user_id, { + message: 'Either user_ids or user_id must be provided.' + }) + .refine(data => !data.user_ids || !data.user_id, { + message: 'Only one of user_ids or user_id can be provided.' + }); + +export { + InitSchema +}; \ No newline at end of file diff --git a/src/lib/express/routes/socket/utils.ts b/src/lib/express/routes/socket/utils.ts new file mode 100644 index 0000000..adb3444 --- /dev/null +++ b/src/lib/express/routes/socket/utils.ts @@ -0,0 +1,51 @@ +import type { ServerSocketOpcodes } from '@/src/types/global'; +import { WebSocket } from 'ws'; + +const Opcodes = config.server.socket.opcodes; + +interface Payload { + t: string | undefined; + op?: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + d?: any; +} + +/** + * Sends a message through the WebSocket connection. + * + * @param {WebSocket} socket - The WebSocket connection. + * @param {number} op - The opcode of the message. + * @param {any} d - The data to send. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function send(socket: WebSocket, op: ServerSocketOpcodes, d?: any): void { + const payload_name = Object.entries(Opcodes).find(([, value]) => value === op)?.[0]; + + const payload: Payload = { + t: payload_name, + op + }; + + if (d) payload.d = d; + + socket.send(JSON.stringify(payload)); +} + +/** + * Disconnects the WebSocket connection. + * + * @param {WebSocket} socket - The WebSocket connection. + * @param {string} id - The ID of the connection. + * @param {string} error - The error message. + */ +function disconnect(socket: WebSocket, id: string | null, error: string): void { + if (id) { + ActiveSockets.delete(id); + logger.log('database', `Websocket connection closed: ${id}`); + } + + send(socket, Opcodes.DISCONNECT, { error }); + socket.close(); +} + +export { send, disconnect }; \ No newline at end of file diff --git a/src/lib/models/EvaulateResult.ts b/src/lib/models/EvaulateResult.ts new file mode 100644 index 0000000..2e00846 --- /dev/null +++ b/src/lib/models/EvaulateResult.ts @@ -0,0 +1,28 @@ +import mongoose from 'mongoose'; + +const Schema = mongoose.Schema; + +const EvaluateResult = new Schema({ + id: { + type: String, + required: true + }, + result: { + type: String, + required: true + }, + hasError: { + type: Boolean, + required: true + }, + executedCode: { + type: String, + required: true + } +}, { + timestamps: true +}); + +export type EvaluateResultType = mongoose.InferSchemaType; + +export default mongoose.model('EvaluateResults', EvaluateResult); \ No newline at end of file diff --git a/src/lib/models/Storage.ts b/src/lib/models/Storage.ts new file mode 100644 index 0000000..d1c2b76 --- /dev/null +++ b/src/lib/models/Storage.ts @@ -0,0 +1,82 @@ +import mongoose from 'mongoose'; +import createUserData from '@/utils/bot/createUserData'; +import { send as socket_send } from '@/express/routes/socket/utils'; +import { ChangeStreamDocument } from 'mongodb'; + +const Schema = mongoose.Schema; + +const Storage = new Schema({ + userId: { + type: String, + required: true + }, + token: { + iv: { + type: String, + required: true + }, + encryptedText: { + type: String, + required: true + }, + tag: { + type: String, + required: true + } + }, + kv: { + type: Map, + of: String, + validate: { + validator(v: Map) { + // Ensure that each key is alphanumeric and doesn't exceed 255 characters + for (const key of v.keys()) { + if (!/^[a-zA-Z0-9]+$/.test(key) || key.length > 255) { + return false; + } + } + + // Ensure that each value doesn't exceed 30,000 characters + for (const value of v.values()) { + if (value.length > 30000) { + return false; + } + } + + return true; + }, + message: () => 'Invalid key-value pair.' + } + } +}, { + timestamps: true, + versionKey: false +}); + +export type StorageType = mongoose.InferSchemaType; + +const Model = mongoose.model('KeyValueStorages', Storage); + +Model.watch().on('change', async (metadata: ChangeStreamDocument) => { + // @ts-expect-error - The _id property is always present + const { documentKey: { _id }, operationType } = metadata; + + // Do not send a socket message if the token was updated + if (operationType === 'update') { + const { updateDescription } = metadata; + if (updateDescription.updatedFields?.token) return; + } + + // Find the storage document that was updated + const foundStorage = await Model.findById(_id).lean(); + if (!foundStorage) return; + + // Send a message to all active sockets that the user's storage has changed + for (const [, data] of ActiveSockets) { + if (data.subscribed === 'ALL' || data.subscribed.includes(foundStorage.userId)) { + socket_send(data.instance, config.server.socket.opcodes.STORAGE_UPDATE, createUserData(foundStorage.userId, foundStorage.kv || {})); + } + } +}); + +export default Model; \ No newline at end of file diff --git a/src/lib/models/User.ts b/src/lib/models/User.ts new file mode 100644 index 0000000..339a48c --- /dev/null +++ b/src/lib/models/User.ts @@ -0,0 +1,16 @@ +import mongoose from 'mongoose'; + +const Schema = mongoose.Schema; + +const User = new Schema({ + id: { + type: String, + required: true + } +}, { + versionKey: false +}); + +export type UserType = mongoose.InferSchemaType; + +export default mongoose.model('Users', User); \ No newline at end of file diff --git a/src/lib/utils/bot/createUserData.ts b/src/lib/utils/bot/createUserData.ts new file mode 100644 index 0000000..eed8ef8 --- /dev/null +++ b/src/lib/utils/bot/createUserData.ts @@ -0,0 +1,160 @@ +import * as Discord from 'discord.js'; +import * as dateFns from 'date-fns'; +import type { UserData } from '@/src/types'; + +/** + * Creates a user data object for a given user ID and key-value storage. + * + * @param {string} user_id - The ID of the user to create data for. + * @param {Map | {}} kv - A key-value storage object or map. + * @returns {UserData} The user data object containing metadata, status, active platforms, activities, and storage. + * @throws {Error} If the base guild or member is not found. + */ +function createUserData(user_id: string, kv: Map | {}): UserData { + const guild = client.guilds.cache.get(config.base_guild_id); + if (!guild) throw new Error('Base guild not found.'); + + const member = guild.members.cache.get(user_id); + if (!member) throw new Error('Member not found.'); + + const activePlatforms = ['desktop', 'mobile', 'web'] + .map(platform => { + if (!member.presence || !member.presence.clientStatus) return { [platform]: 'offline' }; + + const currentPlatformStatus = member.presence.clientStatus as Record; + + return { [platform]: currentPlatformStatus[platform] || 'offline' }; + + }) + .reduce((acc, curr) => ({ ...acc, ...curr }), {}); + + const spotifyActivity = member.presence?.activities.find(activity => activity.name === 'Spotify'); + + if (spotifyActivity) { + // Calculate current human-readable time relative to start time + const currentTime = new Date(); + const startTime = spotifyActivity.timestamps?.start || new Date(); + const endTime = spotifyActivity.timestamps?.end || new Date(); + + const elapsedTime = dateFns.differenceInSeconds(currentTime, startTime); + const currentHumanReadable = dateFns.format(new Date(elapsedTime * 1000), 'mm:ss'); + + // Calculate human-readable end time + const totalDuration = dateFns.differenceInSeconds(startTime, endTime); + const endHumanReadable = dateFns.format(new Date(totalDuration * 1000), 'mm:ss'); + + const artistCount = spotifyActivity.state?.split('; ').length || 0; + + Object.assign(activePlatforms, { + spotify: { + track_id: spotifyActivity.syncId, + song: spotifyActivity.details, + artist: artistCount > 1 ? spotifyActivity.state?.split('; ') : spotifyActivity.state, + album: spotifyActivity.assets?.largeText, + album_cover: spotifyActivity.assets?.largeImageURL(), + start_time: { + unix: spotifyActivity.timestamps?.start?.getTime(), + raw: spotifyActivity.timestamps?.start + }, + end_time: { + unix: spotifyActivity.timestamps?.end?.getTime(), + raw: spotifyActivity.timestamps?.end + }, + time: { + start_human_readable: currentHumanReadable, + end_human_readable: endHumanReadable + } + } + }); + } + + const parsedActivites = []; + + for (const activity of member.presence?.activities || []) { + switch (activity.name) { + case 'Custom Status': + parsedActivites.push({ + name: activity.name, + type: 4, + emoji: activity.emoji, + text: activity.state, + start_time: { + unix: activity.createdTimestamp, + raw: activity.createdAt + }, + end_time: activity.timestamps?.end ? { + unix: activity.timestamps.end.getTime(), + raw: activity.timestamps.end + } : null + }); + break; + default: + var activityData = { + name: activity.name, + type: activity.type as unknown as keyof typeof Discord.ActivityType, + state: activity.state, + details: activity.details, + application_id: activity.applicationId, + created_at: activity.createdTimestamp + }; + + if (activity.assets) { + Object.assign(activityData, { + assets: { + large_image: { + hash: activity.assets.largeImage, + image_url: activity.assets.largeImageURL(), + text: activity.assets.largeText + }, + small_image: { + hash: activity.assets.smallImage, + image_url: activity.assets.smallImageURL(), + text: activity.assets.smallText + } + } + }); + } + + if (activity.timestamps) { + Object.assign(activityData, { + start_time: { + unix: activity.timestamps?.start?.getTime(), + raw: activity.timestamps.start + } + }); + } + + parsedActivites.push(activityData); + + break; + } + } + + return { + metadata: { + id: user_id, + username: member.user.username, + discriminator: member.user.discriminator, + global_name: member.user.globalName, + avatar: member.user.avatar, + avatar_url: member.user.avatarURL(), + display_avatar_url: member.user.displayAvatarURL(), + bot: member.user.bot, + flags: { + human_readable: new Discord.UserFlagsBitField(member.user.flags?.bitfield) + .toArray(), + bitfield: member.user.flags?.bitfield + }, + monitoring_since: { + unix: member.joinedTimestamp, + raw: member.joinedAt + } + }, + status: member.presence?.status || 'offline', + active_platforms: activePlatforms, + activities: parsedActivites, + storage: kv + }; +} + +export default createUserData; \ No newline at end of file diff --git a/src/lib/utils/bot/getApplicationIdFromToken.ts b/src/lib/utils/bot/getApplicationIdFromToken.ts new file mode 100644 index 0000000..cd62c98 --- /dev/null +++ b/src/lib/utils/bot/getApplicationIdFromToken.ts @@ -0,0 +1,14 @@ +/** + * Extracts the application ID from a given token. + * + * The token is expected to be a base64 encoded string, where the application ID + * is located in the first segment of the token (before the first dot). + * + * @param token - The token from which to extract the application ID. + * @returns The decoded application ID as a string. + */ +function getApplicationIdFromToken(token: string) { + return Buffer.from(token.split('.')[0], 'base64').toString(); +} + +export default getApplicationIdFromToken; \ No newline at end of file diff --git a/src/lib/utils/bot/getCommandName.ts b/src/lib/utils/bot/getCommandName.ts new file mode 100644 index 0000000..d4a272e --- /dev/null +++ b/src/lib/utils/bot/getCommandName.ts @@ -0,0 +1,34 @@ +import type { BaseInteraction } from 'discord.js'; + +/** + * Extracts the command name, subcommand, and subcommand group from a given interaction. + * + * @param interaction - The interaction object from which to extract the command details. + * @returns An object containing the command name, subcommand, and subcommand group, or null if the interaction is not a chat input command or autocomplete. + */ +function getCommandName(interaction: BaseInteraction): { name: string, subcommand: string | null, group: string | null } | null { + if (!interaction.isChatInputCommand() && !interaction.isAutocomplete()) { + if (interaction.isContextMenuCommand()) return { + name: interaction.commandName, + subcommand: null, + group: null + }; + + return null; + } + + let name = interaction.commandName; + const subcommand = interaction.options.getSubcommand(false); + const group = interaction.options.getSubcommandGroup(false); + + if (group) name += ` ${group}`; + if (subcommand) name += ` ${subcommand}`; + + return { + name, + subcommand, + group + }; +} + +export default getCommandName; \ No newline at end of file diff --git a/src/lib/utils/bot/syncUsers.ts b/src/lib/utils/bot/syncUsers.ts new file mode 100644 index 0000000..931b38e --- /dev/null +++ b/src/lib/utils/bot/syncUsers.ts @@ -0,0 +1,39 @@ +import User from '@/models/User'; +import Storage from '@/models/Storage'; +import { MongooseError } from 'mongoose'; + +/** + * Synchronizes the users between the Discord guild and the database. + * + * @throws {Error} If the base guild is not found. + * @returns {Promise} A promise that resolves when the synchronization is complete. + */ +async function syncUsers() { + const guild = client.guilds.cache.get(config.base_guild_id); + if (!guild) throw new Error('Base guild not found.'); + + const members = await guild.members.fetch(); + + const allUsers = await User.find().lean(); + const usersToCreate = members.filter(member => !allUsers.some(user => user.id === member.user.id)).map(member => member); + + if (usersToCreate.length > 0) { + await User.insertMany(usersToCreate.map(member => ({ id: member.user.id }))) + .then(() => logger.log('database', `${usersToCreate.length} users have been added to the database.`)) + .catch(error => logger.error(error)); + } + + const usersToDelete = allUsers.filter(user => !members.some(member => user.id === member.user.id)); + + if (usersToDelete.length > 0) { + await User.deleteMany({ id: { $in: usersToDelete.map(user => user.id) } }) + .then(() => logger.log('database', `${usersToDelete.length} users have been removed from the database.`)) + .catch(error => logger.error(error)); + + await Storage.deleteMany({ userId: { $in: usersToDelete.map(user => user.id) } }) + .then(() => logger.log('database', `${usersToDelete.length} user storages have been removed from the database.`)) + .catch((error: MongooseError) => logger.error(error)); + } +} + +export default syncUsers; \ No newline at end of file diff --git a/src/lib/utils/encryption.ts b/src/lib/utils/encryption.ts new file mode 100644 index 0000000..cdb79d7 --- /dev/null +++ b/src/lib/utils/encryption.ts @@ -0,0 +1,46 @@ +import * as crypto from 'crypto'; + +interface EncryptedData { + iv: string; + encryptedText: string; + tag: string; +} + +/** + * Encrypts a given text using AES-256-GCM encryption algorithm. + * + * @param text - The text to be encrypted. + * @param secret - The secret key used for encryption, in hexadecimal format. + * @returns {EncryptedData} - The encrypted data, including the initialization vector (iv), encrypted text, and authentication tag (tag). + * @throws {Error} - Throws an error if the text or secret is invalid. + */ +function encrypt(text: string, secret: string): EncryptedData { + if (!text || !secret) throw new Error('Text and secret are required'); + + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(secret, 'hex'), iv); + const encrypted = Buffer.concat([cipher.update(text, 'utf-8'), cipher.final()]); + const tag = cipher.getAuthTag(); + + return { iv: iv.toString('hex'), encryptedText: encrypted.toString('hex'), tag: tag.toString('hex') }; +} + +/** + * Decrypts the given encrypted data using the provided secret. + * + * @param {EncryptedData} encryptedData - The data to decrypt, including the initialization vector (iv), encrypted text, and authentication tag (tag). + * @param {string} secret - The secret key used for decryption, in hexadecimal format. + * @returns {string} - The decrypted text in UTF-8 format. + * @throws {Error} - Throws an error if the encrypted data or secret is invalid. + */ +function decrypt(encryptedData: EncryptedData, secret: string): string { + if (!encryptedData || !encryptedData.iv || !encryptedData.encryptedText || !encryptedData.tag || !secret) throw new Error('Invalid encrypted data or secret.'); + + const decipher = crypto.createDecipheriv('aes-256-gcm', Buffer.from(secret, 'hex'), Buffer.from(encryptedData.iv, 'hex')); + decipher.setAuthTag(Buffer.from(encryptedData.tag, 'hex')); + const decrypted = Buffer.concat([decipher.update(Buffer.from(encryptedData.encryptedText, 'hex')), decipher.final()]); + + return decrypted.toString('utf-8'); +} + +export { encrypt, decrypt }; \ No newline at end of file diff --git a/src/lib/utils/generateUniqueId.ts b/src/lib/utils/generateUniqueId.ts new file mode 100644 index 0000000..28e75d1 --- /dev/null +++ b/src/lib/utils/generateUniqueId.ts @@ -0,0 +1,13 @@ +import crypto from 'crypto'; + +/** + * Generates a unique identifier string. + * + * @param bytes - The number of bytes to generate. Defaults to 16. + * @returns A unique identifier string in hexadecimal format. + */ +function generateUniqueId(bytes: number = 16) { + return crypto.randomBytes(bytes).toString('hex'); +} + +export default generateUniqueId; \ No newline at end of file diff --git a/src/lib/utils/getValidationError.ts b/src/lib/utils/getValidationError.ts new file mode 100644 index 0000000..7af370c --- /dev/null +++ b/src/lib/utils/getValidationError.ts @@ -0,0 +1,20 @@ +import { Document } from 'mongoose'; + +/** + * Validates a given document and returns the first validation error message, if any. + * + * @param {Document} document - The document to validate. + * @returns {string | null} - The first validation error message, or `null` if there are no errors. + */ +function getValidationError(document: Document): string | null { + const errors = document.validateSync(); + if (errors) { + const error = Object.values(errors.errors)[0]; + + return error?.message || 'An unknown error occurred.'; + } + + return null; +} + +export default getValidationError; \ No newline at end of file diff --git a/src/lib/utils/getZodError.ts b/src/lib/utils/getZodError.ts new file mode 100644 index 0000000..000fdfa --- /dev/null +++ b/src/lib/utils/getZodError.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; + +/** + * Parses the given data using the schema and returns the first error message if validation fails. + * + * @param data - The data to be validated. + * @param schema - The schema to validate the data against. + * @returns The first error message if validation fails, otherwise null. + */ +function getZodError(data: unknown, schema: z.ZodSchema): string | null { + const result = schema.safeParse(data); + if (result.success) return null; + + return result.error.errors[0].message; +} + +export default getZodError; \ No newline at end of file diff --git a/src/lib/utils/sleep.ts b/src/lib/utils/sleep.ts new file mode 100644 index 0000000..a736701 --- /dev/null +++ b/src/lib/utils/sleep.ts @@ -0,0 +1,11 @@ +/** + * Pauses the execution of code for a specified number of milliseconds. + * + * @param ms - The number of milliseconds to sleep. + * @returns A promise that resolves after the specified duration. + */ +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export default sleep; \ No newline at end of file diff --git a/src/scripts/connectDatabase.ts b/src/scripts/connectDatabase.ts new file mode 100644 index 0000000..94f24b6 --- /dev/null +++ b/src/scripts/connectDatabase.ts @@ -0,0 +1,23 @@ +import mongoose from 'mongoose'; +import { CronJob } from 'cron'; +import createMongoBackup from '@/scripts/createMongoBackup'; + +mongoose.connect(process.env.MONGODB_URI, { dbName: process.env.MONGODB_NAME }) + .then(() => { + logger.log('database', 'Connected to database.'); + + if (config.database.backup.enabled) { + logger.log('database', 'Database backup enabled.'); + + new CronJob(config.database.backup.cron_pattern, async () => { + try { + await createMongoBackup(); + + logger.info('Database backup taken successfully.'); + } catch (error) { + logger.error('Failed to take backup:'); + logger.error(error); + } + }, null, true); + } + }); diff --git a/src/scripts/createMongoBackup.ts b/src/scripts/createMongoBackup.ts new file mode 100644 index 0000000..04a2fbd --- /dev/null +++ b/src/scripts/createMongoBackup.ts @@ -0,0 +1,80 @@ +import mongoose from 'mongoose'; +import { exec } from 'node:child_process'; +import { promisify } from 'node:util'; +import { existsSync, mkdirSync, readFile, createWriteStream } from 'node:fs'; +import archiver from 'archiver'; +import * as Discord from 'discord.js'; + +const promisifiedExec = promisify(exec); + +async function createMongoBackup() { + if (!existsSync(config.database.backup.output_dir)) { + mkdirSync(config.database.backup.output_dir, { recursive: true }); + logger.warn('Backup directory not found. Creating new directory:', config.database.backup.output_dir); + } + + const databaseIsReady = mongoose.connection.readyState === mongoose.STATES.connected; + if (!databaseIsReady) throw new Error('Database connection not established.'); + + logger.info('Taking backup of database..'); + + const formattedDate = getFormattedDate(); + const backupPath = generateBackupPath(formattedDate); + + if (existsSync(backupPath)) throw new Error('Backup already exists for today.'); + + const cmd = generateBackupCommand(process.env.MONGODB_URI, backupPath, config.database.backup.exclude_collections); + + await promisifiedExec(cmd); + + if (config.database.backup.discord_channel) { + if (!global.client) throw new Error('Discord client not established.'); + + const channel = client.channels.cache.get(config.database.backup.discord_channel) as Discord.TextChannel; + if (!channel) throw new Error('Discord channel not found.'); + + const zipBuffer = await createZipBuffer(backupPath); + const attachment = new Discord.AttachmentBuilder(zipBuffer, { name: `${new Date().toISOString()}.zip` }); + + await channel.send({ files: [attachment] }); + + logger.info('Database backup sent to Discord channel successfully.'); + } + + logger.info('Database backup taken successfully.'); +} + +function getFormattedDate() { + return new Date().toISOString().split('T')?.[0] || 'unknown-date'; +} + +function generateBackupPath(date: string) { + return `${config.database.backup.output_dir}/${date}`; +} + +function generateBackupCommand(url: string, backupPath: string, exclude_collections: string[]) { + const collectionsToExclude = exclude_collections.map(collection => `--excludeCollection=${collection}`).join(' '); + + return `mongodump --uri="${url}" --gzip --forceTableScan --quiet --out=${backupPath} ${collectionsToExclude}`; +} + +async function createZipBuffer(dirPath: string): Promise { + return new Promise((resolve, reject) => { + const output = createWriteStream(`${dirPath}.zip`) as unknown as NodeJS.WritableStream; + const archive = archiver('zip', { zlib: { level: 9 } }); + + output.on('close', () => { + readFile(`${dirPath}.zip`, (error, data) => { + if (error) reject(error); + else resolve(data); + }); + }); + + archive.on('error', error => reject(error)); + archive.pipe(output); + archive.directory(dirPath, false); + archive.finalize(); + }); +} + +export default createMongoBackup; \ No newline at end of file diff --git a/src/scripts/handleUncaughtExceptions.ts b/src/scripts/handleUncaughtExceptions.ts new file mode 100644 index 0000000..8fded79 --- /dev/null +++ b/src/scripts/handleUncaughtExceptions.ts @@ -0,0 +1,14 @@ +process.on('unhandledRejection', error => logger.error(error)); +process.on('uncaughtException', error => logger.error(error)); + +process.removeAllListeners('warning'); + +process.on('warning', warning => { + const warningsToIgnore = [ + 'The `prompts` module is deprecated' + ]; + + if (warningsToIgnore.some(warningToIgnore => warning.message.includes(warningToIgnore))) return; + + logger.warn(warning); +}); \ No newline at end of file diff --git a/src/scripts/loadConfig.ts b/src/scripts/loadConfig.ts new file mode 100644 index 0000000..15f6f5e --- /dev/null +++ b/src/scripts/loadConfig.ts @@ -0,0 +1,7 @@ +import * as fs from 'node:fs'; +import * as toml from '@iarna/toml'; +import type { Config } from '@/src/types/global'; + +const config = toml.parse(fs.readFileSync('./src/config.toml', 'utf-8')) as Config; + +global.config = config; \ No newline at end of file diff --git a/src/scripts/loadLogger.ts b/src/scripts/loadLogger.ts new file mode 100644 index 0000000..350d062 --- /dev/null +++ b/src/scripts/loadLogger.ts @@ -0,0 +1,54 @@ +import * as winston from 'winston'; +import 'winston-daily-rotate-file'; +import chalk from 'chalk'; + +const highestLevel = Object.keys(config.logger.levels)[Object.keys(config.logger.levels).length - 1]; + +const transports = [ + new winston.transports.Console({ + level: highestLevel + }), + new winston.transports.File({ + filename: 'logs/error.log', + level: 'error', + zippedArchive: true + }), + new winston.transports.File({ + filename: 'logs/combined.log', + zippedArchive: true + }), + new winston.transports.DailyRotateFile({ + filename: 'logs/%DATE%-error.log', + datePattern: 'YYYY-MM-DD', + maxFiles: '14d', + level: 'error' + }), + new winston.transports.DailyRotateFile({ + filename: 'logs/%DATE%-combined.log', + datePattern: 'YYYY-MM-DD', + maxFiles: '14d' + }) +]; + +const logger = winston.createLogger({ + levels: Object.keys(config.logger.levels).map((key, index) => ({ [key]: index })) + .reduce((acc, cur) => ({ ...acc, ...cur }), {}), + level: 'info', + format: winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD hh:mm:ss.SSS A' }), + winston.format.errors({ stack: true }), + winston.format.printf(({ level, message, timestamp, stack }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const color = chalk[config.logger.levels[level] as keyof typeof chalk] as any; + + const formattedMessage = level === 'error' ? chalk.gray(stack || message) : message; + + const paddedLevel = level.toUpperCase().padEnd(9, ' '); + + return `${chalk.gray(timestamp)} ${chalk.bold(color(paddedLevel))}${formattedMessage}`; + }) + ), + transports +}); + +global.logger = logger; \ No newline at end of file diff --git a/src/scripts/registerCommands.ts b/src/scripts/registerCommands.ts new file mode 100644 index 0000000..16b3b81 --- /dev/null +++ b/src/scripts/registerCommands.ts @@ -0,0 +1,15 @@ +import 'dotenv/config'; + +import '@/scripts/handleUncaughtExceptions'; +import '@/scripts/loadConfig'; +import '@/scripts/loadLogger'; +import '@/scripts/validateEnvironmentVariables'; + +import fetchCommands from '@/bot/handlers/commands/fetchCommands'; +import registerCommands from '@/bot/handlers/commands/registerCommands'; +import getApplicationIdFromToken from '@/utils/bot/getApplicationIdFromToken'; + +const commands = await fetchCommands(); + +registerCommands({ token: process.env.DISCORD_BOT_TOKEN, commands, application_id: getApplicationIdFromToken(process.env.DISCORD_BOT_TOKEN), base_guild_id: config.base_guild_id }) + .then(() => process.exit(0)); \ No newline at end of file diff --git a/src/scripts/unregisterCommands.ts b/src/scripts/unregisterCommands.ts new file mode 100644 index 0000000..c496ee6 --- /dev/null +++ b/src/scripts/unregisterCommands.ts @@ -0,0 +1,13 @@ +import 'dotenv/config'; + +import '@/scripts/handleUncaughtExceptions'; +import '@/scripts/loadConfig'; +import '@/scripts/loadLogger'; +import '@/scripts/validateEnvironmentVariables'; + +import * as Discord from 'discord.js'; +import registerCommands from '@/bot/handlers/commands/registerCommands'; +import getApplicationIdFromToken from '@/utils/bot/getApplicationIdFromToken'; + +registerCommands({ token: process.env.DISCORD_BOT_TOKEN, commands: new Discord.Collection(), application_id: getApplicationIdFromToken(process.env.DISCORD_BOT_TOKEN), base_guild_id: config.base_guild_id }) + .then(() => process.exit(0)); \ No newline at end of file diff --git a/src/scripts/validateEnvironmentVariables.ts b/src/scripts/validateEnvironmentVariables.ts new file mode 100644 index 0000000..60929d7 --- /dev/null +++ b/src/scripts/validateEnvironmentVariables.ts @@ -0,0 +1,18 @@ +const envVars = { + DISCORD_BOT_TOKEN: /[\w-]{24}\.[\w-]{6}\.[\w-]{38}/, + MONGODB_URI: /^mongodb:\/\/.+/, + MONGODB_NAME: /^[\w-]+$/, + KV_TOKEN_ENCRYPTION_SECRET: /^[\w]{64}$/, + NODE_ENV: /^(development|production)$/ +}; + +const optionalEnvVars = ['']; + +for (const [key, regex] of Object.entries(envVars)) { + const value = process.env[key]; + if (regex && !regex.test(value || '')) { + if (optionalEnvVars.includes(key) && value === undefined) continue; + + logger.warn(`Environment variable ${key} has an invalid value: ${value}`); + }; +} \ No newline at end of file diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 0000000..087c67b --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,125 @@ +import type { Logger } from 'winston'; +import type { Client } from 'discord.js'; +import type { WebSocket } from 'ws'; +import type * as Discord from 'discord.js'; + +type Config = { + bypass_command_permissions_check: string[]; + base_guild_id: string; + max_bulk_get_users_size: number; + server: ServerConfig; + logger: LoggerConfig; + database: DatabaseConfig; + user_svg_card: UserSVGCardConfig; +} + +type ServerConfig = { + port: number; + socket: ServerSocketConfig; +} + +export declare enum ServerSocketOpcodes { + HELLO = 1, + INIT = 2, + INIT_ACK = 3, + HEARTBEAT = 4, + HEARTBEAT_ACK = 5, + PRESENCE_UPDATE = 6, + USER_LEFT = 7, + USER_JOINED = 8, + DISCONNECT = 9, + STORAGE_UPDATE = 10, + ERROR = 11, + SUBSCRIBE = 12, + SUBSCRIBE_ACK = 13, + UNSUBSCRIBE = 14, + UNSUBSCRIBE_ACK = 15 +} + +type ServerSocketConfig = { + heartbeat_interval: number; + maxpayload: number; + clienttracking: boolean; + keepalive: boolean; + opcodes: typeof ServerSocketOpcodes; + client_allowed_opcodes: number[]; +} + +type LoggerConfig = { + levels: LoggerLevels; +} + +type LoggerLevels = { + error: string; + warn: string; + info: string; + debug: string; + database: string; + bot: string; + http: string; + socket: string; + [key: string]: string; +} + +type DatabaseConfig = { + backup: DatabaseBackupConfig; +} + +type DatabaseBackupConfig = { + output_dir: string; + enabled: boolean; + discord_channel: string; + cron_pattern: string; + exclude_collections: string[]; +} + +type UserSVGCardConfig = { + colors: { + dark: { + background: string; + background_secondary: string; + card: string; + text: { + primary: string; + secondary: string; + } + }, + light: { + background: string; + background_secondary: string; + card: string; + text: { + primary: string; + secondary: string; + } + } + } +} + +type ActiveSocketData = { + instance: WebSocket, + lastHeartbeat: number, + subscribed: string | string[] +} + +export type ActiveSockets = Discord.Collection; + +declare global { + var config: Config; + var logger: Logger; + var client: Client; + var ActiveSockets: ActiveSockets; + var getWss: () => WebSocket; + + namespace NodeJS { + interface ProcessEnv { + NODE_ENV: 'development' | 'production'; + DISCORD_BOT_TOKEN: string; + MONGODB_URI: string; + MONGODB_NAME: string; + KV_TOKEN_ENCRYPTION_SECRET: string; + } + } +} + +export { Config }; \ No newline at end of file diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 0000000..ff46a07 --- /dev/null +++ b/src/types/index.d.ts @@ -0,0 +1,172 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type * as Discord from 'discord.js'; + +type Restrictions = { + guildOnly?: boolean; + ownerOnly?: boolean; + baseGuildOnly?: boolean; + users?: { + allow?: string[]; + deny?: string[]; + }; + roles?: { + allow?: string[] | number[]; + deny?: string[] | number[]; + }; + permissions?: { + allow?: Discord.PermissionResolvable[]; + deny?: Discord.PermissionResolvable[]; + }; +} | null; + +type CommandMetadata = { + global?: boolean; +} | null; + +type AutocompleteOption = { + name: string, + value: string | number +} + +export type CommandType = { + json: Discord.APIApplicationCommand | any, + metadata?: CommandMetadata, + data: { + [key: string]: { + restrictions: Restrictions, + execute: { + command: (interaction: Discord.ChatInputCommandInteraction, { subcommand, group }: { subcommand: string | null, group: string | null }) => Promise | void, + component?: { + [key: string]: (interaction: Discord.MessageComponentInteraction, { args }: { args: string[] }) => Promise | void + }, + modal?: { + [key: string]: (interaction: Discord.ModalSubmitInteraction, { args }: { args: string[] }) => Promise | void + }, + autocomplete?: (interaction: Discord.AutocompleteInteraction, { subcommand, group }: { subcommand: string | null, group: string | null }) => Promise | AutocompleteOption[] + } + } + } +} + +export type CronType = { + pattern: string, + execute: () => Promise, + executeOnStart?: boolean, + name: string +} + +export type EventType = { + [K in keyof Discord.ClientEvents]: { + name: K; + execute: (...args: [...Discord.ClientEvents[K]]) => Promise | void; + }; +}[keyof Discord.ClientEvents]; + +declare module 'express' { + interface Request { + clientIp: string; + } + + interface Response { + sendError: (message: string, statusCode: number) => void; + } +} + +declare module 'discord.js' { + interface Client { + commands: Discord.Collection; + crons: Discord.Collection; + events: Discord.Collection; + } + + interface CommandInteraction { + success: (content: string, options?: Discord.InteractionReplyOptions) => Promise; + error: (content: string, options?: Discord.InteractionReplyOptions) => Promise; + } + + interface MessageComponentInteraction { + success: (content: string, options?: Discord.InteractionReplyOptions) => Promise; + error: (content: string, options?: Discord.InteractionReplyOptions) => Promise; + } +} + +export type UserData = { + metadata: { + id: string, + username: string, + discriminator: string, + global_name: string | null, + avatar: string | null, + avatar_url: string | null, + display_avatar_url: string, + bot: boolean, + flags: { + human_readable: string[], + bitfield: number | null | undefined + }, + monitoring_since: { + unix: number | null, + raw: Date | null + } + }, + status: string, + active_platforms: Record>, + activities: (CustomStatusActivity | OtherActivity)[], + storage: Map | {} +} + +export type CustomStatusActivity = { + name: 'Custom Status', + type: Discord.ActivityType.Custom, + emoji: Discord.Emoji | null, + text: string | null, + start_time: { + unix: number, + raw: Date + }, + end_time: { + unix: number, + raw: Date + } | null +} + +export type OtherActivity = { + name: string, + type: keyof typeof Discord.ActivityType, + state: string | null, + details: string | null, + created_at: number, + assets?: { + large_image: { + hash?: string, + image_url?: string, + text: string + }, + small_image: { + hash?: string, + image_url?: string, + text: string + } + }, + timestamps?: { + start_time: { + unix: number, + raw: Date + } + } +} + +export type APIUsersGETRequestQuery = { + svg?: number; + theme?: 'light' | 'dark'; + borderRadius?: string; + hideGlobalName?: string; + hideStatus?: string; + hideBadges?: string; + hideActivity?: string; + noActivityTitle?: string; + noActivityMessage?: string; +} + +export type CreateSvgOptions = Omit; \ No newline at end of file diff --git a/swagger.json b/swagger.json new file mode 100644 index 0000000..73ce39f --- /dev/null +++ b/swagger.json @@ -0,0 +1,792 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "lantern.rest", + "version": "1", + "description": "API Documentation", + "contact": { + "name": "discord.place", + "url": "https://discord.place", + "email": "support@discord.place" + } + }, + "servers": [ + { + "url": "https://lantern.rest/api/v1" + } + ], + "paths": { + "/users": { + "get": { + "summary": "Get user data by user IDs", + "description": "Fetches user data for an array of user IDs. Users must be monitored by the system.", + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "user_ids", + "in": "query", + "required": true, + "description": "An array of user IDs to fetch data for. Each ID must be a string of 17-19 characters, and there must be no duplicates.", + "schema": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 17, + "maxLength": 19, + "pattern": "^[0-9]{17,19}$" + } + } + } + ], + "responses": { + "200": { + "description": "User data retrieved successfully.", + "content": { + "application/json": {} + } + }, + "400": { + "description": "Bad request. The user IDs parameter is invalid.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "Missing user_ids": { + "summary": "Missing user_ids", + "value": { + "error": "user_ids is required." + } + }, + "Not an array": { + "summary": "user_ids not an array", + "value": { + "error": "user_ids must be an array." + } + }, + "Invalid ID format": { + "summary": "user_ids invalid format", + "value": { + "error": "user_ids must be an array of strings with 17-19 characters long." + } + }, + "Duplicate user_ids": { + "summary": "Duplicate user_ids", + "value": { + "error": "user_ids must not contain duplicates." + } + }, + "Too many users": { + "summary": "Too many user_ids", + "value": { + "error": "You can only request up to {max_bulk_get_users_size} users at once." + } + } + } + } + } + }, + "404": { + "description": "Not Found. None of the requested users are monitored by the system.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Users you requested are not monitored by Lantern." + } + } + } + } + } + } + } + } + }, + "/users/{id}": { + "get": { + "summary": "Get user data by user ID", + "description": "Fetches user data for a user ID. The user must be monitored by the system.", + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The user ID to fetch data for.", + "schema": { + "type": "number", + "minLength": 17, + "maxLength": 19, + "pattern": "^[0-9]{17,19}$" + } + }, + { + "name": "svg", + "in": "query", + "description": "Whether to return the user's avatar as an SVG image.", + "schema": { + "type": "integer", + "enum": [0, 1], + "default": 0 + } + }, + { + "name": "theme", + "in": "query", + "description": "The theme to use for the SVG image.", + "schema": { + "type": "string", + "enum": ["light", "dark"], + "default": "dark" + } + }, + { + "name": "borderRadius", + "in": "query", + "description": "The border radius to use for the SVG image.", + "schema": { + "type": "integer", + "minimum": 0, + "pattern": "^[0-9]+$", + "default": 2 + } + }, + { + "name": "hideGlobalName", + "in": "query", + "description": "Whether to hide the user's global name.", + "schema": { + "type": "integer", + "enum": [0, 1], + "default": 0 + } + }, + { + "name": "hideStatus", + "in": "query", + "description": "Whether to hide the user's status.", + "schema": { + "type": "integer", + "enum": [0, 1], + "default": 0 + } + }, + { + "name": "hideBadges", + "in": "query", + "description": "Whether to hide the user's badges.", + "schema": { + "type": "integer", + "enum": [0, 1], + "default": 0 + } + }, + { + "name": "hideActivity", + "in": "query", + "description": "Whether to hide the user's activity.", + "schema": { + "type": "integer", + "enum": [0, 1], + "default": 0 + } + }, + { + "name": "noActivityTitle", + "in": "query", + "description": "The title to display when the user has no activity.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "default": "No activity" + } + }, + { + "name": "noActivityMessage", + "in": "query", + "description": "The message to display when the user has no activity.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "default": "User is not currently doing anything." + } + } + ], + "responses": { + "200": { + "description": "User data retrieved successfully.", + "content": { + "application/json": {} + } + }, + "404": { + "description": "Bad request. The user ID parameter is invalid.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "User not monitored": { + "summary": "User not monitored", + "value": { + "error": "User {id} is not being monitored by Lantern." + } + } + } + } + } + } + } + } + }, + "/users/{id}/storage": { + "get": { + "summary": "Get user storage data by user ID", + "description": "Fetches user storage data for a user ID. The user must be monitored by the system.", + "tags": [ + "Storage" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The user ID to fetch storage data for.", + "schema": { + "type": "number", + "minLength": 17, + "maxLength": 19, + "pattern": "^[0-9]{17,19}$" + } + } + ], + "responses": { + "200": { + "description": "User storage data retrieved successfully.", + "content": { + "application/json": {} + } + }, + "404": { + "description": "Bad request. The user ID parameter is invalid.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "User not monitored": { + "summary": "User not monitored", + "value": { + "error": "User {id} is not being monitored by Lantern." + } + }, + "No storage": { + "summary": "No storage found", + "value": { + "error": "User {id} does not have any storage." + } + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete user storage data by user ID", + "description": "Deletes all user storage data for a user ID. The user must be monitored by the system.", + "tags": [ + "Storage" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The user ID to delete storage data for.", + "schema": { + "type": "number", + "minLength": 17, + "maxLength": 19, + "pattern": "^[0-9]{17,19}$" + } + } + ], + "responses": { + "200": { + "description": "User storage data deleted successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + } + } + } + } + } + }, + "404": { + "description": "Bad request. The user ID parameter is invalid.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "User not monitored": { + "summary": "User not monitored", + "value": { + "error": "User {id} is not being monitored by Lantern." + } + }, + "No storage": { + "summary": "No storage found", + "value": { + "error": "User {id} does not have any storage." + } + } + } + } + } + } + } + } + }, + "/users/{id}/storage/{key}": { + "get": { + "summary": "Get user storage data by user ID and key", + "description": "Fetches user storage data for a user ID and key. The user must be monitored by the system.", + "tags": [ + "Storage" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The user ID to fetch storage data for.", + "schema": { + "type": "number", + "minLength": 17, + "maxLength": 19, + "pattern": "^[0-9]{17,19}$" + } + }, + { + "name": "key", + "in": "path", + "required": true, + "description": "The key to fetch the value for.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "pattern": "^[a-zA-Z0-9]+$" + } + } + ], + "responses": { + "200": { + "description": "User storage data retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + } + } + } + }, + "404": { + "description": "Bad request. The user ID or key parameter is invalid.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "User not monitored": { + "summary": "User not monitored", + "value": { + "error": "User {id} is not being monitored by Lantern." + } + }, + "No storage": { + "summary": "No storage found", + "value": { + "error": "User {id} does not have any storage." + } + }, + "Key not found": { + "summary": "Key not found", + "value": { + "error": "Key {key} does not exist in the storage." + } + } + } + } + } + } + } + }, + "put": { + "summary": "Create user storage data by user ID and key", + "description": "Creates a new user storage data for a user ID and key. The user must be monitored by the system.", + "tags": [ + "Storage" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The user ID to create storage data for.", + "schema": { + "type": "number", + "minLength": 17, + "maxLength": 19, + "pattern": "^[0-9]{17,19}$" + } + }, + { + "name": "key", + "in": "path", + "required": true, + "description": "The key to create the value for.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "pattern": "^[a-zA-Z0-9]+$" + } + }, + { + "name": "value", + "in": "query", + "required": true, + "description": "The value to assign to the key.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 30000 + } + } + ], + "responses": { + "200": { + "description": "User storage data created successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + } + } + } + } + } + }, + "404": { + "description": "Bad request. The user ID, key, or value parameter is invalid.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "User not monitored": { + "summary": "User not monitored", + "value": { + "error": "User {id} is not being monitored by Lantern." + } + }, + "Key too long": { + "summary": "Key too long", + "value": { + "error": "Key must be between 1 and 255 characters long." + } + }, + "Value too long": { + "summary": "Value too long", + "value": { + "error": "Value must be between 1 and 30000 characters long." + } + } + } + } + } + } + } + }, + "patch": { + "summary": "Update user storage data by user ID and key", + "description": "Updates the value of a user storage data for a user ID and key. The user must be monitored by the system.", + "tags": [ + "Storage" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The user ID to update storage data for.", + "schema": { + "type": "number", + "minLength": 17, + "maxLength": 19, + "pattern": "^[0-9]{17,19}$" + } + }, + { + "name": "key", + "in": "path", + "required": true, + "description": "The key to update the value for.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "pattern": "^[a-zA-Z0-9]+$" + } + }, + { + "name": "value", + "in": "query", + "required": true, + "description": "The new value to assign to the key.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 30000 + } + } + ], + "responses": { + "200": { + "description": "User storage data updated successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + } + } + } + } + } + }, + "404": { + "description": "Bad request. The user ID, key, or value parameter is invalid.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "User not monitored": { + "summary": "User not monitored", + "value": { + "error": "User {id} is not being monitored by Lantern." + } + }, + "Key too long": { + "summary": "Key too long", + "value": { + "error": "Key must be between 1 and 255 characters long." + } + }, + "Value too long": { + "summary": "Value too long", + "value": { + "error": "Value must be between 1 and 30000 characters long." + } + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete user storage data by user ID and key", + "description": "Deletes a user storage data for a user ID and key. The user must be monitored by the system.", + "tags": [ + "Storage" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The user ID to delete storage data for.", + "schema": { + "type": "number", + "minLength": 17, + "maxLength": 19, + "pattern": "^[0-9]{17,19}$" + } + }, + { + "name": "key", + "in": "path", + "required": true, + "description": "The key to delete the value for.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "pattern": "^[a-zA-Z0-9]+$" + } + } + ], + "responses": { + "200": { + "description": "User storage data deleted successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + } + } + } + } + } + }, + "404": { + "description": "Bad request. The user ID or key parameter is invalid.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "User not monitored": { + "summary": "User not monitored", + "value": { + "error": "User {id} is not being monitored by Lantern." + } + }, + "No storage": { + "summary": "No storage found", + "value": { + "error": "User {id} does not have any storage." + } + }, + "Key not found": { + "summary": "Key not found", + "value": { + "error": "Key {key} does not exist in the storage." + } + } + } + } + } + } + } + } + } + }, + "tags": [], + "components": { + "securitySchemes": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header", + "description": "The token you get from the Lantern bot to access the KV storage API." + } + } + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..eb95fdc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "lib": [ + "ESNext", + "dom" + ], + "paths": { + "@/*": ["./*"], + "@/utils/*": ["./src/lib/utils/*"], + "@/scripts/*": ["./src/scripts/*"], + "@/models/*": ["./src/lib/models/*"], + "@/express/*": ["./src/lib/express/*"], + "@/bot/*": ["./src/lib/bot/*"], + "@/constants/*": ["./src/lib/constants/*"], + }, + "rootDir": "./", + "outDir": "./dist", + "module": "Preserve", + "target": "ES2023", + "skipLibCheck": true + }, + "tsc-alias": { + "resolveFullPaths": true + } +} \ No newline at end of file