diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..991f0c1 --- /dev/null +++ b/.env.example @@ -0,0 +1,34 @@ +# Options related to the application. +# Port of the application. +APP_PORT=3000 + +# Options related to the Discord bot. +# Token of the Discord bot. +DISCORD_TOKEN= +# Optional. Channel ID for plugin approval messages (defaults to the Serenity server channel used before). +# DISCORD_PLUGIN_APPROVAL_CHANNEL_ID= + +# Options related to the database engine. +# URL end-point of the database engine. +# Format: postgresql://:@:/ +DATABASE_URL=postgresql://serenityjs:serenityjs@localhost:5432/serenityjs +# Variables below are used by the database engine in Docker compose file. +# User of the database engine. +DATABASE_ENGINE_USER=serenityjs +# Password of the database engine. +DATABASE_ENGINE_PASSWORD=serenityjs +# Name of the database engine. +DATABASE_ENGINE_NAME=serenityjs +# Port of the database engine. +DATABASE_ENGINE_PORT=5432 + +# Options related to the cache engine. +# Whether the cache engine is enabled. +CACHE_ENGINE_ENABLED=true +# This end-point is only required if the cache engine is enabled. +# URL end-point of the cache engine. +# Format: redis://: +CACHE_ENGINE_URL=redis://localhost:6379 +# Variables below are used by the cache engine in Docker compose file. +# Port of the cache engine. +CACHE_ENGINE_PORT=6379 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ea7e25a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +--- +github: SerenityJS diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..bf5a8cf --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,34 @@ +--- +version: 2 +updates: + - package-ecosystem: "bun" + directory: "/" + target-branch: "development" + schedule: + # Libraries do not have to be updated as often as the code itself. + timezone: "Europe/Warsaw" + interval: "semiannually" + commit-message: + prefix: "chore(deps->root):" + # It disables all labels except the dedicated "dependencies" label. + labels: ["🔄 dependencies"] + + - package-ecosystem: "github-actions" + directory: "./github/workflows" + target-branch: "development" + schedule: + timezone: "Europe/Warsaw" + interval: "semiannually" + commit-message: + prefix: "chore(actions->github):" + labels: ["🔄 dependencies"] + + - package-ecosystem: "docker" + directory: "/" + target-branch: "development" + schedule: + timezone: "Europe/Warsaw" + interval: "semiannually" + commit-message: + prefix: "chore(backend->docker):" + labels: ["🔄 dependencies"] diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml new file mode 100644 index 0000000..4654677 --- /dev/null +++ b/.github/workflows/CI.yaml @@ -0,0 +1,51 @@ +name: Validate REST API service's codebase. + +on: + push: + pull_request: + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + validate-code: + name: Validate codebase. + runs-on: ubuntu-latest + env: + NODE_ENV: CI + + steps: + - name: Checkout repository. + uses: actions/checkout@v6 + + - name: Set up Bun. + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies. + run: bun install --frozen-lockfile + + - name: Lint codebase using ESLint. + run: bun run lint + + - name: Lint Dockerfile using Hadolint. + uses: hadolint/hadolint-action@v3.3.0 + with: + dockerfile: Dockerfile + + - name: Check types using TypeScript. + run: bunx tsc --noEmit + + - name: Check formatting of all files using Prettier. + run: bun run format:check + + - name: Run tests. + run: bun test + + - name: Build a production-ready application. + run: bun run build diff --git a/.gitignore b/.gitignore index 8d0f034..c2c988e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,141 +1,27 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -plugins.db - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.* -!.env.example - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Sveltekit cache directory -.svelte-kit/ - -# vitepress build output -**/.vitepress/dist - -# vitepress cache directory -**/.vitepress/cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# Firebase cache directory -.firebase/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v3 -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions - -# Vite logs files -vite.config.js.timestamp-* -vite.config.ts.timestamp-* +# IDE files. +/.idea/ +/.vscode/ +/.vscode-test/ +/.vs/ +/.cursor/ + +# Dependency directories. +/node_modules/ +/jspm_packages/ +/web_modules/ +# And NPM's cache directory. +/npm-cache/ +/.npm/ + +# Distribution files & directories. +/dist/ +/build/ +/**/*.tsbuildinfo + +# Environment variables. +/**/*.env* +!/.env.example + +# Operating system files. +/**/.DS_Store +/**/*.ini diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 0000000..e3f411b --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,17 @@ +# It enforces the use of labels in every single Dockerfile. +strict-labels: true +# Labels that must be provided in every single Dockerfile. +label-schema: + org.opencontainers.image.title: text + org.opencontainers.image.description: text + org.opencontainers.image.version: semver + org.opencontainers.image.authors: text + org.opencontainers.image.licenses: spdx + org.opencontainers.image.url: url + org.opencontainers.image.documentation: url + org.opencontainers.image.source: url +# Overrides the default severity of the rules. +override: + info: + # This error is a warning related to pinning the version of a package. + - DL3008 diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..4fe9424 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,13 @@ +#!/bin/bash +# This script validates commits' message to make sure that they're +# correct with Conventional Commits standard. + +message="$(sed -n '1p' "$1")" + +if printf "%s" "$message" | grep -Eq '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?: .+'; then + exit 0 +fi + +echo "Invalid commit message format." +echo "Use Conventional Commits, for example: feat(pages): improved login page's design" +exit 1 \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..546d921 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/bash +# This file is being executed every single time before you commit via git. + +bunx lint-staged diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..b91221a --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,6 @@ +#!/bin/bash +# This file is being executed every single time before you push +# via git to GitHub repository. + +bun run format:check +bun run lint:fix diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e720b74 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,26 @@ +# IDE files. +/.idea/ +/.vscode/ +/.vscode-test/ +/.vs/ +/.cursor/ + +# Dependency directories. +/node_modules/ +/jspm_packages/ +/web_modules/ +# And NPM's cache directory. +/npm-cache/ +/.npm/ + +# Distribution files & directories. +/dist/ +/build/ +/**/*.tsbuildinfo + +# Migration files. +/drizzle/ + +# Environment variables. +/**/*.env* +!/.env.example diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..9f9528d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,26 @@ +{ + "useTabs": false, + "tabWidth": 4, + "printWidth": 120, + "singleQuote": false, + "trailingComma": "all", + "arrowParens": "always", + "semi": false, + "bracketSpacing": true, + "endOfLine": "auto", + "overrides": [ + { + "files": "**/*.{json,jsonc}", + "options": { + "printWidth": 80, + "trailingComma": "none" + } + }, + { + "files": "**/*.{yaml,yml}", + "options": { + "printWidth": 80 + } + } + ] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..740dc02 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM oven/bun:canary AS builder + +WORKDIR /app +COPY package.json bun.lock ./ + +RUN bun install --frozen-lockfile +COPY . . + +RUN bun run build + +FROM oven/bun:canary-slim AS runner + +LABEL org.opencontainers.image.title="SerenityJS API Service" \ + org.opencontainers.image.description="Rest API Service for SerenityJS." \ + org.opencontainers.image.version="1.0.0" \ + org.opencontainers.image.authors="SerenityJS " \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.url="https://github.com/SerenityJS/api-service" \ + org.opencontainers.image.documentation="https://github.com/SerenityJS/api-service" \ + org.opencontainers.image.source="https://github.com/SerenityJS/api-service" + +WORKDIR /app +ENV NODE_ENV=production +ARG APP_PORT=${APP_PORT:-3000} + +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/scripts ./scripts +COPY --from=builder /app/public ./public +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/bun.lock ./bun.lock + +EXPOSE ${APP_PORT} +CMD ["bun", "run", "start"] diff --git a/README.md b/README.md index 4ea0fa6..20a9a5f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # api-service + Rest API Service for SerenityJS diff --git a/bun.lock b/bun.lock index 5d59e1f..dc80059 100644 --- a/bun.lock +++ b/bun.lock @@ -1,18 +1,31 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "api-service", "dependencies": { "@types/cors": "^2.8.19", - "@types/express": "^5.0.3", + "@types/express": "^5.0.6", "axios": "^1.11.0", - "cors": "^2.8.5", - "discord.js": "^14.22.1", - "express": "^5.1.0", + "cors": "^2.8.6", + "discord.js": "^14.26.3", + "drizzle-orm": "^0.45.2", + "express": "^5.2.1", }, "devDependencies": { + "@eslint/js": "^10.0.1", + "@eslint/json": "^1.2.0", + "@eslint/markdown": "^8.0.1", "@types/bun": "latest", + "drizzle-kit": "^0.31.10", + "eslint": "^10.2.0", + "globals": "^17.5.0", + "husky": "^9.1.7", + "jiti": "^2.6.1", + "lint-staged": "^16.4.0", + "prettier": "^3.8.3", + "typescript-eslint": "^8.58.2", }, "peerDependencies": { "typescript": "^5", @@ -20,18 +33,106 @@ }, }, "packages": { - "@discordjs/builders": ["@discordjs/builders@1.11.3", "", { "dependencies": { "@discordjs/formatters": "^0.6.1", "@discordjs/util": "^1.1.1", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.16", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-p3kf5eV49CJiRTfhtutUCeivSyQ/l2JlKodW1ZquRwwvlOWmG9+6jFShX6x8rUiYhnP6wKI96rgN/SXMy5e5aw=="], + "@discordjs/builders": ["@discordjs/builders@1.14.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.40", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ=="], "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], - "@discordjs/formatters": ["@discordjs/formatters@0.6.1", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg=="], + "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="], - "@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="], + "@discordjs/rest": ["@discordjs/rest@2.6.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.2.0", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.5", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.40", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "6.24.1" } }, "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg=="], - "@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="], + "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.5.5", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w=="], + + "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], + + "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], + + "@eslint/json": ["@eslint/json@1.2.0", "", { "dependencies": { "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanwhocodes/momoa": "^3.3.10", "natural-compare": "^1.4.0" } }, "sha512-CEFEyNgvzu8zn5QwVYDg3FaG+ZKUeUsNYitFpMYJAqoAlnw68EQgNbUfheSmexZr4n0wZPrAkPLuvsLaXO6wRw=="], + + "@eslint/markdown": ["@eslint/markdown@8.0.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "github-slugger": "^2.0.0", "mdast-util-from-markdown": "^2.0.2", "mdast-util-frontmatter": "^2.0.1", "mdast-util-gfm": "^3.1.0", "mdast-util-math": "^3.0.0", "micromark-extension-frontmatter": "^2.0.0", "micromark-extension-gfm": "^3.0.0", "micromark-extension-math": "^3.1.0", "micromark-util-normalize-identifier": "^2.0.1" } }, "sha512-WWKmld/EyNdEB8GMq7JMPX1SDWgyJAM1uhtCi5ySrqYQM4HQjmg11EX/q3ZpnpRXHfdccFtli3NBvvGaYjWyQw=="], + + "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/momoa": ["@humanwhocodes/momoa@3.3.10", "", {}, "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], @@ -46,14 +147,30 @@ "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], - "@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], @@ -64,19 +181,59 @@ "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], - "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], + "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], + + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/type-utils": "8.58.2", "@typescript-eslint/utils": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.2", "@typescript-eslint/types": "^8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2" } }, "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.58.2", "", {}, "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.2", "@typescript-eslint/tsconfig-utils": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], + "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "axios": ["axios@1.11.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA=="], - "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + + "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], @@ -86,8 +243,20 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -96,26 +265,44 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - "discord-api-types": ["discord-api-types@0.38.22", "", {}, "sha512-2gnYrgXN3yTlv2cKBISI/A8btZwsSZLwKpIQXeI1cS8a7W7wP3sFVQOm3mPuuinTD8jJCKGPGNH399zE7Un1kA=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "discord-api-types": ["discord-api-types@0.38.47", "", {}, "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA=="], + + "discord.js": ["discord.js@14.26.3", "", { "dependencies": { "@discordjs/builders": "^1.14.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.1", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.40", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "6.24.1" } }, "sha512-XEKtYn28YFsiJ5l4fLRyikdbo6RD5oFyqfVHQlvXz2104JhH/E8slN28dbky05w3DCrJcNVWvhVvcJCTSl/KIg=="], - "discord.js": ["discord.js@14.22.1", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.16", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w=="], + "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], + + "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -124,30 +311,82 @@ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@10.2.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.4", "@eslint/config-helpers": "^0.5.4", "@eslint/core": "^1.2.0", "@eslint/plugin-kit": "^0.7.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA=="], + + "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "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.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "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.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="], + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@17.5.0", "", {}, "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], @@ -158,32 +397,164 @@ "http-errors": ["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" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "katex": ["katex@0.16.45", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lint-staged": ["lint-staged@16.4.0", "", { "dependencies": { "commander": "^14.0.3", "listr2": "^9.0.5", "picomatch": "^4.0.3", "string-argv": "^0.3.2", "tinyexec": "^1.0.4", "yaml": "^2.8.2" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw=="], + + "listr2": ["listr2@9.0.5", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], - "magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="], + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + + "magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="], + + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -194,19 +565,45 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -214,12 +611,18 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], @@ -228,38 +631,234 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], + "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], - "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], + "typescript-eslint": ["typescript-eslint@8.58.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/parser": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ=="], + + "undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="], "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + "@discordjs/rest/@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="], + "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + "@discordjs/ws/@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="], + + "@discordjs/ws/@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="], + + "@discordjs/ws/discord-api-types": ["discord-api-types@0.38.22", "", {}, "sha512-2gnYrgXN3yTlv2cKBISI/A8btZwsSZLwKpIQXeI1cS8a7W7wP3sFVQOm3mPuuinTD8jJCKGPGNH399zE7Un1kA=="], + + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@typescript-eslint/project-service/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@typescript-eslint/type-utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@typescript-eslint/typescript-estree/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "body-parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "body-parser/qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], + + "eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], + + "express/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + + "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "mdast-util-frontmatter/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "micromark/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "tsx/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "@discordjs/ws/@discordjs/rest/magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="], + + "@discordjs/ws/@discordjs/rest/undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], } } diff --git a/docker-compose.postgres.yaml b/docker-compose.postgres.yaml new file mode 100644 index 0000000..f8838bf --- /dev/null +++ b/docker-compose.postgres.yaml @@ -0,0 +1,37 @@ +volumes: + serenityjs-postgres-volume: + +services: + backend-app: + depends_on: + postgres: + condition: service_healthy + + postgres: + container_name: serenityjs-postgres + image: postgres:18.1-alpine + restart: unless-stopped + env_file: + - .env + environment: + POSTGRES_USER: ${DATABASE_ENGINE_USER:-serenityjs} + POSTGRES_PASSWORD: ${DATABASE_ENGINE_PASSWORD:-serenityjs} + POSTGRES_DB: ${DATABASE_ENGINE_NAME:-serenityjs} + ports: + - "${DATABASE_ENGINE_PORT:-5432}:5432" + healthcheck: + test: + [ + "CMD", + "pg_isready", + "-U", + "${DATABASE_ENGINE_USER:-serenityjs}", + ] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + volumes: + - serenityjs-postgres-volume:/var/lib/postgresql/data + networks: + - serenityjs-network diff --git a/docker-compose.redis.yaml b/docker-compose.redis.yaml new file mode 100644 index 0000000..a237980 --- /dev/null +++ b/docker-compose.redis.yaml @@ -0,0 +1,27 @@ +volumes: + serenityjs-redis-volume: + +services: + backend-app: + depends_on: + redis: + condition: service_healthy + + redis: + container_name: serenityjs-redis + image: redis:8.6-alpine + restart: unless-stopped + env_file: + - .env + ports: + - "${CACHE_ENGINE_PORT:-6379}:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + volumes: + - serenityjs-redis-volume:/data + networks: + - serenityjs-network diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..0196f6c --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,16 @@ +networks: + serenityjs-network: + driver: bridge + +services: + backend-app: + container_name: serenityjs-api-service + build: + dockerfile: Dockerfile + context: . + env_file: + - .env + networks: + - serenityjs-network + ports: + - "{APP_PORT:-4000}:{APP_PORT:-4000}" diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..9870cc3 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from "drizzle-kit" + +/** + * @summary Get the database URL. + * @description Get the database URL. + * @returns The database URL. + */ +const DATABASE_URL = (): string => { + const rawValue: string | undefined = process.env.DATABASE_URL + + if (!rawValue) { + throw new Error("DATABASE_URL is not set and Drizzle cannot handle this operation.") + } + + return rawValue +} + +/** + * @summary Drizzle configuration. + * @description Drizzle configuration. + */ +export default defineConfig({ + schema: "./src/globals/databases/DatabaseSchemas.ts", + dialect: "postgresql", + out: "./drizzle", + dbCredentials: { + url: DATABASE_URL(), + }, +}) diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 0000000..d4efb5a --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,72 @@ +import js from "@eslint/js" +import globals from "globals" +import tseslint from "typescript-eslint" +import json from "@eslint/json" +import markdown from "@eslint/markdown" +import { defineConfig } from "eslint/config" + +/** + * @summary ESLint configuration. + * @description Configuration of the backend application. + * @see {@link https://eslint.org/docs/latest/use/configure/configuration-files} + */ +export default defineConfig([ + tseslint.configs.recommended, + { + ignores: ["**/*.js", "dist/**"], + }, + { + files: ["**/*.ts"], + plugins: { js: js as never }, + extends: ["js/recommended"], + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + }, + rules: { + "no-undef": "off", + "no-empty": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_{1,2}", + varsIgnorePattern: "^_{1,2}", + }, + ], + "no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_{1,2}", + varsIgnorePattern: "^_{1,2}", + }, + ], + }, + }, + { + ignores: ["tsconfig.json"], + files: ["**/*.json"], + plugins: { json: json as never }, + language: "json/json", + extends: ["json/recommended"], + }, + { + files: ["**/*.jsonc"], + plugins: { json: json as never }, + language: "json/jsonc", + extends: ["json/recommended"], + }, + { + files: ["**/*.json5"], + plugins: { json: json as never }, + language: "json/json5", + extends: ["json/recommended"], + }, + { + files: ["**/*.md"], + plugins: { markdown: markdown as never }, + language: "markdown/gfm", + extends: ["markdown/recommended"], + }, +]) diff --git a/package.json b/package.json index ae8c499..15a575c 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,72 @@ { - "private": true, - "name": "api-service", - "module": "src/index.ts", - "type": "module", - "scripts": { - "start": "bun run src/index.ts", - "dev": "bun run --watch src/index.ts" - }, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5" - }, - "dependencies": { - "@types/cors": "^2.8.19", - "@types/express": "^5.0.3", - "axios": "^1.11.0", - "cors": "^2.8.5", - "discord.js": "^14.22.1", - "express": "^5.1.0" - } + "name": "@serenityjs/api-service", + "description": "Rest API Service for SerenityJS.", + "version": "1.0.0", + "private": true, + "license": "MIT", + "author": { + "name": "SerenityJS", + "url": "https://serenityjs.net" + }, + "homepage": "https://github.com/SerenityJS/api-service", + "repository": { + "type": "git", + "url": "https://github.com/SerenityJS/api-service.git" + }, + "bugs": { + "url": "https://github.com/SerenityJS/api-service/issues" + }, + "engines": { + "bun": ">=1.3" + }, + "main": "dist/Main.js", + "type": "module", + "scripts": { + "format": "bunx prettier --write .", + "format:check": "bunx prettier --check .", + "lint": "bunx eslint .", + "lint:fix": "bunx eslint --fix .", + "db:migrate": "bunx drizzle-kit migrate", + "db:push": "bunx drizzle-kit push", + "db:studio": "bunx drizzle-kit studio", + "build": "bun build src/Main.ts --outdir dist --target bun", + "start": "bun run dist/Main.js", + "dev": "bun run --watch src/Main.ts", + "prepare": "husky" + }, + "lint-staged": { + "**/*.{ts,json,jsonc,md}": [ + "bunx eslint --fix", + "bunx prettier --write" + ], + "**/*.{yml,yaml}": [ + "bunx prettier --write" + ] + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@eslint/json": "^1.2.0", + "@eslint/markdown": "^8.0.1", + "@types/bun": "latest", + "drizzle-kit": "^0.31.10", + "eslint": "^10.2.0", + "globals": "^17.5.0", + "husky": "^9.1.7", + "jiti": "^2.6.1", + "lint-staged": "^16.4.0", + "prettier": "^3.8.3", + "typescript-eslint": "^8.58.2" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "axios": "^1.11.0", + "cors": "^2.8.6", + "discord.js": "^14.26.3", + "drizzle-orm": "^0.45.2", + "express": "^5.2.1" + } } diff --git a/src/Main.ts b/src/Main.ts new file mode 100644 index 0000000..0f04742 --- /dev/null +++ b/src/Main.ts @@ -0,0 +1,37 @@ +import { DiscordClient } from "./globals/clients/DiscordClient" +import { ApplicationInstanceManager } from "./globals/managers/ApplicationInstanceManager" +import { EnvironmentVariables } from "./globals/EnvironmentVariables" +import { GitHubSyncService } from "./services/github" + +/** + * @summary Main class. + * @description Main class for the application. + */ +// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars +class Main { + /** + * @summary Private constructor. + * @description Private constructor that prevents the class from being instantiated. + */ + private constructor() {} + + /** + * @summary Static initializer. + * @description Static initializer that initializes the application. + */ + static { + void this.init() + } + + /** + * @summary Initialize the application. + * @description Initialize the application. + */ + private static async init(): Promise { + DiscordClient.connect() + GitHubSyncService.start() + ApplicationInstanceManager.instance.listen(EnvironmentVariables.APP_PORT, () => { + console.log(`Server is running on port ${EnvironmentVariables.APP_PORT}.`) + }) + } +} diff --git a/src/controllers/plugins/PluginsController.ts b/src/controllers/plugins/PluginsController.ts new file mode 100644 index 0000000..d40478c --- /dev/null +++ b/src/controllers/plugins/PluginsController.ts @@ -0,0 +1,60 @@ +import type { Request, Response } from "express" +import { GitHubSyncService } from "../../services/github" + +/** + * @summary Controller for plugins. + * @description Controller for plugins. + */ +class PluginsController { + private static readonly PAGINATION_PAGE_SIZE: number = 25 + + /** + * @summary Private constructor. + * @description Private constructor to prevent instantiation of the class. + */ + private constructor() {} + + /** + * @summary Get plugins pagination. + * @description Get plugins pagination. + * @param request The request object. + * @param response The response object. + * @returns The plugins pagination. + */ + public static async getPluginsPagination(request: Request, response: Response): Promise { + const pageQuery: string | undefined = typeof request.query.page === "string" ? request.query.page : undefined + const page: number = pageQuery === undefined ? 1 : Number.parseInt(pageQuery, 10) + if (!Number.isInteger(page) || page < 1) { + response.status(400).send({ message: "Query parameter `page` must be a positive integer." }) + return + } + + try { + const [count, items] = await Promise.all([ + GitHubSyncService.getPluginsCount(), + GitHubSyncService.getPluginsByPage(page, this.PAGINATION_PAGE_SIZE), + ]) + response.status(200).send({ count, items }) + } catch { + response.status(500).send({ message: "Failed to fetch paginated plugins." }) + } + } + + /** + * @summary Get all plugins. + * @description Get all plugins. + * @param request The request object. + * @param response The response object. + * @returns The all plugins. + */ + public static async getAllPlugins(_: Request, response: Response): Promise { + try { + const plugins = await GitHubSyncService.getAllPlugins() + response.status(200).send(plugins) + } catch { + response.status(500).send({ message: "Failed to fetch all plugins." }) + } + } +} + +export { PluginsController } diff --git a/src/globals/EnvironmentVariables.ts b/src/globals/EnvironmentVariables.ts new file mode 100644 index 0000000..808657c --- /dev/null +++ b/src/globals/EnvironmentVariables.ts @@ -0,0 +1,100 @@ +/** + * @summary Manager for the environment variables. + * @description Manager for the environment variables. + */ +class EnvironmentVariables { + /** + * @summary Private constructor. + * @description Private constructor that prevents the class from being instantiated. + */ + private constructor() {} + + /** + * @summary Get the port of the application. + * @description Get the port of the application. + * @returns The port of the application. + */ + public static get APP_PORT(): number { + const value: string | undefined = process.env.APP_PORT + if (value === undefined) throw new Error("APP_PORT is not set.") + const number: number = Number.parseInt(value, 10) + if (Number.isNaN(number)) throw new Error("APP_PORT is not a number.") + return number + } + + /** + * @summary Get the token of the Discord bot. + * @description Get the token of the Discord bot. + * @returns The token of the Discord bot. + */ + public static get DISCORD_TOKEN(): string { + const value: string | undefined = process.env.DISCORD_TOKEN + if (value === undefined) throw new Error("DISCORD_TOKEN is not set.") + return value + } + + /** + * @summary Get the ID of the Discord plugin approval channel. + * @description Get the ID of the Discord plugin approval channel. + * @returns The ID of the Discord plugin approval channel. + */ + public static get DISCORD_PLUGIN_APPROVAL_CHANNEL_ID(): string { + const value: string | undefined = process.env.DISCORD_PLUGIN_APPROVAL_CHANNEL_ID + return value ?? "1411567293552529462" + } + + /** + * @summary Get the URL of the database. + * @description Get the URL of the database. + * @returns The URL of the database. + */ + public static get DATABASE_URL(): string { + const value: string | undefined = process.env.DATABASE_URL + if (value === undefined) throw new Error("DATABASE_URL is not set.") + return value + } + + /** + * @summary Get the enabled status of the cache engine. + * @description Get the enabled status of the cache engine. + * @returns The enabled status of the cache engine. + */ + public static get CACHE_ENGINE_ENABLED(): boolean { + const value: string | undefined = process.env.CACHE_ENGINE_ENABLED + if (value === undefined) throw new Error("CACHE_ENGINE_ENABLED is not set.") + switch (value.toLowerCase().trim()) { + case "true": + return true + case "false": + return false + default: + throw new Error("CACHE_ENGINE_ENABLED is not a boolean.") + } + } + + /** + * @summary Get the URL of the cache engine. + * @description Get the URL of the cache engine. + * @returns The URL of the cache engine. + */ + public static get CACHE_ENGINE_URL(): string | undefined { + if (!this.CACHE_ENGINE_ENABLED) return undefined + const value: string | undefined = process.env.CACHE_ENGINE_URL + if (value === undefined) throw new Error("CACHE_ENGINE_URL is not set.") + return value + } + + /** + * @summary Get the GitHub token. + * @description Get the GitHub token used for authenticated GitHub API calls. + * @returns The GitHub token if provided. + */ + public static get GITHUB_TOKEN(): string | undefined { + const value: string | undefined = process.env.GITHUB_TOKEN + if (value === undefined) return undefined + const trimmedValue: string = value.trim() + return trimmedValue.length === 0 ? undefined : trimmedValue + } +} + +export { EnvironmentVariables } diff --git a/src/globals/clients/DatabaseClient.ts b/src/globals/clients/DatabaseClient.ts new file mode 100644 index 0000000..129014a --- /dev/null +++ b/src/globals/clients/DatabaseClient.ts @@ -0,0 +1,71 @@ +import { BunSQLDatabase, drizzle } from "drizzle-orm/bun-sql" +import { SQL, sql } from "bun" +import { EnvironmentVariables } from "../EnvironmentVariables" +import * as databaseSchemas from "../databases/DatabaseSchemas" + +/** + * @summary Client of the database. + * @description Client of the database. + */ +class DatabaseClient { + /** + * @summary Private constructor. + * @description Private constructor to prevent instantiation of the class. + */ + private constructor() {} + + /** + * @summary Internal instance of the Drizzle. + */ + private static internalDrizzleInstance: BunSQLDatabase | null = null + + /** + * @summary Internal instance of the native database. + */ + private static internalNativeInstance: SQL | null = null + + /** + * @summary Get the Drizzle instance. + * @description Get the Drizzle instance. + * @returns The Drizzle instance. + */ + public static get drizzleInstance(): BunSQLDatabase { + if (this.internalDrizzleInstance === null) { + this.internalDrizzleInstance = drizzle(this.nativeInstance, { + schema: databaseSchemas, + logger: false, + }) + } + + return this.internalDrizzleInstance + } + + /** + * @summary Get the native instance. + * @description Get the native instance from BunSQL. + * @returns The native instance. + */ + public static get nativeInstance(): SQL { + if (this.internalNativeInstance === null) { + this.internalNativeInstance = new SQL(EnvironmentVariables.DATABASE_URL) + } + + return this.internalNativeInstance + } + + /** + * @summary Ping the database. + * @description Ping the database. + * @returns The status of the database. + */ + public static async ping(): Promise { + try { + await this.nativeInstance(sql`SELECT 1`) + return true + } catch { + return false + } + } +} + +export { DatabaseClient } diff --git a/src/globals/clients/DiscordClient.ts b/src/globals/clients/DiscordClient.ts new file mode 100644 index 0000000..7e22986 --- /dev/null +++ b/src/globals/clients/DiscordClient.ts @@ -0,0 +1,185 @@ +import axios from "axios" +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + Client, + EmbedBuilder, + Events, + GatewayIntentBits, + type GuildTextBasedChannel, + type Interaction, +} from "discord.js" +import type { IPlugin } from "../../models/services/databases/plugins/base/interfaces/IPlugin" +import { PluginsService } from "../../services/databases/plugins/PluginsService" +import { DiscordClientConfiguration } from "../configuration/discord/DiscordClientConfiguration" +import { EnvironmentVariables } from "../EnvironmentVariables" + +/** + * @summary Client for the Discord bot (login, plugin approval flow). + * @description Client for the Discord bot. Connects with `connect()`; posts approval requests to the + * configured channel and handles Approve / Reject button interactions. + */ +class DiscordClient { + /** + * @summary Private constructor. + * @description Private constructor that prevents the class from being instantiated. + */ + private constructor() {} + + /** + * @summary Internal instance of the Discord client. + * @description Internal instance of the Discord client. + * @returns The internal instance of the Discord client. + */ + private static internalInstance: Client | null = null + + /** + * @summary Has connected. + * @description Has connected. + * @returns The has connected. + */ + private static hasConnected: boolean = false + + /** + * @summary Interaction handler registered. + * @description Interaction handler registered. + * @returns The interaction handler registered. + */ + private static interactionHandlerRegistered: boolean = false + + /** + * @summary When ready. + * @description When ready. + * @returns The when ready. + */ + private static whenReady: Promise | null = null + + /** + * @summary Get the instance of the Discord client. + * @description Get the instance of the Discord client. + * @returns The instance of the Discord client. + */ + public static get instance(): Client { + if (this.internalInstance === null) { + this.internalInstance = new Client({ + intents: [GatewayIntentBits.Guilds], + }) + } + return this.internalInstance + } + + /** + * @summary Get the ready promise. + * @description Get the ready promise. + * @returns The ready promise. + */ + private static getReady(): Promise { + if (this.whenReady === null) { + const client: Client = this.instance + this.whenReady = client.isReady() + ? Promise.resolve() + : new Promise((resolve) => { + client.once(Events.ClientReady, () => { + resolve() + }) + }) + } + + return this.whenReady + } + + /** + * @summary Log in the bot and register interaction handling (no-op if `DISCORD_TOKEN` is unset). + * @description Log in the bot and register interaction handling. + */ + public static connect(): void { + const token: string | undefined = process.env.DISCORD_TOKEN?.trim() + if (!token) return + if (this.hasConnected) { + this.ensureInteractionHandler() + return + } + this.hasConnected = true + void this.instance.login(token) + this.ensureInteractionHandler() + } + + private static ensureInteractionHandler(): void { + if (this.interactionHandlerRegistered) return + this.interactionHandlerRegistered = true + this.instance.on(Events.InteractionCreate, (interaction: Interaction) => { + void this.handleInteraction(interaction).catch((error: unknown) => { + console.error("DiscordClient.handleInteraction failed:", error) + }) + }) + } + + /** + * @summary Send an embed with approve / reject actions for a plugin. + * @description Waits for the client to be ready, then posts to the plugin approval channel. + */ + public static async sendPluginApprovalRequest(plugin: IPlugin): Promise { + if (!process.env.DISCORD_TOKEN?.trim()) return + this.connect() + await this.getReady() + const channel = (await this.instance.channels.fetch( + EnvironmentVariables.DISCORD_PLUGIN_APPROVAL_CHANNEL_ID, + )) as GuildTextBasedChannel | null + if (!channel?.isTextBased()) { + console.error( + `Channel with ID ${EnvironmentVariables.DISCORD_PLUGIN_APPROVAL_CHANNEL_ID} not found or is not text-based.`, + ) + return + } + const embed: EmbedBuilder = new EmbedBuilder() + .setTitle("New Plugin Approval Request") + .setDescription( + `A new plugin has been submitted for approval:\n\n**Name:** ${plugin.name}\n**Owner:** ${plugin.owner}\n**URL:** ${plugin.url}\n\n${DiscordClientConfiguration.APPROVAL_INSTRUCTIONS}`, + ) + .setColor(0x8560e9) + .setThumbnail(await this.getPluginLogoUrl(plugin)) + const row: ActionRowBuilder = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId(`approve:${plugin.id}`).setLabel("Approve").setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId(`reject:${plugin.id}`).setLabel("Reject").setStyle(ButtonStyle.Danger), + ) + await channel.send({ embeds: [embed], components: [row] }) + } + + /** + * @summary Get the plugin logo URL. + * @description Get the plugin logo URL. + * @param plugin The plugin. + * @returns The plugin logo URL. + */ + private static async getPluginLogoUrl(plugin: IPlugin): Promise { + const url: string = `https://raw.githubusercontent.com/${plugin.owner}/${plugin.name}/${plugin.branch}/public/logo.png` + try { + const response = await axios.head(url) + return response.status === 200 ? url : DiscordClientConfiguration.DEFAULT_LOGO + } catch { + return DiscordClientConfiguration.DEFAULT_LOGO + } + } + + /** + * @summary Handle the interaction. + * @description Handle the interaction. + * @param interaction The interaction. + */ + private static async handleInteraction(interaction: Interaction): Promise { + if (!interaction.isButton()) return + const [action, idPart] = interaction.customId.split(":") + if (action !== "approve" && action !== "reject") return + const pluginId: number = Number(idPart) + if (Number.isNaN(pluginId)) return + const approved: boolean = action === "approve" + await PluginsService.updatePlugin(pluginId, { approved }) + await interaction.reply({ content: `Plugin ${approved ? "approved" : "rejected"}.` }) + if (interaction.message && "edit" in interaction.message) { + await interaction.message.edit({ components: [] }) + } + } +} + +export { DiscordClient } diff --git a/src/globals/clients/RedisClient.ts b/src/globals/clients/RedisClient.ts new file mode 100644 index 0000000..8342aa3 --- /dev/null +++ b/src/globals/clients/RedisClient.ts @@ -0,0 +1,107 @@ +import { RedisClient as NativeRedisClient } from "bun" +import { EnvironmentVariables } from "../EnvironmentVariables" + +/** + * @summary Client for the Redis cache engine. + * @description Client for the Redis cache engine. + */ +class RedisClient { + /** + * @summary Private constructor. + * @description Private constructor that prevents the class from being instantiated. + */ + private constructor() {} + + /** + * The internal instance of the Redis client. + * @private + */ + private static internalInstance: NativeRedisClient | null = null + + /** + * @summary Get the instance of the Redis client. + * @description Get the instance of the Redis client. + * @returns The instance of the Redis client. + */ + public static get instance(): NativeRedisClient { + if (this.internalInstance === null) { + this.internalInstance = new NativeRedisClient(EnvironmentVariables.CACHE_ENGINE_URL!) + } + + return this.internalInstance + } + + /** + * @summary Get the value of a key. + * @description Get the value of a key. + * @param key The key to get the value of. + * @param defaultValue The default value to return if the key is not found. + * @returns The value of the key. + */ + public static async getValue(key: string, defaultValue: T | null = null): Promise { + if (!EnvironmentVariables.CACHE_ENGINE_ENABLED) return defaultValue + const value: string | null = await this.instance.get(key) + if (value === null) return defaultValue + return JSON.parse(value) as T + } + + /** + * @summary Get the keys of a pattern. + * @description Get the keys of a pattern. + * @param patterns The patterns to get the keys of. + * @returns The keys of the patterns. + */ + public static async getKeys(...patterns: readonly string[]): Promise { + if (!EnvironmentVariables.CACHE_ENGINE_ENABLED) return [] + switch (patterns.length) { + case 0: + return [] + case 1: + return await this.instance.keys(patterns[0]) + default: + return await Promise.all(patterns.map((pattern) => this.instance.keys(pattern))).then((keys) => + keys.flat(), + ) + } + } + + /** + * @summary Set the value of a key. + * @description Set the value of a key. + * @param key The key to set the value of. + * @param value The value to set. + * @param timeToLive The time to live in seconds. + */ + public static async setValue(key: string, value: unknown, timeToLive: number = 60): Promise { + if (!EnvironmentVariables.CACHE_ENGINE_ENABLED) return + await Promise.all([this.instance.set(key, JSON.stringify(value)), this.instance.expire(key, timeToLive)]) + } + + /** + * @summary Delete the values of a key. + * @description Delete the values of a key. + * @param keysOrPatterns The keys or patterns to delete. + */ + public static async deleteValues(...keysOrPatterns: readonly string[]): Promise { + if (!EnvironmentVariables.CACHE_ENGINE_ENABLED || keysOrPatterns.length === 0) return + const keysToDelete: string[] = [] + const patternsToDelete: string[] = [] + + for (const keyOrPattern of keysOrPatterns) { + if (keyOrPattern.includes("*")) { + patternsToDelete.push(keyOrPattern) + } else { + keysToDelete.push(keyOrPattern) + } + } + + await this.instance.del( + ...keysToDelete, + ...(await Promise.all(patternsToDelete.map((pattern) => this.instance.keys(pattern))).then((keys) => + keys.flat(), + )), + ) + } +} + +export { RedisClient } diff --git a/src/globals/configuration/discord/DiscordClientConfiguration.ts b/src/globals/configuration/discord/DiscordClientConfiguration.ts new file mode 100644 index 0000000..1c71def --- /dev/null +++ b/src/globals/configuration/discord/DiscordClientConfiguration.ts @@ -0,0 +1,32 @@ +/** + * @summary Configuration for the Discord client. + * @description Configuration for the Discord client. + */ +class DiscordClientConfiguration { + /** + * @summary Private constructor. + * @description Private constructor to prevent instantiation of the class. + */ + private constructor() {} + + public static readonly DEFAULT_LOGO: string = "https://avatars.githubusercontent.com/u/92610726?s=88&v=4" + + /** + * @summary Approval instructions. + * @description Approval instructions. + * @returns The approval instructions. + */ + public static readonly APPROVAL_INSTRUCTIONS: string = ` + **Verify that the plugin meets the following criteria:** + + - The plugin is relevant to SerenityJS and its ecosystem.\n + - The plugin is well-maintained and has at least a release on GitHub.\n + - The plugin has a proper README file and documentation.\n + - The plugin does not contain any malicious code or vulnerabilities.\n + - The plugin follows best practices for coding and design.\n + + Please review the plugin and **approve** or **reject** it by clicking one of the buttons below. + ` +} + +export { DiscordClientConfiguration } diff --git a/src/globals/databases/DatabaseSchemas.ts b/src/globals/databases/DatabaseSchemas.ts new file mode 100644 index 0000000..50dc3cd --- /dev/null +++ b/src/globals/databases/DatabaseSchemas.ts @@ -0,0 +1,4 @@ +/** + * @sumamry Schemas related to plugins. + */ +export * from "./plugins/PluginsSchemas" diff --git a/src/globals/databases/plugins/PluginsSchemas.ts b/src/globals/databases/plugins/PluginsSchemas.ts new file mode 100644 index 0000000..ad283b9 --- /dev/null +++ b/src/globals/databases/plugins/PluginsSchemas.ts @@ -0,0 +1,16 @@ +import { boolean, pgTable, serial, varchar } from "drizzle-orm/pg-core" + +/** + * @summary Plugins table. + * @description Plugins table. + */ +const pluginsTable = pgTable("plugins", { + id: serial("id").notNull().primaryKey(), + approved: boolean("approved").notNull().default(false), + name: varchar("name", { length: 255 }).notNull(), + owner: varchar("owner", { length: 255 }).notNull(), + url: varchar("url", { length: 255 }).notNull(), + branch: varchar("branch", { length: 255 }).notNull().default("main"), +}) + +export { pluginsTable } diff --git a/src/globals/managers/ApplicationInstanceManager.ts b/src/globals/managers/ApplicationInstanceManager.ts new file mode 100644 index 0000000..201759c --- /dev/null +++ b/src/globals/managers/ApplicationInstanceManager.ts @@ -0,0 +1,48 @@ +import cors from "cors" +import express from "express" +import { pluginsRouter } from "../../routes/plugins" + +/** + * @summary Manager for the application instance. + * @description Manager for the application instance. + */ +class ApplicationInstanceManager { + /** + * @summary Private constructor. + * @description Private constructor that prevents the class from being instantiated. + */ + private constructor() {} + + /** + * The internal instance of the application. + * @private + */ + private static internalInstance: express.Express | null = null + + /** + * @summary Get the instance of the application. + * @description Get the instance of the application. + * @returns The instance of the application. + */ + public static get instance(): express.Express { + if (this.internalInstance === null) { + this.internalInstance = express() + this.internalInstance.use(express.json(), cors()) + this.loadRoutes() + } + + return this.internalInstance + } + + /** + * @summary Load the routes of the application. + * @description Load the routes of the application. + * @remarks Every single router you add to the application must be loaded here. + */ + private static loadRoutes(): void { + if (this.internalInstance === null) return + this.internalInstance.use("/v1/plugins", pluginsRouter) + } +} + +export { ApplicationInstanceManager } diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 598f267..0000000 --- a/src/index.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { Database } from "bun:sqlite"; -import { Server } from "node:http"; - -import express, { type Express } from "express"; -import cors from "cors"; - -import { Routes } from "./routes"; -import { Services, type Service } from "./services"; -import type { StoredPlugin } from "./types"; -import type { Plugin } from "./plugin"; - -class RestAPIService { - /** - * The Express application instance - */ - private readonly express: Express = express(); - - /** - * The HTTP server instance - */ - private readonly server: Server = this.express.listen(4000); - - /** - * The map of service instances - */ - private readonly services = new Map(); - - /** - * The SQLite database instance - */ - private readonly db: Database = new Database("plugins.db"); - - /** - * The in-memory cache of plugins - */ - private readonly plugins = new Map(); - - public constructor() { - // Enable JSON body parsing middleware - this.express.use(express.json()); - this.express.use(cors()); - - // Iterate over each route and register it with the Express app - for (const route of Routes) { - // Switch on the HTTP method and register the route accordingly - switch(route.method as string) { - default: - case "GET": { - this.express.get(route.path, route.handle); - - continue; - } - - case "POST": { - this.express.post(route.path, route.handle); - - continue; - } - } - } - - // Iterate over each service and create an instance - for (const service of Services) { - // Create an instance of the service - const instance = new service(this); - - // Store the service instance in the map - this.services.set(instance.name, instance); - } - - // Prepare the database - this.prepareDatabase(); - } - - public getService(name: string): T | null { - // Get the service instance from the map - const service = this.services.get(name) as T | undefined; - - // Return the service instance or null if not found - return service ?? null; - } - - private prepareDatabase(): void { - // Create a table to store aproved plugins - this.db.prepare(` - CREATE TABLE IF NOT EXISTS plugins ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - approved BOOLEAN DEFAULT 0, - name TEXT NOT NULL, - owner TEXT NOT NULL, - url TEXT NOT NULL, - branch TEXT DEFAULT 'main' - );`).run(); - } - - public hasPlugin(id: number): boolean { - // Check if a plugin with the given id exists in the database - const row = this.db.prepare("SELECT id FROM plugins WHERE id = ?").get(id); - - // Return true if the plugin exists, false otherwise - return !!row; - } - - public addStoredPlugin(plugin: StoredPlugin): void { - // Insert a new plugin into the database - this.db.prepare("INSERT INTO plugins (id, name, owner, url, approved, branch) VALUES (?, ?, ?, ?, ?, ?)") - .run(plugin.id, plugin.name, JSON.stringify(plugin.owner), plugin.url, plugin.approved, plugin.branch); - } - - public getStoredPlugin(id: number): StoredPlugin | null { - // Get a plugin with the given id from the database - const row = this.db.prepare("SELECT * FROM plugins WHERE id = ?").get(id); - - // Return the plugin or null if not found - return row ? { - id: row.id, - name: row.name, - owner: JSON.parse(row.owner), - url: row.url, - branch: row.branch, - approved: row.approved === 1, - } : null; - } - - public updateStoredPlugin(id: number, plugin: Partial): void { - // Prepare a field list and values for the update query - const fields = []; - const values = []; - - // Iterate over the plugin properties and prepare the fields and values - for (const [key, value] of Object.entries(plugin)) { - fields.push(`${key} = ?`); - - // Check if the value is an object and needs to be stringified - if (key === "owner" && typeof value === "object") { - values.push(JSON.stringify(value)); - continue; - } else { - values.push(value as string | number | boolean); - } - } - - // Add the id to the values array for the WHERE clause - values.push(id); - - // Update the plugin in the database - this.db.prepare(`UPDATE plugins SET ${fields.join(", ")} WHERE id = ?`).run(...values); - } - - public addCachedPlugin(plugin: Plugin): void { - // Add a plugin to the in-memory cache - this.plugins.set(plugin.id, plugin); - } - - public clearPluginCache(): void { - // Clear the in-memory cache of plugins - this.plugins.clear(); - } - - public isPluginApproved(id: number): boolean { - // Check if a plugin with the given id is approved in the database - const row = this.db.prepare("SELECT approved FROM plugins WHERE id = ?").get(id); - - // Return true if the plugin is approved, false otherwise - return row?.approved === 1; - } - - public setPluginApproval(id: number, approved: boolean): void { - // Update the approval status of a plugin in the database - this.db.prepare("UPDATE plugins SET approved = ? WHERE id = ?").run(approved ? 1 : 0, id); - } - - public getPluginFromCache(id: number): Plugin | null { - // Get the plugin from the in-memory cache - const plugin = this.plugins.get(id); - - // Return the plugin or null if not found - return plugin ?? null; - } - - public getAllPluginsFromCache(): Array { - // Return all plugins from the in-memory cache as an array - return Array.from(this.plugins.values()); - } -} - -export default new RestAPIService(); -export { RestAPIService }; diff --git a/src/models/services/base/databases/types/NotFoundInDatabaseType.ts b/src/models/services/base/databases/types/NotFoundInDatabaseType.ts new file mode 100644 index 0000000..51ebad4 --- /dev/null +++ b/src/models/services/base/databases/types/NotFoundInDatabaseType.ts @@ -0,0 +1,7 @@ +/** + * @summary Type for not found in cache. + * @description It is a type that represents that the item was not found in the databsae, so it saves it to + */ +type NotFoundInDatabaseType = "__NOT_FOUND_IN_DATABASE__" + +export type { NotFoundInDatabaseType } diff --git a/src/models/services/databases/plugins/base/interfaces/IPlugin.ts b/src/models/services/databases/plugins/base/interfaces/IPlugin.ts new file mode 100644 index 0000000..c0effa9 --- /dev/null +++ b/src/models/services/databases/plugins/base/interfaces/IPlugin.ts @@ -0,0 +1,38 @@ +/** + * @summary Interface for a plugin. + * @description Interface for a plugin. + */ +interface IPlugin { + /** + * @summary The ID of the plugin. + * @description The ID of the plugin. + */ + readonly id: number + /** + * @summary Whether the plugin is approved. + * @description Whether the plugin is approved. + */ + readonly approved: boolean + /** + * @summary The name of the plugin. + * @description The name of the plugin. + */ + readonly name: string + /** + * @summary The owner of the plugin. + * @description The owner of the plugin. + */ + readonly owner: string + /** + * @summary The URL of the plugin. + * @description The URL of the plugin. + */ + readonly url: string + /** + * @summary The branch of the plugin. + * @description The branch of the plugin. + */ + readonly branch: string +} + +export type { IPlugin } diff --git a/src/models/services/databases/plugins/base/interfaces/IPluginCreatePayload.ts b/src/models/services/databases/plugins/base/interfaces/IPluginCreatePayload.ts new file mode 100644 index 0000000..b302066 --- /dev/null +++ b/src/models/services/databases/plugins/base/interfaces/IPluginCreatePayload.ts @@ -0,0 +1,33 @@ +/** + * @summary Interface for a plugin create payload. + * @description Interface for a plugin create payload. + */ +interface IPluginCreatePayload { + /** + * @summary Whether the plugin is approved. + * @description Whether the plugin is approved. + */ + readonly approved: boolean + /** + * @summary The name of the plugin. + * @description The name of the plugin. + */ + readonly name: string + /** + * @summary The owner of the plugin. + * @description The owner of the plugin. + */ + readonly owner: string + /** + * @summary The URL of the plugin. + * @description The URL of the plugin. + */ + readonly url: string + /** + * @summary The branch of the plugin. + * @description The branch of the plugin. + */ + readonly branch: string +} + +export type { IPluginCreatePayload } diff --git a/src/models/services/databases/plugins/base/interfaces/IPluginUpdatePayload.ts b/src/models/services/databases/plugins/base/interfaces/IPluginUpdatePayload.ts new file mode 100644 index 0000000..c5787dd --- /dev/null +++ b/src/models/services/databases/plugins/base/interfaces/IPluginUpdatePayload.ts @@ -0,0 +1,33 @@ +/** + * @summary Interface for a plugin update payload. + * @description Interface for a plugin update payload. + */ +interface IPluginUpdatePayload { + /** + * @summary Whether the plugin is approved. + * @description Whether the plugin is approved. + */ + readonly approved?: boolean + /** + * @summary The name of the plugin. + * @description The name of the plugin. + */ + readonly name?: string + /** + * @summary The owner of the plugin. + * @description The owner of the plugin. + */ + readonly owner?: string + /** + * @summary The URL of the plugin. + * @description The URL of the plugin. + */ + readonly url?: string + /** + * @summary The branch of the plugin. + * @description The branch of the plugin. + */ + readonly branch?: string +} + +export type { IPluginUpdatePayload } diff --git a/src/models/services/github/interfaces/IEnrichedPlugin.ts b/src/models/services/github/interfaces/IEnrichedPlugin.ts new file mode 100644 index 0000000..b0e0132 --- /dev/null +++ b/src/models/services/github/interfaces/IEnrichedPlugin.ts @@ -0,0 +1,30 @@ +import type { IPluginCommit } from "./IPluginCommit" +import type { IPluginContributor } from "./IPluginContributor" +import type { IPluginRelease } from "./IPluginRelease" + +interface IEnrichedPlugin { + readonly id: number + readonly approved: boolean + readonly name: string + readonly owner: string + readonly url: string + readonly branch: string + readonly description: string | null + readonly version: string | null + readonly stars: number + readonly downloads: number + readonly forks: number + readonly issues: number + readonly keywords: string[] + readonly logo: string + readonly banner: string | null + readonly published: string | null + readonly updated: string | null + readonly readme: string | null + readonly gallery: string[] + readonly contributors: IPluginContributor[] + readonly commits: IPluginCommit[] + readonly releases: IPluginRelease[] +} + +export type { IEnrichedPlugin } diff --git a/src/models/services/github/interfaces/IGitHubReleaseAsset.ts b/src/models/services/github/interfaces/IGitHubReleaseAsset.ts new file mode 100644 index 0000000..d7c2751 --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubReleaseAsset.ts @@ -0,0 +1,8 @@ +interface IGitHubReleaseAsset { + readonly name: string + readonly size: number + readonly browser_download_url: string + readonly download_count: number +} + +export type { IGitHubReleaseAsset } diff --git a/src/models/services/github/interfaces/IGitHubRepository.ts b/src/models/services/github/interfaces/IGitHubRepository.ts new file mode 100644 index 0000000..c1f8918 --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepository.ts @@ -0,0 +1,17 @@ +import type { IGitHubRepositoryOwner } from "./IGitHubRepositoryOwner" + +interface IGitHubRepository { + readonly id: number + readonly name: string + readonly owner: IGitHubRepositoryOwner + readonly html_url: string + readonly default_branch: string + readonly description: string | null + readonly stargazers_count: number + readonly forks_count: number + readonly open_issues_count: number + readonly created_at: string + readonly updated_at: string +} + +export type { IGitHubRepository } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryCommit.ts b/src/models/services/github/interfaces/IGitHubRepositoryCommit.ts new file mode 100644 index 0000000..e6f8b8e --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryCommit.ts @@ -0,0 +1,11 @@ +import type { IGitHubRepositoryCommitAuthorUser } from "./IGitHubRepositoryCommitAuthorUser" +import type { IGitHubRepositoryCommitDetails } from "./IGitHubRepositoryCommitDetails" + +interface IGitHubRepositoryCommit { + readonly sha: string + readonly html_url: string + readonly commit: IGitHubRepositoryCommitDetails + readonly author: IGitHubRepositoryCommitAuthorUser | null +} + +export type { IGitHubRepositoryCommit } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryCommitAuthor.ts b/src/models/services/github/interfaces/IGitHubRepositoryCommitAuthor.ts new file mode 100644 index 0000000..a2dfafb --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryCommitAuthor.ts @@ -0,0 +1,6 @@ +interface IGitHubRepositoryCommitAuthor { + readonly name: string + readonly date: string +} + +export type { IGitHubRepositoryCommitAuthor } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryCommitAuthorUser.ts b/src/models/services/github/interfaces/IGitHubRepositoryCommitAuthorUser.ts new file mode 100644 index 0000000..702436e --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryCommitAuthorUser.ts @@ -0,0 +1,5 @@ +interface IGitHubRepositoryCommitAuthorUser { + readonly login: string +} + +export type { IGitHubRepositoryCommitAuthorUser } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryCommitDetails.ts b/src/models/services/github/interfaces/IGitHubRepositoryCommitDetails.ts new file mode 100644 index 0000000..b68d570 --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryCommitDetails.ts @@ -0,0 +1,8 @@ +import type { IGitHubRepositoryCommitAuthor } from "./IGitHubRepositoryCommitAuthor" + +interface IGitHubRepositoryCommitDetails { + readonly message: string + readonly author: IGitHubRepositoryCommitAuthor +} + +export type { IGitHubRepositoryCommitDetails } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryContributor.ts b/src/models/services/github/interfaces/IGitHubRepositoryContributor.ts new file mode 100644 index 0000000..cc7d6a2 --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryContributor.ts @@ -0,0 +1,8 @@ +interface IGitHubRepositoryContributor { + readonly login: string + readonly html_url: string + readonly avatar_url: string + readonly contributions: number +} + +export type { IGitHubRepositoryContributor } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryOwner.ts b/src/models/services/github/interfaces/IGitHubRepositoryOwner.ts new file mode 100644 index 0000000..2e0f2ee --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryOwner.ts @@ -0,0 +1,7 @@ +interface IGitHubRepositoryOwner { + readonly login: string + readonly avatar_url: string + readonly html_url: string +} + +export type { IGitHubRepositoryOwner } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryQuery.ts b/src/models/services/github/interfaces/IGitHubRepositoryQuery.ts new file mode 100644 index 0000000..ec4995e --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryQuery.ts @@ -0,0 +1,9 @@ +import type { IGitHubRepository } from "./IGitHubRepository" + +interface IGitHubRepositoryQuery { + readonly total_count: number + readonly incomplete_results: boolean + readonly items: IGitHubRepository[] +} + +export type { IGitHubRepositoryQuery } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryRelease.ts b/src/models/services/github/interfaces/IGitHubRepositoryRelease.ts new file mode 100644 index 0000000..f70a44f --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryRelease.ts @@ -0,0 +1,13 @@ +import type { IGitHubReleaseAsset } from "./IGitHubReleaseAsset" + +interface IGitHubRepositoryRelease { + readonly name: string | null + readonly tag_name: string + readonly html_url: string + readonly body: string | null + readonly prerelease: boolean + readonly published_at: string + readonly assets: IGitHubReleaseAsset[] +} + +export type { IGitHubRepositoryRelease } diff --git a/src/models/services/github/interfaces/IPluginCommit.ts b/src/models/services/github/interfaces/IPluginCommit.ts new file mode 100644 index 0000000..04b8bd2 --- /dev/null +++ b/src/models/services/github/interfaces/IPluginCommit.ts @@ -0,0 +1,9 @@ +interface IPluginCommit { + readonly sha: string + readonly html_url: string + readonly message: string + readonly date: string + readonly author: string +} + +export type { IPluginCommit } diff --git a/src/models/services/github/interfaces/IPluginContributor.ts b/src/models/services/github/interfaces/IPluginContributor.ts new file mode 100644 index 0000000..12f9e10 --- /dev/null +++ b/src/models/services/github/interfaces/IPluginContributor.ts @@ -0,0 +1,8 @@ +interface IPluginContributor { + readonly username: string + readonly profile_url: string + readonly avatar_url: string + readonly contributions: number +} + +export type { IPluginContributor } diff --git a/src/models/services/github/interfaces/IPluginPackageJSON.ts b/src/models/services/github/interfaces/IPluginPackageJSON.ts new file mode 100644 index 0000000..e30f669 --- /dev/null +++ b/src/models/services/github/interfaces/IPluginPackageJSON.ts @@ -0,0 +1,6 @@ +interface IPluginPackageJSON { + readonly version?: string + readonly keywords?: string[] +} + +export type { IPluginPackageJSON } diff --git a/src/models/services/github/interfaces/IPluginRelease.ts b/src/models/services/github/interfaces/IPluginRelease.ts new file mode 100644 index 0000000..8aca224 --- /dev/null +++ b/src/models/services/github/interfaces/IPluginRelease.ts @@ -0,0 +1,13 @@ +import type { IPluginReleaseAsset } from "./IPluginReleaseAsset" + +interface IPluginRelease { + readonly name: string + readonly tag: string + readonly url: string + readonly description: string + readonly prerelease: boolean + readonly date: string + readonly assets: IPluginReleaseAsset[] +} + +export type { IPluginRelease } diff --git a/src/models/services/github/interfaces/IPluginReleaseAsset.ts b/src/models/services/github/interfaces/IPluginReleaseAsset.ts new file mode 100644 index 0000000..d4c893c --- /dev/null +++ b/src/models/services/github/interfaces/IPluginReleaseAsset.ts @@ -0,0 +1,8 @@ +interface IPluginReleaseAsset { + readonly name: string + readonly size: number + readonly download_url: string + readonly download_count: number +} + +export type { IPluginReleaseAsset } diff --git a/src/models/services/github/interfaces/index.ts b/src/models/services/github/interfaces/index.ts new file mode 100644 index 0000000..24e5c74 --- /dev/null +++ b/src/models/services/github/interfaces/index.ts @@ -0,0 +1,16 @@ +export type * from "./IEnrichedPlugin" +export type * from "./IGitHubReleaseAsset" +export type * from "./IGitHubRepository" +export type * from "./IGitHubRepositoryCommit" +export type * from "./IGitHubRepositoryCommitAuthor" +export type * from "./IGitHubRepositoryCommitAuthorUser" +export type * from "./IGitHubRepositoryCommitDetails" +export type * from "./IGitHubRepositoryContributor" +export type * from "./IGitHubRepositoryOwner" +export type * from "./IGitHubRepositoryQuery" +export type * from "./IGitHubRepositoryRelease" +export type * from "./IPluginCommit" +export type * from "./IPluginContributor" +export type * from "./IPluginPackageJSON" +export type * from "./IPluginRelease" +export type * from "./IPluginReleaseAsset" diff --git a/src/plugin.ts b/src/plugin.ts deleted file mode 100644 index c54ffc9..0000000 --- a/src/plugin.ts +++ /dev/null @@ -1,306 +0,0 @@ -import axios from "axios"; - -import type { PluginCommit, PluginContributor, PluginRelease, Repository, RepositoryContributor, RepositoryRelease, StoredPlugin } from "./types"; -import type { RepositoryCommit } from "./types/repository-commit"; - -class Plugin implements StoredPlugin { - /** - * The unique identifier of the plugin (GitHub repository ID) - */ - public readonly id: number; - - /** - * The name of the plugin (GitHub repository name) - */ - public readonly name: string; - - /** - * The owner of the plugin (GitHub repository owner) - */ - public readonly owner: PluginContributor; - - /** - * The URL of the plugin (GitHub repository URL) - */ - public readonly url: string; - - /** - * The default branch of the plugin repository (assumed to be "main") - */ - public readonly branch: string = "main"; - - /** - * Whether the plugin has been approved for listing - */ - public readonly approved: boolean; - - public description: string | null = null; - - public version: string | null = null; - - public stars: number | null = null; - - public downloads: number | null = null; - - public forks: number | null = null; - - public issues: number | null = null; - - public keywords: Array | null = null; - - public logo: string | null = null; - - public banner: string | null = null; - - public published: string | null = null; - - public updated: string | null = null; - - public readme: string | null = null; - - public gallery: Array = []; - - public contributors: Array = []; - - public commits: Array = []; - - public releases: Array = []; - - /** - * Creates an instance of the Plugin. - * @param data The stored plugin data. - */ - private constructor(data: StoredPlugin, repository: Repository) { - // Load the basic data from the stored plugin object - this.id = data.id; - this.name = data.name; - this.owner = data.owner; - this.url = data.url; - this.approved = data.approved; - - - // Load additional data from the repository object - this.description = repository.description; - this.branch = repository.default_branch; - this.stars = repository.stargazers_count; - this.forks = repository.forks_count; - this.issues = repository.open_issues_count; - this.published = repository.created_at; - this.updated = repository.updated_at; - } - - public static async create(data: StoredPlugin, repository: Repository): Promise { - // Create a new Plugin instance - const plugin = new Plugin(data, repository); - - // Fetch the logo URL - plugin.logo = await this.getLogoURL(data); - - // Fetch the banner URL - plugin.banner = await this.getBannerURL(data); - - // Fetch the releases, commits & map the total downloads - plugin.releases = await this.getReleases(data); - plugin.commits = await this.getCommitHistory(data); - plugin.downloads = plugin.releases.reduce((acc, release) => acc + release.assets.reduce((a, asset) => a + asset.download_count, 0), 0); - - // Fetch the contributors - plugin.contributors = await this.getContributors(data); - - // Fetch the gallery images - plugin.gallery = await plugin.getGallery(); - - // Fetch the readme - plugin.readme = await this.getReadme(data); - - // Fetch the package.json to get the version and keywords - const packageData = await this.getPackageJSON(data); - if (packageData) { - plugin.version = packageData.version; - plugin.keywords = packageData.keywords; - } - - // Return the plugin instance - return plugin; - } - - public static async getReleases(plugin: StoredPlugin): Promise> { - try { - // Check if the plugin has any releases on GitHub - const response = await axios.get>(`https://api.github.com/repos/${plugin.owner.username}/${plugin.name}/releases`, { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - }); - - // Return the releases if the request was successful - return response.data.map(release => ({ - name: release.name || release.tag_name, - tag: release.tag_name, - url: release.html_url, - description: release.body ?? "", - prerelease: release.prerelease, - date: release.published_at, - assets: release.assets.map((asset) => ({ - name: asset.name, - size: asset.size, - download_url: asset.browser_download_url, - download_count: asset.download_count, - })), - })); - } catch { - // Return an empty array if the request failed - return []; - } - } - - public static async getContributors(plugin: StoredPlugin): Promise> { - try { - // Check if the plugin has any contributors on GitHub - const response = await axios.get>(`https://api.github.com/repos/${plugin.owner.username}/${plugin.name}/contributors`, { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - }); - - // Return the contributors if the request was successful - return response.data.map(contributor => ({ - username: contributor.login, - profile_url: contributor.html_url, - avatar_url: contributor.avatar_url, - contributions: contributor.contributions, - })); - } catch { - // Return an empty array if the request failed - return []; - } - } - - public static async getLogoURL(plugin: StoredPlugin): Promise { - // The logo URL is assumed to be at a standard location in the repository - const url = `https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/public/logo.png`; - - try { - // Check if the logo exists by making a HEAD request - const response = await axios.head(url); - - // Return the logo URL if it exists, otherwise return a default logo - return response.status === 200 ? url : "https://avatars.githubusercontent.com/u/92610726?s=88&v=4"; - } catch { - return "https://avatars.githubusercontent.com/u/92610726?s=88&v=4"; - } - } - - public static async getBannerURL(plugin: StoredPlugin): Promise { - // The banner URL is assumed to be at a standard location in the repository - const url = `https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/public/banner.png`; - - try { - // Check if the banner exists by making a HEAD request - const response = await axios.head(url); - - // Return the banner URL if it exists, otherwise return null - return response.status === 200 ? url : null; - } catch { - return null; - } - } - - public static async getReadme(plugin: StoredPlugin): Promise { - try { - // Get the readme file from the repository - const response = await axios.get(`https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/README.md`, { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - }); - - // Return the readme content - return response.data; - } catch { - // Return null if the request failed - return null; - } - } - - public static async getPackageJSON(plugin: StoredPlugin): Promise<{ version: string, keywords: Array } | null> { - try { - // Get the package.json file from the repository - const response = await axios.get<{ version: string, keywords: Array }>(`https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/package.json`, { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - }); - - // Return the package.json content - return response.data; - } catch { - console.log("Failed to fetch package.json"); - - // Return null if the request failed - return null; - } - } - - public async getGallery(): Promise> { - // The gallery images are assumed to be in a standard location in the repository - // Assumed path ./public/gallery/*.png - const images = []; - - // Fetch all images in the gallery (up to 10 images) - for (let i = 1; i <= 10; i++) { - try { - // Construct the image URL - const url = `https://raw.githubusercontent.com/${this.owner.username}/${this.name}/${this.branch}/public/gallery/image${i}.png`; - - // Check if the image exists by making a HEAD request - const response = await axios.head(url); - - // If the image exists, add it to the gallery - if (response.status === 200) { - images.push(url); - } else { - break; - } - } catch { - break; - } - } - - // Return the gallery images - return images; - } - - public static async getRepository(plugin: StoredPlugin): Promise { - try { - // Get the repository details from GitHub - const response = await axios.get(`https://api.github.com/repositories/${plugin.id}`, { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - }); - - // Return the repository details - return response.data; - } catch { - // Return null if the request failed - return null; - } - } - - public static async getCommitHistory(plugin: StoredPlugin): Promise> { - try { - // Get the commit history from GitHub - const response = await axios.get>(`https://api.github.com/repos/${plugin.owner.username}/${plugin.name}/commits`, { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - params: { per_page: 100 }, - }); - - console.log(response.data) - - // Return the commit history - return response.data.map(commit => ({ - sha: commit.sha, - html_url: commit.html_url, - message: commit.commit.message, - date: commit.commit.author.date, - author: commit.author?.login || commit.commit.author.name, - })); - } catch { - // Return an empty array if the request failed - return []; - } - } -} - -export { Plugin }; \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts index 84f77e9..4ddbe90 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,9 +1,8 @@ -import { GetPluginRoute } from "./plugin"; -import { GetPluginsRoute } from "./plugins"; +import type { Express } from "express" +import { pluginsRouter } from "./plugins" -const Routes = [ - GetPluginRoute, - GetPluginsRoute -] +const loadRoutes = (application: Express): void => { + application.use("/plugins", pluginsRouter) +} -export { Routes }; +export { loadRoutes } diff --git a/src/routes/plugin.ts b/src/routes/plugin.ts deleted file mode 100644 index d745b2a..0000000 --- a/src/routes/plugin.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Request, Response } from "express"; -import { Route } from "./route"; - -import RestAPIService from "../index"; - -class GetPluginRoute extends Route { - public static override readonly path: string = "/plugin/{:id}"; - - public static override readonly method = "GET"; - - public static override async handle(req: Request, res: Response): Promise { - // Get the plugin ID from the request parameters - const id = Number(req.params.id); - - // Get the plugin from the cache - const plugin = RestAPIService.getPluginFromCache(id); - - // Check if the plugin was found - if (!plugin) { - // Send a 404 response if the plugin was not found - res.status(404).send({ message: `Plugin with ID ${id} not found` }); - } else { - // Send the plugin as the response - res.status(200).send(plugin); - } - } -} - -export { GetPluginRoute }; \ No newline at end of file diff --git a/src/routes/plugins.ts b/src/routes/plugins.ts index 7d595cd..bfa0fd6 100644 --- a/src/routes/plugins.ts +++ b/src/routes/plugins.ts @@ -1,20 +1,22 @@ -import type { Request, Response } from "express"; +import { Router } from "express" +import { PluginsController } from "../controllers/plugins/PluginsController" -import { Route } from "./route"; -import RestAPIService from "../index"; +/** + * @summary Router for plugins. + * @description Router for plugins. + */ +const pluginsRouter: Router = Router() -class GetPluginsRoute extends Route { - public static override readonly path: string = "/plugins"; +/** + * @summary Get plugins pagination. + * @returns The plugins pagination. + */ +pluginsRouter.get("/", PluginsController.getPluginsPagination) - public static override readonly method = "GET"; +/** + * @summary Get all plugins. + * @returns The all plugins. + */ +pluginsRouter.get("/all", PluginsController.getAllPlugins) - public static override async handle(_req: Request, res: Response): Promise { - // Get all plugins from the cache - const plugins = RestAPIService.getAllPluginsFromCache(); - - // Send the plugins as the response - res.status(200).send(plugins); - } -} - -export { GetPluginsRoute }; \ No newline at end of file +export { pluginsRouter } diff --git a/src/routes/route.ts b/src/routes/route.ts deleted file mode 100644 index 0370894..0000000 --- a/src/routes/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Request, Response } from "express"; - -abstract class Route { - /** - * The path for this route (e.g., "/plugins"). - */ - public static readonly path: string = "/"; - - /** - * The HTTP method for this route (e.g., "GET", "POST"). - */ - public static readonly method: "GET" | "POST" | "PUT" | "DELETE"; - - /** - * Handle an incoming HTTP request. - * @param req The incoming request object. - * @param res The outgoing response object. - */ - public static async handle(_req: Request, _res: Response): Promise { - _res.status(501).send("Route::handle - Not Implemented"); - } -} - -export { Route }; diff --git a/src/services/base/BaseDatabaseService.ts b/src/services/base/BaseDatabaseService.ts new file mode 100644 index 0000000..155db07 --- /dev/null +++ b/src/services/base/BaseDatabaseService.ts @@ -0,0 +1,34 @@ +import type { NotFoundInDatabaseType } from "../../models/services/base/databases/types/NotFoundInDatabaseType" + +/** + * @summary Base class for database services. + * @description Base class for database services. + */ +abstract class BaseDatabaseService { + /** + * @summary Protected constructor. + * @description Protected constructor to prevent instantiation of the class, while allowing inheritance. + */ + protected constructor() {} + + /** + * @summary Not found in database type. + * @description It is a type that represents that the item was not found in the databsae, so it saves it to the database. + */ + protected static readonly NOT_FOUND_IN_DATABASE: NotFoundInDatabaseType = "__NOT_FOUND_IN_DATABASE__" as const + + /** + * @summary Default page size. + * @description Default page size. Do not modify this value, it's only in case when you edit PAGE_SIZE. + * @see `this.PAGE_SIZE` If you want to change the page size, you should edit this value. + */ + public static readonly DEFAULT_PAGE_SIZE: number = 50 + + /** + * @summary Page size. + * @description A property used with pagination. It's set to the default page size by default. + */ + public static readonly PAGE_SIZE: number = this.DEFAULT_PAGE_SIZE +} + +export { BaseDatabaseService } diff --git a/src/services/base/BaseGitHubService.ts b/src/services/base/BaseGitHubService.ts new file mode 100644 index 0000000..2155924 --- /dev/null +++ b/src/services/base/BaseGitHubService.ts @@ -0,0 +1,42 @@ +import type { AxiosRequestConfig } from "axios" +import { EnvironmentVariables } from "../../globals/EnvironmentVariables" + +/** + * @summary Base class for GitHub services. + * @description Provides shared constants and request helpers for GitHub API integrations. + */ +abstract class BaseGitHubService { + /** + * @summary Protected constructor. + * @description Protected constructor to prevent direct instantiation while allowing inheritance. + */ + protected constructor() {} + + protected static readonly GITHUB_API_BASE_URL: string = "https://api.github.com" + protected static readonly GITHUB_RAW_BASE_URL: string = "https://raw.githubusercontent.com" + protected static readonly DEFAULT_REPOSITORY_SEARCH_QUERY: string = "topic:serenityjs-plugin" + + protected static buildRequestConfig(extraConfig?: AxiosRequestConfig): AxiosRequestConfig { + const token: string | undefined = EnvironmentVariables.GITHUB_TOKEN + return { + ...extraConfig, + headers: { + Accept: "application/vnd.github.v3+json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...(extraConfig?.headers ?? {}), + }, + } + } + + protected static buildRepositoryApiUrl(owner: string, name: string, suffix: string = ""): string { + const normalizedSuffix: string = suffix.startsWith("/") || suffix.length === 0 ? suffix : `/${suffix}` + return `${this.GITHUB_API_BASE_URL}/repos/${owner}/${name}${normalizedSuffix}` + } + + protected static buildRawRepositoryFileUrl(owner: string, name: string, branch: string, filePath: string): string { + const normalizedPath: string = filePath.startsWith("/") ? filePath.slice(1) : filePath + return `${this.GITHUB_RAW_BASE_URL}/${owner}/${name}/${branch}/${normalizedPath}` + } +} + +export { BaseGitHubService } diff --git a/src/services/databases/plugins/PluginsService.ts b/src/services/databases/plugins/PluginsService.ts new file mode 100644 index 0000000..de64545 --- /dev/null +++ b/src/services/databases/plugins/PluginsService.ts @@ -0,0 +1,224 @@ +import { and, count, eq } from "drizzle-orm" +import { DatabaseClient } from "../../../globals/clients/DatabaseClient" +import { RedisClient } from "../../../globals/clients/RedisClient" +import { BaseDatabaseService } from "../../base/BaseDatabaseService" +import { pluginsTable } from "../../../globals/databases/DatabaseSchemas" +import type { IPlugin } from "../../../models/services/databases/plugins/base/interfaces/IPlugin" +import type { NotFoundInDatabaseType } from "../../../models/services/base/databases/types/NotFoundInDatabaseType" +import type { IPluginCreatePayload } from "../../../models/services/databases/plugins/base/interfaces/IPluginCreatePayload" +import type { IPluginUpdatePayload } from "../../../models/services/databases/plugins/base/interfaces/IPluginUpdatePayload" + +/** + * @summary Service for plugins. + * @description Service for plugins. + */ +class PluginsService extends BaseDatabaseService { + /** + * @summary Private constructor. + * @description Private constructor to prevent instantiation of the class. + */ + private constructor() { + super() + } + + /** + * @summary Plugins count caching key. + * @description Plugins count caching key. + */ + private static readonly PLUGINS_COUNT_CACHING_KEY: string = "pluginsCount" + private static readonly ALL_PLUGINS_CACHING_KEY: string = "allPlugins" + private static readonly APPROVED_PLUGINS_CACHING_KEY: string = "approvedPlugins" + + /** + * @summary Get the number of plugins. + * @description Get the number of plugins. + * @returns The number of plugins. + */ + public static async getPluginsCount(): Promise { + const cachedValue: number | null = await RedisClient.getValue(this.PLUGINS_COUNT_CACHING_KEY) + if (cachedValue !== null) return cachedValue + + const queriedValue = await DatabaseClient.drizzleInstance + .select({ count: count() }) + .from(pluginsTable) + .execute() + + const finalResponse: number = queriedValue[0].count + await RedisClient.setValue(this.PLUGINS_COUNT_CACHING_KEY, finalResponse) + return finalResponse + } + + /** + * @summary Get the plugins by page. + * @description Get the plugins by page. + * @param page The page number. + * @returns The plugins by page. + */ + public static async getPluginsByPage(page: number): Promise { + const cachingKey: string = `pluginsByPage:${page}` + const cachedValue: IPlugin[] | null = await RedisClient.getValue(cachingKey) + if (cachedValue !== null) return cachedValue + + const queriedValues: IPlugin[] = await DatabaseClient.drizzleInstance + .select() + .from(pluginsTable) + .offset((page - 1) * this.PAGE_SIZE) + .limit(this.PAGE_SIZE) + .execute() + + await RedisClient.setValue(cachingKey, queriedValues) + return queriedValues + } + + /** + * @summary Get all plugins. + * @description Get all plugins from cache or the database. + * @returns All plugins. + */ + public static async getAllPlugins(): Promise { + const cachedValue: IPlugin[] | null = await RedisClient.getValue(this.ALL_PLUGINS_CACHING_KEY) + if (cachedValue !== null) return cachedValue + + const queriedValues: IPlugin[] = await DatabaseClient.drizzleInstance.select().from(pluginsTable).execute() + await RedisClient.setValue(this.ALL_PLUGINS_CACHING_KEY, queriedValues) + return queriedValues + } + + /** + * @summary Get approved plugins. + * @description Get approved plugins from cache or the database. + * @returns Approved plugins. + */ + public static async getApprovedPlugins(): Promise { + const cachedValue: IPlugin[] | null = await RedisClient.getValue(this.APPROVED_PLUGINS_CACHING_KEY) + if (cachedValue !== null) return cachedValue + + const queriedValues: IPlugin[] = await DatabaseClient.drizzleInstance + .select() + .from(pluginsTable) + .where(eq(pluginsTable.approved, true)) + .execute() + + await RedisClient.setValue(this.APPROVED_PLUGINS_CACHING_KEY, queriedValues) + return queriedValues + } + + /** + * @summary Get a plugin by owner and name. + * @description Get a plugin by owner and name from the database. + * @param owner The plugin owner. + * @param name The plugin name. + * @returns The plugin if found. + */ + public static async getPluginByOwnerAndName(owner: string, name: string): Promise { + const queriedValues: IPlugin[] = await DatabaseClient.drizzleInstance + .select() + .from(pluginsTable) + .where(and(eq(pluginsTable.owner, owner), eq(pluginsTable.name, name))) + .limit(1) + .execute() + + if (queriedValues.length === 0) return null + return queriedValues[0] + } + + /** + * @summary Get the plugin by ID. + * @description Get the plugin by ID. + * @param id The ID of the plugin. + * @returns The plugin by ID. + */ + public static async getPluginById(id: number): Promise { + const cachingKey: string = `pluginById:${id}` + const cachedValue: IPlugin | NotFoundInDatabaseType | null = await RedisClient.getValue(cachingKey) + if (cachedValue !== null) return cachedValue === this.NOT_FOUND_IN_DATABASE ? null : cachedValue + + const queriedValue: IPlugin[] = await DatabaseClient.drizzleInstance + .select() + .from(pluginsTable) + .where(eq(pluginsTable.id, id)) + .execute() + + if (queriedValue.length === 0) { + await RedisClient.setValue(cachingKey, this.NOT_FOUND_IN_DATABASE) + return null + } + + const finalResponse: IPlugin = queriedValue[0] + await RedisClient.setValue(cachingKey, finalResponse) + return finalResponse + } + + /** + * @summary Create a plugin. + * @description Create a plugin. + * @param plugin The plugin to create. + * @returns The created plugin. + */ + public static async createPlugin(plugin: IPluginCreatePayload): Promise { + try { + const addedPlugin: IPlugin[] = await DatabaseClient.drizzleInstance + .insert(pluginsTable) + .values(plugin) + .returning() + .execute() + + await RedisClient.deleteValues( + this.PLUGINS_COUNT_CACHING_KEY, + this.ALL_PLUGINS_CACHING_KEY, + this.APPROVED_PLUGINS_CACHING_KEY, + `pluginsByPage:*`, + `pluginById:${addedPlugin[0].id}`, + ) + return addedPlugin[0] + } catch { + return null + } + } + + /** + * @summary Update a plugin. + * @description Update a plugin. + * @param id The ID of the plugin. + * @param plugin The plugin to update. + * @returns The updated plugin. + */ + public static async updatePlugin(id: number, plugin: IPluginUpdatePayload): Promise { + try { + const updatedPlugin: IPlugin[] = await DatabaseClient.drizzleInstance + .update(pluginsTable) + .set(plugin) + .where(eq(pluginsTable.id, id)) + .returning() + .execute() + + await RedisClient.deleteValues( + this.ALL_PLUGINS_CACHING_KEY, + this.APPROVED_PLUGINS_CACHING_KEY, + `pluginsByPage:*`, + `pluginById:${id}`, + ) + return updatedPlugin[0] + } catch { + return null + } + } + + public static async deletePlugin(id: number): Promise { + try { + await DatabaseClient.drizzleInstance.delete(pluginsTable).where(eq(pluginsTable.id, id)).execute() + await RedisClient.deleteValues( + this.PLUGINS_COUNT_CACHING_KEY, + this.ALL_PLUGINS_CACHING_KEY, + this.APPROVED_PLUGINS_CACHING_KEY, + `pluginsByPage:*`, + `pluginById:${id}`, + ) + return true + } catch { + return false + } + } +} + +export { PluginsService } diff --git a/src/services/discord.ts b/src/services/discord.ts deleted file mode 100644 index ec790d9..0000000 --- a/src/services/discord.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { ActionRow, ActionRowBuilder, ButtonBuilder, ButtonStyle, Client, EmbedBuilder, Events, GatewayIntentBits, type GuildTextBasedChannel, type Interaction, type Snowflake, type TextBasedChannel } from "discord.js"; - -import type { RestAPIService } from "../index"; -import { Service } from "./service"; -import type { StoredPlugin } from "../types"; -import type { GHFetch } from "./gh-fetch"; -import { Plugin } from "../plugin"; - -class Discord extends Service { - private readonly pluginApprovalChannelId: Snowflake = "1411567293552529462"; - - /** - * The Discord bot token for the client to login - */ - private readonly token: string = process.env.DISCORD_TOKEN ?? ""; - - /** - * The Discord client instance - */ - private readonly client: Client = new Client({ intents: [GatewayIntentBits.Guilds] }); - - /** - * A promise that resolves when the client is ready - */ - private readonly ready = new Promise((resolve) => this.client.once(Events.ClientReady, () => resolve())); - - /** - * Instructions for approving or rejecting a plugin - */ - private readonly approvalInstructions: string = ` - **Verify that the plugin meets the following criteria:** - - - The plugin is relevant to SerenityJS and its ecosystem.\n - - The plugin is well-maintained and has at least a release on GitHub.\n - - The plugin has a proper README file and documentation.\n - - The plugin does not contain any malicious code or vulnerabilities.\n - - The plugin follows best practices for coding and design.\n - - Please review the plugin and **approve** or **reject** it by clicking one of the buttons below. - `; - - public constructor(api: RestAPIService) { - super(api); - - // Login to Discord with the bot token - this.client.login(this.token); - - // Bind the interaction handler to the client - this.client.on(Events.InteractionCreate, this.handleInteraction.bind(this)); - } - - public async sendPluginApprovalRequest(plugin: StoredPlugin): Promise { - // Wait until the client is ready - await this.ready; - - // Fetch the channel by its ID - const channel = await this.client.channels.fetch(this.pluginApprovalChannelId) as GuildTextBasedChannel | null; - - // Check if the channel exists and is text-based - if (!channel || !channel.isTextBased()) { - console.error(`Channel with ID ${this.pluginApprovalChannelId} not found or is not text-based.`); - return; - } - - // Create the embed message - const embed = new EmbedBuilder() - .setTitle("New Plugin Approval Request") - .setDescription(`A new plugin has been submitted for approval:\n\n**Name:** ${plugin.name}\n**Owner:** ${plugin.owner.username}\n**URL:** ${plugin.url}\n\n${this.approvalInstructions}`) - .setColor(0x8560E9) - .setThumbnail(await Plugin.getLogoURL(plugin)); - - // Create the action row with approve and reject buttons - const row = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId(`approve:${plugin.id}`) - .setLabel("Approve") - .setStyle(ButtonStyle.Success), - new ButtonBuilder() - .setCustomId(`reject:${plugin.id}`) - .setLabel("Reject") - .setStyle(ButtonStyle.Danger), - ); - - // Send the message to the channel - await channel.send({ embeds: [embed], components: [row] }); - } - - private async handleInteraction(interaction: Interaction): Promise { - // Check if the interaction is a button interaction - if (interaction.isButton()) { - // Get the action and plugin ID from the button custom ID - const [action, pluginId] = interaction.customId.split(":"); - - // Check if the action is valid - if (action === "approve" || action === "reject") { - // Determine if the plugin is approved or rejected - const approved = action === "approve"; - - // Update the plugin approval status in the database - this.api.setPluginApproval(Number(pluginId), approved); - - // Disable the buttons after the action is taken - interaction.message.edit({ components: [] }); - - // Reply to the interaction to acknowledge it - await interaction.reply({ content: `Plugin ${approved ? "approved" : "rejected"}.` }); - - // Get the stored plugin from the database - const stored = this.api.getStoredPlugin(Number(pluginId)); - - // If the plugin is approved and the stored plugin exists - if (approved && stored) { - // Get the repository data from GitHub - const repository = await Plugin.getRepository(stored); - - // If the repository data exists - if (!repository) return; - - // Create a Plugin instance from the stored data and repository data - const plugin = await Plugin.create(stored, repository); - - // Add the plugin to the in-memory cache - this.api.addCachedPlugin(plugin); - } - } - } - } -} - -export { Discord }; diff --git a/src/services/gh-fetch.ts b/src/services/gh-fetch.ts deleted file mode 100644 index 1a1260e..0000000 --- a/src/services/gh-fetch.ts +++ /dev/null @@ -1,115 +0,0 @@ -import axios from "axios"; - -import type { RestAPIService } from "../index"; - -import { Service } from "./service"; -import type { PluginContributor, PluginRelease, RepositoryContributor, RepositoryQuery, RepositoryRelease, StoredPlugin } from "../types"; -import type { Discord } from "./discord"; -import { Plugin } from "../plugin"; - -// This service will handle fetching plugins from GitHub - -class GHFetch extends Service { - /** - * The GitHub API endpoint for searching repositories - */ - private readonly endpoint = "https://api.github.com/search/repositories"; - - /** - * The query to search for Serenity/JS plugins - */ - private readonly query = "topic:serenityjs-plugin"; - - /** - * The time interval (in milliseconds) to refresh the plugin list from GitHub (default: 5 minutes) - */ - private refreshTime = 5 * 60 * 1000; - - /** - * The time interval (in milliseconds) to clear the plugin cache (default: 1 hour) - */ - private cacheClearTime = 60 * 60 * 1000; - - public constructor(api: RestAPIService) { - super(api); - - // Fetch the plugins immediately - this.fetchPlugins(); - - // Set an interval to fetch the plugins periodically - setInterval(() => this.fetchPlugins(), this.refreshTime); - - // Set an interval to clear the plugin cache periodically - setInterval(() => {this.api.clearPluginCache(); this.fetchPlugins()}, this.cacheClearTime); - } - - public async fetchPlugins(): Promise { - try { - // Get the list of repositories from the GitHub API - const response = await axios.get(this.endpoint, { - params: { - q: this.query, - }, - headers: { - Accept: "application/vnd.github.v3+json", - }, - }); - - // Iterate over the repositories and log their names - for (const repo of response.data.items) { - // Check if the plugin is already in the database - if (this.api.hasPlugin(repo.id)) { - // Verify if the plugin is approved or if the plugin id already exists in the cache - if (!this.api.isPluginApproved(repo.id) || this.api.getPluginFromCache(repo.id)) continue; - - // Fetch the stored plugin data from the database - const stored = this.api.getStoredPlugin(repo.id) as StoredPlugin; - - // Create a Plugin instance from the stored data and repository data - const plugin = await Plugin.create(stored, repo); - - this.api.addCachedPlugin(plugin); - - console.log(`Plugin: ${repo.name} (${repo.id}) by ${repo.owner.login} - ${repo.url}`); - - } else { - // Create a StoredPlugin object - const storage: StoredPlugin = { - id: repo.id, - name: repo.name, - owner: { - username: repo.owner.login, - profile_url: repo.owner.html_url, - avatar_url: repo.owner.avatar_url, - contributions: 0, - }, - branch: repo.default_branch, - url: repo.html_url, - approved: false, - }; - - // Check if the plugin has releases, if not, skip it - if ((await Plugin.getReleases(storage)).length === 0) { - // Log that the plugin has no releases - console.log(`Plugin: ${repo.name} by ${repo.owner.login} has no releases, skipping...`); - - // Continue to the next repository - continue; - } - - // If not, add it to the database - this.api.addStoredPlugin(storage); - - // Get the Discord service instance - const discord = this.api.getService("Discord"); - - // Send a message to the Discord channel for plugin approval - if (discord) discord.sendPluginApprovalRequest(storage) - } - } - - } catch (error) {} - } -} - -export { GHFetch }; diff --git a/src/services/github/GitHubClientService.ts b/src/services/github/GitHubClientService.ts new file mode 100644 index 0000000..2c91321 --- /dev/null +++ b/src/services/github/GitHubClientService.ts @@ -0,0 +1,126 @@ +import axios from "axios" +import type { + IGitHubRepository, + IGitHubRepositoryCommit, + IGitHubRepositoryContributor, + IGitHubRepositoryQuery, + IGitHubRepositoryRelease, + IPluginPackageJSON, +} from "../../models/services/github/interfaces" +import { BaseGitHubService } from "../base/BaseGitHubService" + +/** + * @summary Service for GitHub API calls. + * @description Encapsulates all HTTP interactions with the GitHub and raw GitHub endpoints. + */ +class GitHubClientService extends BaseGitHubService { + private constructor() { + super() + } + + public static async searchPluginRepositories(): Promise { + try { + const response = await axios.get( + "https://api.github.com/search/repositories", + this.buildRequestConfig({ + params: { + q: this.DEFAULT_REPOSITORY_SEARCH_QUERY, + per_page: 100, + }, + }), + ) + return response.data.items + } catch { + return [] + } + } + + public static async getRepository(owner: string, name: string): Promise { + try { + const response = await axios.get( + this.buildRepositoryApiUrl(owner, name), + this.buildRequestConfig(), + ) + return response.data + } catch { + return null + } + } + + public static async getReleases(owner: string, name: string): Promise { + try { + const response = await axios.get( + this.buildRepositoryApiUrl(owner, name, "/releases"), + this.buildRequestConfig(), + ) + return response.data + } catch { + return [] + } + } + + public static async getContributors(owner: string, name: string): Promise { + try { + const response = await axios.get( + this.buildRepositoryApiUrl(owner, name, "/contributors"), + this.buildRequestConfig(), + ) + return response.data + } catch { + return [] + } + } + + public static async getCommits(owner: string, name: string): Promise { + try { + const response = await axios.get( + this.buildRepositoryApiUrl(owner, name, "/commits"), + this.buildRequestConfig({ + params: { per_page: 100 }, + }), + ) + return response.data + } catch { + return [] + } + } + + public static async getReadme(owner: string, name: string, branch: string): Promise { + try { + const response = await axios.get( + this.buildRawRepositoryFileUrl(owner, name, branch, "README.md"), + this.buildRequestConfig(), + ) + return response.data + } catch { + return null + } + } + + public static async getPackageJSON( + owner: string, + name: string, + branch: string, + ): Promise { + try { + const response = await axios.get( + this.buildRawRepositoryFileUrl(owner, name, branch, "package.json"), + this.buildRequestConfig(), + ) + return response.data + } catch { + return null + } + } + + public static async doesRawFileExist(rawFileUrl: string): Promise { + try { + const response = await axios.head(rawFileUrl, this.buildRequestConfig()) + return response.status === 200 + } catch { + return false + } + } +} + +export { GitHubClientService } diff --git a/src/services/github/GitHubPluginEnrichmentService.ts b/src/services/github/GitHubPluginEnrichmentService.ts new file mode 100644 index 0000000..843f4c5 --- /dev/null +++ b/src/services/github/GitHubPluginEnrichmentService.ts @@ -0,0 +1,139 @@ +import { DiscordClientConfiguration } from "../../globals/configuration/discord/DiscordClientConfiguration" +import type { IPlugin } from "../../models/services/databases/plugins/base/interfaces/IPlugin" +import type { + IEnrichedPlugin, + IPluginCommit, + IPluginContributor, + IPluginRelease, +} from "../../models/services/github/interfaces" +import { BaseGitHubService } from "../base/BaseGitHubService" +import { GitHubClientService } from "./GitHubClientService" + +/** + * @summary Service for plugin enrichment. + * @description Builds rich plugin payloads by collecting metadata from GitHub. + */ +class GitHubPluginEnrichmentService extends BaseGitHubService { + private constructor() { + super() + } + + public static async enrich(plugin: IPlugin): Promise { + const repository = await GitHubClientService.getRepository(plugin.owner, plugin.name) + if (repository === null) return null + + const [releases, contributors, commits, readme, packageJSON, logo, banner, gallery] = await Promise.all([ + GitHubClientService.getReleases(plugin.owner, plugin.name), + GitHubClientService.getContributors(plugin.owner, plugin.name), + GitHubClientService.getCommits(plugin.owner, plugin.name), + GitHubClientService.getReadme(plugin.owner, plugin.name, plugin.branch), + GitHubClientService.getPackageJSON(plugin.owner, plugin.name, plugin.branch), + this.getLogo(plugin), + this.getBanner(plugin), + this.getGallery(plugin), + ]) + + const mappedReleases: IPluginRelease[] = releases.map((release) => ({ + name: release.name ?? release.tag_name, + tag: release.tag_name, + url: release.html_url, + description: release.body ?? "", + prerelease: release.prerelease, + date: release.published_at, + assets: release.assets.map((asset) => ({ + name: asset.name, + size: asset.size, + download_url: asset.browser_download_url, + download_count: asset.download_count, + })), + })) + const mappedContributors: IPluginContributor[] = contributors.map((contributor) => ({ + username: contributor.login, + profile_url: contributor.html_url, + avatar_url: contributor.avatar_url, + contributions: contributor.contributions, + })) + const mappedCommits: IPluginCommit[] = commits.map((commit) => ({ + sha: commit.sha, + html_url: commit.html_url, + message: commit.commit.message, + date: commit.commit.author.date, + author: commit.author?.login ?? commit.commit.author.name, + })) + + const downloads: number = mappedReleases.reduce( + (totalDownloads, release) => + totalDownloads + + release.assets.reduce((releaseDownloads, asset) => releaseDownloads + asset.download_count, 0), + 0, + ) + + return { + id: plugin.id, + approved: plugin.approved, + name: plugin.name, + owner: plugin.owner, + url: plugin.url, + branch: plugin.branch, + description: repository.description, + version: packageJSON?.version ?? null, + stars: repository.stargazers_count, + downloads, + forks: repository.forks_count, + issues: repository.open_issues_count, + keywords: packageJSON?.keywords ?? [], + logo, + banner, + published: repository.created_at, + updated: repository.updated_at, + readme, + gallery, + contributors: mappedContributors, + commits: mappedCommits, + releases: mappedReleases, + } + } + + private static async getLogo(plugin: IPlugin): Promise { + const logoUrl: string = this.buildRawRepositoryFileUrl( + plugin.owner, + plugin.name, + plugin.branch, + "public/logo.png", + ) + const logoExists: boolean = await GitHubClientService.doesRawFileExist(logoUrl) + if (!logoExists) return DiscordClientConfiguration.DEFAULT_LOGO + return logoUrl + } + + private static async getBanner(plugin: IPlugin): Promise { + const bannerUrl: string = this.buildRawRepositoryFileUrl( + plugin.owner, + plugin.name, + plugin.branch, + "public/banner.png", + ) + const bannerExists: boolean = await GitHubClientService.doesRawFileExist(bannerUrl) + if (!bannerExists) return null + return bannerUrl + } + + private static async getGallery(plugin: IPlugin): Promise { + const galleryUrls: string[] = [] + for (let i = 1; i <= 10; i++) { + const imageUrl: string = this.buildRawRepositoryFileUrl( + plugin.owner, + plugin.name, + plugin.branch, + `public/gallery/image${i}.png`, + ) + const exists: boolean = await GitHubClientService.doesRawFileExist(imageUrl) + if (!exists) break + galleryUrls.push(imageUrl) + } + + return galleryUrls + } +} + +export { GitHubPluginEnrichmentService } diff --git a/src/services/github/GitHubSyncService.ts b/src/services/github/GitHubSyncService.ts new file mode 100644 index 0000000..b5a9940 --- /dev/null +++ b/src/services/github/GitHubSyncService.ts @@ -0,0 +1,116 @@ +import { DiscordClient } from "../../globals/clients/DiscordClient" +import { RedisClient } from "../../globals/clients/RedisClient" +import type { IPlugin } from "../../models/services/databases/plugins/base/interfaces/IPlugin" +import type { IEnrichedPlugin } from "../../models/services/github/interfaces" +import { PluginsService } from "../databases/plugins/PluginsService" +import { GitHubClientService } from "./GitHubClientService" +import { GitHubPluginEnrichmentService } from "./GitHubPluginEnrichmentService" + +/** + * @summary Service for GitHub synchronization. + * @description Synchronizes plugin repositories from GitHub into the database and keeps enriched cache fresh. + */ +class GitHubSyncService { + private constructor() {} + + private static readonly ENRICHED_PLUGINS_CACHE_KEY: string = "github:enrichedPlugins" + private static readonly REFRESH_TIME_IN_MILLISECONDS: number = 5 * 60 * 1000 + private static readonly CACHE_REBUILD_TIME_IN_MILLISECONDS: number = 60 * 60 * 1000 + + private static hasStarted: boolean = false + private static isRefreshing: boolean = false + private static enrichedPlugins: IEnrichedPlugin[] = [] + + public static start(): void { + if (this.hasStarted) return + this.hasStarted = true + + void this.refreshAndRebuildCache() + setInterval(() => { + void this.refreshDiscoveredPlugins() + }, this.REFRESH_TIME_IN_MILLISECONDS) + setInterval(() => { + void this.refreshAndRebuildCache() + }, this.CACHE_REBUILD_TIME_IN_MILLISECONDS) + } + + public static async getAllPlugins(): Promise { + if (this.enrichedPlugins.length > 0) return this.enrichedPlugins + + const cachedPlugins: IEnrichedPlugin[] | null = await RedisClient.getValue(this.ENRICHED_PLUGINS_CACHE_KEY) + if (cachedPlugins !== null) { + this.enrichedPlugins = cachedPlugins + return this.enrichedPlugins + } + + await this.rebuildApprovedPluginsCache() + return this.enrichedPlugins + } + + public static async getPluginsByPage(page: number, pageSize: number): Promise { + const plugins: IEnrichedPlugin[] = await this.getAllPlugins() + const offset: number = (page - 1) * pageSize + return plugins.slice(offset, offset + pageSize) + } + + public static async getPluginsCount(): Promise { + const plugins: IEnrichedPlugin[] = await this.getAllPlugins() + return plugins.length + } + + private static async refreshAndRebuildCache(): Promise { + await this.refreshDiscoveredPlugins() + await this.rebuildApprovedPluginsCache() + } + + private static async refreshDiscoveredPlugins(): Promise { + if (this.isRefreshing) return + this.isRefreshing = true + + try { + const repositories = await GitHubClientService.searchPluginRepositories() + for (const repository of repositories) { + const owner: string = repository.owner.login + const name: string = repository.name + + const existingPlugin: IPlugin | null = await PluginsService.getPluginByOwnerAndName(owner, name) + if (existingPlugin !== null) continue + + const releases = await GitHubClientService.getReleases(owner, name) + if (releases.length === 0) continue + + const createdPlugin: IPlugin | null = await PluginsService.createPlugin({ + approved: false, + name, + owner, + url: repository.html_url, + branch: repository.default_branch, + }) + + if (createdPlugin !== null) { + await DiscordClient.sendPluginApprovalRequest(createdPlugin) + } + } + } catch (error: unknown) { + console.error("GitHubSyncService.refreshDiscoveredPlugins failed:", error) + } finally { + this.isRefreshing = false + } + } + + private static async rebuildApprovedPluginsCache(): Promise { + const approvedPlugins: IPlugin[] = await PluginsService.getApprovedPlugins() + const enrichedPluginsSettled: Array = await Promise.all( + approvedPlugins.map((plugin) => GitHubPluginEnrichmentService.enrich(plugin)), + ) + + this.enrichedPlugins = enrichedPluginsSettled.filter((plugin): plugin is IEnrichedPlugin => plugin !== null) + await RedisClient.setValue( + this.ENRICHED_PLUGINS_CACHE_KEY, + this.enrichedPlugins, + Math.floor(this.CACHE_REBUILD_TIME_IN_MILLISECONDS / 1000), + ) + } +} + +export { GitHubSyncService } diff --git a/src/services/github/index.ts b/src/services/github/index.ts new file mode 100644 index 0000000..bad0b49 --- /dev/null +++ b/src/services/github/index.ts @@ -0,0 +1,3 @@ +export { GitHubClientService } from "./GitHubClientService" +export { GitHubPluginEnrichmentService } from "./GitHubPluginEnrichmentService" +export { GitHubSyncService } from "./GitHubSyncService" diff --git a/src/services/index.ts b/src/services/index.ts deleted file mode 100644 index c0304da..0000000 --- a/src/services/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Discord } from "./discord"; -import { GHFetch } from "./gh-fetch"; - -export * from "./service"; - -const Services = [ - GHFetch, - Discord -]; - -export { Services }; \ No newline at end of file diff --git a/src/services/service.ts b/src/services/service.ts deleted file mode 100644 index 4f2b1d1..0000000 --- a/src/services/service.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { RestAPIService } from "../index"; - -abstract class Service { - /** - * The name of the service - */ - public readonly name: string = this.constructor.name; - - /** - * The api service instance - */ - public readonly api: RestAPIService; - - /** - * Creates an instance of the service. - * @param api The api service instance. - */ - public constructor(api: RestAPIService) { - this.api = api; - } -} - -export { Service }; diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index 7489c0a..0000000 --- a/src/types/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from "./repository"; -export * from "./release-asset"; -export * from "./repository-user"; -export * from "./repository-query"; -export * from "./repository-license"; -export * from "./repository-release"; -export * from "./repository-contributor"; -export * from "./stored-plugin"; -export * from "./plugin"; \ No newline at end of file diff --git a/src/types/plugin.ts b/src/types/plugin.ts deleted file mode 100644 index 57d5e03..0000000 --- a/src/types/plugin.ts +++ /dev/null @@ -1,33 +0,0 @@ -interface PluginRelease { - name: string; - tag: string; - url: string; - description: string; - prerelease: boolean; - date: string; - assets: Array; -} - -interface PluginReleaseAsset { - name: string; - size: number; - download_url: string; - download_count: number; -} - -interface PluginContributor { - username: string; - profile_url: string; - avatar_url: string; - contributions: number; -} - -interface PluginCommit { - sha: string; - html_url: string; - message: string; - date: string; - author: string; -} - -export type { PluginRelease, PluginReleaseAsset, PluginContributor, PluginCommit }; diff --git a/src/types/release-asset.ts b/src/types/release-asset.ts deleted file mode 100644 index d6057aa..0000000 --- a/src/types/release-asset.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { RepositoryUser } from "./repository-user"; - -interface ReleaseAsset { - url: string; - id: number; - node_id: string; - name: string; - label: string | null; - uploader: RepositoryUser; - content_type: string; - state: "uploaded" | "open" | "temporary" | string; - size: number; - download_count: number; - created_at: string; // ISO date - updated_at: string; // ISO date - browser_download_url: string; -} - -export type { ReleaseAsset }; diff --git a/src/types/repository-commit.ts b/src/types/repository-commit.ts deleted file mode 100644 index ee5b177..0000000 --- a/src/types/repository-commit.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { RepositoryUser } from "./repository-user"; - -// GitHub "List commits" item → https://docs.github.com/rest/commits/commits#list-commits -export interface RepositoryCommit { - sha: string; - node_id: string; - commit: GitCommit; // metadata from the commit object itself - url: string; // API URL for this commit - html_url: string; // Web URL for this commit - comments_url: string; - author: RepositoryUser | null; // GitHub account (may be null if email not linked) - committer: RepositoryUser | null; // GitHub account (may be null) - parents: GitParent[]; // parent commits -} - -export interface GitCommit { - author: GitCommitAuthor; // name/email/date from the commit - committer: GitCommitAuthor; // name/email/date from the commit - message: string; - tree: { sha: string; url: string }; - url: string; - comment_count: number; - verification: GitCommitVerification; -} - -export interface GitCommitAuthor { - name: string; - email: string; - date: string; // ISO date -} - -export interface GitCommitVerification { - verified: boolean; - reason: string; - signature: string | null; - payload: string | null; - // Some repos may include extra fields; keep optional: - verified_at?: string | null; - verifier?: RepositoryUser | null; -} - -export interface GitParent { - sha: string; - url: string; - html_url: string; -} diff --git a/src/types/repository-contributor.ts b/src/types/repository-contributor.ts deleted file mode 100644 index b046de7..0000000 --- a/src/types/repository-contributor.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { RepositoryUser } from "./repository-user"; - -interface RepositoryContributor extends RepositoryUser { - contributions: number; -} - -export type { RepositoryContributor }; diff --git a/src/types/repository-license.ts b/src/types/repository-license.ts deleted file mode 100644 index f0d3c07..0000000 --- a/src/types/repository-license.ts +++ /dev/null @@ -1,9 +0,0 @@ -interface RepositoryLicense { - key: string; - name: string; - spdx_id: string | null; - url: string | null; - node_id: string; -} - -export type { RepositoryLicense }; diff --git a/src/types/repository-query.ts b/src/types/repository-query.ts deleted file mode 100644 index 41ff05e..0000000 --- a/src/types/repository-query.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Repository } from "./repository"; - -interface RepositoryQuery { - total_count: number; - incomplete_results: boolean; - items: Repository[]; -} - -export type { RepositoryQuery }; diff --git a/src/types/repository-release.ts b/src/types/repository-release.ts deleted file mode 100644 index 532d898..0000000 --- a/src/types/repository-release.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ReleaseAsset } from "./release-asset"; -import type { RepositoryUser } from "./repository-user"; - -interface RepositoryRelease { - url: string; - assets_url: string; - upload_url: string; // templated: ...{?name,label} - html_url: string; - id: number; - author: RepositoryUser; - node_id: string; - tag_name: string; - target_commitish: string; - name: string | null; - draft: boolean; - immutable: boolean; - prerelease: boolean; - created_at: string; // ISO date - updated_at: string; // ISO date - published_at: string; // ISO date - assets: Array; - tarball_url: string; - zipball_url: string; - body: string | null; -} - -export type { RepositoryRelease }; diff --git a/src/types/repository-user.ts b/src/types/repository-user.ts deleted file mode 100644 index 5b1353a..0000000 --- a/src/types/repository-user.ts +++ /dev/null @@ -1,23 +0,0 @@ -interface RepositoryUser { - login: string; - id: number; - node_id: string; - avatar_url: string; - gravatar_id: string; - url: string; - html_url: string; - followers_url: string; - following_url: string; - gists_url: string; - starred_url: string; - subscriptions_url: string; - organizations_url: string; - repos_url: string; - events_url: string; - received_events_url: string; - type: "User" | "Organization" | string; - user_view_type: "public" | "private" | string; - site_admin: boolean; -} - -export type { RepositoryUser }; diff --git a/src/types/repository.ts b/src/types/repository.ts deleted file mode 100644 index 4899a65..0000000 --- a/src/types/repository.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { RepositoryLicense } from "./repository-license"; -import type { RepositoryUser } from "./repository-user"; - -interface Repository { - id: number; - node_id: string; - name: string; - full_name: string; - private: boolean; - owner: RepositoryUser; - html_url: string; - description: string | null; - fork: boolean; - url: string; - forks_url: string; - keys_url: string; - collaborators_url: string; - teams_url: string; - hooks_url: string; - issue_events_url: string; - events_url: string; - assignees_url: string; - branches_url: string; - tags_url: string; - blobs_url: string; - git_tags_url: string; - git_refs_url: string; - trees_url: string; - statuses_url: string; - languages_url: string; - stargazers_url: string; - contributors_url: string; - subscribers_url: string; - subscription_url: string; - commits_url: string; - git_commits_url: string; - comments_url: string; - issue_comment_url: string; - contents_url: string; - compare_url: string; - merges_url: string; - archive_url: string; - downloads_url: string; - issues_url: string; - pulls_url: string; - milestones_url: string; - notifications_url: string; - labels_url: string; - releases_url: string; - deployments_url: string; - created_at: string; // ISO date - updated_at: string; // ISO date - pushed_at: string; // ISO date - git_url: string; - ssh_url: string; - clone_url: string; - svn_url: string; - homepage: string | null; - size: number; - stargazers_count: number; - watchers_count: number; - language: string | null; - has_issues: boolean; - has_projects: boolean; - has_downloads: boolean; - has_wiki: boolean; - has_pages: boolean; - has_discussions: boolean; - forks_count: number; - mirror_url: string | null; - archived: boolean; - disabled: boolean; - open_issues_count: number; - license: RepositoryLicense | null; - allow_forking: boolean; - is_template: boolean; - web_commit_signoff_required: boolean; - topics: string[]; - visibility: "public" | "private" | string; - forks: number; - open_issues: number; - watchers: number; - default_branch: string; - score?: number; -} - -export type { Repository }; diff --git a/src/types/stored-plugin.ts b/src/types/stored-plugin.ts deleted file mode 100644 index f985419..0000000 --- a/src/types/stored-plugin.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { PluginContributor } from "./plugin"; - -interface StoredPlugin { - id: number; - name: string; - owner: PluginContributor; - url: string; - branch: string; - approved: boolean; -} - -export type { StoredPlugin }; diff --git a/tests/ExampleTest.test.ts b/tests/ExampleTest.test.ts new file mode 100644 index 0000000..3a00cbb --- /dev/null +++ b/tests/ExampleTest.test.ts @@ -0,0 +1,162 @@ +import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test" +import type { Request, Response } from "express" +import { PluginsController } from "../src/controllers/plugins/PluginsController" +import { EnvironmentVariables } from "../src/globals/EnvironmentVariables" +import { GitHubSyncService } from "../src/services/github" + +describe("EnvironmentVariables.APP_PORT", () => { + const OriginalEnvironment = { ...process.env } + + beforeEach(() => { + process.env = { ...OriginalEnvironment } + }) + + afterEach(() => { + process.env = { ...OriginalEnvironment } + }) + + test("returns a parsed port number when APP_PORT is valid", () => { + process.env.APP_PORT = "3000" + expect(EnvironmentVariables.APP_PORT).toBe(3000) + }) + + test("throws when APP_PORT is missing", () => { + delete process.env.APP_PORT + expect(() => EnvironmentVariables.APP_PORT).toThrow("APP_PORT is not set.") + }) +}) + +describe("EnvironmentVariables.CACHE_ENGINE_ENABLED", () => { + const OriginalEnvironment = { ...process.env } + + beforeEach(() => { + process.env = { ...OriginalEnvironment } + }) + + afterEach(() => { + process.env = { ...OriginalEnvironment } + }) + + test("returns true for a true value", () => { + process.env.CACHE_ENGINE_ENABLED = "true" + expect(EnvironmentVariables.CACHE_ENGINE_ENABLED).toBe(true) + }) + + test("throws for invalid boolean values", () => { + process.env.CACHE_ENGINE_ENABLED = "enabled" + expect(() => EnvironmentVariables.CACHE_ENGINE_ENABLED).toThrow("CACHE_ENGINE_ENABLED is not a boolean.") + }) +}) + +describe("EnvironmentVariables.GITHUB_TOKEN", () => { + const OriginalEnvironment = { ...process.env } + + beforeEach(() => { + process.env = { ...OriginalEnvironment } + }) + + afterEach(() => { + process.env = { ...OriginalEnvironment } + }) + + test("returns undefined for whitespace-only token", () => { + process.env.GITHUB_TOKEN = " " + expect(EnvironmentVariables.GITHUB_TOKEN).toBeUndefined() + }) + + test("returns trimmed token value", () => { + process.env.GITHUB_TOKEN = " token-value " + expect(EnvironmentVariables.GITHUB_TOKEN).toBe("token-value") + }) +}) + +describe("PluginsController.getPluginsPagination", () => { + test("returns 400 when page query is invalid", async () => { + const RequestObject = { query: { page: "0" } } as unknown as Request + const StatusSpy = mock(() => ResponseObject) + const SendSpy = mock(() => ResponseObject) + const ResponseObject = { status: StatusSpy, send: SendSpy } as unknown as Response + const CountSpy = spyOn(GitHubSyncService, "getPluginsCount") + const PageSpy = spyOn(GitHubSyncService, "getPluginsByPage") + + await PluginsController.getPluginsPagination(RequestObject, ResponseObject) + + expect(StatusSpy).toHaveBeenCalledWith(400) + expect(SendSpy).toHaveBeenCalledWith({ + message: "Query parameter `page` must be a positive integer.", + }) + expect(CountSpy).not.toHaveBeenCalled() + expect(PageSpy).not.toHaveBeenCalled() + CountSpy.mockRestore() + PageSpy.mockRestore() + }) + + test("returns count and paged items for a valid page", async () => { + const RequestObject = { query: { page: "2" } } as unknown as Request + const StatusSpy = mock(() => ResponseObject) + const SendSpy = mock(() => ResponseObject) + const ResponseObject = { status: StatusSpy, send: SendSpy } as unknown as Response + const CountSpy = spyOn(GitHubSyncService, "getPluginsCount").mockResolvedValue(51) + const Items = [{ owner: "serenityjs", name: "sample-plugin" }] + const PageSpy = spyOn(GitHubSyncService, "getPluginsByPage").mockResolvedValue(Items as never) + + await PluginsController.getPluginsPagination(RequestObject, ResponseObject) + + expect(CountSpy).toHaveBeenCalledTimes(1) + expect(PageSpy).toHaveBeenCalledWith(2, 25) + expect(StatusSpy).toHaveBeenCalledWith(200) + expect(SendSpy).toHaveBeenCalledWith({ + count: 51, + items: Items, + }) + CountSpy.mockRestore() + PageSpy.mockRestore() + }) + + test("returns 500 when services fail for paginated plugins", async () => { + const RequestObject = { query: { page: "1" } } as unknown as Request + const StatusSpy = mock(() => ResponseObject) + const SendSpy = mock(() => ResponseObject) + const ResponseObject = { status: StatusSpy, send: SendSpy } as unknown as Response + const CountSpy = spyOn(GitHubSyncService, "getPluginsCount").mockRejectedValue(new Error("database down")) + const PageSpy = spyOn(GitHubSyncService, "getPluginsByPage") + + await PluginsController.getPluginsPagination(RequestObject, ResponseObject) + + expect(StatusSpy).toHaveBeenCalledWith(500) + expect(SendSpy).toHaveBeenCalledWith({ message: "Failed to fetch paginated plugins." }) + CountSpy.mockRestore() + PageSpy.mockRestore() + }) +}) + +describe("PluginsController.getAllPlugins", () => { + test("returns all plugins on success", async () => { + const RequestObject = {} as unknown as Request + const StatusSpy = mock(() => ResponseObject) + const SendSpy = mock(() => ResponseObject) + const ResponseObject = { status: StatusSpy, send: SendSpy } as unknown as Response + const Items = [{ owner: "serenityjs", name: "plugin-a" }] + const AllSpy = spyOn(GitHubSyncService, "getAllPlugins").mockResolvedValue(Items as never) + + await PluginsController.getAllPlugins(RequestObject, ResponseObject) + + expect(StatusSpy).toHaveBeenCalledWith(200) + expect(SendSpy).toHaveBeenCalledWith(Items) + AllSpy.mockRestore() + }) + + test("returns 500 when listing all plugins fails", async () => { + const RequestObject = {} as unknown as Request + const StatusSpy = mock(() => ResponseObject) + const SendSpy = mock(() => ResponseObject) + const ResponseObject = { status: StatusSpy, send: SendSpy } as unknown as Response + const AllSpy = spyOn(GitHubSyncService, "getAllPlugins").mockRejectedValue(new Error("network error")) + + await PluginsController.getAllPlugins(RequestObject, ResponseObject) + + expect(StatusSpy).toHaveBeenCalledWith(500) + expect(SendSpy).toHaveBeenCalledWith({ message: "Failed to fetch all plugins." }) + AllSpy.mockRestore() + }) +}) diff --git a/tsconfig.json b/tsconfig.json index bfa0fea..93e6284 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,29 +1,28 @@ { - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, + "compileOnSave": true, + "compilerOptions": { + // Environment and module settings. + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "moduleResolution": "Bundler", - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } + // Language and structural settings. + "allowJs": false, + "checkJs": false, + "strict": true, + // This applies to distribution builds. + "removeComments": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noImplicitAny": true, + "rootDir": "./src", + // This only applies to building the project. + "outDir": "./dist" + }, + "include": ["src"], + "exclude": ["node_modules/", "dist/", "build/"] }