diff --git a/.commitlintrc.mjs b/.commitlintrc.mjs new file mode 100644 index 0000000..803dc4f --- /dev/null +++ b/.commitlintrc.mjs @@ -0,0 +1,3 @@ +export default { + extends: ['@commitlint/config-conventional'], +}; diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6918d36 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,42 @@ +# Dependencies +node_modules +npm-debug.log +yarn-error.log +pnpm-debug.log +infra/k6/node_modules + +# Build output +dist +artifacts +out + +# Environment variables +.env +.env.production +.env.local +!.env.example + +# Docker / Infrastructure +docker-compose.yml +docker-compose.*.yml +Dockerfile +Dockerfile.* +.dockerignore + +# Git +.git +.gitignore +.gitattributes + +# Editor / OS +.vscode +.idea +.DS_Store +*.swp +*.log + +# Tests and Coverage +coverage +test-results +*.spec.ts +*.e2e-spec.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..44b4e59 --- /dev/null +++ b/.env.example @@ -0,0 +1,52 @@ +# --- APP --- +PORT=3000 +NODE_ENV=development +COOKIE_SECRET=same-serious-secret +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 +THROTTLE_LIMIT=100 +THROTTLE_TTL=60000 + +# --- POSTGRES --- +DB_SCHEMA=base + +# ВАЖНО: +# Для работы ВНУТРИ Docker: используй хост 'database' и порт 5432 +# DATABASE_URL=postgres://${DB_USERNAME}:${DB_PASSWORD}@database:5432/${DB_DATABASE} + +# Для работы ЛОКАЛЬНО (без докера): используй 'localhost' и порт 6000 +DATABASE_URL=postgres://${DB_USERNAME}:${DB_PASSWORD}@localhost:${DB_PORT}/${DB_DATABASE} + +# --- REDIS --- +# in the docker network will be, not show port redis, at prod env +# REDIS_HOST=redis +# at development mode +REDIS_HOST=127.0.0.1 +REDIS_PORT=7000 +REDIS_PASSWORD=same-password + +JWT_AUDIENCE="task-tracker-client" + +JWT_ACCESS_SECRET=same-same-same-same-same +JWT_ACCESS_EXPIRES_IN=15m + +JWT_REFRESH_SECRET=same-same-same-same-same +JWT_REFRESH_EXPIRES_IN=15m + +# --- MAIL SETTINGS --- +MAIL_HOST=smtp.gmail.com +MAIL_PORT=465 +MAIL_USER=example@gmail.com + +# 16x password +MAIL_PASSWORD=xxxxxxxxyyyyyyyy +MAIL_FROM_NAME="Task Tracker" +MAIL_FROM_EMAIL=example@gmail.com + +S3_BUCKET_NAME='' +S3_ENDPOINT='' +S3_REGION='' +S3_ACCESS_KEY='' +S3_SECRET_KEY='' + +IMAGOR_SECRET='' +IMAGOR_URL='' diff --git a/.eslintrc.js b/.eslintrc.js index 259de13..14746fb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,25 +1,46 @@ module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - project: 'tsconfig.json', - tsconfigRootDir: __dirname, - sourceType: 'module', - }, - plugins: ['@typescript-eslint/eslint-plugin'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], - root: true, - env: { - node: true, - jest: true, - }, - ignorePatterns: ['.eslintrc.js'], - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - }, + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: ['plugin:@typescript-eslint/recommended'], + root: true, + env: { + node: true, + }, + globals: { + describe: 'readonly', + it: 'readonly', + expect: 'readonly', + beforeEach: 'readonly', + afterEach: 'readonly', + vi: 'readonly', + }, + ignorePatterns: [ + '.eslintrc.js', + '*.config.{js,ts}', + 'migrations', + 'infra', + '.github', + 'dist', + 'node_modules', + ], + rules: { + 'prettier/prettier': 'off', + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + }, }; diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..26f3a9a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,41 @@ +name: 'Bug Report' +description: 'Сообщить об ошибке в работе приложения' +labels: ['bug', 'triage'] +body: + - type: markdown + attributes: + value: | + Спасибо, что решили помочь сделать проект лучше! + - type: input + id: version + attributes: + label: 'Версия приложения' + description: 'Какую версию вы используете? (например, 0.0.1)' + placeholder: '0.0.x' + validations: + required: true + - type: textarea + id: steps + attributes: + label: 'Шаги воспроизведения' + description: 'Как нам увидеть эту ошибку?' + placeholder: | + 1. Запустить docker-compose + 2. Отправить POST запрос на /api/v1/auth... + validations: + required: true + - type: dropdown + id: environment + attributes: + label: 'Окружение' + options: + - Docker + - Local (pnpm) + - Production + validations: + required: true + - type: textarea + id: expected + attributes: + label: 'Ожидаемое поведение' + placeholder: 'Что должно было произойти?' diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..c6c9292 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,10 @@ +blank_issues_enabled: false + +contact_links: + - name: '❓ Вопросы по использованию' + url: 'https://github.com/Task-Tracker-Lab/task-tracker-backend/discussions/new?category=q-a' + about: 'Если вы не уверены, баг это или нет, или вам нужна помощь в настройке — спросите здесь.' + + - name: '💡 Идеи и предложения' + url: 'https://github.com/Task-Tracker-Lab/task-tracker-backend/discussions/new?category=ideas' + about: 'Хотите обсудить новую крутую фичу перед тем, как заводить задачу? Вам сюда.' diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..f356707 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,18 @@ +name: 'Feature Request' +description: 'Предложить новую идею или улучшение' +labels: ['enhancement'] +body: + - type: textarea + id: problem + attributes: + label: 'Какую проблему мы решаем?' + description: 'Опишите, почему текущего функционала недостаточно.' + validations: + required: true + - type: textarea + id: solution + attributes: + label: 'Ваше предложение' + description: 'Как именно вы видите реализацию этой фичи?' + validations: + required: true diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..35ee000 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,56 @@ +core: + - changed-files: + - any-glob-to-any-file: 'src/**/*' + - all-globs-to-all-files: + - '!src/shared/entities/**/*' + - '!src/**/*.spec.{ts,js}' + +database: + - changed-files: + - any-glob-to-any-file: + - 'src/shared/entities/**/*' + - 'libs/database/**/*' + - 'migrations/**/*' + - 'drizzle.config.ts' + +dependencies: + - changed-files: + - any-glob-to-any-file: + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + +devops: + - changed-files: + - any-glob-to-any-file: + - 'infra/**/*' + - '.github/workflows/**/*' + - 'Dockerfile*' + - '.dockerignore' + +testing: + - changed-files: + - any-glob-to-any-file: + - 'test/**/*' + - 'src/**/*.spec.{ts,js}' + - 'k6/**/*' + - 'vitest.config*' + +libs: + - changed-files: + - any-glob-to-any-file: 'libs/**/*' + - all-globs-to-all-files: + - '!libs/database/**/*' + +dx: + - changed-files: + - any-glob-to-any-file: + - 'pnpm-workspace.yaml' + - '.*' + - '!package.json' + +documentation: + - changed-files: + - any-glob-to-any-file: + - '**/*.md' + - 'LICENSE' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..fdc2afc --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,73 @@ +name: Build and Push + +on: + push: + branches: [main, dev, 'feat/**', 'fix/**', 'refactor/**', 'chore/**'] + pull_request: + branches: [main, dev] + workflow_dispatch: + inputs: + force_push: + description: 'Force push image to registry?' + type: boolean + default: false + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-push: + runs-on: ubuntu-latest + env: + IS_BASE_BRANCH: ${{ github.ref_name == 'main' || github.ref_name == 'dev' }} + IS_PUSH: ${{ github.event_name == 'push' }} + FORCE_PUSH: ${{ github.event.inputs.force_push == 'true' }} + + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + if: ${{ (env.IS_PUSH == 'true' && env.IS_BASE_BRANCH == 'true') || + env.FORCE_PUSH == 'true' }} + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=sha,format=short + # latest вешаем только когда мерджим в main + type=raw,value=latest,enable=${{ github.ref_name == 'main' }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile.prod + push: ${{ (env.IS_PUSH == 'true' && env.IS_BASE_BRANCH == 'true') || + env.FORCE_PUSH == 'true' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..763a6e5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,65 @@ +name: CI + +on: + push: + branches: + - main + - dev + - 'feat/**' + - 'fix/**' + - 'refactor/**' + - 'chore/**' + - 'perf/**' + - 'build/**' + - 'ci/**' + paths-ignore: + - '**.md' + - 'infra/**' + - '.gitignore' + - 'docker-compose.yml' + + pull_request: + branches: + - main + - dev + paths-ignore: + - '**.md' + - 'infra/**' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + quality-check: + name: Lint & Test + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile --prefer-offline + + - name: Run Lint + run: pnpm run lint + + - name: Type Check + run: pnpm exec tsc --noEmit + + - name: Run Tests + run: pnpm run test diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml new file mode 100644 index 0000000..407b8a9 --- /dev/null +++ b/.github/workflows/cleanup.yml @@ -0,0 +1,45 @@ +name: 'Cleanup' + +on: + schedule: + - cron: '0 0 * * 0' + workflow_dispatch: + +permissions: + actions: write + contents: read + +jobs: + garbage-collector: + name: 'Purge Storage' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: 'Clean Actions Cache' + shell: bash + run: | + echo "::group::Deleting Caches" + gh cache delete --all --succeed-on-no-caches || echo "Caches already empty or cleared" + echo "::endgroup::" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: 'Clean Old Artifacts' + shell: bash + run: | + echo "::group::Deleting Artifacts" + artifacts=$(gh api repos/${{ github.repository }}/actions/artifacts --paginate -q '.artifacts[].id' || echo "") + + if [ -n "$artifacts" ]; then + for id in $artifacts; do + gh api -X DELETE repos/${{ github.repository }}/actions/artifacts/$id || true + done + echo "Artifacts cleared." + else + echo "No artifacts found." + fi + echo "::endgroup::" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..cc77c3f --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,46 @@ +name: 'CodeQL' + +on: + push: + branches: [main, dev] + paths-ignore: + - '**.md' + - 'infra/**' + - 'migrations/**' + pull_request: + branches: [main, dev] + paths-ignore: + - '**.md' + schedule: + - cron: '15 13 * * 5' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + timeout-minutes: 360 + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: javascript-typescript + queries: security-extended + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: '/language:javascript-typescript' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..7b94cf6 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,38 @@ +name: 'Pull Request Labeler' + +on: + # Важно: target позволяет работать в PR из форков + pull_request_target: + types: [opened, synchronize] + +jobs: + label: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Ensure Labels Exist + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + labels=( + "database:5319e7:Database schema and migrations" + "core:e99695:Main application logic" + "testing:ff69b4:Unit and E2E tests" + "devops:006b75:Infrastructure and CI/CD" + "shared-libs:bfdadc:Shared libraries in libs/" + "dx:eeeeee:Developer experience and configs" + "documentation:0075ca:Documentation and markdown files" + ) + + for label in "${labels[@]}"; do + IFS=":" read -r name color desc <<< "$label" + gh label create "$name" --color "$color" --description "$desc" --repo ${{ github.repository }} || true + done + + - name: Run Labeler + uses: actions/labeler@v5 + with: + configuration-path: .github/labeler.yml + sync-labels: true diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml new file mode 100644 index 0000000..4847ac5 --- /dev/null +++ b/.github/workflows/migrations.yml @@ -0,0 +1,85 @@ +name: 'Database Consistency Check' + +on: + pull_request: + branches: [main, dev] + paths: + - 'src/shared/entities/**' + - 'migrations/**' + - 'drizzle.config.ts' + - 'package.json' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + DB_USER: tracker + DB_PASSWORD: super-tracker-password + DB_NAME: test + DB_URL: postgres://tracker:super-tracker-password@localhost:5432/test + +jobs: + check: + name: 'Check & Test' + runs-on: ubuntu-latest + timeout-minutes: 10 + + services: + postgres: + image: postgres:15-alpine + env: + POSTGRES_USER: ${{ env.DB_USER }} + POSTGRES_PASSWORD: ${{ env.DB_PASSWORD }} + POSTGRES_DB: ${{ env.DB_NAME }} + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: 'pnpm' + + - name: Install + run: | + echo "::group::pnpm install" + pnpm install --frozen-lockfile + echo "::endgroup::" + + - name: Drift Check + env: + DATABASE_URL: ${{ env.DB_URL }} + run: | + echo "::group::Drizzle Check" + pnpm drizzle-kit check + echo "::endgroup::" + + - name: Seed + run: | + echo "::group::Seed Data" + # pnpm ts-node ./scripts/seed-ci.ts + echo "Done" + echo "::endgroup::" + + - name: Migrate + env: + DATABASE_URL: ${{ env.DB_URL }} + run: | + echo "::group::Apply Schema" + pnpm drizzle-kit push --force + echo "::endgroup::" diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..c7cfee1 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,72 @@ +name: release-please + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + release-please: + runs-on: ubuntu-latest + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + - uses: googleapis/release-please-action@v4 + id: release + with: + release-type: node + + publish-release: + needs: release-please + if: ${{ needs.release-please.outputs.release_created }} + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + # Версия из тега (например, 1.2.3) + type=semver,pattern={{version}},value=${{ needs.release-please.outputs.tag_name }} + # Мажорная версия (например, 1) + type=semver,pattern={{major}},value=${{ needs.release-please.outputs.tag_name }} + # Всегда обновляем latest для продакшена + type=raw,value=latest + + - name: Build and push Production Image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile.prod + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..fac0b76 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,37 @@ +name: 'Close stale issues and PRs' + +on: + schedule: + - cron: '30 1 * * *' + workflow_dispatch: + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: >- + Эта задача давно не обновлялась. Она будет закрыта через 5 + дней, если не появится новой активности. 🤖 + stale-pr-message: >- + Этот PR замер. Мы закроем его через 5 дней, чтобы не + копить очередь, но вы всегда можете переоткрыть его + позже. 🤖 + + days-before-stale: 30 + days-before-close: 5 + + exempt-issue-labels: 'bug,priority:high,pinned,security' + exempt-pr-labels: 'dependencies,security' + exempt-all-milestones: true + + stale-issue-label: 'stale' + stale-pr-label: 'stale' + + operations-per-run: 50 + ascending: true diff --git a/.gitignore b/.gitignore index 5866f4e..b0b2412 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,6 @@ pids *.pid *.seed *.pid.lock -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json \ No newline at end of file +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +infra/k6/data/*.json \ No newline at end of file diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..0a4b97d --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +npx --no -- commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..e02c24e --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm lint-staged \ No newline at end of file diff --git a/.lintstagedrc.mjs b/.lintstagedrc.mjs new file mode 100644 index 0000000..0ce6883 --- /dev/null +++ b/.lintstagedrc.mjs @@ -0,0 +1,4 @@ +export default { + '*.{ts,js}': ['eslint --fix', 'prettier --write'], + '*.{json,css,md,yaml,yml}': ['prettier --write'], +}; diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..c8d0ce7 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +dist +node_modules +pnpm-lock.yaml + +migrations + +*.sql +Dockerfile* +.dockerignore \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index dcb7279..a6643be 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,7 @@ { - "singleQuote": true, - "trailingComma": "all" -} \ No newline at end of file + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 4, + "semi": true +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6aa8cca --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# Contributing Guidelines 🤝 + +Добро пожаловать в проект! Чтобы поддерживать высокое качество кода и чистоту истории коммитов, мы следуем строгим стандартам разработки. + +## 1. Workflow (Работа с ветками) + +Мы используем **Trunk-based** подход. Это означает, что `main` (trunk) — единственный источник истины. + +- **Запрет на прямые пуши:** Пуш напрямую в `main` (или `master/dev`) строго запрещен. Любые изменения вносятся **только через Pull Request (PR)**. +- **Code Review:** Каждый PR должен пройти обязательное код-ревью перед мерджем. +- **Short-lived branches:** Ветки должны быть короткими (1-2 дня). + +**Правила именования веток:** + +- `feat/` — для новых функциональных возможностей. +- `fix/` — для исправления багов. +- `refactor/` — для переписывания кода без изменения логики. +- `chore/` — для технических задач, настройки окружения и зависимостей +- `docs/` — для обновления документации. + +## 2. Commit Message Convention + +Мы используем стандарт **Conventional Commits**. Это позволяет автоматически генерировать логи изменений и поддерживать историю в чистоте. + +**Формат:** `(): ` + +- **Пример:** `feat(database): add user entity and drizzle migrations` +- **Пример:** `fix(auth): resolve jwt expiration issue` + +## 3. Разработка и стандарты кода + +- **Validation (Zod):** Использование схем **Zod** для эндпоинтов (DTO) обязательно. Без них не будет работать валидация данных и автоматическая типизация в Swagger. +- **Linting & Formatting:** Перед каждым коммитом обязательно запускайте `pnpm lint` и `pnpm format`. Код, не прошедший проверку линтером, не будет принят. +- **Drizzle Migrations:** **Никаких ручных изменений в базе данных.** Все изменения схем должны сопровождаться миграциями. + - Команда: `pnpm db:generate` +- **No God-commits:** Разделяйте свои изменения на небольшие логические коммиты. + +## 4. Pull Request (PR) Process + +Прежде чем отправить PR, убедитесь, что: + +1. **Self-Review:** Вы сами перечитали свой код и удалили отладочные логи (`console.log`). +2. **Checks:** Все автоматические тесты (`pnpm test`) и линтер проходят успешно. +3. **Description:** В описании PR четко указано, что именно было сделано и зачем. + +_PR не принимается, если тесты или линтер упали на этапе CI._ + +## 5. Local Setup + +Для быстрой настройки локального окружения обратитесь к разделу **Quick Start** в [README.md](./README.md). + +Краткий список команд для старта: + +```bash +pnpm install +cp .env.example .env +pnpm db:generate +pnpm db:migrate +pnpm start:dev +``` diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..55b2adf --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,14 @@ +FROM node:23-alpine + +RUN corepack enable && corepack prepare pnpm@latest --activate + +WORKDIR /app + +COPY package.json pnpm-lock.yaml ./ +COPY tsconfig* ./ + +RUN pnpm install --no-frozen-lockfile + +COPY . . + +CMD ["pnpm", "run", "start:dev"] \ No newline at end of file diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..e6a6c37 --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,54 @@ +FROM node:22-alpine AS base + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + +RUN corepack enable + +WORKDIR /app + +FROM base AS fetch + +COPY pnpm-lock.yaml package.json pnpm-workspace.yaml ./ + +# Загружаем всё в виртуальное хранилище. +# Если lock-файл не менялся, этот слой будет взят из кэша +ENV CI=true +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm fetch + +FROM fetch AS build +COPY package.json ./ + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm install -w --frozen-lockfile --offline + +COPY . . + +RUN pnpm run build + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm prune --prod --ignore-scripts + +FROM node:22-alpine AS runner + +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=${PORT:-1010} + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nestjs + +COPY --from=build --chown=nestjs:nodejs /app/dist ./dist +COPY --from=build --chown=nestjs:nodejs /app/node_modules ./node_modules +COPY --from=build --chown=nestjs:nodejs /app/templates ./templates +COPY --from=build --chown=nestjs:nodejs /app/package.json ./ +COPY --from=build --chown=nestjs:nodejs /app/migrations ./migrations +COPY --from=build --chown=nestjs:nodejs /app/drizzle.config.ts ./drizzle.config.ts + +USER nestjs + +EXPOSE $PORT + +CMD ["node", "dist/main"] diff --git a/README.md b/README.md index 6d83c08..cd789c0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,55 @@ -# task-tracker-backend -Task-tracker-backend +# Task Tracker Backend 🚀 + +Современная лёгкая open-source система управления IT-проектами (альтернатива Jira/Yandex Tracker). Бэкенд построен на высокопроизводительном стеке с упором на типизацию и скорость разработки. + +**Статус:** `In Development` + +## Технологический стек + +- **Runtime:** Node.js 22+ (pnpm) +- **Framework:** NestJS 11 (**Fastify**) +- **Database:** PostgreSQL + **Drizzle ORM** +- **Validation:** Zod +- **API:** Swagger (OpenAPI) +- **Infrastructure:** Docker (Multi-stage builds) +- **Testing:** Vitest + +## Quick Start + +### 1. Окружение + +Скопируйте пример файла окружения и настройте переменные (БД, API ключи DeepSeek): + +```bash +cp .env.example .env +``` + +### 2. Запуск через Docker (Рекомендуется) + +Проект полностью контейнеризирован: + +```bash +docker-compose up --build +``` + +### 3. Локальный запуск + +Если вы хотите запустить проект без Docker: + +```bash +pnpm install +pnpm db:generate +pnpm db:migrate +pnpm start:dev +``` + +## API Documentation + +После запуска проекта документация доступна по адресу: + +**http://localhost:3000/api/v1/docs** + +## Infrastructure + +- CI/CD: Настроены GitHub Actions для автоматической проверки типов, линтинга и запуска тестов. +- Docker: Используются оптимизированные multi-stage образы для минимизации размера production-билда. diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..a4b3b6c --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/shared/entities/index.ts', + out: './migrations', + dialect: 'postgresql', + breakpoints: false, + casing: 'snake_case', + migrations: { + prefix: 'index', + table: 'migrations', + schema: 'drizzle', + }, + strict: true, + verbose: true, + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 0000000..faec638 --- /dev/null +++ b/infra/README.md @@ -0,0 +1,32 @@ +# Инфраструктура проекта + +Данный каталог содержит конфигурации для локальной разработки и инструменты для нагрузочного тестирования. + +## Модули инфраструктуры + +### dev + +Конфигурации Docker Compose для поднятия окружения разработки (базы данных, очереди, кеш). + +Команда для запуска из корня проекта: + +```sh +docker compose -f ./infra/dev/compose.dev.yaml --env-file .env --profile infra up --build -d -V +``` + +### k6 + +Сценарии нагрузочного и стресс-тестирования модулей API. Инструкции по установке и запуску находятся в infra/k6/README.md. + +Команды запуска из **корня** **(../cwd)** проекта: + +```sh + pnpm run k6:all + pnpm run k6:auth + pnpm run k6:team + pnpm run k6:projects + pnpm run k6:user + pnpm run k6:board + pnpm run k6:tasks + pnpm run k6:smoke +``` diff --git a/infra/dev/README.md b/infra/dev/README.md new file mode 100644 index 0000000..ac46b3a --- /dev/null +++ b/infra/dev/README.md @@ -0,0 +1,41 @@ +# Файл для фронт разрабов + +## Описание + +Данный конфиг разворачивает полный инстанс бэкенда (API + DB + Redis) +для локальной разработки фронтенда. + +## ТРЕБОВАНИЯ: + +1. Положить актуальный файл .env в директорию с этим файлом + (путь: ./infra/dev/.env). +2. Наличие Docker Desktop / Docker Engine. + +## ЗАПУСК: + +Выполните команду из корня проекта: + +```sh +docker compose -f ./infra/dev/compose.dev.yaml --profile infra up --pull always --build -d -V +``` + +## ЧТО ВНУТРИ: + +- API: http://localhost:3000 +- Postgres: localhost:6000 (пароли и база берутся из .env) +- Redis: localhost:7000 + +## ОСОБЕННОСТИ: + +- Авто-миграции: Приложение само накатит SQL-схему при старте. +- Healthchecks: Контейнер API не поднимется, пока DB и Redis + не станут доступны (status: healthy). +- Изоляция: Используется выделенная сеть 'task-tracker-gateway'. + +## RESET: + +Если нужно полностью очистить базу и начать с нуля: + +```sh +docker compose -f ./infra/dev/compose.dev.yaml --profile infra down -v +``` diff --git a/infra/dev/compose.dev.yaml b/infra/dev/compose.dev.yaml new file mode 100644 index 0000000..c4413e3 --- /dev/null +++ b/infra/dev/compose.dev.yaml @@ -0,0 +1,152 @@ +version: '3.9' + +name: task-tracker-api + +services: + api: + hostname: api + container_name: api + image: ghcr.io/task-tracker-lab/task-tracker-backend:dev + env_file: + - .env + ports: + - '3000:3000' + depends_on: + database: + condition: service_healthy + redis: + condition: service_healthy + networks: + - backend + deploy: + resources: + limits: + cpus: '2.0' + memory: 1024M + reservations: + cpus: '0.5' + memory: 256M + + database: + hostname: database + container_name: database + image: postgres:16-alpine + restart: always + env_file: + - .env + environment: + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_DATABASE} + ports: + - '6000:5432' + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - backend + healthcheck: + test: [ + 'CMD-SHELL', + 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" -q + || exit 1', + ] + interval: 5s + timeout: 5s + retries: 5 + profiles: ['infra'] + + redis: + hostname: redis + container_name: redis + image: redis:7-alpine + restart: always + ports: + - '${REDIS_PORT:-6999}:6379' + command: redis-server --save 60 1 --loglevel notice + volumes: + - redis_data:/data + networks: + - backend + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 5s + timeout: 3s + retries: 5 + profiles: ['infra'] + + minio: + hostname: minio + container_name: minio + image: minio/minio:latest + restart: always + environment: + MINIO_ROOT_USER: ${S3_ACCESS_KEY} + MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY} + ports: + - '9000:9000' # API + - '9001:9001' # Console (UI) + command: server /data --console-address ":9001" + volumes: + - minio_data:/data + networks: + - backend + profiles: ['infra'] + + minio-init: + image: minio/mc:latest + depends_on: + - minio + environment: + MINIO_ROOT_USER: ${S3_ACCESS_KEY} + MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY} + networks: + - backend + profiles: ['infra'] + entrypoint: > + /bin/sh -c " sleep 5; mc alias set myminio http://minio:9000 + ${S3_ACCESS_KEY} ${S3_SECRET_KEY}; mc mb myminio/${S3_BUCKET_NAME} + --ignore-existing; mc anonymous set download + myminio/${S3_BUCKET_NAME}; exit 0; " + imagor: + image: shumc/imagor:latest + container_name: imagor + restart: always + environment: + - IMAGOR_SECRET=${IMAGOR_SECRET:-supersecret} + - HTTP_LOADER_DISABLE=1 + + - AWS_ACCESS_KEY_ID=${S3_ACCESS_KEY} + - AWS_SECRET_ACCESS_KEY=${S3_SECRET_KEY} + - AWS_REGION=${S3_REGION} + + - S3_ENDPOINT=${S3_ENDPOINT} + - S3_FORCE_PATH_STYLE=1 + + - S3_LOADER_BUCKET=${S3_BUCKET_NAME} + - S3_LOADER_BASE_DIR=sources + + - S3_STORAGE_BUCKET=${S3_BUCKET_NAME} + - S3_STORAGE_BASE_DIR= + - S3_STORAGE_ACL=private + + - S3_RESULT_STORAGE_BUCKET=${S3_BUCKET_NAME} + - S3_RESULT_STORAGE_ACL=private + + - VIPS_CONCURRENCY=1 + - DEBUG=1 + ports: + - '8000:8000' + depends_on: + - minio + networks: + - backend + profiles: ['infra'] + +volumes: + postgres_data: + redis_data: + minio_data: + +networks: + backend: + name: task-tracker-gateway diff --git a/infra/k6/README.md b/infra/k6/README.md new file mode 100644 index 0000000..c7ad017 --- /dev/null +++ b/infra/k6/README.md @@ -0,0 +1,78 @@ +# Нагрузочное тестирование k6 + +В данном каталоге расположены сценарии для проведения нагрузочного и стресс-тестирования API task-backend. Инструментарий базируется на k6 и интегрирован в монорепозиторий как отдельный пакет воркспейса. + +## Предварительные требования + +Для выполнения сценариев необходимо наличие установленного бинарного файла k6 в операционной системе. Пакет не устанавливается через менеджеры пакетов Node.js автоматически. + +### Инструкции по установке + +- Windows: winget install k6 +- macOS: brew install k6 +- Linux: sudo apt-get install k6 + +После установки необходимо перезапустить терминал для обновления переменных окружения. + +## Структура каталогов + +- modules/ — Атомарные функции для взаимодействия с конкретными модулями системы (auth, team, projects, user, board, board-columns, tasks). Содержат логику запросов и проверки статусов. +- scenarios/ — Комплексные пользовательские сценарии, имитирующие реальное поведение (например, создание полной структуры от проекта до задачи). +- scripts/ — Вспомогательные скрипты и быстрые тесты (Smoke tests). +- common/ — Общая конфигурация, параметры нагрузки (stages), пороговые значения (thresholds) и функции генерации тестовых данных. + +## Команды запуска + +Все команды должны запускаться из корневой директории проекта. + +- pnpm k6:seed — Сидинг тестовых данных для k6 (PostgreSQL + Redis). +- pnpm k6:clear — Удаление k6-тестовых данных из PostgreSQL и Redis. +- pnpm k6:smoke — Запуск проверочного теста с минимальной нагрузкой. +- pnpm k6:all — Проведение полного стресс-теста всех модулей API. +- pnpm k6:auth — Тестирование производительности модуля авторизации. +- pnpm k6:team — Тестирование производительности модуля команд. +- pnpm k6:projects — Тестирование производительности модуля проектов. +- pnpm k6:user — Тестирование производительности модуля пользователей. +- pnpm k6:board — Тестирование производительности модуля досок. +- pnpm k6:tasks — Тестирование производительности модуля задач. + +## Использование переменных окружения + +Для смены целевого адреса сервера без изменения кода сценариев используется флаг -e: + +pnpm --filter @project/performance-tests exec k6 run -e BASE_URL=https://api.example.com scenarios/stress-full.js + +## Анализ результатов + +При анализе отчетов следует ориентироваться на следующие метрики: + +- http_req_duration: Общее время обработки запроса сервером. +- http_req_failed: Процент запросов, завершившихся ошибкой. +- vus: Количество виртуальных пользователей в активной фазе теста. +- thresholds: Статус выполнения установленных критериев качества (SLA). + +## Конфигурация и профили нагрузки + +Система поддерживает динамическое переключение интенсивности тестирования с помощью переменных окружения. + +### Доступные профили (PROFILE) + +В `common/config.js` определены следующие уровни нагрузки: + +| Профиль | Описание | Нагрузка | +| :------- | :--------------------------- | :------------------- | +| `smoke` | Быстрая проверка доступности | 1 VU, 10 секунд | +| `low` | Базовая стабильность | 10 VUs, разгон 30с | +| `medium` | Стандартная рабочая нагрузка | 50 VUs, плато 3 мин | +| `high` | Проверка предела прочности | 300 VUs, плато 5 мин | + +### Примеры запуска с профилями + +Для управления нагрузкой передайте переменную `PROFILE`: + +```sh +pnpm k6:tasks -e PROFILE=medium + +# Запуск стресс-теста на кастомный URL +pnpm k6:all -e PROFILE=high -e BASE_URL=[http://staging-api.local](http://staging-api.local) +``` diff --git a/infra/k6/common/api-client.js b/infra/k6/common/api-client.js new file mode 100644 index 0000000..9fae98b --- /dev/null +++ b/infra/k6/common/api-client.js @@ -0,0 +1,166 @@ +import http from 'k6/http'; +import { check } from 'k6'; +import { BASE_URL } from './config.js'; + +/** + * Обертка над стандартным HTTP-клиентом k6. + */ +export class ApiClient { + /** + * @param {string} baseUrl - Базовый адрес API. + * @param {string|null} [token=null] - Bearer токен для авторизации. + */ + constructor({ baseUrl = BASE_URL, token = null } = {}) { + this.baseUrl = baseUrl; + this.token = token; + } + + /** + * Формирует заголовки запроса. + * @private + * @returns {Object.} + */ + _getHeaders(useJsonDefault = true, extraHeaders = {}) { + const headers = {}; + if (this.token) { + headers['Authorization'] = `Bearer ${this.token}`; + } + if (useJsonDefault) { + headers['Content-Type'] = 'application/json'; + } + return Object.assign(headers, extraHeaders); + } + + /** + * Формирует параметры запроса (headers/cookies/tags). + * @private + * @param {Object} [options] - Доп. параметры запроса. + * @param {Object.} [options.headers] - Доп. заголовки. + * @param {Object.} [options.cookies] - Cookies для запроса. + * @param {Object.} [options.tags] - Tags для метрик k6. + * @param {boolean} [useJsonDefault=true] - Добавлять ли JSON Content-Type по умолчанию. + * @returns {Object} + */ + _buildOptions(options = {}, useJsonDefault = true) { + const headers = this._getHeaders(useJsonDefault, options.headers || {}); + const reqOptions = { headers }; + + if (options.cookies) { + reqOptions.cookies = options.cookies; + } + if (options.tags) { + reqOptions.tags = options.tags; + } + + return reqOptions; + } + + /** + * Формирует строку query-параметров. + * @private + * @param {Object.} [params] - Query-параметры. + * @returns {string} + */ + _buildQuery(params = {}) { + return Object.keys(params).length + ? `?${Object.entries(params) + .map(([k, v]) => `${k}=${v}`) + .join('&')}` + : ''; + } + + /** + * Выполняет GET запрос. + * @param {string} path - Относительный путь (напр. '/tasks'). + * @param {Object.} [params] - Query-параметры. + * @returns {import('k6/http').RefinedResponse} + */ + get(path, params = {}, options = {}) { + const query = this._buildQuery(params); + const res = http.get(`${this.baseUrl}${path}${query}`, this._buildOptions(options)); + this._logError(res, 'GET', path); + return res; + } + + /** + * Выполняет POST запрос. + * @param {string} path - Относительный путь. + * @param {Object} body - Объект данных (будет преобразован в JSON). + * @returns {import('k6/http').RefinedResponse} + */ + post(path, body, options = {}) { + const useJsonDefault = !options.rawBody; + const payload = options.rawBody ? body : JSON.stringify(body); + const res = http.post( + `${this.baseUrl}${path}`, + payload, + this._buildOptions(options, useJsonDefault), + ); + this._logError(res, 'POST', path); + return res; + } + + /** + * Выполняет PATCH запрос. + * @param {string} path - Относительный путь. + * @param {Object} body - Данные для частичного обновления. + * @returns {import('k6/http').RefinedResponse} + */ + patch(path, body, options = {}) { + const useJsonDefault = !options.rawBody; + const payload = options.rawBody ? body : JSON.stringify(body); + const res = http.patch( + `${this.baseUrl}${path}`, + payload, + this._buildOptions(options, useJsonDefault), + ); + this._logError(res, 'PATCH', path); + return res; + } + + /** + * Выполняет PUT запрос. + * @param {string} path - Относительный путь. + * @param {Object} body - Данные для полного обновления. + * @returns {import('k6/http').RefinedResponse} + */ + put(path, body, options = {}) { + const useJsonDefault = !options.rawBody; + const payload = options.rawBody ? body : JSON.stringify(body); + const res = http.put( + `${this.baseUrl}${path}`, + payload, + this._buildOptions(options, useJsonDefault), + ); + this._logError(res, 'PUT', path); + return res; + } + + /** + * Выполняет DELETE запрос. + * @param {string} path - Относительный путь. + * @returns {import('k6/http').RefinedResponse} + */ + delete(path, options = {}) { + const res = http.del(`${this.baseUrl}${path}`, null, this._buildOptions(options, false)); + this._logError(res, 'DELETE', path); + return res; + } + + /** + * Внутренняя валидация ответа и логирование ошибок. + * @private + * @param {import('k6/http').RefinedResponse} res - Объект ответа k6. + * @param {string} method - Название HTTP метода для лога. + * @param {string} path - Путь запроса для лога. + */ + _logError(res, method, path) { + check(res, { + [`${method} ${path} status is 2xx`]: (r) => r.status >= 200 && r.status < 300, + }); + + if (res.status >= 400) { + console.error(`Error on ${method} ${path}: [${res.status}] ${res.body}`); + } + } +} diff --git a/infra/k6/common/config.js b/infra/k6/common/config.js new file mode 100644 index 0000000..9b86f40 --- /dev/null +++ b/infra/k6/common/config.js @@ -0,0 +1,73 @@ +export const BASE_URL = __ENV.BASE_URL || 'http://0.0.0.0:3000/v1'; +export const REDIS_URL = __ENV.REDIS_URL || 'http://localhost:7000'; + +/** + * Профили нагрузки (Workload Profiles). + * Описывают поведение виртуальных пользователей (VUs) во времени. + * * @typedef {Object} Stage + * @property {string} duration - Продолжительность этапа (напр. '2m') + * @property {number} target - Целевое количество активных пользователей + * * @typedef {Object} Profile + * @property {number} [vus] - Фиксированное количество пользователей + * @property {string} [duration] - Общая продолжительность теста + * @property {Stage[]} [stages] - Этапы изменения нагрузки + */ +/** @type {Object.} */ +export const PROFILES = { + /** Минимальная проверка доступности: 1 юзер, 10 секунд */ + smoke: { + vus: 1, + duration: '10s', + }, + /** Низкая нагрузка: проверка базовой стабильности (10 юзеров) */ + low: { + stages: [ + { duration: '30s', target: 10 }, + { duration: '1m', target: 10 }, + { duration: '30s', target: 0 }, + ], + }, + /** Средняя нагрузка: имитация нормальной рабочей нагрузки (50 юзеров) */ + medium: { + stages: [ + { duration: '1m', target: 50 }, + { duration: '3m', target: 50 }, + { duration: '1m', target: 0 }, + ], + }, + /** Высокая нагрузка: поиск предела производительности (300 юзеров) */ + high: { + stages: [ + { duration: '2m', target: 300 }, + { duration: '5m', target: 300 }, + { duration: '2m', target: 0 }, + ], + }, +}; + +/** * Критерии успеха (Thresholds). + * Если метрики выходят за эти пределы, k6 завершает тест с ошибкой. + * @type {Object.} + */ +export const THRESHOLDS = { + /** Допустимый процент ошибок: менее 1% */ + http_req_failed: ['rate<0.01'], + /** Допустимое время ответа: 95-й перцентиль должен быть быстрее 200мс */ + http_req_duration: ['p(95)<200'], +}; + +/** + * Автоматически выбирает профиль на основе переменной окружения. + * Использование в сценарии: export const options = GET_OPTIONS(); + */ +export const GET_OPTIONS = () => { + const profileName = __ENV.PROFILE || 'smoke'; + const profile = PROFILES[profileName] || PROFILES.smoke; + + return { + vus: profile.vus, + duration: profile.duration, + stages: profile.stages, + thresholds: THRESHOLDS, + }; +}; diff --git a/infra/k6/common/redis-client.js b/infra/k6/common/redis-client.js new file mode 100644 index 0000000..4644f88 --- /dev/null +++ b/infra/k6/common/redis-client.js @@ -0,0 +1,64 @@ +import redis from 'k6/x/redis'; +import { REDIS_URL } from './config.js'; + +/** + * Обертка для работы с Redis в k6. + */ +export class RedisClient { + /** + * @param {string} url - URL редиса (напр. 'redis://localhost:6379'). + */ + constructor(url = REDIS_URL) { + this.client = redis.connect(url); + } + + /** + * Формирует ключи по тем же правилам, что и бэкенд/сидер. + * @private + */ + _keys = { + invite: (code) => `inv:code:${code}`, + teamInvites: (teamId) => `team:invites:${teamId}`, + userInvites: (email) => `user:invites:${email.toLowerCase()}`, + otp: (email) => `otp:${email.toLowerCase()}`, + }; + + /** + * Получает OTP код для юзера. + * @param {string} email + * @returns {string|null} + */ + getOtp(email) { + return redis.get(this.client, this._keys.otp(email)); + } + + /** + * Получает данные инвайта по коду. + * @param {string} code + * @returns {Object|null} + */ + getInvite(code) { + const data = redis.get(this.client, this._keys.invite(code)); + return data ? JSON.parse(data) : null; + } + + /** + * Получает все коды инвайтов для конкретной команды (из Set). + * @param {string} teamId + * @returns {string[]} + */ + getTeamInvitesCodes(teamId) { + return redis.smembers(this.client, this._keys.teamInvites(teamId)); + } + + getUserInvitesCodes(email) { + return redis.smembers(this.client, this._keys.userInvites(email)); + } + + /** + * Удаляет ключ + */ + del(key) { + redis.del(this.client, key); + } +} diff --git a/infra/k6/data/user-avatar.png b/infra/k6/data/user-avatar.png new file mode 100644 index 0000000..2386e31 Binary files /dev/null and b/infra/k6/data/user-avatar.png differ diff --git a/infra/k6/modules/.gitkeep b/infra/k6/modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/infra/k6/package.json b/infra/k6/package.json new file mode 100644 index 0000000..b82ea65 --- /dev/null +++ b/infra/k6/package.json @@ -0,0 +1,16 @@ +{ + "name": "@project/performance-tests", + "version": "0.0.1", + "description": "Нагрузочные и стресс-тесты API бэкенда с использованием k6", + "scripts": { + "test:all": "k6 run scenarios/stress-full.js", + "test:auth": "k6 run scenarios/auth.js", + "test:teams": "k6 run scenarios/teams.js", + "test:projects": "k6 run scenarios/projects.js", + "test:users": "k6 run scenarios/users.js", + "test:board": "k6 run scenarios/board-full.js", + "test:tasks": "k6 run scenarios/tasks.js", + "smoke": "k6 run smoke.js" + }, + "packageManager": "pnpm@10.33.0" +} diff --git a/infra/k6/scenarios/.gitkeep b/infra/k6/scenarios/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/infra/k6/scenarios/auth.js b/infra/k6/scenarios/auth.js new file mode 100644 index 0000000..5a76e9c --- /dev/null +++ b/infra/k6/scenarios/auth.js @@ -0,0 +1,55 @@ +import { SharedArray } from 'k6/data'; +import { sleep } from 'k6'; +import { GET_OPTIONS } from '../common/config.js'; +import getAuthUser from '../shared/get-auth-user.js'; + +const users = new SharedArray('test users', function () { + return JSON.parse(open('../data/users.json')); +}); + +const baseOptions = GET_OPTIONS(); +baseOptions.thresholds = Object.assign({}, baseOptions.thresholds, { + 'http_req_duration{name:auth-sign-in}': ['p(95)<333'], + 'http_req_duration{name:auth-refresh}': ['p(95)<333'], + 'http_req_duration{name:auth-sign-out}': ['p(95)<333'], +}); + +export const options = baseOptions; + +export default function () { + const user = users[(__VU - 1) % users.length]; + const { client, token, refreshCookie } = getAuthUser(user); + + sleep(1); + + // --- REFRESH --- + const refreshRes = client.post('/auth/refresh', null, { + cookies: { refresh: refreshCookie }, + tags: { name: 'auth-refresh' }, + }); + + const newAccessToken = refreshRes.json().token; + const newRefreshCookie = refreshRes.cookies.refresh + ? refreshRes.cookies.refresh[0].value + : 'NOT_ROTATED'; + + sleep(1); + + // --- SIGN OUT --- + const refreshToken = newAccessToken || token; + const signOutCookie = newRefreshCookie !== 'NOT_ROTATED' ? newRefreshCookie : refreshCookie; + + client.post( + '/auth/sign-out', + {}, + { + headers: { + Authorization: `Bearer ${refreshToken}`, + }, + cookies: { refresh: signOutCookie }, + tags: { name: 'auth-sign-out' }, + }, + ); + + sleep(1); +} diff --git a/infra/k6/scenarios/projects.js b/infra/k6/scenarios/projects.js new file mode 100644 index 0000000..9fe8785 --- /dev/null +++ b/infra/k6/scenarios/projects.js @@ -0,0 +1,117 @@ +import { SharedArray } from 'k6/data'; +import { sleep, check } from 'k6'; +import { GET_OPTIONS } from '../common/config.js'; +import getAuthUser from '../shared/get-auth-user.js'; + +const users = new SharedArray('test users', function () { + return JSON.parse(open('../data/users.json')); +}); +const teams = new SharedArray('test teams', function () { + return JSON.parse(open('../data/teams.json')); +}); +const randomStr = (len = 8) => + Math.random() + .toString(36) + .substring(2, 2 + len); +const randomNum = (min = 1, max = 100) => Math.floor(Math.random() * (max - min + 1)) + min; +const baseOptions = GET_OPTIONS(); +baseOptions.thresholds = Object.assign({}, baseOptions.thresholds, { + 'http_req_duration{name:auth-sign-in}': ['p(95)<333'], + 'http_req_duration{name:post-teams-projects}': ['p(95)<333'], + 'http_req_duration{name:teams-projects}': ['p(95)<333'], + 'http_req_duration{name:teams-projects-id}': ['p(95)<333'], + 'http_req_duration{name:teams-projects-generate-token}': ['p(95)<333'], + 'http_req_duration{name:teams-projects-archive}': ['p(95)<333'], + 'http_req_duration{name:delete-teams-projects}': ['p(95)<333'], +}); + +export const options = baseOptions; + +export default function () { + const user = users[(__VU - 1) % users.length]; + const team = teams[(__VU - 1) % users.length]; + const { client } = getAuthUser(user); + + sleep(1); + + // --- create project --- + const projectName = randomStr(); + const project = { + name: projectName, + key: `QWE${randomNum(1000, 9999)}`, + description: 'description for k6_test_project', + visibility: 'public', + }; + const createRes = client.post(`/teams/${team.slug}/projects`, project, { + tags: { name: 'post-teams-projects' }, + }); + const projectId = createRes.json().projectId; + + sleep(1); + + // --- update project --- + const newProjectName = randomStr(); + const updatedProject = { + name: newProjectName, + }; + client.patch(`/teams/${team.slug}/projects/${projectId}`, updatedProject, { + tags: { name: 'patch-teams-projects' }, + }); + + sleep(1); + + // --- get all teams projects --- + + const getAllRes = client.get( + `/teams/${team.slug}/projects`, + {}, + { tags: { name: 'get-teams-projects' } }, + ); + + check(getAllRes, { 'projects list has meta': (r) => r.json().meta !== undefined }); + + sleep(1); + + // --- get one team project --- + client.get( + `/teams/${team.slug}/projects/${projectId}`, + {}, + { tags: { name: 'teams-projects-id' } }, + ); + + sleep(1); + + // --- generate share token --- + const shareTokenRes = client.post( + `/teams/${team.slug}/projects/${projectId}/share`, + {}, + { tags: { name: 'teams-projects-generate-token' } }, + ); + + check(shareTokenRes, { + 'POST /teams/:slug/projects/:id/share: has token': (r) => + r.json().payload.token !== undefined, + }); + + sleep(1); + + // --- archive project --- + + client.post( + `/teams/${team.slug}/projects/${projectId}/archive`, + {}, + { tags: { name: 'teams-projects-archive' } }, + ); + + sleep(1); + + // --- delete project --- + + client.delete( + `/teams/${team.slug}/projects/${projectId}`, + {}, + { tags: { name: 'delete-teams-projects' } }, + ); + + sleep(1); +} diff --git a/infra/k6/scenarios/teams.js b/infra/k6/scenarios/teams.js new file mode 100644 index 0000000..96a385e --- /dev/null +++ b/infra/k6/scenarios/teams.js @@ -0,0 +1,68 @@ +import { SharedArray } from 'k6/data'; +import { sleep } from 'k6'; +import { GET_OPTIONS } from '../common/config.js'; +import getAuthUser from '../shared/get-auth-user.js'; + +const users = new SharedArray('test users', function () { + return JSON.parse(open('../data/users.json')); +}); +const randomStr = (len = 8) => + Math.random() + .toString(36) + .substring(2, 2 + len); +const baseOptions = GET_OPTIONS(); +baseOptions.thresholds = Object.assign({}, baseOptions.thresholds, { + 'http_req_duration{name:auth-sign-in}': ['p(95)<333'], + 'http_req_duration{name:teams-create}': ['p(95)<333'], + 'http_req_duration{name:teams-check-slug}': ['p(95)<333'], + 'http_req_duration{name:teams-find-one}': ['p(95)<333'], + 'http_req_duration{name:teams-update}': ['p(95)<333'], + 'http_req_duration{name:teams-delete}': ['p(95)<333'], +}); + +export const options = baseOptions; + +export default function () { + const user = users[(__VU - 1) % users.length]; + const { client } = getAuthUser(user); + + sleep(1); + + // --- POST /teams --- + const slug = randomStr(10); + const team = { + name: 'k6_team_' + slug, + description: randomStr(15), + slug: slug, + }; + client.post('/teams', team, { tags: { name: 'teams-create' } }); + + sleep(1); + + // --- GET /check-slug/:slug --- + client.get(`/teams/check-slug/${slug}`, {}, { tags: { name: 'teams-check-slug' } }); + + sleep(1); + + // --- GET /:slug --- + client.get(`/teams/${slug}`, {}, { tags: { name: 'teams-find-one' } }); + + sleep(1); + + // --- PATCH /:slug --- + const updatedTeam = { + description: randomStr(25), + }; + client.patch(`/teams/${slug}`, updatedTeam, { + tags: { name: 'teams-update' }, + }); + + sleep(1); + + // --- DELETE /:slug --- + client.delete(`/teams/${slug}`, { + tags: { name: 'teams-delete' }, + }); + + sleep(1); +} diff --git a/infra/k6/scenarios/users.js b/infra/k6/scenarios/users.js new file mode 100644 index 0000000..b025bfe --- /dev/null +++ b/infra/k6/scenarios/users.js @@ -0,0 +1,97 @@ +import { SharedArray } from 'k6/data'; +import http from 'k6/http'; +import { sleep } from 'k6'; +import { FormData } from 'https://jslib.k6.io/formdata/0.0.2/index.js'; +import { GET_OPTIONS } from '../common/config.js'; +import getAuthUser from '../shared/get-auth-user.js'; + +const users = new SharedArray('test users', function () { + return JSON.parse(open('../data/users.json')); +}); + +const baseOptions = GET_OPTIONS(); +baseOptions.thresholds = Object.assign({}, baseOptions.thresholds, { + 'http_req_duration{name:auth-sign-in}': ['p(95)<333'], + 'http_req_duration{name:users-me}': ['p(95)<333'], + 'http_req_duration{name:users-activity}': ['p(95)<333'], + 'http_req_duration{name:users-patch}': ['p(95)<333'], + 'http_req_duration{name:users-avatar}': ['p(95)<333'], + 'http_req_duration{name:users-notifications}': ['p(95)<333'], +}); + +export const options = baseOptions; + +const avatar = open('../data/user-avatar.png', 'b'); +const randomBool = () => Math.random() < 0.5; +const randomStr = (len = 8) => + Math.random() + .toString(36) + .substring(2, 2 + len); + +export default function () { + const user = users[(__VU - 1) % users.length]; + const { client } = getAuthUser(user); + + sleep(1); + + // --- GET /me --- + client.get('/users/me', {}, { tags: { name: 'users-me' } }); + + sleep(1); + + // --- GET /me/activity --- + const randomPage = Math.floor(Math.random() * 5) + 1; + const randomLimit = Math.floor(Math.random() * 15) + 5; + client.get( + '/users/me/activity', + { page: randomPage, limit: randomLimit }, + { tags: { name: 'users-activity' } }, + ); + + sleep(1); + + // --- PATCH /me --- + const meBody = { + firstName: `Name_${randomStr(5)}`, + lastName: `Surname_${randomStr(5)}`, + bio: `Testing bio with random data: ${randomStr(30)}`, + language: Math.random() > 0.5 ? 'ru' : 'en', + }; + + client.patch('/users/me', meBody, { tags: { name: 'users-patch' } }); + + sleep(1); + + // --- POST /me/avatar --- + const fd = new FormData(); + fd.append('file', http.file(avatar, 'avatar.png', 'image/png')); + + client.post('/users/me/avatar', fd.body(), { + rawBody: true, + headers: { + 'Content-Type': `multipart/form-data; boundary=${fd.boundary}`, + }, + tags: { name: 'users-avatar' }, + }); + + sleep(1); + + // --- PATCH /me/notifications --- + const notificationsBody = { + email: { + task_assigned: randomBool(), + mentions: randomBool(), + daily_summary: randomBool(), + }, + push: { + task_assigned: randomBool(), + reminders: randomBool(), + }, + }; + + client.patch('/users/me/notifications', notificationsBody, { + tags: { name: 'users-notifications' }, + }); + + sleep(1); +} diff --git a/infra/k6/scripts/clear-k6-data.ts b/infra/k6/scripts/clear-k6-data.ts new file mode 100644 index 0000000..ccee9fe --- /dev/null +++ b/infra/k6/scripts/clear-k6-data.ts @@ -0,0 +1,50 @@ +import Redis from 'ioredis'; +import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import * as sc from '../../../src/shared/entities'; +import { sql } from 'drizzle-orm'; +import postgres from 'postgres'; +import { assertEnv, DB_URL, REDIS_URL } from './k6-env'; +import { KEYS } from './k6-data-keys'; + +async function clearDB(db: PostgresJsDatabase) { + console.log('Cleaning up ONLY k6 test data from DB...'); + return await db.transaction(async (tx) => { + await tx.delete(sc.users).where(sql`${sc.users.email} LIKE 'k6_user_%'`); + await tx.delete(sc.teams).where(sql`${sc.teams.name} LIKE 'k6_team_%'`); + await tx.delete(sc.tags).where(sql`${sc.tags.name} LIKE 'k6_tag_%'`); + }); +} + +async function clearRedis(redis: Redis) { + console.log('Cleaning up ONLY k6 test data from Redis...'); + const SCAN_PATTERNS = Object.values(KEYS).map((fn) => fn('*')); + + for (const pattern of SCAN_PATTERNS) { + let cursor = '0'; + do { + const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100); + cursor = nextCursor; + if (keys.length > 0) await redis.del(...keys); + } while (cursor !== '0'); + } +} + +async function main() { + assertEnv(); + const redis = new Redis(REDIS_URL); + const queryClient = postgres(DB_URL, { max: 1 }); + const db = drizzle(queryClient, { schema: sc }); + + try { + await clearDB(db); + await clearRedis(redis); + } catch (e) { + console.error('Error:', e); + process.exit(1); + } finally { + await queryClient.end(); + await redis.quit(); + } +} + +main(); diff --git a/infra/k6/scripts/k6-data-keys.ts b/infra/k6/scripts/k6-data-keys.ts new file mode 100644 index 0000000..025f0ea --- /dev/null +++ b/infra/k6/scripts/k6-data-keys.ts @@ -0,0 +1,5 @@ +export const KEYS = { + INVITE: (code: string) => `inv:code:${code}`, + TEAM_INVITES: (teamId: string) => `team:invites:${teamId}`, + USER_INVITES: (email: string) => `user:invites:${email.toLowerCase()}`, +}; diff --git a/infra/k6/scripts/k6-env.ts b/infra/k6/scripts/k6-env.ts new file mode 100644 index 0000000..d95a666 --- /dev/null +++ b/infra/k6/scripts/k6-env.ts @@ -0,0 +1,13 @@ +import 'dotenv/config'; + +export const DB_URL = process.env.DATABASE_URL; +export const REDIS_URL = + process.env.REDIS_HOST && process.env.REDIS_PORT + ? `redis://${process.env.REDIS_HOST}:${process.env.REDIS_PORT}` + : undefined; + +export function assertEnv() { + if (!DB_URL || !REDIS_URL) { + throw new Error('DATABASE_URL OR REDIS_HOST, REDIS_PORT is not defined in .env'); + } +} diff --git a/infra/k6/scripts/seed-k6-data.ts b/infra/k6/scripts/seed-k6-data.ts new file mode 100644 index 0000000..82fbb1f --- /dev/null +++ b/infra/k6/scripts/seed-k6-data.ts @@ -0,0 +1,212 @@ +import { createId } from '@paralleldrive/cuid2'; +import * as argon from 'argon2'; +import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import { writeFileSync, mkdirSync, readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import * as sc from '../../../src/shared/entities/index'; +import Redis from 'ioredis'; +import { assertEnv, DB_URL, REDIS_URL } from './k6-env'; +import { KEYS } from './k6-data-keys'; + +async function seed_db(db: PostgresJsDatabase) { + const COUNT = 1000; + const OUT_USERS_FILE = resolve(process.cwd(), 'infra/k6/data/users.json'); + const OUT_TEAMS_FILE = resolve(process.cwd(), 'infra/k6/data/teams.json'); + const OUT_TAGS_FILE = resolve(process.cwd(), 'infra/k6/data/tags.json'); + + console.log(`Start seeding using pg driver...`); + + const password = 'TestPassword123!'; + const passwordHash = await argon.hash(password); + + const usersToInsert = []; + const securityToInsert = []; + const notificationsToInsert = []; + const activitiesToInsert = []; + const usersData = []; + const teamsData = []; + const tagsData = []; + const teamsToInsert = []; + const tagsToInsert = []; + const teamsToTagsToInsert = []; + const teamMembersToInsert = []; + + for (let i = 0; i < COUNT; i++) { + const userId = createId(); + const teamId = createId(); + const tagId = createId(); + const email = `k6_user_${i}@tasktracker.com`; + + const user = { + id: userId, + email, + firstName: 'K6', + lastName: `User ${i}`, + timezone: 'UTC', + language: 'ru', + }; + const team = { + id: teamId, + ownerId: userId, + name: `k6_team_${i}`, + slug: `k6_team_${i}`, + description: `description team - ${i}`, + }; + const tag = { + id: tagId, + name: `k6_tag_${i}`, + }; + const teamMember = { + teamId: teamId, + userId: userId, + role: 'owner', + status: 'active', + joinedAt: new Date(), + }; + + usersToInsert.push(user); + teamsToInsert.push(team); + tagsToInsert.push(tag); + teamsToTagsToInsert.push({ + teamId, + tagId, + }); + teamMembersToInsert.push(teamMember); + securityToInsert.push({ userId, passwordHash }); + notificationsToInsert.push({ userId }); + + usersData.push({ email, password }); + teamsData.push(team); + tagsData.push(tag); + + for (let j = 0; j < 10; j++) { + activitiesToInsert.push({ + id: createId(), + userId: userId, + eventType: 'SIGN_IN', + entityId: userId, + metadata: { + description: `K6 Load Test Iteration ${j}`, + ip: '127.0.0.1', + userAgent: 'k6-test-agent', + }, + createdAt: new Date(Date.now() - j * 1000 * 60 * 60), + }); + } + } + + await db.transaction(async (tx) => { + await tx.insert(sc.users).values(usersToInsert); + await tx.insert(sc.userSecurity).values(securityToInsert); + await tx.insert(sc.userNotifications).values(notificationsToInsert); + + const chunkSize = 1000; + for (let i = 0; i < activitiesToInsert.length; i += chunkSize) { + const chunk = activitiesToInsert.slice(i, i + chunkSize); + await tx.insert(sc.userActivity).values(chunk); + } + await tx.insert(sc.teams).values(teamsToInsert); + await tx.insert(sc.tags).values(tagsToInsert); + await tx.insert(sc.teamsToTags).values(teamsToTagsToInsert); + await tx.insert(sc.teamMembers).values(teamMembersToInsert); + }); + + const filesToSave = [ + { path: OUT_USERS_FILE, data: usersData }, + { path: OUT_TEAMS_FILE, data: teamsData }, + { path: OUT_TAGS_FILE, data: tagsData }, + ]; + + for (const { path, data } of filesToSave) { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, JSON.stringify(data, null, 2)); + } + + console.log(`Success! Created ${COUNT} entries for each entity`); + console.log(`User data saved to: ${OUT_USERS_FILE}`); + console.log(`Teams data saved to: ${OUT_TEAMS_FILE}`); + console.log(`Tags data saved to: ${OUT_TAGS_FILE}`); +} + +async function seed_redis(redis: Redis) { + console.log('Seeding Redis with OTP codes...'); + const multi = redis.multi(); + + const dataDir = resolve(process.cwd(), 'infra/k6/data'); + const users = JSON.parse(readFileSync(`${dataDir}/users.json`, 'utf-8')) as { + email: string; + }[]; + const teams = JSON.parse(readFileSync(`${dataDir}/teams.json`, 'utf-8')) as { + id: string; + ownerId: string; + name: string; + slug: string; + description: string; + }[]; + + const INVITE_TTL = 86400; + const INVITES_PER_TEAM = 10; + + const invitesData = []; + + teams.forEach((team, teamIdx) => { + for (let j = 1; j <= INVITES_PER_TEAM; j++) { + const inviteeIdx = (teamIdx + j) % users.length; + const invitee = users[inviteeIdx]; + + const code = `INV_${teamIdx}_${inviteeIdx}`; + + const inviteData = { + teamId: team.id, + teamName: team.name, + teamAvatar: + 'https://cdn.pixabay.com/photo/2016/08/08/09/17/avatar-1577909_1280.png', + email: invitee.email, + role: 'member', + inviterId: team.ownerId, + inviterName: `Owner of ${team.name}`, + createdAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + INVITE_TTL * 1000).toISOString(), + }; + + multi.set(KEYS.INVITE(code), JSON.stringify(inviteData), 'EX', INVITE_TTL); + multi.sadd(KEYS.TEAM_INVITES(team.id), code); + multi.sadd(KEYS.USER_INVITES(invitee.email), code); + + invitesData.push({ + code, + email: invitee.email, + teamSlug: team.slug, + }); + } + }); + + await multi.exec(); + + const OUT_FILE = `${dataDir}/invites.json`; + writeFileSync(OUT_FILE, JSON.stringify(invitesData, null, 2)); + + console.log(`Success! Redis seeded. Created ${invitesData.length} unique invites.`); + console.log(`Invites data saved to: ${OUT_FILE}`); +} + +async function main() { + assertEnv(); + const redis = new Redis(REDIS_URL); + const queryClient = postgres(DB_URL, { max: 1 }); + const db = drizzle(queryClient, { schema: sc }); + + try { + await seed_db(db); + await seed_redis(redis); + } catch (e) { + console.error('Error:', e); + process.exit(1); + } finally { + await queryClient.end(); + await redis.quit(); + } +} + +main(); diff --git a/infra/k6/shared/get-auth-user.js b/infra/k6/shared/get-auth-user.js new file mode 100644 index 0000000..c6e813b --- /dev/null +++ b/infra/k6/shared/get-auth-user.js @@ -0,0 +1,36 @@ +import { check } from 'k6'; +import { ApiClient } from '../common/api-client.js'; + +export default function getAuthUser(user, options = {}) { + const client = new ApiClient(); + const requestOptions = Object.assign({}, options); + + if (!requestOptions.tags) { + requestOptions.tags = { name: 'auth-sign-in' }; + } + + const signInRes = client.post( + '/auth/sign-in', + { + email: user.email, + password: user.password, + }, + requestOptions, + ); + + check(signInRes, { + 'POST /auth/sign-in has token': (r) => r.json().token !== undefined, + }); + + const token = signInRes.json().token; + const refreshCookie = signInRes.cookies.refresh + ? signInRes.cookies.refresh[0].value + : 'MISSING'; + + return { + client: new ApiClient({ token }), + token, + refreshCookie, + signInRes, + }; +} diff --git a/infra/k6/smoke.js b/infra/k6/smoke.js new file mode 100644 index 0000000..db6cd61 --- /dev/null +++ b/infra/k6/smoke.js @@ -0,0 +1,12 @@ +import { sleep } from 'k6'; +import { ApiClient } from './common/client.js'; +import { BASE_URL, GET_OPTIONS } from './common/config.js'; + +export const options = GET_OPTIONS(); + +const client = new ApiClient(BASE_URL); + +export default function () { + client.get('/health'); + sleep(1); +} diff --git a/libs/bootstrap/src/bootstrap.ts b/libs/bootstrap/src/bootstrap.ts new file mode 100644 index 0000000..50a3818 --- /dev/null +++ b/libs/bootstrap/src/bootstrap.ts @@ -0,0 +1,141 @@ +import { Logger, VersioningType } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { NestFactory } from '@nestjs/core'; +import { DEFAULT_THROTTLER_OPTIONS } from './configs/throttler'; +import { setupCors, setupLogger, setupThrottler, setupSwagger } from './setups'; +import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; +import type { BootstrapOptions } from './interfaces/options.interface'; +import fastifyCookie from '@fastify/cookie'; +import fastifyCompress from '@fastify/compress'; +import fastifyMultipart from '@fastify/multipart'; +import fastifyCsrf from '@fastify/csrf-protection'; +import { createId } from '@paralleldrive/cuid2'; + +export async function bootstrapApp(options: BootstrapOptions) { + const startTime = performance.now(); + const adapter = new FastifyAdapter({ + requestIdHeader: 'x-request-id', + requestIdLogLabel: 'request', + genReqId: (req) => { + return (req.headers['x-request-id'] as string) || createId(); + }, + }); + + const { + appModule, + apiPrefix, + version = 'v1', + serviceName = 'App', + portEnvKey = 'PORT', + defaultPort = 3000, + setupApp, + useCookieParser = true, + useCors = true, + throttlerOptions = DEFAULT_THROTTLER_OPTIONS, + swaggerOptions, + } = options; + + let rootModule = appModule; + + if (throttlerOptions) { + rootModule = setupThrottler(rootModule, throttlerOptions); + } + + const app = await NestFactory.create(rootModule, adapter, { + rawBody: true, + bufferLogs: true, + }); + + const logger = new Logger(serviceName[0].toUpperCase() + serviceName.slice(1)); + const configService = app.get(ConfigService); + const port = configService.getOrThrow(portEnvKey, defaultPort); + const origins = configService.getOrThrow('CORS_ALLOWED_ORIGINS'); + + app.enableShutdownHooks(); + + app.getHttpAdapter() + .getInstance() + .addHook('onSend', async (request, reply, payload) => { + reply.header('x-request-id', request.id); + return payload; + }); + + await setupLogger(app, options.serviceName); + + await app.register(fastifyCompress, { + global: true, + threshold: 1024, + }); + + await app.register(fastifyMultipart, { + limits: { + fileSize: 5 * 1024 * 1024, + fieldNameSize: 100, + files: 5, + }, + }); + + if (apiPrefix) app.setGlobalPrefix(apiPrefix); + if (version) { + const hasV = version.startsWith('v'); + + app.enableVersioning({ + type: VersioningType.URI, + prefix: hasV ? 'v' : '', + defaultVersion: hasV ? version.slice(1) : version, + }); + } + if (useCors) setupCors(app, origins); + if (swaggerOptions) { + const { path = 'docs', ...metadata } = swaggerOptions; + + const domain = configService.get('DOMAIN'); + const stage = configService.get('STAGE_DOMAIN'); + + const fullOptions = { + ...metadata, + path, + server: { + port, + domain, + stage, + }, + }; + + await setupSwagger(app, fullOptions); + } + if (useCookieParser) { + const secret = configService.getOrThrow('COOKIE_SECRET'); + await app.register(fastifyCookie, { secret }); + await app.register(fastifyCsrf, { + cookieOpts: { + signed: true, + httpOnly: true, + sameSite: 'strict', + secure: configService.getOrThrow('NODE_ENV') === 'production', + }, + }); + } + if (setupApp) setupApp(app); + + await app.listen(port, '0.0.0.0', (_err, address) => { + const prefix = [apiPrefix, version].filter(Boolean).join('/'); + const baseUrl = `${address}${prefix ? '/' + prefix : ''}`; + + const swaggerBase = `${address}${apiPrefix ? '/' + apiPrefix : ''}`; + const swaggerPath = swaggerOptions?.path ?? 'docs'; + + if (_err) { + logger.error(_err); + process.exit(1); + } + + const startupTime = (performance.now() - startTime).toFixed(2); + logger.verbose(`Environment: ${process.env.NODE_ENV || 'development'}`); + logger.verbose(`API Endpoint: ${baseUrl}`); + logger.verbose(`Health Check: ${baseUrl}/health`); + logger.verbose(`Swagger UI: ${swaggerBase}/${swaggerPath}`); + logger.verbose(`OpenAPI (Specs): ${swaggerBase}/${swaggerPath}/s/{json,yaml}`); + logger.verbose(`Boot Time: ${startupTime}ms`); + }); +} diff --git a/libs/bootstrap/src/configs/swagger.ts b/libs/bootstrap/src/configs/swagger.ts new file mode 100644 index 0000000..a13eb68 --- /dev/null +++ b/libs/bootstrap/src/configs/swagger.ts @@ -0,0 +1,7 @@ +import type { SwaggerOptions } from '../interfaces/options.interface'; + +export const SWAGGER_DEFAULTS: SwaggerOptions = { + title: 'API', + description: 'API Documentation', + version: '1.0.0', +}; diff --git a/libs/bootstrap/src/configs/throttler.ts b/libs/bootstrap/src/configs/throttler.ts new file mode 100644 index 0000000..95532f9 --- /dev/null +++ b/libs/bootstrap/src/configs/throttler.ts @@ -0,0 +1,10 @@ +import 'dotenv/config'; +import type { ThrottlerModuleOptions } from '@nestjs/throttler'; + +export const DEFAULT_THROTTLER_OPTIONS: ThrottlerModuleOptions = [ + { + ttl: process.env.THROTTLE_TTL ? parseInt(process.env.THROTTLE_LIMIT) : 60000, + limit: process.env.THROTTLE_LIMIT ? parseInt(process.env.THROTTLE_LIMIT) : 100, + skipIf: (context) => context.getType() !== 'http', + }, +]; diff --git a/libs/bootstrap/src/index.ts b/libs/bootstrap/src/index.ts new file mode 100644 index 0000000..cbc96b3 --- /dev/null +++ b/libs/bootstrap/src/index.ts @@ -0,0 +1 @@ +export { bootstrapApp } from './bootstrap'; diff --git a/libs/bootstrap/src/interfaces/index.ts b/libs/bootstrap/src/interfaces/index.ts new file mode 100644 index 0000000..4d45ac8 --- /dev/null +++ b/libs/bootstrap/src/interfaces/index.ts @@ -0,0 +1 @@ +export type { BootstrapOptions, SwaggerOptions } from './options.interface'; diff --git a/libs/bootstrap/src/interfaces/options.interface.ts b/libs/bootstrap/src/interfaces/options.interface.ts new file mode 100644 index 0000000..3d42a76 --- /dev/null +++ b/libs/bootstrap/src/interfaces/options.interface.ts @@ -0,0 +1,36 @@ +import type { Config } from '@libs/config'; +import type { Type } from '@nestjs/common'; +import type { NestFastifyApplication } from '@nestjs/platform-fastify'; +import type { ThrottlerModuleOptions } from '@nestjs/throttler'; + +export interface SwaggerMetadata { + title?: string; + description?: string; + version?: string; + path?: string; +} + +export interface SwaggerInfrastructure { + server?: { + port?: string | number; + domain?: string; + stage?: string; + }; + services?: { name: string; port: number }[]; +} + +export interface SwaggerOptions extends SwaggerMetadata, SwaggerInfrastructure {} + +export interface BootstrapOptions { + apiPrefix?: string; + version?: string; + appModule: Type; + defaultPort?: number; + portEnvKey?: keyof Config; + serviceName: string; + setupApp?: (app: NestFastifyApplication) => Promise | void; + swaggerOptions?: SwaggerMetadata; + throttlerOptions?: ThrottlerModuleOptions; + useCookieParser?: boolean; + useCors?: boolean; +} diff --git a/libs/bootstrap/src/setups/cors.ts b/libs/bootstrap/src/setups/cors.ts new file mode 100644 index 0000000..59a7959 --- /dev/null +++ b/libs/bootstrap/src/setups/cors.ts @@ -0,0 +1,36 @@ +import fastifyCors from '@fastify/cors'; +import type { NestFastifyApplication } from '@nestjs/platform-fastify'; + +export function setupCors(app: NestFastifyApplication, origins: string[]) { + app.getHttpAdapter() + .getInstance() + .register(fastifyCors, { + origin: (origin, callback) => { + // server-to-server / curl / healthcheck + if (!origin) { + return callback(null, true); + } + + try { + const { hostname } = new URL(origin); + const allowedHostnames = origins.map((o) => new URL(o).hostname); + + if ( + allowedHostnames.some( + (allowed) => hostname === allowed || hostname.endsWith(`.${allowed}`), + ) + ) { + return callback(null, origin); + } + + callback(new Error('Not allowed by CORS'), false); + } catch (e) { + callback(new Error('Invalid origin format'), false); + } + }, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'], + preflightContinue: false, + optionsSuccessStatus: 204, + }); +} diff --git a/libs/bootstrap/src/setups/index.ts b/libs/bootstrap/src/setups/index.ts new file mode 100644 index 0000000..e5d3f07 --- /dev/null +++ b/libs/bootstrap/src/setups/index.ts @@ -0,0 +1,4 @@ +export { setupCors } from './cors'; +export { setupThrottler } from './throttler'; +export { setupSwagger } from './swagger'; +export { setupLogger } from './logger'; diff --git a/libs/bootstrap/src/setups/logger.ts b/libs/bootstrap/src/setups/logger.ts new file mode 100644 index 0000000..c0456b3 --- /dev/null +++ b/libs/bootstrap/src/setups/logger.ts @@ -0,0 +1,223 @@ +import { + Injectable, + NestInterceptor, + type ExecutionContext, + type CallHandler, + Logger, +} from '@nestjs/common'; +import { Observable, throwError } from 'rxjs'; +import { tap, catchError } from 'rxjs/operators'; +import type { FastifyRequest } from 'fastify'; +import { WinstonModule, utilities } from 'nest-winston'; +import { format, transports } from 'winston'; +import type { NestFastifyApplication } from '@nestjs/platform-fastify'; +import { ConfigService } from '@nestjs/config'; + +export function setupLogger(app: NestFastifyApplication, service: string) { + const cfg = app.get(ConfigService); + const isProduction = cfg.get('NODE_ENV') === 'production'; + + const logger = WinstonModule.createLogger({ + level: isProduction ? 'info' : 'debug', + format: format.combine( + format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }), + format.errors({ stack: true }), + isProduction + ? format.json() + : format.combine(format.ms(), utilities.format.nestLike(service, { colors: true })), + ), + transports: [new transports.Console()], + }); + + app.useLogger(logger); + app.useGlobalInterceptors(new LoggingInterceptor()); +} + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + private readonly logger = new Logger('HTTP'); + private readonly sensitiveFields = ['password', 'token', 'access', 'refresh', 'code', 'secret']; + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + const startTime = Date.now(); + + const baseCtx = { + request_id: request.id || request.headers['x-request-id'] || 'unknown', + method: request.method, + url: request.url, + path: request.url.split('?')[0], + controller: context.getClass().name, + handler: context.getHandler().name, + ip: request.ip, + referer: request.headers['referer'] || 'direct', + user_agent: request.headers['user-agent'] || 'unknown', + triggered_by: 'interceptor', + }; + + this.logger.log(`Incoming ${baseCtx.method} ${baseCtx.url}`, { + ...baseCtx, + type: 'request', + body: this.sanitize(request.body), + query: request.query, + }); + + return next.handle().pipe( + tap(() => { + const delay_num = Date.now() - startTime; + + this.logger.log(`${baseCtx.method} ${baseCtx.path} | 200 | ${delay_num}ms`, { + ...baseCtx, + type: 'response', + status_code: 200, + delay_num, + }); + }), + catchError((err) => { + const delay_num = Date.now() - startTime; + const status_code = err.status || err.statusCode || 500; + + this.logger.error( + `${baseCtx.method} ${baseCtx.path} | ${status_code} | ${delay_num}ms`, + { + ...baseCtx, + type: 'error', + status_code, + delay_num, + stack: err.stack, + error_details: err.response || err.message, + }, + ); + + return throwError(() => err); + }), + ); + } + + private sanitize(data: T) { + if (!data || typeof data !== 'object') return data; + if (Array.isArray(data)) return data.map((v) => this.sanitize(v)); + + const cleanData = JSON.parse(JSON.stringify(data)); + + return Object.keys(cleanData).reduce((acc, key) => { + const isSensitive = this.sensitiveFields.some((field) => + key.toLowerCase().includes(field), + ); + + if (isSensitive) { + acc[key] = '***'; + } else if (typeof cleanData[key] === 'object') { + acc[key] = this.sanitize(cleanData[key]); + } else { + acc[key] = cleanData[key]; + } + return acc; + }, {}); + } +} + +/** + * Represents a structured application log payload for Grafana Loki. + * This object is flattened to ensure each property is indexed as a top-level label/column. + * + * @typedef {Object} TLog + */ +export type TLog = { + /** + * The severity level of the log. + * Used by Grafana to color-code rows and for alerting. + * @type {'info' | 'error' | 'warn'} + */ + level: 'info' | 'error' | 'warn'; + /** + * Human-readable summary of the event. + * @example 'Request completed POST /v1/auth/sign-in | 200 | 145ms' + * @type {string} + */ + message: string; + /** + * Event occurrence time in ISO 8601 format. + * @example '2026-05-09T01:17:29.000Z' + * @type {string} + */ + timestamp: string; + /** + * Unique identifier for the HTTP request (e.g., UUID, NanoID). + * Used to correlate all logs produced within a single request lifecycle. + * @type {string} + */ + request_id: string; + /** + * The system component that triggered the log entry. + * @type {'interceptor' | 'filter_exception' | 'guard' | 'service'} + */ + triggered_by: 'interceptor' | 'filter_exception' | 'guard' | 'service'; + /** + * The logical type of the event within the request/response flow. + * @type {'request' | 'response' | 'error' | 'system'} + */ + type: 'request' | 'response' | 'error' | 'system'; + /** + * The HTTP method used for the request. + * @type {'POST' | 'GET' | 'DELETE' | 'PATCH' | 'PUT' | 'OPTIONS' | 'HEAD'} + */ + method: 'POST' | 'GET' | 'DELETE' | 'PATCH' | 'PUT' | 'OPTIONS' | 'HEAD'; + /** + * The full URL of the request, including query parameters. + * @example '/v1/auth/sign-in?source=mobile' + * @type {string} + */ + url: string; + /** + * The sanitized API path, including versioning but excluding query parameters. + * Ideal for aggregating statistics per endpoint. + * @example '/v1/auth/sign-in' + * @type {string} + */ + path: string; + /** + * The HTTP status code returned to the client. + * @example 200 + * @type {number} + */ + status_code: number; + /** + * Request processing time in milliseconds. + * Note: Typically undefined for entries with type 'request'. + * @type {number} + */ + delay_num?: number; + /** + * The client's IP address. + * @type {string} + */ + ip: string; + /** + * The client's application or browser identification string. + * @type {string} + */ + user_agent: string; + /** + * The name of the NestJS controller handling the request. + * @example 'AuthController' + * @type {string} + */ + controller: string; + /** + * The name of the specific controller method (handler). + * @example 'signIn' + * @type {string} + */ + handler: string; + /** + * The error stack trace. Only populated when level is 'error'. + * @type {string} + */ + stack?: string; + /** + * Additional contextual data for debugging (e.g., Zod validation issues, DB error details). + * @type {any} + */ + error_details?: any; +}; diff --git a/libs/bootstrap/src/setups/swagger.ts b/libs/bootstrap/src/setups/swagger.ts new file mode 100644 index 0000000..b18afe4 --- /dev/null +++ b/libs/bootstrap/src/setups/swagger.ts @@ -0,0 +1,59 @@ +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { cleanupOpenApiDoc } from 'nestjs-zod'; +import type { NestFastifyApplication } from '@nestjs/platform-fastify'; +import type { SwaggerOptions } from '../interfaces'; +import { SWAGGER_DEFAULTS } from '../configs/swagger'; +import { GlobalErrorResponse } from '@shared/error/schema'; + +async function getCustomCSS() { + const rawUrl = 'https://gist.githubusercontent.com/soorq/f745e5c44cfe27aa928048d6d4ccb18a/raw'; + const res = await fetch(rawUrl); + if (!res.ok) { + return ''; + } + return res.text(); +} + +export async function setupSwagger(app: NestFastifyApplication, options: SwaggerOptions = {}) { + const { title, description, version, path, server } = { + ...SWAGGER_DEFAULTS, + ...options, + }; + + const { domain, port, stage } = server || {}; + + const builder = new DocumentBuilder() + .setTitle(title) + .setDescription(description) + .setVersion(version) + .addBearerAuth(); + + if ((!stage || !domain) && port) { + builder.addServer(`http://localhost:${port}`, 'Local'); + } + if (stage) { + builder.addServer(`https://api.${stage}`, 'Staging'); + } + if (domain) { + builder.addServer(`https://api.${domain}`, 'Production'); + } + + const document = SwaggerModule.createDocument(app, builder.build(), { + extraModels: [GlobalErrorResponse.Output], + }); + + const customCss = await getCustomCSS(); + + SwaggerModule.setup(path, app, cleanupOpenApiDoc(document), { + jsonDocumentUrl: `${path}/s/json`, + yamlDocumentUrl: `${path}/s/yaml`, + useGlobalPrefix: true, + ui: true, + customCss, + swaggerOptions: { + persistAuthorization: true, + tagsSorter: 'alpha', + operationsSorter: 'alpha', + }, + }); +} diff --git a/libs/bootstrap/src/setups/throttler.ts b/libs/bootstrap/src/setups/throttler.ts new file mode 100644 index 0000000..29f683b --- /dev/null +++ b/libs/bootstrap/src/setups/throttler.ts @@ -0,0 +1,19 @@ +import { Module, type Type } from '@nestjs/common'; +import type { ThrottlerModuleOptions } from '@nestjs/throttler'; +import { APP_GUARD } from '@nestjs/core'; +import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; + +export function setupThrottler(module: Type, options: ThrottlerModuleOptions) { + @Module({ + imports: [module, ThrottlerModule.forRoot(options)], + providers: [ + { + provide: APP_GUARD, + useClass: ThrottlerGuard, + }, + ], + }) + class RootModule {} + + return RootModule; +} diff --git a/libs/bootstrap/tsconfig.lib.json b/libs/bootstrap/tsconfig.lib.json new file mode 100644 index 0000000..909ede0 --- /dev/null +++ b/libs/bootstrap/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/bootstrap" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/config/src/config.module.ts b/libs/config/src/config.module.ts new file mode 100644 index 0000000..48e55bf --- /dev/null +++ b/libs/config/src/config.module.ts @@ -0,0 +1,45 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule as NestConfigModule } from '@nestjs/config'; +import * as path from 'path'; +import { ConfigSchema } from './config.schema'; +import { ZodError } from 'zod/v4'; + +const validateConfig = (config: Record) => { + try { + return ConfigSchema.parse(config); + } catch (error) { + if (error instanceof ZodError) { + console.group('\nENVIRONMENT_VALIDATION_ERROR\n'); + + error.issues.forEach((issue) => { + const field = issue.path.join('.') || 'ROOT'; + + console.group(`Field: ${field}`); + console.error(`Message: ${issue.message}`); + console.error(`Code: ${issue.code.toUpperCase()}`); + console.groupEnd(); + console.error('\n'); + }); + + console.groupEnd(); + + throw new Error('Invalid environment configuration'); + } + throw error; + } +}; + +@Module({ + imports: [ + NestConfigModule.forRoot({ + isGlobal: true, + envFilePath: path.resolve(process.cwd(), '.env'), + validate: validateConfig, + validationOptions: { + abortEarly: true, + }, + }), + ], + exports: [NestConfigModule], +}) +export class ConfigModule {} diff --git a/libs/config/src/config.schema.ts b/libs/config/src/config.schema.ts new file mode 100644 index 0000000..0a6fd41 --- /dev/null +++ b/libs/config/src/config.schema.ts @@ -0,0 +1,96 @@ +import { z } from 'zod/v4'; +import { jwtSecretValidation } from './helpers/jwt-secren-validation'; + +const timeStringSchema = z.string().regex(/^[0-9]+[smhdw]$/, { + message: 'Invalid time format. Use: s, m, h, d, w (e.g., 15m, 24h, 30d)', +}); + +export const ConfigSchema = z.object({ + PORT: z.coerce.number().default(3000), + NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), + COOKIE_SECRET: z.string({ error: 'COOKIE_SECRET is missing' }), + DB_SCHEMA: z.string({ error: 'DB_SCHEMA is missing' }), + DATABASE_URL: z.string().nonempty('DATABASE_URL must be a valid connection string'), + REDIS_HOST: z.string().default('redis'), + REDIS_PORT: z.coerce.number().optional().default(6379), + REDIS_PASSWORD: z.string().optional(), + IMAGOR_SECRET: z.string().optional(), + IMAGOR_URL: z.string().nonempty('Укажите адрес сервера Imagor'), + DOMAIN: z + .string() + .toLowerCase() + .refine((val) => !val || /^[a-z0-9.-]+\.[a-z]{2,}$/.test(val), { + message: 'DOMAIN must be a valid hostname (e.g., example.com)', + }) + .optional(), + STAGE_DOMAIN: z + .string() + .toLowerCase() + .refine((val) => !val || /^[a-z0-9.-]+\.[a-z]{2,}$/.test(val), { + message: 'STAGE_DOMAIN must be a valid hostname', + }) + .optional(), + CORS_ALLOWED_ORIGINS: z + .string() + .min(1, "CORS_ALLOWED_ORIGINS can't be empty") + .transform((val) => val.split(',').map((s) => s.trim())) + .pipe(z.array(z.string().url('Each origin must be a valid URL'))), + JWT_AUDIENCE: z + .string({ + error: 'JWT_AUDIENCE is required', + }) + .min(1), + JWT_ACCESS_SECRET: z.string().refine(jwtSecretValidation, { + message: + 'JWT_ACCESS_SECRET must be at least 32 characters long OR contain at least 5 words separated by hyphens', + }), + JWT_REFRESH_SECRET: z.string().refine(jwtSecretValidation, { + message: + 'JWT_REFRESH_SECRET must be at least 32 characters long OR contain at least 5 words separated by hyphens', + }), + JWT_ACCESS_EXPIRES_IN: timeStringSchema.default('15m'), + JWT_REFRESH_EXPIRES_IN: timeStringSchema.default('30d'), + MAIL_HOST: z + .string({ + error: 'Mail server host (MAIL_HOST) is not specified', + }) + .min(1, 'MAIL_HOST cannot be empty'), + MAIL_PORT: z.coerce.number({ + error: 'Mail port (MAIL_PORT) is not specified', + }), + MAIL_USER: z + .string({ + error: 'Sender email (MAIL_USER) is not specified', + }) + .email('MAIL_USER must be a valid email address'), + MAIL_PASSWORD: z + .string({ + error: 'Mail password (MAIL_PASSWORD) is required', + }) + .min(1, 'Mail password cannot be empty'), + MAIL_FROM_NAME: z + .string({ + error: 'Sender name (MAIL_FROM_NAME) is not specified', + }) + .min(1, 'Sender name cannot be empty'), + MAIL_FROM_EMAIL: z.string().email('Invalid MAIL_FROM_EMAIL format').optional(), + S3_BUCKET_NAME: z + .string({ + error: "S3_BUCKET_NAME is required. Example: 'avatars'", + }) + .min(1), + S3_ENDPOINT: z + .string({ + error: "S3_ENDPOINT is required. Example: 'http://localhost:9000'", + }) + .url('S3_ENDPOINT must be a valid URL'), + S3_REGION: z.string().default('us-east-1'), + S3_ACCESS_KEY: z.string({ + error: 'S3_ACCESS_KEY is missing (MinIO root user or IAM user)', + }), + S3_SECRET_KEY: z.string({ + error: 'S3_SECRET_KEY is missing (MinIO root password or IAM secret)', + }), +}); + +export type Config = z.infer; diff --git a/libs/config/src/config.types.d.ts b/libs/config/src/config.types.d.ts new file mode 100644 index 0000000..c47f988 --- /dev/null +++ b/libs/config/src/config.types.d.ts @@ -0,0 +1,15 @@ +import '@nestjs/config'; +import { Config } from './config.schema'; + +declare module '@nestjs/config' { + interface ConfigService<_K = unknown, _WasValidated extends boolean = false> { + /** + * Переопределяем метод get, чтобы он предлагал ключи из нашей схемы + */ + get(key: T): Config[T]; + /** + * Переопределяем метод getOrThrow, чтобы он предлагал ключи из нашей схемы + */ + getOrThrow(key: T): Config[T]; + } +} diff --git a/libs/config/src/helpers/jwt-secren-validation.ts b/libs/config/src/helpers/jwt-secren-validation.ts new file mode 100644 index 0000000..27a4e18 --- /dev/null +++ b/libs/config/src/helpers/jwt-secren-validation.ts @@ -0,0 +1,7 @@ +export function jwtSecretValidation(val: string) { + const isLongEnough = val.length >= 32; + const words = val.split('-'); + const hasFiveWords = words.length >= 5 && words.every((word) => word.length > 0); + + return isLongEnough || hasFiveWords; +} diff --git a/libs/config/src/index.ts b/libs/config/src/index.ts new file mode 100644 index 0000000..0c71077 --- /dev/null +++ b/libs/config/src/index.ts @@ -0,0 +1,2 @@ +export * from './config.module'; +export * from './config.schema'; diff --git a/libs/config/tsconfig.lib.json b/libs/config/tsconfig.lib.json new file mode 100644 index 0000000..855908d --- /dev/null +++ b/libs/config/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/config" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/database/src/constants.ts b/libs/database/src/constants.ts new file mode 100644 index 0000000..087de13 --- /dev/null +++ b/libs/database/src/constants.ts @@ -0,0 +1,2 @@ +export const DATABASE_SERVICE = 'DATABASE_SERVICE'; +export const SQL_CLIENT = 'SQL_CLIENT'; diff --git a/libs/database/src/database-healthcheck.service.ts b/libs/database/src/database-healthcheck.service.ts new file mode 100644 index 0000000..875d2ed --- /dev/null +++ b/libs/database/src/database-healthcheck.service.ts @@ -0,0 +1,20 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { SQL_CLIENT } from '@libs/database/constants'; +import { Sql } from 'postgres'; + +@Injectable() +export class DatabaseHealthcheckService { + constructor( + @Inject(SQL_CLIENT) + private readonly sql: Sql, + ) {} + + async isAlive() { + try { + await this.sql`SELECT 1`; + return true; + } catch { + return false; + } + } +} diff --git a/libs/database/src/database.module-definition.ts b/libs/database/src/database.module-definition.ts new file mode 100644 index 0000000..a9cbc33 --- /dev/null +++ b/libs/database/src/database.module-definition.ts @@ -0,0 +1,16 @@ +import { ConfigurableModuleBuilder } from '@nestjs/common'; +import type { DatabaseModuleOptions } from './interfaces'; + +export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = + new ConfigurableModuleBuilder() + .setClassMethodName('register') + .setExtras( + { + global: false, + }, + (definition, extras) => ({ + ...definition, + global: extras.global, + }), + ) + .build(); diff --git a/libs/database/src/database.module.ts b/libs/database/src/database.module.ts new file mode 100644 index 0000000..a21f90a --- /dev/null +++ b/libs/database/src/database.module.ts @@ -0,0 +1,82 @@ +import { Inject, Logger, Module, OnApplicationShutdown } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import { DATABASE_SERVICE, SQL_CLIENT } from './constants'; +import { MigrationService } from './migration.service'; +import { + ConfigurableModuleClass, + MODULE_OPTIONS_TOKEN, + OPTIONS_TYPE, +} from './database.module-definition'; +import { DatabaseHealthcheckService } from '@libs/database/database-healthcheck.service'; + +@Module({ + providers: [ + MigrationService, + DatabaseHealthcheckService, + { + provide: SQL_CLIENT, + inject: [ConfigService, MODULE_OPTIONS_TOKEN], + useFactory: (configService: ConfigService, opts: typeof OPTIONS_TYPE) => { + const baseUrl = configService.getOrThrow('DATABASE_URL'); + const url = new URL(baseUrl); + + if (opts.schemaName) { + url.searchParams.set('options', `-c search_path=${opts.schemaName}`); + } + + return postgres(url.toString(), { + onnotice: (msg) => new Logger('PostgresJS').verbose(msg), + backoff: (attempt) => Math.min(attempt * 100, 3000), + target_session_attrs: 'read-write', + publications: 'alltables', + connect_timeout: 2, + idle_timeout: 5, + max_lifetime: 60 * 60, + keep_alive: 30, + transform: { + undefined: null, + }, + ...opts.pool, + }); + }, + }, + { + provide: DATABASE_SERVICE, + inject: [SQL_CLIENT, MODULE_OPTIONS_TOKEN], + useFactory: (sql: postgres.Sql, opts: typeof OPTIONS_TYPE) => { + const logger = new Logger('Drizzle'); + + return drizzle(sql, { + schema: opts.schema, + logger: opts.logging + ? { + logQuery(query, params) { + logger.debug(`SQL: ${query}`); + if (params?.length) + logger.debug(`Params: ${JSON.stringify(params)}`); + }, + } + : false, + }); + }, + }, + ], + exports: [DATABASE_SERVICE, DatabaseHealthcheckService], +}) +export class DatabaseModule extends ConfigurableModuleClass implements OnApplicationShutdown { + private readonly logger = new Logger(DatabaseModule.name); + + constructor( + @Inject(SQL_CLIENT) + private readonly sql: postgres.Sql, + ) { + super(); + } + + async onApplicationShutdown() { + this.logger.log('Closing database connections...'); + await this.sql.end(); + } +} diff --git a/libs/database/src/index.ts b/libs/database/src/index.ts new file mode 100644 index 0000000..e258d47 --- /dev/null +++ b/libs/database/src/index.ts @@ -0,0 +1,3 @@ +export * from './database.module'; +export { DATABASE_SERVICE } from './constants'; +export type { DatabaseService } from './interfaces'; diff --git a/libs/database/src/interfaces/index.ts b/libs/database/src/interfaces/index.ts new file mode 100644 index 0000000..5e3751a --- /dev/null +++ b/libs/database/src/interfaces/index.ts @@ -0,0 +1 @@ +export * from './module.interface'; diff --git a/libs/database/src/interfaces/module.interface.ts b/libs/database/src/interfaces/module.interface.ts new file mode 100644 index 0000000..bbe4021 --- /dev/null +++ b/libs/database/src/interfaces/module.interface.ts @@ -0,0 +1,62 @@ +import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import type { Options } from 'postgres'; + +export interface DatabaseModuleOptions { + /** + * Схема базы данных PostgreSQL (устанавливает `search_path`). + * * Все запросы без явного указания схемы будут выполняться в этом пространстве имен. + * @default 'public' + * @example 'auth_service' + */ + schemaName?: string; + + /** + * Объект схемы Drizzle, содержащий определения таблиц и связей. + * * Рекомендуется импортировать целиком: `import * as schema from './schema'`. + * @example schema + */ + schema: Record; + + /** + * Настройки драйвера `postgres.js`. + * * Позволяет настроить пул соединений, таймауты и SSL. + * * **Внимание:** Параметры отличаются от драйвера `pg`. + * @see https://github.com/porsager/postgres#options + * @example { max: 20, idle_timeout: 30, connect_timeout: 5 } + */ + pool?: Options; + + /** + * Включение или выключение логирования SQL-запросов в консоль через NestJS Logger. + * @default false + */ + logging?: boolean; + + /** + * Флаг для автоматического запуска миграций при старте приложения. + * * Полезно для локальной разработки и стейджинга. + * @default true + */ + runMigrations?: boolean; + + /** + * Абсолютный путь к директории с файлами миграций (SQL или JS/TS). + * * Если не указано, используется путь `./migrations` от корня проекта. + * @default path.resolve(process.cwd(), 'migrations') + */ + migrationsPath?: string; +} + +/** + * Тип для внедрения Drizzle ORM в репозитории. + * Использует драйвер postgres-js под капотом. + * + * @example + * // В репозитории: + * constructor( + * @Inject(DATABASE_SERVICE) private readonly db: DatabaseService + * ) {} + * + * @template TSchema - Тип вашей схемы данных (например, `typeof schema`). + */ +export type DatabaseService> = PostgresJsDatabase; diff --git a/libs/database/src/migration.service.ts b/libs/database/src/migration.service.ts new file mode 100644 index 0000000..2aeefdc --- /dev/null +++ b/libs/database/src/migration.service.ts @@ -0,0 +1,37 @@ +import { Inject, Injectable, OnModuleInit, Logger } from '@nestjs/common'; +import { migrate } from 'drizzle-orm/postgres-js/migrator'; +import { DATABASE_SERVICE } from './constants'; +import type { DatabaseService } from './interfaces'; +import * as path from 'path'; +import { MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } from './database.module-definition'; + +@Injectable() +export class MigrationService implements OnModuleInit { + private readonly logger = new Logger(MigrationService.name); + + constructor( + @Inject(DATABASE_SERVICE) + private readonly db: DatabaseService, + @Inject(MODULE_OPTIONS_TOKEN) + private readonly options: typeof OPTIONS_TYPE, + ) {} + + async onModuleInit() { + if (this.options.runMigrations === false) { + return; + } + + const migrationsFolder = path.resolve(process.cwd(), 'migrations'); + + this.logger.debug('Checking for database migrations...'); + try { + await migrate(this.db, { + migrationsFolder, + }); + this.logger.debug('Migrations completed or already up to date'); + } catch (error) { + this.logger.error('Migration failed', error); + process.exit(1); + } + } +} diff --git a/libs/database/tsconfig.lib.json b/libs/database/tsconfig.lib.json new file mode 100644 index 0000000..0add6d4 --- /dev/null +++ b/libs/database/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/database" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/health/src/controller/health.controller.ts b/libs/health/src/controller/health.controller.ts new file mode 100644 index 0000000..0530359 --- /dev/null +++ b/libs/health/src/controller/health.controller.ts @@ -0,0 +1,44 @@ +import { Controller, Get, HttpStatus } from '@nestjs/common'; +import { SkipThrottle } from '@nestjs/throttler'; +import { HealthService } from '../health.service'; +import { GetHealthSwagger, GetPingSwagger } from './health.swagger'; +import { ApiTags } from '@nestjs/swagger'; +import { BaseException } from '@shared/error'; + +@SkipThrottle() +@Controller() +@ApiTags('System') +export class HealthController { + constructor(private readonly service: HealthService) {} + + @Get('health') + @GetHealthSwagger() + async checkHealth() { + const pingData = await this.service.getHealthData(); + + if (!pingData.status) { + throw new BaseException( + { + code: 'SERVICE_UNHEALTHY', + message: `Сервис ${pingData.service} временно недоступен или работает некорректно`, + details: [ + { + target: pingData.service, + status: pingData.status, + timestamp: new Date().toISOString(), + }, + ], + }, + HttpStatus.SERVICE_UNAVAILABLE, + ); + } + + return 'healthy'; + } + + @Get('ping') + @GetPingSwagger() + async ping() { + return this.service.getHealthData(); + } +} diff --git a/libs/health/src/controller/health.controlller.spec.ts b/libs/health/src/controller/health.controlller.spec.ts new file mode 100644 index 0000000..5865763 --- /dev/null +++ b/libs/health/src/controller/health.controlller.spec.ts @@ -0,0 +1,67 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { HealthController } from './health.controller'; +import { HttpStatus, Logger } from '@nestjs/common'; + +describe('HealthController', () => { + let controller: HealthController; + let healthServiceMock: { getHealthData: ReturnType }; + + const SERVICE_NAME = 'MyService'; + + beforeEach(() => { + healthServiceMock = { + getHealthData: vi.fn(), + }; + + controller = new HealthController(healthServiceMock as any); + + vi.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined); + }); + + it('should throw SERVICE_UNAVAILABLE when service status is false (down)', async () => { + healthServiceMock.getHealthData.mockResolvedValue({ + service: SERVICE_NAME, + status: false, + components: { database: 'down' }, + }); + + await expect(controller.checkHealth()).rejects.toMatchObject({ + status: HttpStatus.SERVICE_UNAVAILABLE, + response: { + code: 'SERVICE_UNHEALTHY', + message: expect.stringContaining(SERVICE_NAME), + details: expect.arrayContaining([ + expect.objectContaining({ + target: SERVICE_NAME, + status: false, + }), + ]), + }, + }); + }); + + it('should return "healthy" when status is true', async () => { + healthServiceMock.getHealthData.mockResolvedValue({ service: SERVICE_NAME, status: true }); + + const result = await controller.checkHealth(); + + expect(result).toBe('healthy'); + }); + + describe('ping', () => { + it('should return the full health payload', async () => { + const mockPayload = { + service: SERVICE_NAME, + status: true, + components: {}, + time: { uptime: '1h 0m 0s' }, + }; + healthServiceMock.getHealthData.mockResolvedValue(mockPayload); + + const result = await controller.ping(); + + expect(result).toEqual(mockPayload); + expect(healthServiceMock.getHealthData).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/libs/health/src/controller/health.swagger.ts b/libs/health/src/controller/health.swagger.ts new file mode 100644 index 0000000..2271969 --- /dev/null +++ b/libs/health/src/controller/health.swagger.ts @@ -0,0 +1,26 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { HealthResponse } from '../dtos'; + +export const GetHealthSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Краткий статус (Health Check)', + description: 'Используется внешними системами для проверки доступности сервиса.', + }), + ApiResponse({ status: 200, description: 'Сервис работает нормально', type: String }), + ApiResponse({ status: 503, description: 'Сервис недоступен или критическая ошибка' }), + ); + +export const GetPingSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Детальный дамп состояния', + description: 'Возвращает аптайм, время старта и метрики памяти.', + }), + ApiResponse({ + status: 200, + description: 'Полная статистика сервиса', + type: HealthResponse.Output, + }), + ); diff --git a/libs/health/src/dtos/health.dto.ts b/libs/health/src/dtos/health.dto.ts new file mode 100644 index 0000000..f57390a --- /dev/null +++ b/libs/health/src/dtos/health.dto.ts @@ -0,0 +1,23 @@ +import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; + +const HealthResponseSchema = z.object({ + service: z.string().describe('Название сервиса'), + status: z.boolean().describe('Общий статус работоспособности (true — ок, false — есть сбои)'), + components: z + .record(z.string(), z.enum(['up', 'down'])) + .describe('Статусы отдельных компонентов (например, database, redis)'), + info: z.object({ + version: z.string().describe('Версия приложения'), + node: z.string().describe('Версия Node.js'), + }), + time: z.object({ + now: z.string().datetime().describe('Текущее время сервера (ISO)'), + startedAt: z.string().datetime().describe('Время старта сервера (ISO)'), + uptime: z.string().describe('Аптайм в читаемом формате (h m s)'), + uptimeSeconds: z.number().describe('Аптайм в секундах'), + }), + loaded: z.string().describe('Средняя нагрузка на CPU за последнюю минуту (Load Average)'), +}); + +export class HealthResponse extends createZodDto(HealthResponseSchema) {} diff --git a/libs/health/src/dtos/index.ts b/libs/health/src/dtos/index.ts new file mode 100644 index 0000000..718605a --- /dev/null +++ b/libs/health/src/dtos/index.ts @@ -0,0 +1 @@ +export { HealthResponse } from './health.dto'; diff --git a/libs/health/src/health.module-definition.ts b/libs/health/src/health.module-definition.ts new file mode 100644 index 0000000..c0c7639 --- /dev/null +++ b/libs/health/src/health.module-definition.ts @@ -0,0 +1,16 @@ +import { ConfigurableModuleBuilder } from '@nestjs/common'; +import { HealthModuleOptions } from './interfaces'; + +export const { ASYNC_OPTIONS_TYPE, ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } = + new ConfigurableModuleBuilder() + .setClassMethodName('register') + .setExtras( + { + global: false, + }, + (definition, extras) => ({ + ...definition, + global: extras.global, + }), + ) + .build(); diff --git a/libs/health/src/health.module.ts b/libs/health/src/health.module.ts new file mode 100644 index 0000000..3206758 --- /dev/null +++ b/libs/health/src/health.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { HealthController } from './controller/health.controller'; +import { HealthService } from './health.service'; +import { ConfigurableModuleClass } from './health.module-definition'; + +@Module({ + controllers: [HealthController], + providers: [HealthService], + exports: [HealthService], +}) +export class HealthModule extends ConfigurableModuleClass {} diff --git a/libs/health/src/health.service.spec.ts b/libs/health/src/health.service.spec.ts new file mode 100644 index 0000000..802b89f --- /dev/null +++ b/libs/health/src/health.service.spec.ts @@ -0,0 +1,92 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +vi.mock('os', async () => { + const actual = await vi.importActual('os'); + return { + ...actual, + loadavg: () => [1.23, 0.5, 0.1], + }; +}); +import { HealthService } from './health.service'; +import type { HealthModuleOptions } from './interfaces'; + +describe('HealthService', () => { + const BASE_TIME = new Date('2026-05-15T10:00:00.000Z'); + + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(BASE_TIME); + vi.spyOn(process, 'uptime').mockReturnValue(3661); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it('returns healthy payload when all indicators are ok', async () => { + const options: HealthModuleOptions = { + serviceName: 'MyService', + version: 'v2.0.0', + indicators: { + database: () => true, + redis: async () => true, + }, + }; + + const service = new HealthService(options); + const data = await service.getHealthData(); + + expect(data).toMatchObject({ + service: 'MyService', + status: true, + components: { database: 'up', redis: 'up' }, + info: { version: 'v2.0.0', node: process.version }, + time: { + now: BASE_TIME.toISOString(), + startedAt: BASE_TIME.toISOString(), + uptime: '1h 1m 1s', + uptimeSeconds: 3661, + }, + loaded: '1.23', + }); + }); + + it('marks status as false when any indicator fails or throws', async () => { + const options: HealthModuleOptions = { + serviceName: 'MyService', + indicators: { + cache: () => true, + database: () => false, + storage: () => { + throw new Error('boom'); + }, + }, + }; + + const service = new HealthService(options); + const data = await service.getHealthData(); + + expect(data.status).toBe(false); + expect(data.components).toEqual({ database: 'down', storage: 'down', cache: 'up' }); + }); + + it('marks indicator as down on timeout', async () => { + const options: HealthModuleOptions = { + serviceName: 'MyService', + indicators: { + http: () => new Promise(() => undefined), + }, + }; + + const service = new HealthService(options); + const resultPromise = service.getHealthData(); + + await vi.advanceTimersByTimeAsync(5000); + + const data = await resultPromise; + + expect(data.status).toBe(false); + expect(data.components).toEqual({ http: 'down' }); + }); +}); diff --git a/libs/health/src/health.service.ts b/libs/health/src/health.service.ts new file mode 100644 index 0000000..c5de0a3 --- /dev/null +++ b/libs/health/src/health.service.ts @@ -0,0 +1,69 @@ +import { Inject, Injectable } from '@nestjs/common'; +import * as os from 'os'; +import { MODULE_OPTIONS_TOKEN } from './health.module-definition'; +import type { HealthModuleOptions } from './interfaces'; + +@Injectable() +export class HealthService { + private readonly startTime: Date; + + constructor( + @Inject(MODULE_OPTIONS_TOKEN) + private readonly options: HealthModuleOptions, + ) { + this.startTime = new Date(); + } + + async getHealthData() { + const { serviceName, version = 'v1.0.0', indicators = {} } = this.options; + + const uptimeSeconds = Math.floor(process.uptime()); + + const results = await Promise.all( + Object.entries(indicators).map(async ([name, check]) => { + let timeoutId: NodeJS.Timeout; + + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new Error('Timeout')), 5000); + }); + + try { + const result = await Promise.race([check(), timeoutPromise]); + return { name, ok: !!result }; + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + return { name, ok: false, error: message }; + } finally { + clearTimeout(timeoutId); + } + }), + ); + + const isAllOk = results.every((r) => r.ok); + const components = Object.fromEntries(results.map((r) => [r.name, r.ok ? 'up' : 'down'])); + + return { + service: serviceName, + status: isAllOk, + components, + info: { + version, + node: process.version, + }, + time: { + now: new Date().toISOString(), + startedAt: this.startTime.toISOString(), + uptime: this.formatUptime(uptimeSeconds), + uptimeSeconds: uptimeSeconds, + }, + loaded: os.loadavg()[0].toFixed(2), + }; + } + + private formatUptime(seconds: number) { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + return `${h}h ${m}m ${s}s`; + } +} diff --git a/libs/health/src/index.ts b/libs/health/src/index.ts new file mode 100644 index 0000000..f0f0421 --- /dev/null +++ b/libs/health/src/index.ts @@ -0,0 +1 @@ +export * from './health.module'; diff --git a/libs/health/src/interfaces/index.ts b/libs/health/src/interfaces/index.ts new file mode 100644 index 0000000..22f53a1 --- /dev/null +++ b/libs/health/src/interfaces/index.ts @@ -0,0 +1 @@ +export type * from './module.interface'; diff --git a/libs/health/src/interfaces/module.interface.ts b/libs/health/src/interfaces/module.interface.ts new file mode 100644 index 0000000..054caaf --- /dev/null +++ b/libs/health/src/interfaces/module.interface.ts @@ -0,0 +1,9 @@ +export type HealthIndicatorsServices = 'redis' | 'database' | 'storage' | 'http'; +export type HealthIndicatorKey = HealthIndicatorsServices | (string & NonNullable); +export type HealthIndicatorFn = () => boolean | Promise; + +export interface HealthModuleOptions { + serviceName: string; + version?: string; + indicators?: Partial>; +} diff --git a/libs/health/tsconfig.lib.json b/libs/health/tsconfig.lib.json new file mode 100644 index 0000000..5c0ebaa --- /dev/null +++ b/libs/health/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/health" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/imagor/src/imagor.module-definition.ts b/libs/imagor/src/imagor.module-definition.ts new file mode 100644 index 0000000..b958a9b --- /dev/null +++ b/libs/imagor/src/imagor.module-definition.ts @@ -0,0 +1,16 @@ +import { ConfigurableModuleBuilder } from '@nestjs/common'; +import type { ImagorModuleOptions } from './interfaces'; + +export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = + new ConfigurableModuleBuilder() + .setClassMethodName('forRoot') + .setExtras( + { + global: true, + }, + (definition, extras) => ({ + ...definition, + global: extras.global, + }), + ) + .build(); diff --git a/libs/imagor/src/imagor.module.ts b/libs/imagor/src/imagor.module.ts new file mode 100644 index 0000000..763626a --- /dev/null +++ b/libs/imagor/src/imagor.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ConfigurableModuleClass } from './imagor.module-definition'; +import { ImagorService } from './imagor.service'; + +@Module({ + providers: [ImagorService], + exports: [ImagorService], +}) +export class ImagorModule extends ConfigurableModuleClass {} diff --git a/libs/imagor/src/imagor.service.ts b/libs/imagor/src/imagor.service.ts new file mode 100644 index 0000000..bcfc28d --- /dev/null +++ b/libs/imagor/src/imagor.service.ts @@ -0,0 +1,84 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { MODULE_OPTIONS_TOKEN } from './imagor.module-definition'; +import type { ImagorModuleOptions, Filters } from './interfaces'; +import { createHmac } from 'crypto'; +import { HttpService } from '@nestjs/axios'; +import { ImagorPathBuilder } from './utils'; +import { catchError, firstValueFrom, throwError } from 'rxjs'; +import { AxiosError } from 'axios'; + +@Injectable() +export class ImagorService { + private logger = new Logger(ImagorService.name); + + constructor( + @Inject(MODULE_OPTIONS_TOKEN) + private options: ImagorModuleOptions, + private readonly http: HttpService, + ) {} + + /** + * Выполняет GET запрос к Imagor с применением фильтров и пресетов + * @param path Путь к исходному файлу в хранилище + * @param presetOrFilters Название пресета или объект с фильтрами (width, height, smart и т.д.) + */ + async get(path: string, presetOrFilters?: string | Filters): Promise { + const host = this.options.url.replace(/\/+$/, ''); + const transformPath = this.buildTransformPath(path, presetOrFilters); + const signature = this.getFullSignedPath(transformPath); + const url = `${host}/${signature}`; + + try { + this.logger.debug(url); + const response = await firstValueFrom( + this.http.get(url, { responseType: 'arraybuffer' }).pipe( + catchError((error: AxiosError) => { + console.error('Imagor Get Error:', error.response?.data || error.message); + return throwError(() => error); + }), + ), + ); + + return Buffer.from(response.data); + } catch (error) { + throw error; + } + } + + private buildTransformPath(path: string, presetOrFilters?: string | Filters): string { + const builder = new ImagorPathBuilder(path, this.options.storageRoot); + + const globalFilters = this.options.filters || {}; + let localFilters: Filters = {}; + + if (typeof presetOrFilters === 'string') { + localFilters = this.options.presets?.[presetOrFilters] || {}; + } else if (presetOrFilters) { + localFilters = presetOrFilters; + } + + const merged = { ...globalFilters, ...localFilters }; + + if (merged.width || merged.height) builder.resize(merged.width ?? 0, merged.height ?? 0); + if (merged.smart) builder.smart(true); + if (merged.fit) builder.fit(merged.fit); + + builder.applyFilters(merged); + + return builder.build(); + } + + private getFullSignedPath(path: string): string { + if (!this.options.secret) { + return `unsafe/${path}`; + } + + const hash = createHmac('sha1', this.options.secret) + .update(path) + .digest('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_'); + + return `${hash}/${path}`; + } +} diff --git a/libs/imagor/src/index.ts b/libs/imagor/src/index.ts new file mode 100644 index 0000000..f32b3e4 --- /dev/null +++ b/libs/imagor/src/index.ts @@ -0,0 +1,2 @@ +export { ImagorModule } from './imagor.module'; +export { ImagorService } from './imagor.service'; diff --git a/libs/imagor/src/interfaces/filters.interface.ts b/libs/imagor/src/interfaces/filters.interface.ts new file mode 100644 index 0000000..1fe3471 --- /dev/null +++ b/libs/imagor/src/interfaces/filters.interface.ts @@ -0,0 +1,173 @@ +import type { Format } from './formats.interface'; + +/** + * Режимы вписывания изображения в заданные размеры. + * - 'fit-in': Вписывает изображение целиком, сохраняя пропорции (могут появиться пустые поля). + * - 'stretch': Растягивает изображение строго под размеры, игнорируя пропорции. + * - 'dashed': Специфический режим Imagor для обработки прозрачности или границ. + */ +type Fit = 'fit-in' | 'stretch' | 'dashed'; + +/** + * Набор фильтров и трансформаций Imagor. + * Порядок применения фильтров в URL обычно соответствует порядку их перечисления. + * @see https://github.com/cshum/imagor#filters + */ +export interface Filters { + /** + * Ширина выходного изображения в пикселях. + * Используйте 'orig', чтобы сохранить исходную ширину. + */ + width?: number | 'orig'; + + /** + * Высота выходного изображения в пикселях. + * Используйте 'orig', чтобы сохранить исходную высоту. + */ + height?: number | 'orig'; + + /** + * Включает умную обрезку (Smart Cropping). + * Imagor попытается найти наиболее важные области (лица, контрастные объекты) и сфокусироваться на них. + */ + smart?: boolean; + + /** + * Режим вписывания. + * Если не указан, по умолчанию используется обрезка (Crop) для заполнения всей области. + */ + fit?: Fit; + + /** + * Устанавливает качество выходного изображения. + * @param {number} quality Число от 0 до 100. + */ + quality?: number; + + /** + * Принудительно устанавливает формат выходного изображения. + * WebP и AVIF рекомендуются для лучшего сжатия. + */ + format?: Format; + + /** + * Если true, автоматически конвертирует изображения с прозрачностью в JPEG, + * заменяя прозрачные области фоном (белым по умолчанию). + */ + autojpg?: boolean; + + /** Удаляет EXIF метаданные из выходного изображения. Полезно для приватности и уменьшения размера. */ + strip_exif?: boolean; + + /** Удаляет ICC профили цвета. */ + strip_icc?: boolean; + + /** + * Регулирует яркость изображения. + * @param {number} brightness Число от -100 до 100. Положительные — ярче, отрицательные — темнее. + */ + brightness?: number; + + /** + * Регулирует контрастность изображения. + * @param {number} contrast Число от -100 до 100. + */ + contrast?: number; + + /** Преобразует изображение в черно-белое (grayscale). */ + grayscale?: boolean; + + /** + * Настройка цветовых каналов RGB. + * @property {number} r Красный (-100 до 100) + * @property {number} g Зеленый (-100 до 100) + * @property {number} b Синий (-100 до 100) + */ + rgb?: { r: number; g: number; b: number }; + + /** + * Изменяет общую насыщенность цветов. + * @param {number} proportion Число от 0 до 100. + */ + proportion?: number; + + /** + * Применяет размытие Гаусса. + * Можно передать число (радиус) или объект для более точной настройки сигмы. + */ + blur?: number | { radius: number; sigma?: number }; + + /** + * Повышает резкость изображения. + * @property {number} amount Степень резкости. + * @property {number} radius Радиус фильтра. + * @property {number} threshold Порог срабатывания. + */ + sharpen?: { + amount: number; + radius: number; + threshold: number; + }; + + /** + * Добавляет шум на изображение. + * @param {number} noise Уровень шума от 0 до 100. + */ + noise?: number; + + /** Поворачивает изображение на заданный угол по часовой стрелке. */ + rotate?: 90 | 180 | 270; + + /** + * Определяет цвет заполнения пустых областей при использовании режима 'fit-in'. + * @example 'ff0000' (hex), 'white' (name) или 'auto' (главный цвет изображения). + */ + fill?: string; + + /** Устанавливает цвет фона для прозрачных изображений (например, PNG). */ + background_color?: string; + + /** + * Наложение водяного знака поверх основного изображения. + */ + watermark?: { + /** Путь к файлу водяного знака в хранилище. */ + image: string; + /** Позиция по горизонтали или смещение в пикселях. */ + x?: number | 'center' | 'left' | 'right'; + /** Позиция по вертикали или смещение в пикселях. */ + y?: number | 'center' | 'top' | 'bottom'; + /** Прозрачность водяного знака (0 - прозрачный, 100 - непрозрачный). */ + alpha?: number; + /** Относительная ширина знака в процентах (0.0 - 1.0) от основного изображения. */ + w_ratio?: number; + /** Относительная высота знака в процентах (0.0 - 1.0). */ + h_ratio?: number; + }; + + /** + * Указывает точку фокуса для кропа. + * Полезно, если вы знаете координаты лица или важного объекта. + */ + focal?: { x: number; y: number }; + + /** + * Скругление углов изображения. + * @property {number} radius Радиус скругления в пикселях. + * @property {string} color Цвет заливки углов (например, 'transparent' или 'ffffff'). + */ + round_corner?: { + radius: number; + color?: string; + }; + + /** + * Ограничивает размер файла (в байтах). Imagor будет снижать качество, пока не впишется в лимит. + */ + max_bytes?: number; + + /** + * Запрещает увеличивать изображение, если его исходные размеры меньше запрошенных (width/height). + */ + no_upscale?: boolean; +} diff --git a/libs/imagor/src/interfaces/formats.interface.ts b/libs/imagor/src/interfaces/formats.interface.ts new file mode 100644 index 0000000..d9b6897 --- /dev/null +++ b/libs/imagor/src/interfaces/formats.interface.ts @@ -0,0 +1,10 @@ +export const FORMATS = { + JPEG: 'jpeg', + PNG: 'png', + WEBP: 'webp', + AVIF: 'avif', + JP2: 'jp2', + GIF: 'gif', +} as const; + +export type Format = (typeof FORMATS)[keyof typeof FORMATS]; diff --git a/libs/imagor/src/interfaces/index.ts b/libs/imagor/src/interfaces/index.ts new file mode 100644 index 0000000..5ea0a92 --- /dev/null +++ b/libs/imagor/src/interfaces/index.ts @@ -0,0 +1,2 @@ +export type * from './module.interface'; +export type * from './filters.interface'; diff --git a/libs/imagor/src/interfaces/module.interface.ts b/libs/imagor/src/interfaces/module.interface.ts new file mode 100644 index 0000000..5f77397 --- /dev/null +++ b/libs/imagor/src/interfaces/module.interface.ts @@ -0,0 +1,27 @@ +import type { Filters } from './filters.interface'; + +/** + * Опции конфигурации модуля Imagor + */ +export interface ImagorModuleOptions { + /** Базовый URL вашего инстанса Imagor (например, https://imagor.example.com) */ + url: string; + + /** Секретный ключ для генерации HMAC подписи (безопасные URL) */ + secret?: string; + + /** Глобальные фильтры, которые будут применяться ко всем изображениям по умолчанию */ + filters?: Filters; + + /** Базовый путь в S3/хранилище (например, 'products/') */ + storageRoot?: string; + + /** + * Именованные пресеты для часто используемых трансформаций. + * @example { 'thumb': { width: 150, height: 150, smart: true } } + */ + presets?: Record; + + /** Включает логирование процесса генерации URL для отладки */ + debug?: boolean; +} diff --git a/libs/imagor/src/utils/imagor-path-builder.ts b/libs/imagor/src/utils/imagor-path-builder.ts new file mode 100644 index 0000000..811950d --- /dev/null +++ b/libs/imagor/src/utils/imagor-path-builder.ts @@ -0,0 +1,112 @@ +import type { Filters } from '../interfaces'; + +export class ImagorPathBuilder { + private _width: number | 'orig' = 0; + private _height: number | 'orig' = 0; + private _isSmart = false; + private _fitMode?: 'fit-in' | 'stretch' | 'dashed'; + private _filters: Filters = {}; + + constructor( + private readonly path: string, + private readonly storageRoot?: string, + ) {} + + resize(width: number | 'orig', height: number | 'orig' = 0): this { + this._width = width; + this._height = height; + return this; + } + + smart(enabled = true): this { + this._isSmart = enabled; + return this; + } + + fit(mode: 'fit-in' | 'stretch' | 'dashed'): this { + this._fitMode = mode; + return this; + } + + applyFilters(filters: Filters): this { + this._filters = { ...this._filters, ...filters }; + return this; + } + + build(): string { + const parts: string[] = []; + + if (this._fitMode) parts.push(this._fitMode); + + if (this._width || this._height) { + parts.push(`${this._width}x${this._height}`); + } + + if (this._isSmart) parts.push('smart'); + + const filterString = this.serializeAllFilters(this._filters); + if (filterString) parts.push(filterString); + + const fullPath = this.storageRoot + ? `${this.storageRoot}/${this.path}`.replace(/\/+/g, '/') + : this.path; + + parts.push(fullPath.replace(/^\/+/, '')); + + return parts.join('/'); + } + + private serializeAllFilters(f: Filters): string { + const s: string[] = []; + + if (f.quality) s.push(`quality(${f.quality})`); + if (f.format) s.push(`format(${f.format})`); + if (f.autojpg) s.push('autojpg()'); + if (f.strip_exif) s.push('strip_exif()'); + if (f.strip_icc) s.push('strip_icc()'); + + if (f.brightness !== undefined) s.push(`brightness(${f.brightness})`); + if (f.contrast !== undefined) s.push(`contrast(${f.contrast})`); + if (f.grayscale) s.push('grayscale()'); + if (f.proportion !== undefined) s.push(`proportion(${f.proportion})`); + if (f.rgb) s.push(`rgb(${f.rgb.r},${f.rgb.g},${f.rgb.b})`); + + if (f.blur) { + const b = f.blur; + s.push(typeof b === 'number' ? `blur(${b})` : `blur(${b.radius},${b.sigma || 0})`); + } + if (f.sharpen) { + s.push(`sharpen(${f.sharpen.amount},${f.sharpen.radius},${f.sharpen.threshold})`); + } + if (f.noise) s.push(`noise(${f.noise})`); + if (f.rotate) s.push(`rotate(${f.rotate})`); + + if (f.fill) s.push(`fill(${f.fill})`); + if (f.background_color) s.push(`background_color(${f.background_color})`); + + if (f.watermark) { + const w = f.watermark; + const params = [ + w.image, + w.x ?? 0, + w.y ?? 0, + w.alpha ?? 0, + w.w_ratio ?? 0, + w.h_ratio ?? 0, + ]; + s.push(`watermark(${params.join(',')})`); + } + + if (f.focal) s.push(`focal(${f.focal.x}x${f.focal.y})`); + if (f.round_corner) { + s.push( + `round_corner(${f.round_corner.radius}${f.round_corner.color ? ',' + f.round_corner.color : ''})`, + ); + } + + if (f.max_bytes) s.push(`max_bytes(${f.max_bytes})`); + if (f.no_upscale) s.push('no_upscale()'); + + return s.length ? `filters:${s.join(':')}` : ''; + } +} diff --git a/libs/imagor/src/utils/index.ts b/libs/imagor/src/utils/index.ts new file mode 100644 index 0000000..dc2c7ef --- /dev/null +++ b/libs/imagor/src/utils/index.ts @@ -0,0 +1 @@ +export { ImagorPathBuilder } from './imagor-path-builder'; diff --git a/libs/imagor/tsconfig.lib.json b/libs/imagor/tsconfig.lib.json new file mode 100644 index 0000000..1895e7a --- /dev/null +++ b/libs/imagor/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/imagor" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/s3/src/constants.ts b/libs/s3/src/constants.ts new file mode 100644 index 0000000..58fc36c --- /dev/null +++ b/libs/s3/src/constants.ts @@ -0,0 +1 @@ +export const S3_CLIENT = 'S3_CLIENT'; diff --git a/libs/s3/src/index.ts b/libs/s3/src/index.ts new file mode 100644 index 0000000..d819c35 --- /dev/null +++ b/libs/s3/src/index.ts @@ -0,0 +1,2 @@ +export * from './s3.module'; +export * from './s3.service'; diff --git a/libs/s3/src/interfaces/index.ts b/libs/s3/src/interfaces/index.ts new file mode 100644 index 0000000..dc08195 --- /dev/null +++ b/libs/s3/src/interfaces/index.ts @@ -0,0 +1 @@ +export type { S3ModuleOptions } from './module.interface'; diff --git a/libs/s3/src/interfaces/module.interface.ts b/libs/s3/src/interfaces/module.interface.ts new file mode 100644 index 0000000..922d127 --- /dev/null +++ b/libs/s3/src/interfaces/module.interface.ts @@ -0,0 +1,20 @@ +import type { S3ClientConfig } from '@aws-sdk/client-s3'; + +export interface S3Connection extends Pick< + S3ClientConfig, + 'credentials' | 'endpoint' | 'region' +> { + endpoint: string; + region: string; +} + +export interface S3Config extends Omit< + S3ClientConfig, + keyof S3Connection +> { } + +export interface S3ModuleOptions { + connection: S3Connection; + bucket: string; + config?: S3Config; +} diff --git a/libs/s3/src/s3.module-definition.ts b/libs/s3/src/s3.module-definition.ts new file mode 100644 index 0000000..3648deb --- /dev/null +++ b/libs/s3/src/s3.module-definition.ts @@ -0,0 +1,16 @@ +import { ConfigurableModuleBuilder } from '@nestjs/common'; +import { S3ModuleOptions } from './interfaces'; + +export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = + new ConfigurableModuleBuilder() + .setClassMethodName('register') + .setExtras( + { + global: false, + }, + (definition, extras) => ({ + ...definition, + global: extras.global, + }), + ) + .build(); diff --git a/libs/s3/src/s3.module.ts b/libs/s3/src/s3.module.ts new file mode 100644 index 0000000..656b979 --- /dev/null +++ b/libs/s3/src/s3.module.ts @@ -0,0 +1,36 @@ +import { Inject, Module, OnApplicationShutdown } from '@nestjs/common'; +import type { S3ModuleOptions } from './interfaces'; +import { S3Service } from './s3.service'; +import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } from './s3.module-definition'; +import { S3_CLIENT } from './constants'; +import { S3Client } from '@aws-sdk/client-s3'; + +@Module({ + providers: [ + { + provide: S3_CLIENT, + inject: [MODULE_OPTIONS_TOKEN], + useFactory: (options: S3ModuleOptions) => { + const { connection, config } = options; + + return new S3Client({ + ...connection, + ...config + }); + }, + }, + S3Service, + ], + exports: [S3Service], +}) +export class S3Module extends ConfigurableModuleClass implements OnApplicationShutdown { + constructor( + @Inject(S3_CLIENT) private readonly client: S3Client + ) { + super(); + } + + async onApplicationShutdown() { + this.client.destroy(); + } +} diff --git a/libs/s3/src/s3.service.ts b/libs/s3/src/s3.service.ts new file mode 100644 index 0000000..e42ba07 --- /dev/null +++ b/libs/s3/src/s3.service.ts @@ -0,0 +1,87 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DeleteObjectCommand, HeadBucketCommand, S3Client } from '@aws-sdk/client-s3'; +import { S3_CLIENT } from './constants'; +import { S3ModuleOptions } from './interfaces'; +import { PutObjectCommand } from '@aws-sdk/client-s3'; +import { randomUUID } from 'crypto'; +import { extname } from 'path'; +import { MODULE_OPTIONS_TOKEN } from './s3.module-definition'; + +@Injectable() +export class S3Service { + constructor( + @Inject(S3_CLIENT) + private s3Client: S3Client, + @Inject(MODULE_OPTIONS_TOKEN) + private options: S3ModuleOptions, + ) { } + + private get bucket(): string { + return this.options.bucket; + } + + async isAlive(): Promise { + try { + await this.s3Client.send( + new HeadBucketCommand({ + Bucket: this.bucket, + }), + ); + return true; + } catch (error) { + return false; + } + } + + async delete(fileUrl: string): Promise { + try { + const url = new URL(fileUrl); + const pathParts = url.pathname.split('/'); + const key = pathParts.slice(2).join('/'); + + await this.s3Client.send( + new DeleteObjectCommand({ + Bucket: this.bucket, + Key: key, + }), + ); + } catch (error) { + console.error('S3 Rollback failed:', error); + } + } + + async upload( + file: Buffer, + fileOptions: { + original: string; + mimetype: string; + cacheControl?: string; + path?: + | { + folder: string; + key?: string; + } + | string; + }, + ): Promise { + const { mimetype, original, path, cacheControl } = fileOptions; + + const folder = typeof path === 'object' ? path.folder : ''; + const key = + (typeof path === 'object' ? path.key : path) || `${randomUUID()}${extname(original)}`; + + const fileName = [folder, key].filter(Boolean).join('/').replace(/\/+/g, '/'); + + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: fileName, + Body: file, + CacheControl: cacheControl || 'public, max-age=31536000, immutable', + ContentType: mimetype, + }); + + await this.s3Client.send(command); + + return fileName; + } +} diff --git a/libs/s3/tsconfig.lib.json b/libs/s3/tsconfig.lib.json new file mode 100644 index 0000000..0cd20fa --- /dev/null +++ b/libs/s3/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/s3" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/migrations/0000_stale_sunspot.sql b/migrations/0000_stale_sunspot.sql new file mode 100644 index 0000000..a615183 --- /dev/null +++ b/migrations/0000_stale_sunspot.sql @@ -0,0 +1 @@ +CREATE SCHEMA "base"; diff --git a/migrations/0001_solid_kronos.sql b/migrations/0001_solid_kronos.sql new file mode 100644 index 0000000..faed36a --- /dev/null +++ b/migrations/0001_solid_kronos.sql @@ -0,0 +1,57 @@ +CREATE TABLE "base"."user_activity" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "event_type" varchar(50) NOT NULL, + "entity_id" varchar, + "metadata" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); + +CREATE TABLE "base"."user_notifications" ( + "user_id" text PRIMARY KEY NOT NULL, + "settings" jsonb DEFAULT '{"email":{"task_assigned":true,"mentions":true,"daily_summary":false},"push":{"task_assigned":true,"reminders":true}}'::jsonb NOT NULL +); + +CREATE TABLE "base"."user_security" ( + "user_id" text PRIMARY KEY NOT NULL, + "password_hash" varchar(255) NOT NULL, + "is_2fa_enabled" boolean DEFAULT false NOT NULL, + "two_factor_secret" text, + "last_password_change" timestamp with time zone DEFAULT now() NOT NULL +); + +CREATE TABLE "base"."users" ( + "id" text PRIMARY KEY NOT NULL, + "first_name" varchar(50) NOT NULL, + "last_name" varchar(50) NOT NULL, + "middle_name" varchar(50), + "email" varchar(255) NOT NULL, + "bio" text, + "avatar_url" varchar(512), + "timezone" varchar(50) DEFAULT 'UTC' NOT NULL, + "language" varchar(5) DEFAULT 'ru' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "users_email_unique" UNIQUE("email") +); + +CREATE TABLE "base"."sessions" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "device_type" varchar(20), + "browser" varchar(50), + "os" varchar(50), + "user_agent" text NOT NULL, + "ip" varchar(45) NOT NULL, + "city" varchar(100), + "country_code" varchar(5), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "is_revoked" boolean DEFAULT false NOT NULL +); + +ALTER TABLE "base"."user_activity" ADD CONSTRAINT "user_activity_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "base"."users"("id") ON DELETE cascade ON UPDATE no action; +ALTER TABLE "base"."user_notifications" ADD CONSTRAINT "user_notifications_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "base"."users"("id") ON DELETE cascade ON UPDATE no action; +ALTER TABLE "base"."user_security" ADD CONSTRAINT "user_security_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "base"."users"("id") ON DELETE cascade ON UPDATE no action; +ALTER TABLE "base"."sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "base"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/migrations/0002_pink_krista_starr.sql b/migrations/0002_pink_krista_starr.sql new file mode 100644 index 0000000..e44a9d8 --- /dev/null +++ b/migrations/0002_pink_krista_starr.sql @@ -0,0 +1,56 @@ +CREATE TYPE "base"."team_role" AS ENUM ('admin', 'moderator', 'member'); + +CREATE TYPE "base"."member_status" AS ENUM ('pending', 'active', 'declined', 'banned'); + +CREATE TABLE + "base"."tags" ( + "id" text PRIMARY KEY NOT NULL, + "name" varchar(50) NOT NULL, + CONSTRAINT "tags_name_unique" UNIQUE ("name") + ); + +CREATE TABLE + "base"."team_members" ( + "team_id" text NOT NULL, + "user_id" text NOT NULL, + "role" "base"."team_role" DEFAULT 'member' NOT NULL, + "status" "base"."member_status" DEFAULT 'pending' NOT NULL, + "joined_at" timestamp, + "created_at" timestamp DEFAULT now () NOT NULL, + CONSTRAINT "team_members_team_id_user_id_pk" PRIMARY KEY ("team_id", "user_id") + ); + +CREATE TABLE + "base"."teams" ( + "id" text PRIMARY KEY NOT NULL, + "slug" varchar(120) NOT NULL, + "name" varchar(100) NOT NULL, + "description" text, + "avatar_url" text, + "cover_url" text, + "owner_id" text, + "created_at" timestamp DEFAULT now () NOT NULL, + "updated_at" timestamp DEFAULT now () NOT NULL, + CONSTRAINT "teams_slug_unique" UNIQUE ("slug") + ); + +CREATE TABLE + "base"."teams_to_tags" ( + "team_id" text NOT NULL, + "tag_id" text NOT NULL, + CONSTRAINT "teams_to_tags_team_id_tag_id_pk" PRIMARY KEY ("team_id", "tag_id") + ); + +ALTER TABLE "base"."team_members" ADD CONSTRAINT "team_members_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "base"."teams" ("id") ON DELETE cascade ON UPDATE no action; + +ALTER TABLE "base"."team_members" ADD CONSTRAINT "team_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "base"."users" ("id") ON DELETE cascade ON UPDATE no action; + +ALTER TABLE "base"."teams" ADD CONSTRAINT "teams_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "base"."users" ("id") ON DELETE no action ON UPDATE no action; + +ALTER TABLE "base"."teams_to_tags" ADD CONSTRAINT "teams_to_tags_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "base"."teams" ("id") ON DELETE cascade ON UPDATE no action; + +ALTER TABLE "base"."teams_to_tags" ADD CONSTRAINT "teams_to_tags_tag_id_tags_id_fk" FOREIGN KEY ("tag_id") REFERENCES "base"."tags" ("id") ON DELETE cascade ON UPDATE no action; + +CREATE INDEX "member_status_idx" ON "base"."team_members" USING btree ("status"); + +CREATE INDEX "team_slug_idx" ON "base"."teams" USING btree ("slug"); \ No newline at end of file diff --git a/migrations/0003_open_oracle.sql b/migrations/0003_open_oracle.sql new file mode 100644 index 0000000..4fe9269 --- /dev/null +++ b/migrations/0003_open_oracle.sql @@ -0,0 +1,18 @@ +ALTER TYPE "base"."team_role" ADD VALUE 'owner' BEFORE 'admin'; +ALTER TYPE "base"."team_role" ADD VALUE 'lead' BEFORE 'moderator'; +ALTER TYPE "base"."team_role" ADD VALUE 'viewer'; +ALTER TABLE "base"."teams" DROP CONSTRAINT "teams_owner_id_users_id_fk"; + +ALTER TABLE "base"."team_members" ALTER COLUMN "status" SET DATA TYPE text; +ALTER TABLE "base"."team_members" ALTER COLUMN "status" SET DEFAULT 'inactive'::text; +DROP TYPE "base"."member_status"; +CREATE TYPE "base"."member_status" AS ENUM('active', 'banned', 'inactive'); +ALTER TABLE "base"."team_members" ALTER COLUMN "status" SET DEFAULT 'inactive'::"base"."member_status"; +ALTER TABLE "base"."team_members" ALTER COLUMN "status" SET DATA TYPE "base"."member_status" USING "status"::"base"."member_status"; +ALTER TABLE "base"."teams" ADD COLUMN "deleted_at" timestamp; +ALTER TABLE "base"."teams" ADD CONSTRAINT "teams_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "base"."users"("id") ON DELETE set null ON UPDATE no action; +CREATE INDEX "member_role_idx" ON "base"."team_members" USING btree ("user_id","role"); +CREATE UNIQUE INDEX "team_active_slug_idx" ON "base"."teams" USING btree ("slug") WHERE "base"."teams"."deleted_at" is null; +CREATE INDEX "team_owner_idx" ON "base"."teams" USING btree ("owner_id"); +CREATE INDEX "team_deleted_at_idx" ON "base"."teams" USING btree ("deleted_at"); +CREATE INDEX "teams_to_tags_tag_id_idx" ON "base"."teams_to_tags" USING btree ("tag_id"); \ No newline at end of file diff --git a/migrations/0004_chief_talkback.sql b/migrations/0004_chief_talkback.sql new file mode 100644 index 0000000..dc1073f --- /dev/null +++ b/migrations/0004_chief_talkback.sql @@ -0,0 +1,29 @@ +CREATE TYPE "base"."project_status" AS ENUM('active', 'archived', 'template'); +CREATE TYPE "base"."project_visibility" AS ENUM('public', 'private'); +CREATE TABLE "base"."projects" ( + "id" text PRIMARY KEY NOT NULL, + "team_id" text NOT NULL, + "key" varchar(10) NOT NULL, + "name" varchar(100) NOT NULL, + "description" text, + "icon" varchar(255), + "color" varchar(7), + "status" "base"."project_status" DEFAULT 'active' NOT NULL, + "task_sequence" integer DEFAULT 0 NOT NULL, + "owner_id" text, + "visibility" "base"."project_visibility" DEFAULT 'public' NOT NULL, + "is_publicly_viewable" boolean DEFAULT false NOT NULL, + "share_token" varchar(64), + "settings" jsonb DEFAULT '{}'::jsonb, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "deleted_at" timestamp, + CONSTRAINT "projects_share_token_unique" UNIQUE("share_token") +); + +ALTER TABLE "base"."projects" ADD CONSTRAINT "projects_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "base"."teams"("id") ON DELETE cascade ON UPDATE no action; +ALTER TABLE "base"."projects" ADD CONSTRAINT "projects_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "base"."users"("id") ON DELETE set null ON UPDATE no action; +CREATE UNIQUE INDEX "project_team_key_idx" ON "base"."projects" USING btree ("team_id","key") WHERE "base"."projects"."deleted_at" is null; +CREATE INDEX "project_owner_id_idx" ON "base"."projects" USING btree ("owner_id"); +CREATE INDEX "project_team_id_idx" ON "base"."projects" USING btree ("team_id"); +CREATE INDEX "project_share_token_idx" ON "base"."projects" USING btree ("share_token"); \ No newline at end of file diff --git a/migrations/0005_calm_vivisector.sql b/migrations/0005_calm_vivisector.sql new file mode 100644 index 0000000..2e1e4e9 --- /dev/null +++ b/migrations/0005_calm_vivisector.sql @@ -0,0 +1,33 @@ +CREATE TABLE + "base"."project_shares" ( + "id" text PRIMARY KEY NOT NULL, + "project_id" text NOT NULL, + "token" text NOT NULL, + "expires_at" timestamp + with + time zone, + "created_by" text NOT NULL, + "created_at" timestamp DEFAULT now () NOT NULL, + CONSTRAINT "project_shares_token_unique" UNIQUE ("token") + ); + +ALTER TABLE "base"."projects" +DROP CONSTRAINT "projects_share_token_unique"; + +DROP INDEX "base"."project_share_token_idx"; + +ALTER TABLE "base"."project_shares" ADD CONSTRAINT "project_shares_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "base"."projects" ("id") ON DELETE cascade ON UPDATE no action; + +CREATE INDEX "token_idx" ON "base"."project_shares" USING btree ("token"); + +CREATE INDEX "project_share_project_id_idx" ON "base"."project_shares" USING btree ("project_id"); + +CREATE UNIQUE INDEX "project_team_name_idx" ON "base"."projects" USING btree ("team_id", "name") +WHERE + "base"."projects"."deleted_at" is null; + +ALTER TABLE "base"."projects" +DROP COLUMN "is_publicly_viewable"; + +ALTER TABLE "base"."projects" +DROP COLUMN "share_token"; \ No newline at end of file diff --git a/migrations/meta/0000_snapshot.json b/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..f99863e --- /dev/null +++ b/migrations/meta/0000_snapshot.json @@ -0,0 +1,20 @@ +{ + "id": "a40dfb7f-7d44-4721-bf37-a197b5f1e479", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": {}, + "enums": {}, + "schemas": { + "base": "base" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/0001_snapshot.json b/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..3cbcbd8 --- /dev/null +++ b/migrations/meta/0001_snapshot.json @@ -0,0 +1,369 @@ +{ + "id": "c5575cbf-cbee-46d8-af83-95b96a2afceb", + "prevId": "a40dfb7f-7d44-4721-bf37-a197b5f1e479", + "version": "7", + "dialect": "postgresql", + "tables": { + "base.user_activity": { + "name": "user_activity", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_activity_user_id_users_id_fk": { + "name": "user_activity_user_id_users_id_fk", + "tableFrom": "user_activity", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_notifications": { + "name": "user_notifications", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"email\":{\"task_assigned\":true,\"mentions\":true,\"daily_summary\":false},\"push\":{\"task_assigned\":true,\"reminders\":true}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_notifications_user_id_users_id_fk": { + "name": "user_notifications_user_id_users_id_fk", + "tableFrom": "user_notifications", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_security": { + "name": "user_security", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "is_2fa_enabled": { + "name": "is_2fa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_password_change": { + "name": "last_password_change", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_security_user_id_users_id_fk": { + "name": "user_security_user_id_users_id_fk", + "tableFrom": "user_security", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.users": { + "name": "users", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "middle_name": { + "name": "middle_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "language": { + "name": "language", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'ru'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.sessions": { + "name": "sessions", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "browser": { + "name": "browser", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "os": { + "name": "os", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "is_revoked": { + "name": "is_revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": { + "base": "base" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/migrations/meta/0002_snapshot.json b/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..80c77c1 --- /dev/null +++ b/migrations/meta/0002_snapshot.json @@ -0,0 +1,717 @@ +{ + "id": "995af10c-f9b7-416a-b20b-85034dbd20d5", + "prevId": "c5575cbf-cbee-46d8-af83-95b96a2afceb", + "version": "7", + "dialect": "postgresql", + "tables": { + "base.user_activity": { + "name": "user_activity", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_activity_user_id_users_id_fk": { + "name": "user_activity_user_id_users_id_fk", + "tableFrom": "user_activity", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_notifications": { + "name": "user_notifications", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"email\":{\"task_assigned\":true,\"mentions\":true,\"daily_summary\":false},\"push\":{\"task_assigned\":true,\"reminders\":true}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_notifications_user_id_users_id_fk": { + "name": "user_notifications_user_id_users_id_fk", + "tableFrom": "user_notifications", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_security": { + "name": "user_security", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "is_2fa_enabled": { + "name": "is_2fa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_password_change": { + "name": "last_password_change", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_security_user_id_users_id_fk": { + "name": "user_security_user_id_users_id_fk", + "tableFrom": "user_security", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.users": { + "name": "users", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "middle_name": { + "name": "middle_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "language": { + "name": "language", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'ru'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.sessions": { + "name": "sessions", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "browser": { + "name": "browser", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "os": { + "name": "os", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "is_revoked": { + "name": "is_revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.tags": { + "name": "tags", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tags_name_unique": { + "name": "tags_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.team_members": { + "name": "team_members", + "schema": "base", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "team_role", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "member_status", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_status_idx": { + "name": "member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "schemaTo": "base", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_members_team_id_user_id_pk": { + "name": "team_members_team_id_user_id_pk", + "columns": [ + "team_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.teams": { + "name": "teams", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_url": { + "name": "cover_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_slug_idx": { + "name": "team_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_owner_id_users_id_fk": { + "name": "teams_owner_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_slug_unique": { + "name": "teams_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.teams_to_tags": { + "name": "teams_to_tags", + "schema": "base", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "teams_to_tags_team_id_teams_id_fk": { + "name": "teams_to_tags_team_id_teams_id_fk", + "tableFrom": "teams_to_tags", + "tableTo": "teams", + "schemaTo": "base", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_to_tags_tag_id_tags_id_fk": { + "name": "teams_to_tags_tag_id_tags_id_fk", + "tableFrom": "teams_to_tags", + "tableTo": "tags", + "schemaTo": "base", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "teams_to_tags_team_id_tag_id_pk": { + "name": "teams_to_tags_team_id_tag_id_pk", + "columns": [ + "team_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "base.team_role": { + "name": "team_role", + "schema": "base", + "values": [ + "admin", + "moderator", + "member" + ] + }, + "base.member_status": { + "name": "member_status", + "schema": "base", + "values": [ + "pending", + "active", + "declined", + "banned" + ] + } + }, + "schemas": { + "base": "base" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/0003_snapshot.json b/migrations/meta/0003_snapshot.json new file mode 100644 index 0000000..3845d25 --- /dev/null +++ b/migrations/meta/0003_snapshot.json @@ -0,0 +1,808 @@ +{ + "id": "6fbd096d-2d73-46c8-b4f9-a337fb5cb1c2", + "prevId": "995af10c-f9b7-416a-b20b-85034dbd20d5", + "version": "7", + "dialect": "postgresql", + "tables": { + "base.user_activity": { + "name": "user_activity", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_activity_user_id_users_id_fk": { + "name": "user_activity_user_id_users_id_fk", + "tableFrom": "user_activity", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_notifications": { + "name": "user_notifications", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"email\":{\"task_assigned\":true,\"mentions\":true,\"daily_summary\":false},\"push\":{\"task_assigned\":true,\"reminders\":true}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_notifications_user_id_users_id_fk": { + "name": "user_notifications_user_id_users_id_fk", + "tableFrom": "user_notifications", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_security": { + "name": "user_security", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "is_2fa_enabled": { + "name": "is_2fa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_password_change": { + "name": "last_password_change", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_security_user_id_users_id_fk": { + "name": "user_security_user_id_users_id_fk", + "tableFrom": "user_security", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.users": { + "name": "users", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "middle_name": { + "name": "middle_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "language": { + "name": "language", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'ru'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.sessions": { + "name": "sessions", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "browser": { + "name": "browser", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "os": { + "name": "os", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "is_revoked": { + "name": "is_revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.tags": { + "name": "tags", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tags_name_unique": { + "name": "tags_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.team_members": { + "name": "team_members", + "schema": "base", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "team_role", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "member_status", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'inactive'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_status_idx": { + "name": "member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_role_idx": { + "name": "member_role_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "schemaTo": "base", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_members_team_id_user_id_pk": { + "name": "team_members_team_id_user_id_pk", + "columns": [ + "team_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.teams": { + "name": "teams", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_url": { + "name": "cover_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "team_active_slug_idx": { + "name": "team_active_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"base\".\"teams\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_slug_idx": { + "name": "team_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_owner_idx": { + "name": "team_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_deleted_at_idx": { + "name": "team_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_owner_id_users_id_fk": { + "name": "teams_owner_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_slug_unique": { + "name": "teams_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.teams_to_tags": { + "name": "teams_to_tags", + "schema": "base", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "teams_to_tags_tag_id_idx": { + "name": "teams_to_tags_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_to_tags_team_id_teams_id_fk": { + "name": "teams_to_tags_team_id_teams_id_fk", + "tableFrom": "teams_to_tags", + "tableTo": "teams", + "schemaTo": "base", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_to_tags_tag_id_tags_id_fk": { + "name": "teams_to_tags_tag_id_tags_id_fk", + "tableFrom": "teams_to_tags", + "tableTo": "tags", + "schemaTo": "base", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "teams_to_tags_team_id_tag_id_pk": { + "name": "teams_to_tags_team_id_tag_id_pk", + "columns": [ + "team_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "base.team_role": { + "name": "team_role", + "schema": "base", + "values": [ + "owner", + "admin", + "lead", + "moderator", + "member", + "viewer" + ] + }, + "base.member_status": { + "name": "member_status", + "schema": "base", + "values": [ + "active", + "banned", + "inactive" + ] + } + }, + "schemas": { + "base": "base" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/0004_snapshot.json b/migrations/meta/0004_snapshot.json new file mode 100644 index 0000000..b6f710d --- /dev/null +++ b/migrations/meta/0004_snapshot.json @@ -0,0 +1,1054 @@ +{ + "id": "55316de9-3aec-4333-b5b7-b1b6a78f8ce1", + "prevId": "6fbd096d-2d73-46c8-b4f9-a337fb5cb1c2", + "version": "7", + "dialect": "postgresql", + "tables": { + "base.user_activity": { + "name": "user_activity", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_activity_user_id_users_id_fk": { + "name": "user_activity_user_id_users_id_fk", + "tableFrom": "user_activity", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_notifications": { + "name": "user_notifications", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"email\":{\"task_assigned\":true,\"mentions\":true,\"daily_summary\":false},\"push\":{\"task_assigned\":true,\"reminders\":true}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_notifications_user_id_users_id_fk": { + "name": "user_notifications_user_id_users_id_fk", + "tableFrom": "user_notifications", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_security": { + "name": "user_security", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "is_2fa_enabled": { + "name": "is_2fa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_password_change": { + "name": "last_password_change", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_security_user_id_users_id_fk": { + "name": "user_security_user_id_users_id_fk", + "tableFrom": "user_security", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.users": { + "name": "users", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "middle_name": { + "name": "middle_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "language": { + "name": "language", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'ru'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.sessions": { + "name": "sessions", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "browser": { + "name": "browser", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "os": { + "name": "os", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "is_revoked": { + "name": "is_revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.tags": { + "name": "tags", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tags_name_unique": { + "name": "tags_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.team_members": { + "name": "team_members", + "schema": "base", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "team_role", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "member_status", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'inactive'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_status_idx": { + "name": "member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_role_idx": { + "name": "member_role_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "schemaTo": "base", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_members_team_id_user_id_pk": { + "name": "team_members_team_id_user_id_pk", + "columns": [ + "team_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.teams": { + "name": "teams", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_url": { + "name": "cover_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "team_active_slug_idx": { + "name": "team_active_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"base\".\"teams\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_slug_idx": { + "name": "team_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_owner_idx": { + "name": "team_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_deleted_at_idx": { + "name": "team_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_owner_id_users_id_fk": { + "name": "teams_owner_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_slug_unique": { + "name": "teams_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.teams_to_tags": { + "name": "teams_to_tags", + "schema": "base", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "teams_to_tags_tag_id_idx": { + "name": "teams_to_tags_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_to_tags_team_id_teams_id_fk": { + "name": "teams_to_tags_team_id_teams_id_fk", + "tableFrom": "teams_to_tags", + "tableTo": "teams", + "schemaTo": "base", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_to_tags_tag_id_tags_id_fk": { + "name": "teams_to_tags_tag_id_tags_id_fk", + "tableFrom": "teams_to_tags", + "tableTo": "tags", + "schemaTo": "base", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "teams_to_tags_team_id_tag_id_pk": { + "name": "teams_to_tags_team_id_tag_id_pk", + "columns": [ + "team_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.projects": { + "name": "projects", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "project_status", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "task_sequence": { + "name": "task_sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "project_visibility", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "is_publicly_viewable": { + "name": "is_publicly_viewable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_token": { + "name": "share_token", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "project_team_key_idx": { + "name": "project_team_key_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"base\".\"projects\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_owner_id_idx": { + "name": "project_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_team_id_idx": { + "name": "project_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_share_token_idx": { + "name": "project_share_token_idx", + "columns": [ + { + "expression": "share_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_team_id_teams_id_fk": { + "name": "projects_team_id_teams_id_fk", + "tableFrom": "projects", + "tableTo": "teams", + "schemaTo": "base", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "projects_owner_id_users_id_fk": { + "name": "projects_owner_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_share_token_unique": { + "name": "projects_share_token_unique", + "nullsNotDistinct": false, + "columns": [ + "share_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "base.team_role": { + "name": "team_role", + "schema": "base", + "values": [ + "owner", + "admin", + "lead", + "moderator", + "member", + "viewer" + ] + }, + "base.member_status": { + "name": "member_status", + "schema": "base", + "values": [ + "active", + "banned", + "inactive" + ] + }, + "base.project_status": { + "name": "project_status", + "schema": "base", + "values": [ + "active", + "archived", + "template" + ] + }, + "base.project_visibility": { + "name": "project_visibility", + "schema": "base", + "values": [ + "public", + "private" + ] + } + }, + "schemas": { + "base": "base" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/0005_snapshot.json b/migrations/meta/0005_snapshot.json new file mode 100644 index 0000000..1e3716c --- /dev/null +++ b/migrations/meta/0005_snapshot.json @@ -0,0 +1,1144 @@ +{ + "id": "4cc11042-2c5e-4ffe-bf71-faedea5219e3", + "prevId": "55316de9-3aec-4333-b5b7-b1b6a78f8ce1", + "version": "7", + "dialect": "postgresql", + "tables": { + "base.user_activity": { + "name": "user_activity", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_activity_user_id_users_id_fk": { + "name": "user_activity_user_id_users_id_fk", + "tableFrom": "user_activity", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_notifications": { + "name": "user_notifications", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"email\":{\"task_assigned\":true,\"mentions\":true,\"daily_summary\":false},\"push\":{\"task_assigned\":true,\"reminders\":true}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_notifications_user_id_users_id_fk": { + "name": "user_notifications_user_id_users_id_fk", + "tableFrom": "user_notifications", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_security": { + "name": "user_security", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "is_2fa_enabled": { + "name": "is_2fa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_password_change": { + "name": "last_password_change", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_security_user_id_users_id_fk": { + "name": "user_security_user_id_users_id_fk", + "tableFrom": "user_security", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.users": { + "name": "users", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "middle_name": { + "name": "middle_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "language": { + "name": "language", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'ru'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.sessions": { + "name": "sessions", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "browser": { + "name": "browser", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "os": { + "name": "os", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "is_revoked": { + "name": "is_revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.tags": { + "name": "tags", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tags_name_unique": { + "name": "tags_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.team_members": { + "name": "team_members", + "schema": "base", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "team_role", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "member_status", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'inactive'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_status_idx": { + "name": "member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_role_idx": { + "name": "member_role_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "schemaTo": "base", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_members_team_id_user_id_pk": { + "name": "team_members_team_id_user_id_pk", + "columns": [ + "team_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.teams": { + "name": "teams", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_url": { + "name": "cover_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "team_active_slug_idx": { + "name": "team_active_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"base\".\"teams\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_slug_idx": { + "name": "team_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_owner_idx": { + "name": "team_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_deleted_at_idx": { + "name": "team_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_owner_id_users_id_fk": { + "name": "teams_owner_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_slug_unique": { + "name": "teams_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.teams_to_tags": { + "name": "teams_to_tags", + "schema": "base", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "teams_to_tags_tag_id_idx": { + "name": "teams_to_tags_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_to_tags_team_id_teams_id_fk": { + "name": "teams_to_tags_team_id_teams_id_fk", + "tableFrom": "teams_to_tags", + "tableTo": "teams", + "schemaTo": "base", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_to_tags_tag_id_tags_id_fk": { + "name": "teams_to_tags_tag_id_tags_id_fk", + "tableFrom": "teams_to_tags", + "tableTo": "tags", + "schemaTo": "base", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "teams_to_tags_team_id_tag_id_pk": { + "name": "teams_to_tags_team_id_tag_id_pk", + "columns": [ + "team_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.project_shares": { + "name": "project_shares", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "token_idx": { + "name": "token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_share_project_id_idx": { + "name": "project_share_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_shares_project_id_projects_id_fk": { + "name": "project_shares_project_id_projects_id_fk", + "tableFrom": "project_shares", + "tableTo": "projects", + "schemaTo": "base", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "project_shares_token_unique": { + "name": "project_shares_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.projects": { + "name": "projects", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "project_status", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "task_sequence": { + "name": "task_sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "project_visibility", + "typeSchema": "base", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "project_team_key_idx": { + "name": "project_team_key_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"base\".\"projects\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_team_name_idx": { + "name": "project_team_name_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"base\".\"projects\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_owner_id_idx": { + "name": "project_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_team_id_idx": { + "name": "project_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_team_id_teams_id_fk": { + "name": "projects_team_id_teams_id_fk", + "tableFrom": "projects", + "tableTo": "teams", + "schemaTo": "base", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "projects_owner_id_users_id_fk": { + "name": "projects_owner_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "base.team_role": { + "name": "team_role", + "schema": "base", + "values": [ + "owner", + "admin", + "lead", + "moderator", + "member", + "viewer" + ] + }, + "base.member_status": { + "name": "member_status", + "schema": "base", + "values": [ + "active", + "banned", + "inactive" + ] + }, + "base.project_status": { + "name": "project_status", + "schema": "base", + "values": [ + "active", + "archived", + "template" + ] + }, + "base.project_visibility": { + "name": "project_visibility", + "schema": "base", + "values": [ + "public", + "private" + ] + } + }, + "schemas": { + "base": "base" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json new file mode 100644 index 0000000..baeab62 --- /dev/null +++ b/migrations/meta/_journal.json @@ -0,0 +1,48 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1775839169154, + "tag": "0000_stale_sunspot", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1775925642197, + "tag": "0001_solid_kronos", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1776100122085, + "tag": "0002_pink_krista_starr", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1776171079742, + "tag": "0003_open_oracle", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1776262066530, + "tag": "0004_chief_talkback", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1776614072462, + "tag": "0005_calm_vivisector", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/nest-cli.json b/nest-cli.json index f9aa683..110f81c 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -1,8 +1,65 @@ { - "$schema": "https://json.schemastore.org/nest-cli", - "collection": "@nestjs/schematics", - "sourceRoot": "src", - "compilerOptions": { - "deleteOutDir": true - } + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true, + "webpack": true + }, + "projects": { + "bootstrap": { + "type": "library", + "root": "libs/bootstrap", + "entryFile": "index", + "sourceRoot": "libs/bootstrap/src", + "compilerOptions": { + "tsConfigPath": "libs/bootstrap/tsconfig.lib.json" + } + }, + "config": { + "type": "library", + "root": "libs/config", + "entryFile": "index", + "sourceRoot": "libs/config/src", + "compilerOptions": { + "tsConfigPath": "libs/config/tsconfig.lib.json" + } + }, + "database": { + "type": "library", + "root": "libs/database", + "entryFile": "index", + "sourceRoot": "libs/database/src", + "compilerOptions": { + "tsConfigPath": "libs/database/tsconfig.lib.json" + } + }, + "health": { + "type": "library", + "root": "libs/health", + "entryFile": "index", + "sourceRoot": "libs/health/src", + "compilerOptions": { + "tsConfigPath": "libs/health/tsconfig.lib.json" + } + }, + "s3": { + "type": "library", + "root": "libs/s3", + "entryFile": "index", + "sourceRoot": "libs/s3/src", + "compilerOptions": { + "tsConfigPath": "libs/s3/tsconfig.lib.json" + } + }, + "imagor": { + "type": "library", + "root": "libs/imagor", + "entryFile": "index", + "sourceRoot": "libs/imagor/src", + "compilerOptions": { + "tsConfigPath": "libs/imagor/tsconfig.lib.json" + } + } + } } diff --git a/package.json b/package.json index 1b859d3..12809ee 100644 --- a/package.json +++ b/package.json @@ -1,70 +1,107 @@ { - "name": "task-backend", - "version": "0.0.1", - "description": "", - "author": "", - "private": true, - "license": "UNLICENSED", - "scripts": { - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" - }, - "dependencies": { - "@nestjs/common": "^10.0.0", - "@nestjs/core": "^10.0.0", - "@nestjs/platform-express": "^10.0.0", - "reflect-metadata": "^0.2.0", - "rxjs": "^7.8.1" - }, - "devDependencies": { - "@nestjs/cli": "^10.0.0", - "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", - "@types/express": "^4.17.17", - "@types/jest": "^29.5.2", - "@types/node": "^20.3.1", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.42.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", - "jest": "^29.5.0", - "prettier": "^3.0.0", - "source-map-support": "^0.5.21", - "supertest": "^6.3.3", - "ts-jest": "^29.1.0", - "ts-loader": "^9.4.3", - "ts-node": "^10.9.1", - "tsconfig-paths": "^4.2.0", - "typescript": "^5.1.3" - }, - "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": "src", - "testRegex": ".*\\.spec\\.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" + "name": "task-backend", + "version": "0.0.1", + "description": "Основной API-сервис управления задачами (NestJS + Fastify + Drizzle ORM)", + "author": "", + "private": true, + "license": "MIT", + "scripts": { + "build": "nest build", + "format": "prettier --write \".\"", + "start:prod": "nest start", + "start:dev": "nest start -w", + "start:debug": "nest start -d -w", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "vitest run", + "test:w": "vitest", + "test:c": "vitest run --coverage", + "test:d": "vitest --inspect-brk --inspect --logHeapUsage --threads=false", + "test:e2e": "vitest run -c vitest.config.e2e.ts", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "prepare": "husky", + "k6:all": "pnpm --filter @project/performance-tests test:all", + "k6:auth": "pnpm --filter @project/performance-tests test:auth", + "k6:teams": "pnpm --filter @project/performance-tests test:teams", + "k6:projects": "pnpm --filter @project/performance-tests test:projects", + "k6:users": "pnpm --filter @project/performance-tests test:users", + "k6:board": "pnpm --filter @project/performance-tests test:board", + "k6:tasks": "pnpm --filter @project/performance-tests test:tasks", + "k6:smoke": "pnpm --filter @project/performance-tests smoke", + "k6:seed": "npx tsx infra/k6/scripts/seed-k6-data.ts", + "k6:clear": "npx tsx infra/k6/scripts/clear-k6-data.ts" }, - "collectCoverageFrom": [ - "**/*.(t|j)s" - ], - "coverageDirectory": "../coverage", - "testEnvironment": "node" - }, - "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319" + "dependencies": { + "@aws-sdk/client-s3": "^3.1029.0", + "@aws-sdk/s3-request-presigner": "^3.1029.0", + "@bull-board/api": "^6.21.0", + "@bull-board/fastify": "^6.21.0", + "@bull-board/nestjs": "^6.21.0", + "@fastify/compress": "^8.3.1", + "@fastify/cookie": "^11.0.2", + "@fastify/cors": "^11.2.0", + "@fastify/csrf-protection": "^7.1.0", + "@fastify/multipart": "^10.0.0", + "@fastify/static": "^9.1.0", + "@nestjs-modules/ioredis": "^2.2.1", + "@nestjs/axios": "^4.0.1", + "@nestjs/bullmq": "^11.0.4", + "@nestjs/common": "^11.1.18", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.1.18", + "@nestjs/event-emitter": "^3.1.0", + "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-fastify": "^11.1.18", + "@nestjs/swagger": "^11.2.7", + "@nestjs/throttler": "^6.5.0", + "@paralleldrive/cuid2": "^3.3.0", + "@willsoto/nestjs-prometheus": "^6.1.0", + "argon2": "^0.44.0", + "axios": "^1.16.0", + "bullmq": "^5.73.4", + "dotenv": "^17.4.2", + "drizzle-orm": "^0.45.2", + "drizzle-zod": "^0.8.3", + "fastify": "^5.8.4", + "handlebars": "^4.7.9", + "ioredis": "^5.10.1", + "nest-winston": "^1.10.2", + "nestjs-zod": "^5.3.0", + "nodemailer": "^8.0.5", + "otplib": "^13.4.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "postgres": "^3.4.9", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.1", + "transliteration": "^2.6.1", + "ua-parser-js": "^2.0.9", + "winston": "^3.19.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@commitlint/cli": "^20.5.0", + "@commitlint/config-conventional": "^20.5.0", + "@nestjs/cli": "^11.0.19", + "@nestjs/schematics": "^11.0.10", + "@nestjs/testing": "^11.1.18", + "@types/node": "^20.3.1", + "@types/nodemailer": "^8.0.0", + "@types/passport-jwt": "^4.0.1", + "@types/ua-parser-js": "^0.7.39", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "@vitest/coverage-v8": "^4.1.4", + "drizzle-kit": "^0.31.10", + "eslint": "^8.42.0", + "husky": "^9.1.7", + "lint-staged": "^16.4.0", + "prettier": "^3.0.0", + "ts-loader": "^9.4.3", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3", + "vitest": "^4.1.4" + }, + "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39f752a..ef2954e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,3472 +8,6567 @@ importers: .: dependencies: - '@nestjs/common': + '@aws-sdk/client-s3': + specifier: ^3.1029.0 + version: 3.1029.0 + '@aws-sdk/s3-request-presigner': + specifier: ^3.1029.0 + version: 3.1029.0 + '@bull-board/api': + specifier: ^6.21.0 + version: 6.21.0(@bull-board/ui@6.21.0) + '@bull-board/fastify': + specifier: ^6.21.0 + version: 6.21.0 + '@bull-board/nestjs': + specifier: ^6.21.0 + version: 6.21.0(@bull-board/api@6.21.0(@bull-board/ui@6.21.0))(@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)))(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@fastify/compress': + specifier: ^8.3.1 + version: 8.3.1 + '@fastify/cookie': + specifier: ^11.0.2 + version: 11.0.2 + '@fastify/cors': + specifier: ^11.2.0 + version: 11.2.0 + '@fastify/csrf-protection': + specifier: ^7.1.0 + version: 7.1.0 + '@fastify/multipart': specifier: ^10.0.0 - version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 10.0.0 + '@fastify/static': + specifier: ^9.1.0 + version: 9.1.0 + '@nestjs-modules/ioredis': + specifier: ^2.2.1 + version: 2.2.1(@nestjs/axios@4.0.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.16.0)(rxjs@7.8.2))(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(ioredis@5.10.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/axios': + specifier: ^4.0.1 + version: 4.0.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.16.0)(rxjs@7.8.2) + '@nestjs/bullmq': + specifier: ^11.0.4 + version: 11.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(bullmq@5.73.4) + '@nestjs/common': + specifier: ^11.1.18 + version: 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': + specifier: ^4.0.4 + version: 4.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': - specifier: ^10.0.0 - version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/platform-express': - specifier: ^10.0.0 - version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/event-emitter': + specifier: ^3.1.0 + version: 3.1.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/jwt': + specifier: ^11.0.2 + version: 11.0.2(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/passport': + specifier: ^11.0.5 + version: 11.0.5(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + '@nestjs/platform-fastify': + specifier: ^11.1.18 + version: 11.1.18(@fastify/static@9.1.0)(@fastify/view@11.1.1)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/swagger': + specifier: ^11.2.7 + version: 11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + '@nestjs/throttler': + specifier: ^6.5.0 + version: 6.5.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + '@paralleldrive/cuid2': + specifier: ^3.3.0 + version: 3.3.0 + '@willsoto/nestjs-prometheus': + specifier: ^6.1.0 + version: 6.1.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(prom-client@15.1.3) + argon2: + specifier: ^0.44.0 + version: 0.44.0 + axios: + specifier: ^1.16.0 + version: 1.16.0 + bullmq: + specifier: ^5.73.4 + version: 5.73.4 + dotenv: + specifier: ^17.4.2 + version: 17.4.2 + drizzle-orm: + specifier: ^0.45.2 + version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0)(postgres@3.4.9) + drizzle-zod: + specifier: ^0.8.3 + version: 0.8.3(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0)(postgres@3.4.9))(zod@4.3.6) + fastify: + specifier: ^5.8.4 + version: 5.8.4 + handlebars: + specifier: ^4.7.9 + version: 4.7.9 + ioredis: + specifier: ^5.10.1 + version: 5.10.1 + nest-winston: + specifier: ^1.10.2 + version: 1.10.2(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(winston@3.19.0) + nestjs-zod: + specifier: ^5.3.0 + version: 5.3.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) + nodemailer: + specifier: ^8.0.5 + version: 8.0.5 + otplib: + specifier: ^13.4.0 + version: 13.4.0 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + postgres: + specifier: ^3.4.9 + version: 3.4.9 reflect-metadata: specifier: ^0.2.0 version: 0.2.2 rxjs: specifier: ^7.8.1 version: 7.8.2 + transliteration: + specifier: ^2.6.1 + version: 2.6.1 + ua-parser-js: + specifier: ^2.0.9 + version: 2.0.9 + winston: + specifier: ^3.19.0 + version: 3.19.0 + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: + '@commitlint/cli': + specifier: ^20.5.0 + version: 20.5.0(@types/node@20.19.39)(conventional-commits-parser@6.4.0)(typescript@5.9.3) + '@commitlint/config-conventional': + specifier: ^20.5.0 + version: 20.5.0 '@nestjs/cli': - specifier: ^10.0.0 - version: 10.4.9 + specifier: ^11.0.19 + version: 11.0.19(@swc/core@1.15.24)(@types/node@20.19.39)(esbuild@0.27.7) '@nestjs/schematics': - specifier: ^10.0.0 - version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) + specifier: ^11.0.10 + version: 11.0.10(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': - specifier: ^10.0.0 - version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/platform-express@10.4.22) - '@types/express': - specifier: ^4.17.17 - version: 4.17.25 - '@types/jest': - specifier: ^29.5.2 - version: 29.5.14 + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@types/node': specifier: ^20.3.1 version: 20.19.39 - '@types/supertest': - specifier: ^6.0.0 - version: 6.0.3 + '@types/nodemailer': + specifier: ^8.0.0 + version: 8.0.0 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 + '@types/ua-parser-js': + specifier: ^0.7.39 + version: 0.7.39 '@typescript-eslint/eslint-plugin': specifier: ^6.0.0 version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': specifier: ^6.0.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@vitest/coverage-v8': + specifier: ^4.1.4 + version: 4.1.4(vitest@4.1.4) + drizzle-kit: + specifier: ^0.31.10 + version: 0.31.10 eslint: specifier: ^8.42.0 version: 8.57.1 - eslint-config-prettier: - specifier: ^9.0.0 - version: 9.1.2(eslint@8.57.1) - eslint-plugin-prettier: - specifier: ^5.0.0 - version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.8.2) - jest: - specifier: ^29.5.0 - version: 29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) + husky: + specifier: ^9.1.7 + version: 9.1.7 + lint-staged: + specifier: ^16.4.0 + version: 16.4.0 prettier: specifier: ^3.0.0 version: 3.8.2 - source-map-support: - specifier: ^0.5.21 - version: 0.5.21 - supertest: - specifier: ^6.3.3 - version: 6.3.4 - ts-jest: - specifier: ^29.1.0 - version: 29.4.9(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.4.3 - version: 9.5.7(typescript@5.9.3)(webpack@5.97.1) - ts-node: - specifier: ^10.9.1 - version: 10.9.2(@types/node@20.19.39)(typescript@5.9.3) + version: 9.5.7(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7)) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 typescript: specifier: ^5.1.3 version: 5.9.3 + vitest: + specifier: ^4.1.4 + version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + + infra/k6: {} packages: - '@angular-devkit/core@17.3.11': - resolution: {integrity: sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==} - engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@angular-devkit/core@19.2.23': + resolution: {integrity: sha512-RazHPQkUEsNU/OZ75w9UeHxGFMthRiuAW2B/uA7eXExBj/1meHrrBfoCA56ujW2GUxVjRtSrMjylKh4R4meiYA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/core@19.2.24': + resolution: {integrity: sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: - chokidar: ^3.5.2 + chokidar: ^4.0.0 peerDependenciesMeta: chokidar: optional: true - '@angular-devkit/schematics-cli@17.3.11': - resolution: {integrity: sha512-kcOMqp+PHAKkqRad7Zd7PbpqJ0LqLaNZdY1+k66lLWmkEBozgq8v4ASn/puPWf9Bo0HpCiK+EzLf0VHE8Z/y6Q==} - engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@angular-devkit/schematics-cli@19.2.24': + resolution: {integrity: sha512-bsStZQG67J1HBqTmWxtIcobvgrn32L4UOdL7hGyOru5VxDWPNA8pRnDYavT3hnJeBkJYPoQIw8u7Dm0ecoQprw==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true - '@angular-devkit/schematics@17.3.11': - resolution: {integrity: sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==} - engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@angular-devkit/schematics@19.2.23': + resolution: {integrity: sha512-Jzs7YM4X6azmHU7Mw5tQSPMuvaqYS8SLnZOJbtiXCy1JyuW9bm/WBBecNHMiuZ8LHXKhvQ6AVX1tKrzF6uiDmw==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} + '@angular-devkit/schematics@19.2.24': + resolution: {integrity: sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - '@babel/compat-data@7.29.0': - resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} - engines: {node: '>=6.9.0'} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} - engines: {node: '>=6.9.0'} + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} - engines: {node: '>=6.9.0'} + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} - engines: {node: '>=6.9.0'} + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} - engines: {node: '>=6.9.0'} + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@babel/helper-plugin-utils@7.28.6': - resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} - engines: {node: '>=6.9.0'} + '@aws-sdk/client-s3@3.1029.0': + resolution: {integrity: sha512-OuA8RZTxsAaHDcI25j2NGLMaYFI2WpJdDzK3uLmVBmaHwjQKQZOUDVVBcln8pNo3IgkY+HRSJhRR4/xlM//UyQ==} + engines: {node: '>=20.0.0'} - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} + '@aws-sdk/core@3.973.27': + resolution: {integrity: sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A==} + engines: {node: '>=20.0.0'} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} + '@aws-sdk/crc64-nvme@3.972.6': + resolution: {integrity: sha512-NMbiqKdruhwwgI6nzBVe2jWMkXjaoQz2YOs3rFX+2F3gGyrJDkDPwMpV/RsTFeq2vAQ055wZNtOXFK4NYSkM8g==} + engines: {node: '>=20.0.0'} - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} + '@aws-sdk/credential-provider-env@3.972.25': + resolution: {integrity: sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A==} + engines: {node: '>=20.0.0'} - '@babel/helpers@7.29.2': - resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} - engines: {node: '>=6.9.0'} + '@aws-sdk/credential-provider-http@3.972.27': + resolution: {integrity: sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ==} + engines: {node: '>=20.0.0'} - '@babel/parser@7.29.2': - resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} - engines: {node: '>=6.0.0'} - hasBin: true + '@aws-sdk/credential-provider-ini@3.972.29': + resolution: {integrity: sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@aws-sdk/credential-provider-login@3.972.29': + resolution: {integrity: sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-bigint@7.8.3': - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@aws-sdk/credential-provider-node@3.972.30': + resolution: {integrity: sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@aws-sdk/credential-provider-process@3.972.25': + resolution: {integrity: sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@aws-sdk/credential-provider-sso@3.972.29': + resolution: {integrity: sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-import-attributes@7.28.6': - resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@aws-sdk/credential-provider-web-identity@3.972.29': + resolution: {integrity: sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@aws-sdk/middleware-bucket-endpoint@3.972.9': + resolution: {integrity: sha512-COToYKgquDyligbcAep7ygs48RK+mwe/IYprq4+TSrVFzNOYmzWvHf6werpnKV5VYpRiwdn+Wa5ZXkPqLVwcTg==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@aws-sdk/middleware-expect-continue@3.972.9': + resolution: {integrity: sha512-V/FNCjFxnh4VGu+HdSiW4Yg5GELihA1MIDSAdsEPvuayXBVmr0Jaa6jdLAZLH38KYXl/vVjri9DQJWnTAujHEA==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-jsx@7.28.6': - resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@aws-sdk/middleware-flexible-checksums@3.974.7': + resolution: {integrity: sha512-uU4/ch2CLHB8Phu1oTKnnQ4e8Ujqi49zEnQYBhWYT53zfFvtJCdGsaOoypBr8Fm/pmCBssRmGoIQ4sixgdLP9w==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@aws-sdk/middleware-host-header@3.972.9': + resolution: {integrity: sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@aws-sdk/middleware-location-constraint@3.972.9': + resolution: {integrity: sha512-TyfOi2XNdOZpNKeTJwRUsVAGa+14nkyMb2VVGG+eDgcWG/ed6+NUo72N3hT6QJioxym80NSinErD+LBRF0Ir1w==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@aws-sdk/middleware-logger@3.972.9': + resolution: {integrity: sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@aws-sdk/middleware-recursion-detection@3.972.10': + resolution: {integrity: sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@aws-sdk/middleware-sdk-s3@3.972.28': + resolution: {integrity: sha512-qJHcJQH9UNPUrnPlRtCozKjtqAaypQ5IgQxTNoPsVYIQeuwNIA8Rwt3NvGij1vCDYDfCmZaPLpnJEHlZXeFqmg==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@aws-sdk/middleware-ssec@3.972.9': + resolution: {integrity: sha512-wSA2BR7L0CyBNDJeSrleIIzC+DzL93YNTdfU0KPGLiocK6YsRv1nPAzPF+BFSdcs0Qa5ku5Kcf4KvQcWwKGenQ==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@aws-sdk/middleware-user-agent@3.972.29': + resolution: {integrity: sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ==} + engines: {node: '>=20.0.0'} - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} + '@aws-sdk/nested-clients@3.996.19': + resolution: {integrity: sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/region-config-resolver@3.972.11': + resolution: {integrity: sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/s3-request-presigner@3.1029.0': + resolution: {integrity: sha512-YbHPaha4DYgJWdPorGV5ZSCCqHafGj4GiyqXmXFlCJSsqlOd3xEcemhOZGjrB9epdiVEUtB3DDJXGYYj55ITdQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.16': + resolution: {integrity: sha512-EMdXYB4r/k5RWq86fugjRhid5JA+Z6MpS7n4sij4u5/C+STrkvuf9aFu41rJA9MjUzxCLzv8U2XL8cH2GSRYpQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1026.0': + resolution: {integrity: sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.7': + resolution: {integrity: sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-arn-parser@3.972.3': + resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.996.6': + resolution: {integrity: sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-format-url@3.972.9': + resolution: {integrity: sha512-fNJXHrs0ZT7Wx0KGIqKv7zLxlDXt2vqjx9z6oKUQFmpE5o4xxnSryvVHfHpIifYHWKz94hFccIldJ0YSZjlCBw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.5': + resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.972.9': + resolution: {integrity: sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw==} + + '@aws-sdk/util-user-agent-node@3.973.15': + resolution: {integrity: sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w==} + engines: {node: '>=20.0.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true - '@babel/plugin-syntax-typescript@7.28.6': - resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + '@aws-sdk/xml-builder@3.972.17': + resolution: {integrity: sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} '@borewit/text-codec@0.2.2': resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + '@bull-board/api@6.21.0': + resolution: {integrity: sha512-5bX3U8baU4OulDLeXwqWI6/FZolpi1APfoJVXndR4fKdmuYr9cdbH8cg7juublfzX01T+3zoiZkveX7iD5y8gA==} + peerDependencies: + '@bull-board/ui': 6.21.0 + + '@bull-board/fastify@6.21.0': + resolution: {integrity: sha512-2Og70c0Br9fKF6cX5MKLt2WTvGw3yiu+4OG2K8UAE+yFBrm+VNHxEmfvXvsyoVlnT1bzBpLzaxqC21NWCzY6SA==} + + '@bull-board/nestjs@6.21.0': + resolution: {integrity: sha512-h4UhJw9Hc4ehQcs4y+fd7CgSTyIxHN1uFttwWiFuPpMkA+t5/OcAdlB0THigjxwmL2vYgcFzuk9nKb0qHtlRkw==} + peerDependencies: + '@bull-board/api': ^6.21.0 + '@nestjs/bull-shared': ^10.0.0 || ^11.0.0 + '@nestjs/common': ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + rxjs: ^7.8.1 + + '@bull-board/ui@6.21.0': + resolution: {integrity: sha512-SemKRipdrZVqboae/Xhl7CTdIwWJ+F3G/DEP7XHi1Qt1kXZUIKJkySXlFHILunygCiHRpCJ6/Ax/XNdHI/n3QA==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@commitlint/cli@20.5.0': + resolution: {integrity: sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==} + engines: {node: '>=v18'} + hasBin: true - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@commitlint/config-conventional@20.5.0': + resolution: {integrity: sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==} + engines: {node: '>=v18'} - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@commitlint/config-validator@20.5.0': + resolution: {integrity: sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==} + engines: {node: '>=v18'} - '@eslint/js@8.57.1': - resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@commitlint/ensure@20.5.0': + resolution: {integrity: sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==} + engines: {node: '>=v18'} - '@humanwhocodes/config-array@0.13.0': - resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead + '@commitlint/execute-rule@20.0.0': + resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} + engines: {node: '>=v18'} - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} + '@commitlint/format@20.5.0': + resolution: {integrity: sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==} + engines: {node: '>=v18'} - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead + '@commitlint/is-ignored@20.5.0': + resolution: {integrity: sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==} + engines: {node: '>=v18'} - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} + '@commitlint/lint@20.5.0': + resolution: {integrity: sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==} + engines: {node: '>=v18'} - '@istanbuljs/load-nyc-config@1.1.0': - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} + '@commitlint/load@20.5.0': + resolution: {integrity: sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==} + engines: {node: '>=v18'} - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} + '@commitlint/message@20.4.3': + resolution: {integrity: sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==} + engines: {node: '>=v18'} - '@jest/console@29.7.0': - resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@commitlint/parse@20.5.0': + resolution: {integrity: sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA==} + engines: {node: '>=v18'} - '@jest/core@29.7.0': - resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + '@commitlint/read@20.5.0': + resolution: {integrity: sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==} + engines: {node: '>=v18'} - '@jest/environment@29.7.0': - resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@commitlint/resolve-extends@20.5.0': + resolution: {integrity: sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==} + engines: {node: '>=v18'} - '@jest/expect-utils@29.7.0': - resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@commitlint/rules@20.5.0': + resolution: {integrity: sha512-5NdQXQEdnDPT5pK8O39ZA7HohzPRHEsDGU23cyVCNPQy4WegAbAwrQk3nIu7p2sl3dutPk8RZd91yKTrMTnRkQ==} + engines: {node: '>=v18'} - '@jest/expect@29.7.0': - resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@commitlint/to-lines@20.0.0': + resolution: {integrity: sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==} + engines: {node: '>=v18'} - '@jest/fake-timers@29.7.0': - resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@commitlint/top-level@20.4.3': + resolution: {integrity: sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==} + engines: {node: '>=v18'} - '@jest/globals@29.7.0': - resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@commitlint/types@20.5.0': + resolution: {integrity: sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==} + engines: {node: '>=v18'} - '@jest/reporters@29.7.0': - resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@conventional-changelog/git-client@2.7.0': + resolution: {integrity: sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==} + engines: {node: '>=18'} peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + conventional-commits-filter: ^5.0.0 + conventional-commits-parser: ^6.4.0 peerDependenciesMeta: - node-notifier: + conventional-commits-filter: + optional: true + conventional-commits-parser: optional: true - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} - '@jest/source-map@29.6.3': - resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} - '@jest/test-result@29.7.0': - resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} - '@jest/test-sequencer@29.7.0': - resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} - '@jest/transform@29.7.0': - resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@jest/types@29.6.3': - resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@epic-web/invariant@1.0.0': + resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] - '@jridgewell/source-map@0.3.11': - resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] - '@ljharb/through@2.3.14': - resolution: {integrity: sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==} - engines: {node: '>= 0.4'} + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] - '@lukeed/csprng@1.1.0': - resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} - engines: {node: '>=8'} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] - '@nestjs/cli@10.4.9': - resolution: {integrity: sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA==} - engines: {node: '>= 16.14'} - hasBin: true - peerDependencies: - '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 - '@swc/core': ^1.3.62 - peerDependenciesMeta: - '@swc/cli': - optional: true - '@swc/core': - optional: true + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] - '@nestjs/common@10.4.22': - resolution: {integrity: sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==} - peerDependencies: - class-transformer: '*' - class-validator: '*' - reflect-metadata: ^0.1.12 || ^0.2.0 - rxjs: ^7.1.0 - peerDependenciesMeta: - class-transformer: - optional: true - class-validator: - optional: true + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] - '@nestjs/core@10.4.22': - resolution: {integrity: sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==} - peerDependencies: - '@nestjs/common': ^10.0.0 - '@nestjs/microservices': ^10.0.0 - '@nestjs/platform-express': ^10.0.0 - '@nestjs/websockets': ^10.0.0 - reflect-metadata: ^0.1.12 || ^0.2.0 - rxjs: ^7.1.0 - peerDependenciesMeta: - '@nestjs/microservices': - optional: true - '@nestjs/platform-express': - optional: true - '@nestjs/websockets': - optional: true + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] - '@nestjs/platform-express@10.4.22': - resolution: {integrity: sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==} - peerDependencies: - '@nestjs/common': ^10.0.0 - '@nestjs/core': ^10.0.0 + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] - '@nestjs/schematics@10.2.3': - resolution: {integrity: sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==} - peerDependencies: - typescript: '>=4.8.2' + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] - '@nestjs/testing@10.4.22': - resolution: {integrity: sha512-HO9aPus3bAedAC+jKVAA8jTdaj4fs5M9fing4giHrcYV2txe9CvC1l1WAjwQ9RDhEHdugjY4y+FZA/U/YqPZrA==} - peerDependencies: - '@nestjs/common': ^10.0.0 - '@nestjs/core': ^10.0.0 - '@nestjs/microservices': ^10.0.0 - '@nestjs/platform-express': ^10.0.0 - peerDependenciesMeta: - '@nestjs/microservices': - optional: true - '@nestjs/platform-express': - optional: true + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] - '@noble/hashes@1.8.0': - resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} - engines: {node: ^14.21.3 || >=16} + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] - '@nuxtjs/opencollective@0.3.2': - resolution: {integrity: sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==} - engines: {node: '>=8.0.0', npm: '>=5.0.0'} - hasBin: true + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] - '@paralleldrive/cuid2@2.3.1': - resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] - '@pkgr/core@0.2.9': - resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] - '@sinclair/typebox@0.27.10': - resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] - '@sinonjs/commons@3.0.1': - resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] - '@sinonjs/fake-timers@10.3.0': - resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] - '@tokenizer/inflate@0.2.7': - resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} + cpu: [arm64] + os: [linux] - '@tokenizer/token@0.3.0': - resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] - '@tsconfig/node10@1.0.12': - resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] - '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] - '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] - '@tsconfig/node16@1.0.4': - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] - '@types/body-parser@1.19.6': - resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] - '@types/cookiejar@2.1.5': - resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] - '@types/eslint-scope@3.7.7': - resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] - '@types/eslint@9.6.1': - resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] - '@types/express-serve-static-core@4.19.8': - resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] - '@types/express@4.17.25': - resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] - '@types/graceful-fs@4.1.9': - resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] - '@types/http-errors@2.0.5': - resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] - '@types/istanbul-lib-report@3.0.3': - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] - '@types/istanbul-reports@3.0.4': - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - - '@types/jest@29.5.14': - resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/methods@1.1.4': - resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} - - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - - '@types/node@20.19.39': - resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} - - '@types/qs@6.15.0': - resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - - '@types/semver@7.7.1': - resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - - '@types/send@0.17.6': - resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} - - '@types/send@1.2.1': - resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - - '@types/serve-static@1.15.10': - resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] - '@types/superagent@8.1.9': - resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] - '@types/supertest@6.0.3': - resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] - '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] - '@types/yargs@17.0.35': - resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] - '@typescript-eslint/eslint-plugin@6.21.0': - resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] - '@typescript-eslint/parser@6.21.0': - resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] - '@typescript-eslint/scope-manager@6.21.0': - resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} - engines: {node: ^16.0.0 || >=18.0.0} + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] - '@typescript-eslint/type-utils@6.21.0': - resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] - '@typescript-eslint/types@6.21.0': - resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} - engines: {node: ^16.0.0 || >=18.0.0} + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] - '@typescript-eslint/typescript-estree@6.21.0': - resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] - '@typescript-eslint/utils@6.21.0': - resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] - '@typescript-eslint/visitor-keys@6.21.0': - resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} - engines: {node: ^16.0.0 || >=18.0.0} + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] - '@webassemblyjs/ast@1.14.1': - resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] - '@webassemblyjs/floating-point-hex-parser@1.13.2': - resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] - '@webassemblyjs/helper-api-error@1.13.2': - resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] - '@webassemblyjs/helper-buffer@1.14.1': - resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] - '@webassemblyjs/helper-numbers@1.13.2': - resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] - '@webassemblyjs/helper-wasm-bytecode@1.13.2': - resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] - '@webassemblyjs/helper-wasm-section@1.14.1': - resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] - '@webassemblyjs/ieee754@1.13.2': - resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] - '@webassemblyjs/leb128@1.13.2': - resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] - '@webassemblyjs/utf8@1.13.2': - resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] - '@webassemblyjs/wasm-edit@1.14.1': - resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] - '@webassemblyjs/wasm-gen@1.14.1': - resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] - '@webassemblyjs/wasm-opt@1.14.1': - resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] - '@webassemblyjs/wasm-parser@1.14.1': - resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@webassemblyjs/wast-printer@1.14.1': - resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@xtuc/ieee754@1.2.0': - resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@xtuc/long@4.2.2': - resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} + '@fastify/accept-negotiator@2.0.1': + resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} - acorn-walk@8.3.5: - resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} - engines: {node: '>=0.4.0'} + '@fastify/busboy@3.2.0': + resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true + '@fastify/compress@8.3.1': + resolution: {integrity: sha512-BUpItLr6MUX9e9ukg5Y6xekyA/7pBFG8QWtFCrUDm9ctoBc3R2/nA16yOaOWtVoccpXGjdDEYA/MxAb5+8cxag==} - ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true + '@fastify/cookie@11.0.2': + resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==} - ajv-keywords@3.5.2: - resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} - peerDependencies: - ajv: ^6.9.1 + '@fastify/cors@11.2.0': + resolution: {integrity: sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==} - ajv-keywords@5.1.0: - resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} - peerDependencies: - ajv: ^8.8.2 + '@fastify/csrf-protection@7.1.0': + resolution: {integrity: sha512-I2TDd4SRRYQivKCMHdB/8py+CPO9DT0e63lh4DO8MDCJh8NROq8HD/iO0IjYtwhsD3bZhr0cBXsFdfPvyTmzNw==} - ajv@6.14.0: - resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + '@fastify/csrf@8.0.1': + resolution: {integrity: sha512-dAmCrdfJ3CV/A/hHHK/rRBjjLRRSIltgJB0BxiVfbhr/31G6fgF8l2I8evtH8mjS5kTIvd0JOh7MOA3HA6eYDw==} - ajv@8.12.0: - resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + '@fastify/deepmerge@3.2.1': + resolution: {integrity: sha512-N5Oqvltoa2r9z1tbx4xjky0oRR60v+T47Ic4J1ukoVQcptLOrIdRnCSdTGmOmajZuHVKlTnfcmrjyqsGEW1ztA==} - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} + '@fastify/formbody@8.0.2': + resolution: {integrity: sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA==} - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + '@fastify/multipart@10.0.0': + resolution: {integrity: sha512-pUx3Z1QStY7E7kwvDTIvB6P+rF5lzP+iqPgZyJyG3yBJVPvQaZxzDHYbQD89rbY0ciXrMOyGi8ezHDVexLvJDA==} - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} + '@fastify/send@4.1.0': + resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==} - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} + '@fastify/static@9.1.0': + resolution: {integrity: sha512-EPRNQYqEYEYTK8yyGbcM0iHpyJaupb94bey5O6iCQfLTADr02kaZU+qeHSdd9H9TiMwTBVkrMa59V8CMbn3avQ==} - append-field@1.0.0: - resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + '@fastify/view@11.1.1': + resolution: {integrity: sha512-GiHqT3R2eKJgWmy0s45eELTC447a4+lTM2o+8fSWeKwBe9VToeePuHJcKtOEXPrKGSddGO0RsNayULiS3aeHeQ==} - arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} - array-timsort@1.0.3: - resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - asap@2.0.6: - resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - babel-jest@29.7.0: - resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} peerDependencies: - '@babel/core': ^7.8.0 + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - babel-plugin-istanbul@6.1.1: - resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} - engines: {node: '>=8'} + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - babel-plugin-jest-hoist@29.6.3: - resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} - babel-preset-current-node-syntax@1.2.0: - resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} peerDependencies: - '@babel/core': ^7.0.0 || ^8.0.0-0 + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - babel-preset-jest@29.6.3: - resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} peerDependencies: - '@babel/core': ^7.0.0 - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - baseline-browser-mapping@2.10.17: - resolution: {integrity: sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==} - engines: {node: '>=6.0.0'} - hasBin: true + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} + '@inquirer/prompts@7.3.2': + resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - body-parser@1.20.4: - resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - brace-expansion@1.1.13: - resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - brace-expansion@2.0.3: - resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} + '@ioredis/commands@1.5.1': + resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} - bser@2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} - call-bind@1.0.9: - resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} - engines: {node: '>= 0.4'} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] - caniuse-lite@1.0.30001787: - resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} + '@napi-rs/wasm-runtime@1.1.3': + resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + '@nestjs-modules/ioredis@2.2.1': + resolution: {integrity: sha512-wQ08XvlV2s9V+01SKcC5XmFoQ2hMAHP0KuVja8UFZyE/dM0bKI5HSHr+3wQ5ChRpsyhfxF/vKrlPXMlJIr7FIg==} + peerDependencies: + '@nestjs/common': '>=6.7.0' + '@nestjs/core': '>=6.7.0' + ioredis: '>=5.0.0' - char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} + '@nestjs/axios@4.0.1': + resolution: {integrity: sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + axios: ^1.3.1 + rxjs: ^7.0.0 - chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + '@nestjs/bull-shared@11.0.4': + resolution: {integrity: sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} + '@nestjs/bullmq@11.0.4': + resolution: {integrity: sha512-wBzK9raAVG0/6NTMdvLGM4/FQ1lsB35/pYS8L6a0SDgkTiLpd7mAjQ8R692oMx5s7IjvgntaZOuTUrKYLNfIkA==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + bullmq: ^3.0.0 || ^4.0.0 || ^5.0.0 - chrome-trace-event@1.0.4: - resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} - engines: {node: '>=6.0'} + '@nestjs/cli@11.0.19': + resolution: {integrity: sha512-9htODqTVVNH4lJqyeIotsAgfeaYngDi020cVCd6JhJRKuOT83c/t4JDSky6+xr0lhHyNTNMgZmulxqcMNZFfrw==} + engines: {node: '>= 20.11'} + hasBin: true + peerDependencies: + '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0 + '@swc/core': ^1.3.62 + peerDependenciesMeta: + '@swc/cli': + optional: true + '@swc/core': + optional: true - ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} + '@nestjs/common@11.1.18': + resolution: {integrity: sha512-0sLq8Z+TIjLnz1Tqp0C/x9BpLbqpt1qEu0VcH4/fkE0y3F5JxhfK1AdKQ/SPbKhKgwqVDoY4gS8GQr2G6ujaWg==} + peerDependencies: + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true - cjs-module-lexer@1.4.3: - resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + '@nestjs/config@4.0.4': + resolution: {integrity: sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + rxjs: ^7.1.0 - cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} + '@nestjs/core@11.1.18': + resolution: {integrity: sha512-wR3DtGyk/LUAiPtbXDuWJJwVkWElKBY0sqnTzf9d4uM3+X18FRZhK7WFc47czsIGOdWuRsMeLYV+1Z9dO4zDEQ==} + engines: {node: '>= 20'} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true - cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} + '@nestjs/event-emitter@3.1.0': + resolution: {integrity: sha512-DOY/4XBGyIjYyOJKkO6jl1kzFE0ZfX0wV+M2HR5NWymPT9Z0zdCEcZGxTXXkoMRwPtglnvCGJALSjOpXPIcM3g==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 - cli-table3@0.6.5: - resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} - engines: {node: 10.* || >= 12.*} + '@nestjs/jwt@11.0.2': + resolution: {integrity: sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 - cli-width@3.0.0: - resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} - engines: {node: '>= 10'} + '@nestjs/mapped-types@2.1.1': + resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true - cli-width@4.1.0: - resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} - engines: {node: '>= 12'} + '@nestjs/passport@11.0.5': + resolution: {integrity: sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + passport: ^0.5.0 || ^0.6.0 || ^0.7.0 - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} + '@nestjs/platform-fastify@11.1.18': + resolution: {integrity: sha512-iJtbqQz51k7Z1vOTUEHO1mU8PsDO1WdgPSJ/6CuXBnazkrkePXoszhefFaPwJreBVn35GE3WTd/6ou7bFwnhmA==} + peerDependencies: + '@fastify/static': ^8.0.0 || ^9.0.0 + '@fastify/view': ^10.0.0 || ^11.0.0 + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + '@fastify/view': + optional: true - clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} + '@nestjs/schematics@11.0.10': + resolution: {integrity: sha512-q9lr0wGwgBHLarD4uno3XiW4JX60WPlg2VTgbqPHl/6bT4u1IEEzj+q9Tad3bVnqL5mlDF3vrZ2tj+x13CJpmw==} + peerDependencies: + typescript: '>=4.8.2' - co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + '@nestjs/swagger@11.2.7': + resolution: {integrity: sha512-+e1KWSyZMAQeyZ8nbQSvm3fhzqdxxBNQENvpjO2dVyD7KJmLTTQyXpRb1nM5O04oFdDTUtG3SHMl4+e+zgCK2A==} + peerDependencies: + '@fastify/static': ^8.0.0 || ^9.0.0 + '@nestjs/common': ^11.0.1 + '@nestjs/core': ^11.0.1 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true - collect-v8-coverage@1.0.3: - resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + '@nestjs/terminus@11.1.1': + resolution: {integrity: sha512-Ssql79H+EQY/Wg108eJqN4NiNsO/tLrj+qbzOWSQUf2JE4vJQ2RG3WTqUOrYjfjWmVHD3+Ys0+azed7LSMKScw==} + peerDependencies: + '@grpc/grpc-js': '*' + '@grpc/proto-loader': '*' + '@mikro-orm/core': '*' + '@mikro-orm/nestjs': '*' + '@nestjs/axios': ^2.0.0 || ^3.0.0 || ^4.0.0 + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + '@nestjs/microservices': ^10.0.0 || ^11.0.0 + '@nestjs/mongoose': ^11.0.0 + '@nestjs/sequelize': ^10.0.0 || ^11.0.0 + '@nestjs/typeorm': ^10.0.0 || ^11.0.0 + '@prisma/client': '*' + mongoose: '*' + reflect-metadata: 0.1.x || 0.2.x + rxjs: 7.x + sequelize: '*' + typeorm: '*' + peerDependenciesMeta: + '@grpc/grpc-js': + optional: true + '@grpc/proto-loader': + optional: true + '@mikro-orm/core': + optional: true + '@mikro-orm/nestjs': + optional: true + '@nestjs/axios': + optional: true + '@nestjs/microservices': + optional: true + '@nestjs/mongoose': + optional: true + '@nestjs/sequelize': + optional: true + '@nestjs/typeorm': + optional: true + '@prisma/client': + optional: true + mongoose: + optional: true + sequelize: + optional: true + typeorm: + optional: true - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + '@nestjs/testing@11.1.18': + resolution: {integrity: sha512-frzwNlpBgtAzI3hp/qo57DZoRO4RMTH1wST3QUYEhRTHyfPkLpzkWz3jV/mhApXjD0yT56Ptlzn6zuYPLh87Lw==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + '@nestjs/throttler@6.5.0': + resolution: {integrity: sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} - comment-json@4.2.5: - resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} - engines: {node: '>= 6'} + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} - component-emitter@1.3.1: - resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + '@nuxt/opencollective@0.4.1': + resolution: {integrity: sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==} + engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} + hasBin: true - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} - concat-stream@2.0.0: - resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} - engines: {'0': node >= 6.0} + '@otplib/core@13.4.0': + resolution: {integrity: sha512-JqOGcvZQi2wIkEQo8f3/iAjstavpXy6gouIDMHygjNuH6Q0FjbHOiXMdcE94RwfgDNMABhzwUmvaPsxvgm9NYw==} - consola@2.15.3: - resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + '@otplib/hotp@13.4.0': + resolution: {integrity: sha512-MJjE0x06mn2ptymz5qZmQveb+vWFuaIftqE0b5/TZZqUOK7l97cV8lRTmid5BpAQMwJDNLW6RnYxGeCRiNdekw==} - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} + '@otplib/plugin-base32-scure@13.4.0': + resolution: {integrity: sha512-/t9YWJmMbB8bF5z8mXrBZc2FXBe8B/3hG5FhWr9K8cFwFhyxScbPysmZe8s1UTzSA6N+s8Uv8aIfCtVXPNjJWw==} - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} + '@otplib/plugin-crypto-noble@13.4.0': + resolution: {integrity: sha512-KrvE4m7Zv+TT1944HzgqFJWJpKb6AyoxDbvhPStmBqdMlv5Gekb80d66cuFRL08kkPgJ5gXUSb5SFpYeB+bACg==} - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + '@otplib/totp@13.4.0': + resolution: {integrity: sha512-dK+vl0f0ekzf6mCENRI9AKS2NJUC7OjI3+X8e7QSnhQ2WM7I+i4PGpb3QxKi5hxjTtwVuoZwXR2CFtXdcRtNdQ==} - cookie-signature@1.0.7: - resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + '@otplib/uri@13.4.0': + resolution: {integrity: sha512-x1ozBa5bPbdZCrrTL/HK21qchiK7jYElTu+0ft22abeEhiLYgH1+SIULvOcVk3CK8YwF4kdcidvkq4ciejucJA==} - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} + '@oxc-project/types@0.124.0': + resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} - cookiejar@2.1.4: - resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + '@paralleldrive/cuid2@3.3.0': + resolution: {integrity: sha512-OqiFvSOF0dBSesELYY2CAMa4YINvlLpvKOz/rv6NeZEqiyttlHgv98Juwv4Ch+GrEV7IZ8jfI2VcEoYUjXXCjw==} + hasBin: true - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + '@phc/format@1.0.0': + resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} + engines: {node: '>=10'} - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} - cosmiconfig@8.3.6: - resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true + '@rolldown/binding-android-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] - create-jest@29.7.0: - resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.15': + resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + + '@scure/base@2.0.0': + resolution: {integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==} + + '@simple-libs/child-process-utils@1.0.2': + resolution: {integrity: sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==} + engines: {node: '>=18'} - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + '@simple-libs/stream-utils@1.2.0': + resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} + engines: {node: '>=18'} - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + '@smithy/chunked-blob-reader-native@4.2.3': + resolution: {integrity: sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==} + engines: {node: '>=18.0.0'} - dedent@1.7.2: - resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true + '@smithy/chunked-blob-reader@5.2.2': + resolution: {integrity: sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==} + engines: {node: '>=18.0.0'} - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + '@smithy/config-resolver@4.4.14': + resolution: {integrity: sha512-N55f8mPEccpzKetUagdvmAy8oohf0J5cuj9jLI1TaSceRlq0pJsIZepY3kmAXAhyxqXPV6hDerDQhqQPKWgAoQ==} + engines: {node: '>=18.0.0'} - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} + '@smithy/core@3.23.14': + resolution: {integrity: sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg==} + engines: {node: '>=18.0.0'} - defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + '@smithy/credential-provider-imds@4.2.13': + resolution: {integrity: sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ==} + engines: {node: '>=18.0.0'} - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} + '@smithy/eventstream-codec@4.2.13': + resolution: {integrity: sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ==} + engines: {node: '>=18.0.0'} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} + '@smithy/eventstream-serde-browser@4.2.13': + resolution: {integrity: sha512-wwybfcOX0tLqCcBP378TIU9IqrDuZq/tDV48LlZNydMpCnqnYr+hWBAYbRE+rFFf/p7IkDJySM3bgiMKP2ihPg==} + engines: {node: '>=18.0.0'} - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} + '@smithy/eventstream-serde-config-resolver@4.3.13': + resolution: {integrity: sha512-ied1lO559PtAsMJzg2TKRlctLnEi1PfkNeMMpdwXDImk1zV9uvS/Oxoy/vcy9uv1GKZAjDAB5xT6ziE9fzm5wA==} + engines: {node: '>=18.0.0'} - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + '@smithy/eventstream-serde-node@4.2.13': + resolution: {integrity: sha512-hFyK+ORJrxAN3RYoaD6+gsGDQjeix8HOEkosoajvXYZ4VeqonM3G4jd9IIRm/sWGXUKmudkY9KdYjzosUqdM8A==} + engines: {node: '>=18.0.0'} - detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} + '@smithy/eventstream-serde-universal@4.2.13': + resolution: {integrity: sha512-kRrq4EKLGeOxhC2CBEhRNcu1KSzNJzYY7RK3S7CxMPgB5dRrv55WqQOtRwQxQLC04xqORFLUgnDlc6xrNUULaA==} + engines: {node: '>=18.0.0'} - dezalgo@1.0.4: - resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + '@smithy/fetch-http-handler@5.3.16': + resolution: {integrity: sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ==} + engines: {node: '>=18.0.0'} - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@smithy/hash-blob-browser@4.2.14': + resolution: {integrity: sha512-rtQ5es8r/5v4rav7q5QTsfx9CtCyzrz/g7ZZZBH2xtMmd6G/KQrLOWfSHTvFOUPlVy59RQvxeBYJaLRoybMEyA==} + engines: {node: '>=18.0.0'} - diff@4.0.4: - resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} - engines: {node: '>=0.3.1'} + '@smithy/hash-node@4.2.13': + resolution: {integrity: sha512-4/oy9h0jjmY80a2gOIo75iLl8TOPhmtx4E2Hz+PfMjvx/vLtGY4TMU/35WRyH2JHPfT5CVB38u4JRow7gnmzJA==} + engines: {node: '>=18.0.0'} - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} + '@smithy/hash-stream-node@4.2.13': + resolution: {integrity: sha512-WdQ7HwUjINXETeh6dqUeob1UHIYx8kAn9PSp1HhM2WWegiZBYVy2WXIs1lB07SZLan/udys9SBnQGt9MQbDpdg==} + engines: {node: '>=18.0.0'} - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} + '@smithy/invalid-dependency@4.2.13': + resolution: {integrity: sha512-jvC0RB/8BLj2SMIkY0Npl425IdnxZJxInpZJbu563zIRnVjpDMXevU3VMCRSabaLB0kf/eFIOusdGstrLJ8IDg==} + engines: {node: '>=18.0.0'} - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + '@smithy/is-array-buffer@4.2.2': + resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} + engines: {node: '>=18.0.0'} - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + '@smithy/md5-js@4.2.13': + resolution: {integrity: sha512-cNm7I9NXolFxtS20ojROddOEpSAeI1Obq6pd1Kj5HtHws3s9Fkk8DdHDfQSs5KuxCewZuVK6UqrJnfJmiMzDuQ==} + engines: {node: '>=18.0.0'} - electron-to-chromium@1.5.334: - resolution: {integrity: sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==} + '@smithy/middleware-content-length@4.2.13': + resolution: {integrity: sha512-IPMLm/LE4AZwu6qiE8Rr8vJsWhs9AtOdySRXrOM7xnvclp77Tyh7hMs/FRrMf26kgIe67vFJXXOSmVxS7oKeig==} + engines: {node: '>=18.0.0'} - emittery@0.13.1: - resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} - engines: {node: '>=12'} + '@smithy/middleware-endpoint@4.4.29': + resolution: {integrity: sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw==} + engines: {node: '>=18.0.0'} - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + '@smithy/middleware-retry@4.5.1': + resolution: {integrity: sha512-/zY+Gp7Qj2D2hVm3irkCyONER7E9MiX3cUUm/k2ZmhkzZkrPgwVS4aJ5NriZUEN/M0D1hhjrgjUmX04HhRwdWA==} + engines: {node: '>=18.0.0'} - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + '@smithy/middleware-serde@4.2.17': + resolution: {integrity: sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ==} + engines: {node: '>=18.0.0'} - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} + '@smithy/middleware-stack@4.2.13': + resolution: {integrity: sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw==} + engines: {node: '>=18.0.0'} - enhanced-resolve@5.20.1: - resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} - engines: {node: '>=10.13.0'} + '@smithy/node-config-provider@4.3.13': + resolution: {integrity: sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw==} + engines: {node: '>=18.0.0'} - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + '@smithy/node-http-handler@4.5.2': + resolution: {integrity: sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA==} + engines: {node: '>=18.0.0'} - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} + '@smithy/property-provider@4.2.13': + resolution: {integrity: sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ==} + engines: {node: '>=18.0.0'} - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} + '@smithy/protocol-http@5.3.13': + resolution: {integrity: sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg==} + engines: {node: '>=18.0.0'} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + '@smithy/querystring-builder@4.2.13': + resolution: {integrity: sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ==} + engines: {node: '>=18.0.0'} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} + '@smithy/querystring-parser@4.2.13': + resolution: {integrity: sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA==} + engines: {node: '>=18.0.0'} - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} + '@smithy/service-error-classification@4.2.13': + resolution: {integrity: sha512-a0s8XZMfOC/qpqq7RCPvJlk93rWFrElH6O++8WJKz0FqnA4Y7fkNi/0mnGgSH1C4x6MFsuBA8VKu4zxFrMe5Vw==} + engines: {node: '>=18.0.0'} - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} + '@smithy/shared-ini-file-loader@4.4.8': + resolution: {integrity: sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw==} + engines: {node: '>=18.0.0'} - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + '@smithy/signature-v4@5.3.13': + resolution: {integrity: sha512-YpYSyM0vMDwKbHD/JA7bVOF6kToVRpa+FM5ateEVRpsTNu564g1muBlkTubXhSKKYXInhpADF46FPyrZcTLpXg==} + engines: {node: '>=18.0.0'} - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} + '@smithy/smithy-client@4.12.9': + resolution: {integrity: sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ==} + engines: {node: '>=18.0.0'} - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} + '@smithy/types@4.14.0': + resolution: {integrity: sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ==} + engines: {node: '>=18.0.0'} - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} + '@smithy/url-parser@4.2.13': + resolution: {integrity: sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw==} + engines: {node: '>=18.0.0'} - eslint-config-prettier@9.1.2: - resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' + '@smithy/util-base64@4.3.2': + resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} + engines: {node: '>=18.0.0'} - eslint-plugin-prettier@5.5.5: - resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - '@types/eslint': '>=8.0.0' - eslint: '>=8.0.0' - eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' - prettier: '>=3.0.0' - peerDependenciesMeta: - '@types/eslint': - optional: true - eslint-config-prettier: - optional: true + '@smithy/util-body-length-browser@4.2.2': + resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==} + engines: {node: '>=18.0.0'} - eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} + '@smithy/util-body-length-node@4.2.3': + resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==} + engines: {node: '>=18.0.0'} - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@smithy/util-buffer-from@4.2.2': + resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} + engines: {node: '>=18.0.0'} - eslint@8.57.1: - resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true + '@smithy/util-config-provider@4.2.2': + resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} + engines: {node: '>=18.0.0'} - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@smithy/util-defaults-mode-browser@4.3.45': + resolution: {integrity: sha512-ag9sWc6/nWZAuK3Wm9KlFJUnRkXLrXn33RFjIAmCTFThqLHY+7wCst10BGq56FxslsDrjhSie46c8OULS+BiIw==} + engines: {node: '>=18.0.0'} - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true + '@smithy/util-defaults-mode-node@4.2.49': + resolution: {integrity: sha512-jlN6vHwE8gY5AfiFBavtD3QtCX2f7lM3BKkz7nFKSNfFR5nXLXLg6sqXTJEEyDwtxbztIDBQCfjsGVXlIru2lQ==} + engines: {node: '>=18.0.0'} - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} + '@smithy/util-endpoints@3.3.4': + resolution: {integrity: sha512-BKoR/ubPp9KNKFxPpg1J28N1+bgu8NGAtJblBP7yHy8yQPBWhIAv9+l92SlQLpolGm71CVO+btB60gTgzT0wog==} + engines: {node: '>=18.0.0'} - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} + '@smithy/util-hex-encoding@4.2.2': + resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} + engines: {node: '>=18.0.0'} - estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} + '@smithy/util-middleware@4.2.13': + resolution: {integrity: sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow==} + engines: {node: '>=18.0.0'} - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} + '@smithy/util-retry@4.3.1': + resolution: {integrity: sha512-FwmicpgWOkP5kZUjN3y+3JIom8NLGqSAJBeoIgK0rIToI817TEBHCrd0A2qGeKQlgDeP+Jzn4i0H/NLAXGy9uQ==} + engines: {node: '>=18.0.0'} - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} + '@smithy/util-stream@4.5.22': + resolution: {integrity: sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew==} + engines: {node: '>=18.0.0'} - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} + '@smithy/util-uri-escape@4.2.2': + resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} + engines: {node: '>=18.0.0'} - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} + '@smithy/util-utf8@4.2.2': + resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} + engines: {node: '>=18.0.0'} - exit@0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} - engines: {node: '>= 0.8.0'} + '@smithy/util-waiter@4.2.15': + resolution: {integrity: sha512-oUt9o7n8hBv3BL56sLSneL0XeigZSuem0Hr78JaoK33D9oKieyCvVP8eTSe3j7g2mm/S1DvzxKieG7JEWNJUNg==} + engines: {node: '>=18.0.0'} - expect@29.7.0: - resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@smithy/uuid@1.1.2': + resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} + engines: {node: '>=18.0.0'} - express@4.22.1: - resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} - engines: {node: '>= 0.10.0'} + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} - external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + '@swc/core-darwin-arm64@1.15.24': + resolution: {integrity: sha512-uM5ZGfFXjtvtJ+fe448PVBEbn/CSxS3UAyLj3O9xOqKIWy3S6hPTXSPbszxkSsGDYKi+YFhzAsR4r/eXLxEQ0g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] - fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + '@swc/core-darwin-x64@1.15.24': + resolution: {integrity: sha512-fMIb/Zfn929pw25VMBhV7Ji2Dl+lCWtUPNdYJQYOke+00E5fcQ9ynxtP8+qhUo/HZc+mYQb1gJxwHM9vty+lXg==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} + '@swc/core-linux-arm-gnueabihf@1.15.24': + resolution: {integrity: sha512-vOkjsyjjxnoYx3hMEWcGxQrMgnNrRm6WAegBXrN8foHtDAR+zpdhpGF5a4lj1bNPgXAvmysjui8cM1ov/Clkaw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + '@swc/core-linux-arm64-gnu@1.15.24': + resolution: {integrity: sha512-h/oNu+upkXJ6Cicnq7YGVj9PkdfarLCdQa8l/FlHYvfv8CEiMaeeTnpLU7gSBH/rGxosM6Qkfa/J9mThGF9CLA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + '@swc/core-linux-arm64-musl@1.15.24': + resolution: {integrity: sha512-ZpF/pRe1guk6sKzQI9D1jAORtjTdNlyeXn9GDz8ophof/w2WhojRblvSDJaGe7rJjcPN8AaOkhwdRUh7q8oYIg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] - fast-safe-stringify@2.1.1: - resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + '@swc/core-linux-ppc64-gnu@1.15.24': + resolution: {integrity: sha512-QZEsZfisHTSJlmyChgDFNmKPb3W6Lhbfo/O76HhIngfEdnQNmukS38/VSe1feho+xkV5A5hETyCbx3sALBZKAQ==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + '@swc/core-linux-s390x-gnu@1.15.24': + resolution: {integrity: sha512-DLdJKVsJgglqQrJBuoUYNmzm3leI7kUZhLbZGHv42onfKsGf6JDS3+bzCUQfte/XOqDjh/tmmn1DR/CF/tCJFw==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + libc: [glibc] - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + '@swc/core-linux-x64-gnu@1.15.24': + resolution: {integrity: sha512-IpLYfposPA/XLxYOKpRfeccl1p5dDa3+okZDHHTchBkXEaVCnq5MADPmIWwIYj1tudt7hORsEHccG5no6IUQRw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] - fb-watchman@2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + '@swc/core-linux-x64-musl@1.15.24': + resolution: {integrity: sha512-JHy3fMSc0t/EPWgo74+OK5TGr51aElnzqfUPaiRf2qJ/BfX5CUCfMiWVBuhI7qmVMBnk1jTRnL/xZnOSHDPLYg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] - fflate@0.8.2: - resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + '@swc/core-win32-arm64-msvc@1.15.24': + resolution: {integrity: sha512-Txj+qUH1z2bUd1P3JvwByfjKFti3cptlAxhWgmunBUUxy/IW3CXLZ6l6Gk4liANadKkU71nIU1X30Z5vpMT3BA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] - figures@3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} + '@swc/core-win32-ia32-msvc@1.15.24': + resolution: {integrity: sha512-15D/nl3XwrhFpMv+MADFOiVwv3FvH9j8c6Rf8EXBT3Q5LoMh8YnDnSgPYqw1JzPnksvsBX6QPXLiPqmcR/Z4qQ==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + '@swc/core-win32-x64-msvc@1.15.24': + resolution: {integrity: sha512-PR0PlTlPra2JbaDphrOAzm6s0v9rA0F17YzB+XbWD95B4g2cWcZY9LAeTa4xll70VLw9Jr7xBrlohqlQmelMFQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] - file-type@20.4.1: - resolution: {integrity: sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==} - engines: {node: '>=18'} + '@swc/core@1.15.24': + resolution: {integrity: sha512-5Hj8aNasue7yusUt8LGCUe/AjM7RMAce8ZoyDyiFwx7Al+GbYKL+yE7g4sJk8vEr1dKIkTRARkNIJENc4CjkBQ==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - finalhandler@1.3.2: - resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} - engines: {node: '>= 0.8'} + '@swc/types@0.1.26': + resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - fork-ts-checker-webpack-plugin@9.0.2: - resolution: {integrity: sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==} - engines: {node: '>=12.13.0', yarn: '>=1.0.0'} - peerDependencies: - typescript: '>3.6.0' - webpack: ^5.11.0 + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - formidable@2.1.5: - resolution: {integrity: sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==} + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} - fs-monkey@1.1.0: - resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} + '@types/node@20.19.39': + resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} + '@types/nodemailer@8.0.0': + resolution: {integrity: sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==} - get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} + '@types/passport-jwt@4.0.1': + resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==} - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} + '@types/passport-strategy@0.2.38': + resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==} - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} + '@types/passport@1.0.17': + resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} + '@types/qs@6.15.0': + resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} - glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - - handlebars@4.7.9: - resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} - engines: {node: '>=0.4.7'} - hasBin: true - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} + '@types/ua-parser-js@0.7.39': + resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==} - has-own-prop@2.0.0: - resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} - engines: {node: '>=8'} - - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} + '@typescript-eslint/eslint-plugin@6.21.0': + resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} + '@typescript-eslint/parser@6.21.0': + resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + '@typescript-eslint/scope-manager@6.21.0': + resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} + engines: {node: ^16.0.0 || >=18.0.0} - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} + '@typescript-eslint/type-utils@6.21.0': + resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} + '@typescript-eslint/types@6.21.0': + resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} + engines: {node: ^16.0.0 || >=18.0.0} - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} + '@typescript-eslint/typescript-estree@6.21.0': + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + '@typescript-eslint/utils@6.21.0': + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} + '@typescript-eslint/visitor-keys@6.21.0': + resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} + engines: {node: ^16.0.0 || >=18.0.0} - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - import-local@3.2.0: - resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} - engines: {node: '>=8'} - hasBin: true + '@vitest/coverage-v8@4.1.4': + resolution: {integrity: sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==} + peerDependencies: + '@vitest/browser': 4.1.4 + vitest: 4.1.4 + peerDependenciesMeta: + '@vitest/browser': + optional: true - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} + '@vitest/expect@4.1.4': + resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==} - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + '@vitest/mocker@4.1.4': + resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + '@vitest/pretty-format@4.1.4': + resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} - inquirer@8.2.6: - resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} - engines: {node: '>=12.0.0'} + '@vitest/runner@4.1.4': + resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==} - inquirer@9.2.15: - resolution: {integrity: sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==} - engines: {node: '>=18'} + '@vitest/snapshot@4.1.4': + resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==} - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} + '@vitest/spy@4.1.4': + resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==} - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + '@vitest/utils@4.1.4': + resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} - is-generator-fn@2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} - is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} - istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} - istanbul-lib-instrument@5.2.1: - resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} - engines: {node: '>=8'} + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} - istanbul-lib-instrument@6.0.3: - resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} - engines: {node: '>=10'} + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} + '@willsoto/nestjs-prometheus@6.1.0': + resolution: {integrity: sha512-lrCEnJBBSzUIYWGR+PsZw1YXs1B9jzxFEuNAa3RzTxuFAFdI+sW7Fp52il/U/dX2MWoHc32x06OS0nm56QwyzQ==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + prom-client: ^15.0.0 - istanbul-lib-source-maps@4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} - engines: {node: '>=10'} + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - istanbul-reports@3.2.0: - resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} - engines: {node: '>=8'} + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - iterare@1.2.1: - resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} - engines: {node: '>=6'} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} - jest-changed-files@29.7.0: - resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 - jest-circus@29.7.0: - resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - jest-cli@29.7.0: - resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} hasBin: true + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + ajv: ^8.0.0 peerDependenciesMeta: - node-notifier: + ajv: optional: true - jest-config@29.7.0: - resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' + ajv: ^8.0.0 peerDependenciesMeta: - '@types/node': - optional: true - ts-node: + ajv: optional: true - jest-diff@29.7.0: - resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 - jest-docblock@29.7.0: - resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 - jest-each@29.7.0: - resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - jest-environment-node@29.7.0: - resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} - jest-get-type@29.6.3: - resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} - jest-haste-map@29.7.0: - resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} - jest-leak-detector@29.7.0: - resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} - jest-matcher-utils@29.7.0: - resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} - jest-message-util@29.7.0: - resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} - jest-mock@29.7.0: - resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} - jest-pnp-resolver@1.2.3: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} - jest-regex-util@29.6.3: - resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} - jest-resolve-dependencies@29.7.0: - resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + argon2@0.44.0: + resolution: {integrity: sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==} + engines: {node: '>=16.17.0'} - jest-resolve@29.7.0: - resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - jest-runner@29.7.0: - resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} - jest-runtime@29.7.0: - resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} - jest-snapshot@29.7.0: - resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} - jest-util@29.7.0: - resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} - jest-validate@29.7.0: - resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + ast-v8-to-istanbul@1.0.0: + resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} - jest-watcher@29.7.0: - resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - jest-worker@27.5.1: - resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} - engines: {node: '>= 10.13.0'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - jest-worker@29.7.0: - resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} - jest@29.7.0: - resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + avvio@9.2.0: + resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + axios@1.16.0: + resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==} - js-yaml@3.14.2: - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} - hasBin: true + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.17: + resolution: {integrity: sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==} + engines: {node: '>=6.0.0'} hasBin: true - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + bintrees@1.0.2: + resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + boxen@5.1.2: + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true + brace-expansion@1.1.13: + resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} - jsonc-parser@3.2.1: - resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + brace-expansion@2.0.3: + resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} - jsonc-parser@3.3.1: - resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} - jsonfile@6.2.0: - resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - loader-runner@4.3.1: - resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} - engines: {node: '>=6.11.5'} + bullmq@5.73.4: + resolution: {integrity: sha512-Q+NeFLtdKSD3GDPYSX4pH+Mc9E4OZVKimXwrnZ5WmndNy31COMy4vQV9zfhgfHGSUFrlpsBicfKYbSjx9FbO+A==} - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + caniuse-lite@1.0.30001787: + resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} - lodash@4.18.1: - resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} - log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + check-disk-space@3.4.0: + resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==} + engines: {node: '>=16'} - magic-string@0.30.8: - resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} - engines: {node: '>=12'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} - make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} - makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} - memfs@3.5.3: - resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} - engines: {node: '>= 4.0.0'} + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} - merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} - methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - mime@2.6.0: - resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} - engines: {node: '>=4.0.0'} - hasBin: true + color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} - minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} - minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - minimatch@9.0.9: - resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} - engines: {node: '>=16 || 14 >=14.17'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} - minipass@7.1.3: - resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} - engines: {node: '>=16 || 14 >=14.17'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + comment-json@4.6.2: + resolution: {integrity: sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==} + engines: {node: '>= 6'} - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} - multer@2.0.2: - resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} - engines: {node: '>= 10.16.0'} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - mute-stream@0.0.8: - resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} - mute-stream@1.0.0: - resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + conventional-changelog-angular@8.3.1: + resolution: {integrity: sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==} + engines: {node: '>=18'} - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} + conventional-changelog-conventionalcommits@9.3.1: + resolution: {integrity: sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==} + engines: {node: '>=18'} - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + conventional-commits-parser@6.4.0: + resolution: {integrity: sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==} + engines: {node: '>=18'} + hasBin: true - node-abort-controller@3.1.1: - resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - node-emoji@1.11.0: - resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cosmiconfig-typescript-loader@6.3.0: + resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} peerDependencies: - encoding: ^0.1.0 + typescript: '>=4.9.5' peerDependenciesMeta: - encoding: + typescript: optional: true - node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + cosmiconfig@9.0.1: + resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true - node-releases@2.0.37: - resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} + cross-env@10.1.0: + resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} + engines: {node: '>=20'} + hasBin: true - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} + detect-europe-js@0.1.2: + resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==} - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} - ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} - os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} + dotenv@17.4.1: + resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} + engines: {node: '>=12'} - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + drizzle-kit@0.31.10: + resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} + hasBin: true - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + drizzle-orm@0.45.2: + resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} + 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' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} + drizzle-zod@0.8.3: + resolution: {integrity: sha512-66yVOuvGhKJnTdiqj1/Xaaz9/qzOdRJADpDa68enqS6g3t0kpNkwNYjUuaeXgZfO/UWuIM9HIhSlJ6C5ZraMww==} + peerDependencies: + drizzle-orm: '>=0.36.0' + zod: ^3.25.0 || ^4.0.0 - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} + duplexify@3.7.1: + resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} + hasBin: true - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} + electron-to-chromium@1.5.334: + resolution: {integrity: sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==} - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - path-to-regexp@0.1.13: - resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} - path-to-regexp@3.3.0: - resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} - picomatch@2.3.2: - resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} - engines: {node: '>=8.6'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} - picomatch@4.0.1: - resolution: {integrity: sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==} - engines: {node: '>=12'} + error-causes@3.0.2: + resolution: {integrity: sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw==} - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} - pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} - prettier-linter-helpers@1.0.1: - resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} - engines: {node: '>=6.0.0'} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} - prettier@3.8.2: - resolution: {integrity: sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==} - engines: {node: '>=14'} - hasBin: true + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - pure-rand@6.1.0: - resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - qs@6.14.2: - resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} - engines: {node: '>=0.6'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} - qs@6.15.1: - resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} - engines: {node: '>=0.6'} + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - raw-body@2.5.3: - resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} - engines: {node: '>= 0.8'} + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} - reflect-metadata@0.2.2: - resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} - repeat-string@1.6.1: - resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} - engines: {node: '>=0.10'} + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - resolve-cwd@3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} + eventemitter2@6.4.9: + resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - resolve.exports@2.0.3: - resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} - engines: {node: '>=10'} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} - engines: {node: '>= 0.4'} - hasBin: true + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} - restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} - run-async@2.4.1: - resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} - engines: {node: '>=0.12.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - run-async@3.0.0: - resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} - engines: {node: '>=0.12.0'} + fast-json-stringify@6.3.0: + resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} - rxjs@7.8.2: - resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + fast-xml-builder@1.1.4: + resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} - schema-utils@3.3.0: - resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} - engines: {node: '>= 10.13.0'} + fast-xml-parser@5.5.8: + resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} + hasBin: true - schema-utils@4.3.3: - resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} - engines: {node: '>= 10.13.0'} + fastify-plugin@5.1.0: + resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true + fastify@5.8.4: + resolution: {integrity: sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ==} - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - send@0.19.2: - resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} - engines: {node: '>= 0.8.0'} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true - serve-static@1.16.3: - resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} - engines: {node: '>= 0.8.0'} + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} + engines: {node: '>=20'} - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + filelist@1.0.6: + resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - side-channel-list@1.0.1: - resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} - engines: {node: '>= 0.4'} + find-my-way@9.5.0: + resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} + engines: {node: '>=20'} - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + fork-ts-checker-webpack-plugin@9.1.0: + resolution: {integrity: sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==} + engines: {node: '>=14.21.3'} + peerDependencies: + typescript: '>3.6.0' + webpack: ^5.11.0 - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} - source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} - string-length@4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} + git-raw-commits@5.0.1: + resolution: {integrity: sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==} + engines: {node: '>=18'} + hasBin: true - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - strip-ansi@7.2.0: - resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} - engines: {node: '>=12'} + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} - strtok3@10.3.5: - resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} - engines: {node: '>=18'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - superagent@8.1.2: - resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} - engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - supertest@6.3.4: - resolution: {integrity: sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==} - engines: {node: '>=6.4.0'} - deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} - symbol-observable@4.0.0: - resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} - engines: {node: '>=0.10'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} - synckit@0.11.12: - resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} - engines: {node: ^14.18.0 || >=16.0.0} + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} - tapable@2.3.2: - resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} - engines: {node: '>=6'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - terser-webpack-plugin@5.4.0: - resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@swc/core': '*' - esbuild: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - esbuild: - optional: true - uglify-js: - optional: true + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} - terser@5.46.1: - resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} - engines: {node: '>=10'} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} hasBin: true - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} - tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - token-types@6.1.2: - resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} - engines: {node: '>=14.16'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true + ioredis@5.10.1: + resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} + engines: {node: '>=12.22.0'} - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} - ts-jest@29.4.9: - resolution: {integrity: sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==} - engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/transform': ^29.0.0 || ^30.0.0 - '@jest/types': ^29.0.0 || ^30.0.0 - babel-jest: ^29.0.0 || ^30.0.0 - esbuild: '*' - jest: ^29.0.0 || ^30.0.0 - jest-util: ^29.0.0 || ^30.0.0 - typescript: '>=4.3 <7' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/transform': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - jest-util: - optional: true + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - ts-loader@9.5.7: - resolution: {integrity: sha512-/ZNrKgA3K3PtpMYOC71EeMWIloGw3IYEa5/t1cyz2r5/PyUwTXGzYJvcD3kfUvmhlfpz1rhV8B2O6IVTQ0avsg==} - engines: {node: '>=12.0.0'} - peerDependencies: - typescript: '*' - webpack: ^5.0.0 + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} - ts-node@10.9.2: - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} - tsconfig-paths-webpack-plugin@4.2.0: - resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} - engines: {node: '>=10.13.0'} + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} - tsconfig-paths@4.2.0: - resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} - engines: {node: '>=6'} + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-standalone-pwa@0.1.1: + resolution: {integrity: sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - typedarray@0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} - typescript@5.7.2: - resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} - engines: {node: '>=14.17'} + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} hasBin: true - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - uglify-js@3.19.3: - resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} - engines: {node: '>=0.8.0'} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - uid@2.0.2: - resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} - engines: {node: '>=8'} + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - uint8array-extras@1.5.0: - resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} - engines: {node: '>=18'} + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} - v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - v8-to-istanbul@9.3.0: - resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} - engines: {node: '>=10.12.0'} + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} - watchpack@2.5.1: - resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} - engines: {node: '>=10.13.0'} + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} - wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] - webpack-node-externals@3.0.0: - resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} - engines: {node: '>=6'} + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] - webpack-sources@3.3.4: - resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} - engines: {node: '>=10.13.0'} + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] - webpack@5.97.1: - resolution: {integrity: sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==} - engines: {node: '>=10.13.0'} + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lint-staged@16.4.0: + resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} + engines: {node: '>=20.17'} hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + load-esm@1.0.3: + resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} + engines: {node: '>=13.2.0'} + + loader-runner@4.3.1: + resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} + engines: {node: '>=6.11.5'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + lru-cache@11.3.3: + resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==} + engines: {node: 20 || >=22} + + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} hasBin: true - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} - wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} engines: {node: '>=10'} - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.5: + resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + nest-winston@1.10.2: + resolution: {integrity: sha512-Z9IzL/nekBOF/TEwBHUJDiDPMaXUcFquUQOFavIRet6xF0EbuWnOzslyN/ksgzG+fITNgXhMdrL/POp9SdaFxA==} + peerDependencies: + '@nestjs/common': ^5.0.0 || ^6.6.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + winston: ^3.0.0 + + nestjs-zod@5.3.0: + resolution: {integrity: sha512-QY6imXm9heMOpWigjFHgMWPvc1ZQHeNQ7pdogo9Q5xj5F8HpqZ972vKlVdkaTyzYlOXJP/yVy3wlF1EjubDQPg==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/swagger': ^7.4.2 || ^8.0.0 || ^11.0.0 + rxjs: ^7.0.0 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + '@nestjs/swagger': + optional: true + + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-addon-api@8.7.0: + resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==} + engines: {node: ^18 || ^20 || >= 21} + + node-emoji@1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + + nodemailer@8.0.5: + resolution: {integrity: sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==} + engines: {node: '>=6.0.0'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + otplib@13.4.0: + resolution: {integrity: sha512-RUcYcRMCgRWhUE/XabRppXpUwCwaWBNHe5iPXhdvP8wwDGpGpsIf/kxX/ec3zFsOaM1Oq8lEhUqDwk6W7DHkwg==} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + passport-jwt@4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + + passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + + passport@0.7.0: + resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==} + engines: {node: '>= 0.4.0'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pause@0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + + peek-stream@1.1.3: + resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} + + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.12.0: + resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.13.0: + resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.20.0: + resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + postcss@8.5.9: + resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres@3.4.9: + resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==} + engines: {node: '>=12'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.2: + resolution: {integrity: sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==} + engines: {node: '>=14'} + hasBin: true + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + prom-client@15.1.3: + resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} + engines: {node: ^16 || ^18 || >=20} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + pumpify@2.0.1: + resolution: {integrity: sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-info@3.1.0: + resolution: {integrity: sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rolldown@1.0.0-rc.15: + resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex2@5.1.0: + resolution: {integrity: sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==} + hasBin: true + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + engines: {node: '>=20'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strnum@2.2.3: + resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} + + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} + engines: {node: '>=18'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + swagger-ui-dist@5.32.2: + resolution: {integrity: sha512-t6Ns52nS8LU2hqi0+rezMjFO1ZrCsCrnommXrU7Nfrg2va2dWahdvM6TuSwzdHpG29v6BHJyU1c/UWFhgVZzVQ==} + + symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + engines: {node: '>=6'} + + tdigest@0.1.2: + resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} + + terser-webpack-plugin@5.4.0: + resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + + terser@5.46.1: + resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} + engines: {node: '>=10'} + hasBin: true + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + transliteration@2.6.1: + resolution: {integrity: sha512-hJ9BhrQAOnNTbpOr1MxsNjZISkn7ppvF5TKUeFmTE1mG4ZPD/XVxF0L0LUoIUCWmQyxH0gJpVtfYLAWf298U9w==} + engines: {node: '>=20.0.0'} + hasBin: true + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-loader@9.5.7: + resolution: {integrity: sha512-/ZNrKgA3K3PtpMYOC71EeMWIloGw3IYEa5/t1cyz2r5/PyUwTXGzYJvcD3kfUvmhlfpz1rhV8B2O6IVTQ0avsg==} + engines: {node: '>=12.0.0'} + peerDependencies: + typescript: '*' + webpack: ^5.0.0 + + tsconfig-paths-webpack-plugin@4.2.0: + resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} + engines: {node: '>=10.13.0'} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ua-is-frozen@0.1.2: + resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==} + + ua-parser-js@2.0.9: + resolution: {integrity: sha512-OsqGhxyo/wGdLSXMSJxuMGN6H4gDnKz6Fb3IBm4bxZFMnyy0sdf6MN96Ie8tC6z/btdO+Bsy8guxlvLdwT076w==} + hasBin: true + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + vite@8.0.8: + resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.4: + resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.4 + '@vitest/browser-preview': 4.1.4 + '@vitest/browser-webdriverio': 4.1.4 + '@vitest/coverage-istanbul': 4.1.4 + '@vitest/coverage-v8': 4.1.4 + '@vitest/ui': 4.1.4 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + watchpack@2.5.1: + resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} + engines: {node: '>=10.13.0'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webpack-node-externals@3.0.0: + resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} + engines: {node: '>=6'} + + webpack-sources@3.3.4: + resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} + engines: {node: '>=10.13.0'} + + webpack@5.106.0: + resolution: {integrity: sha512-Pkx5joZ9RrdgO5LBkyX1L2ZAJeK/Taz3vqZ9CbcP0wS5LEMx5QkKsEwLl29QJfihZ+DKRBFldzy1O30pJ1MDpA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} + engines: {node: '>= 12.0.0'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@angular-devkit/core@19.2.23(chokidar@4.0.3)': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.4 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/core@19.2.24(chokidar@4.0.3)': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.4 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/schematics-cli@19.2.24(@types/node@20.19.39)(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + '@inquirer/prompts': 7.3.2(@types/node@20.19.39) + ansi-colors: 4.1.3 + symbol-observable: 4.0.0 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - '@types/node' + - chokidar + + '@angular-devkit/schematics@19.2.23(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.23(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@angular-devkit/schematics@19.2.24(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1029.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.27 + '@aws-sdk/credential-provider-node': 3.972.30 + '@aws-sdk/middleware-bucket-endpoint': 3.972.9 + '@aws-sdk/middleware-expect-continue': 3.972.9 + '@aws-sdk/middleware-flexible-checksums': 3.974.7 + '@aws-sdk/middleware-host-header': 3.972.9 + '@aws-sdk/middleware-location-constraint': 3.972.9 + '@aws-sdk/middleware-logger': 3.972.9 + '@aws-sdk/middleware-recursion-detection': 3.972.10 + '@aws-sdk/middleware-sdk-s3': 3.972.28 + '@aws-sdk/middleware-ssec': 3.972.9 + '@aws-sdk/middleware-user-agent': 3.972.29 + '@aws-sdk/region-config-resolver': 3.972.11 + '@aws-sdk/signature-v4-multi-region': 3.996.16 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-endpoints': 3.996.6 + '@aws-sdk/util-user-agent-browser': 3.972.9 + '@aws-sdk/util-user-agent-node': 3.973.15 + '@smithy/config-resolver': 4.4.14 + '@smithy/core': 3.23.14 + '@smithy/eventstream-serde-browser': 4.2.13 + '@smithy/eventstream-serde-config-resolver': 4.3.13 + '@smithy/eventstream-serde-node': 4.2.13 + '@smithy/fetch-http-handler': 5.3.16 + '@smithy/hash-blob-browser': 4.2.14 + '@smithy/hash-node': 4.2.13 + '@smithy/hash-stream-node': 4.2.13 + '@smithy/invalid-dependency': 4.2.13 + '@smithy/md5-js': 4.2.13 + '@smithy/middleware-content-length': 4.2.13 + '@smithy/middleware-endpoint': 4.4.29 + '@smithy/middleware-retry': 4.5.1 + '@smithy/middleware-serde': 4.2.17 + '@smithy/middleware-stack': 4.2.13 + '@smithy/node-config-provider': 4.3.13 + '@smithy/node-http-handler': 4.5.2 + '@smithy/protocol-http': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.45 + '@smithy/util-defaults-mode-node': 4.2.49 + '@smithy/util-endpoints': 3.3.4 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-retry': 4.3.1 + '@smithy/util-stream': 4.5.22 + '@smithy/util-utf8': 4.2.2 + '@smithy/util-waiter': 4.2.15 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.973.27': + dependencies: + '@aws-sdk/types': 3.973.7 + '@aws-sdk/xml-builder': 3.972.17 + '@smithy/core': 3.23.14 + '@smithy/node-config-provider': 4.3.13 + '@smithy/property-provider': 4.2.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/signature-v4': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/util-base64': 4.3.2 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/crc64-nvme@3.972.6': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.25': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.27': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/types': 3.973.7 + '@smithy/fetch-http-handler': 5.3.16 + '@smithy/node-http-handler': 4.5.2 + '@smithy/property-provider': 4.2.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/util-stream': 4.5.22 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.29': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/credential-provider-env': 3.972.25 + '@aws-sdk/credential-provider-http': 3.972.27 + '@aws-sdk/credential-provider-login': 3.972.29 + '@aws-sdk/credential-provider-process': 3.972.25 + '@aws-sdk/credential-provider-sso': 3.972.29 + '@aws-sdk/credential-provider-web-identity': 3.972.29 + '@aws-sdk/nested-clients': 3.996.19 + '@aws-sdk/types': 3.973.7 + '@smithy/credential-provider-imds': 4.2.13 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.29': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/nested-clients': 3.996.19 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.30': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.25 + '@aws-sdk/credential-provider-http': 3.972.27 + '@aws-sdk/credential-provider-ini': 3.972.29 + '@aws-sdk/credential-provider-process': 3.972.25 + '@aws-sdk/credential-provider-sso': 3.972.29 + '@aws-sdk/credential-provider-web-identity': 3.972.29 + '@aws-sdk/types': 3.973.7 + '@smithy/credential-provider-imds': 4.2.13 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.972.25': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.29': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/nested-clients': 3.996.19 + '@aws-sdk/token-providers': 3.1026.0 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.29': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/nested-clients': 3.996.19 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-bucket-endpoint@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-arn-parser': 3.972.3 + '@smithy/node-config-provider': 4.3.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.974.7': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.973.27 + '@aws-sdk/crc64-nvme': 3.972.6 + '@aws-sdk/types': 3.973.7 + '@smithy/is-array-buffer': 4.2.2 + '@smithy/node-config-provider': 4.3.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-stream': 4.5.22 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.7 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.28': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-arn-parser': 3.972.3 + '@smithy/core': 3.23.14 + '@smithy/node-config-provider': 4.3.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/signature-v4': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-stream': 4.5.22 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.29': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-endpoints': 3.996.6 + '@smithy/core': 3.23.14 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-retry': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.996.19': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.27 + '@aws-sdk/middleware-host-header': 3.972.9 + '@aws-sdk/middleware-logger': 3.972.9 + '@aws-sdk/middleware-recursion-detection': 3.972.10 + '@aws-sdk/middleware-user-agent': 3.972.29 + '@aws-sdk/region-config-resolver': 3.972.11 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-endpoints': 3.996.6 + '@aws-sdk/util-user-agent-browser': 3.972.9 + '@aws-sdk/util-user-agent-node': 3.973.15 + '@smithy/config-resolver': 4.4.14 + '@smithy/core': 3.23.14 + '@smithy/fetch-http-handler': 5.3.16 + '@smithy/hash-node': 4.2.13 + '@smithy/invalid-dependency': 4.2.13 + '@smithy/middleware-content-length': 4.2.13 + '@smithy/middleware-endpoint': 4.4.29 + '@smithy/middleware-retry': 4.5.1 + '@smithy/middleware-serde': 4.2.17 + '@smithy/middleware-stack': 4.2.13 + '@smithy/node-config-provider': 4.3.13 + '@smithy/node-http-handler': 4.5.2 + '@smithy/protocol-http': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.45 + '@smithy/util-defaults-mode-node': 4.2.49 + '@smithy/util-endpoints': 3.3.4 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-retry': 4.3.1 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/config-resolver': 4.4.14 + '@smithy/node-config-provider': 4.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/s3-request-presigner@3.1029.0': + dependencies: + '@aws-sdk/signature-v4-multi-region': 3.996.16 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-format-url': 3.972.9 + '@smithy/middleware-endpoint': 4.4.29 + '@smithy/protocol-http': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.16': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.972.28 + '@aws-sdk/types': 3.973.7 + '@smithy/protocol-http': 5.3.13 + '@smithy/signature-v4': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1026.0': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/nested-clients': 3.996.19 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.973.7': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.972.3': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.996.6': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-endpoints': 3.3.4 + tslib: 2.8.1 + + '@aws-sdk/util-format-url@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/querystring-builder': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/types': 4.14.0 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.973.15': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.29 + '@aws-sdk/types': 3.973.7 + '@smithy/node-config-provider': 4.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.17': + dependencies: + '@smithy/types': 4.14.0 + fast-xml-parser: 5.5.8 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@1.0.2': {} + + '@borewit/text-codec@0.2.2': {} + + '@bull-board/api@6.21.0(@bull-board/ui@6.21.0)': + dependencies: + '@bull-board/ui': 6.21.0 + redis-info: 3.1.0 + + '@bull-board/fastify@6.21.0': + dependencies: + '@bull-board/api': 6.21.0(@bull-board/ui@6.21.0) + '@bull-board/ui': 6.21.0 + '@fastify/static': 9.1.0 + '@fastify/view': 11.1.1 + ejs: 3.1.10 + + '@bull-board/nestjs@6.21.0(@bull-board/api@6.21.0(@bull-board/ui@6.21.0))(@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)))(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@bull-board/api': 6.21.0(@bull-board/ui@6.21.0) + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + + '@bull-board/ui@6.21.0': + dependencies: + '@bull-board/api': 6.21.0(@bull-board/ui@6.21.0) + + '@colors/colors@1.5.0': + optional: true + + '@colors/colors@1.6.0': {} + + '@commitlint/cli@20.5.0(@types/node@20.19.39)(conventional-commits-parser@6.4.0)(typescript@5.9.3)': + dependencies: + '@commitlint/format': 20.5.0 + '@commitlint/lint': 20.5.0 + '@commitlint/load': 20.5.0(@types/node@20.19.39)(typescript@5.9.3) + '@commitlint/read': 20.5.0(conventional-commits-parser@6.4.0) + '@commitlint/types': 20.5.0 + tinyexec: 1.1.1 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - conventional-commits-filter + - conventional-commits-parser + - typescript + + '@commitlint/config-conventional@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + conventional-changelog-conventionalcommits: 9.3.1 + + '@commitlint/config-validator@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + ajv: 8.18.0 + + '@commitlint/ensure@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + + '@commitlint/execute-rule@20.0.0': {} + + '@commitlint/format@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + picocolors: 1.1.1 + + '@commitlint/is-ignored@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + semver: 7.7.4 + + '@commitlint/lint@20.5.0': + dependencies: + '@commitlint/is-ignored': 20.5.0 + '@commitlint/parse': 20.5.0 + '@commitlint/rules': 20.5.0 + '@commitlint/types': 20.5.0 + + '@commitlint/load@20.5.0(@types/node@20.19.39)(typescript@5.9.3)': + dependencies: + '@commitlint/config-validator': 20.5.0 + '@commitlint/execute-rule': 20.0.0 + '@commitlint/resolve-extends': 20.5.0 + '@commitlint/types': 20.5.0 + cosmiconfig: 9.0.1(typescript@5.9.3) + cosmiconfig-typescript-loader: 6.3.0(@types/node@20.19.39)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3) + is-plain-obj: 4.1.0 + lodash.mergewith: 4.6.2 + picocolors: 1.1.1 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/message@20.4.3': {} + + '@commitlint/parse@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + conventional-changelog-angular: 8.3.1 + conventional-commits-parser: 6.4.0 + + '@commitlint/read@20.5.0(conventional-commits-parser@6.4.0)': + dependencies: + '@commitlint/top-level': 20.4.3 + '@commitlint/types': 20.5.0 + git-raw-commits: 5.0.1(conventional-commits-parser@6.4.0) + minimist: 1.2.8 + tinyexec: 1.1.1 + transitivePeerDependencies: + - conventional-commits-filter + - conventional-commits-parser + + '@commitlint/resolve-extends@20.5.0': + dependencies: + '@commitlint/config-validator': 20.5.0 + '@commitlint/types': 20.5.0 + global-directory: 4.0.1 + import-meta-resolve: 4.2.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + + '@commitlint/rules@20.5.0': + dependencies: + '@commitlint/ensure': 20.5.0 + '@commitlint/message': 20.4.3 + '@commitlint/to-lines': 20.0.0 + '@commitlint/types': 20.5.0 + + '@commitlint/to-lines@20.0.0': {} + + '@commitlint/top-level@20.4.3': + dependencies: + escalade: 3.2.0 + + '@commitlint/types@20.5.0': + dependencies: + conventional-commits-parser: 6.4.0 + picocolors: 1.1.1 + + '@conventional-changelog/git-client@2.7.0(conventional-commits-parser@6.4.0)': + dependencies: + '@simple-libs/child-process-utils': 1.0.2 + '@simple-libs/stream-utils': 1.2.0 + semver: 7.7.4 + optionalDependencies: + conventional-commits-parser: 6.4.0 + + '@dabh/diagnostics@2.0.8': + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + + '@drizzle-team/brocli@0.10.2': {} + + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@epic-web/invariant@1.0.0': {} + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.13.7 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@fastify/accept-negotiator@2.0.1': {} + + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + fast-uri: 3.1.0 + + '@fastify/busboy@3.2.0': {} + + '@fastify/compress@8.3.1': + dependencies: + '@fastify/accept-negotiator': 2.0.1 + fastify-plugin: 5.1.0 + mime-db: 1.52.0 + minipass: 7.1.3 + peek-stream: 1.1.3 + pump: 3.0.4 + pumpify: 2.0.1 + readable-stream: 4.7.0 + + '@fastify/cookie@11.0.2': + dependencies: + cookie: 1.1.1 + fastify-plugin: 5.1.0 + + '@fastify/cors@11.2.0': + dependencies: + fastify-plugin: 5.1.0 + toad-cache: 3.7.0 + + '@fastify/csrf-protection@7.1.0': + dependencies: + '@fastify/csrf': 8.0.1 + '@fastify/error': 4.2.0 + fastify-plugin: 5.1.0 + + '@fastify/csrf@8.0.1': {} + + '@fastify/deepmerge@3.2.1': {} + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.3.0 + + '@fastify/formbody@8.0.2': + dependencies: + fast-querystring: 1.1.2 + fastify-plugin: 5.1.0 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/multipart@10.0.0': + dependencies: + '@fastify/busboy': 3.2.0 + '@fastify/deepmerge': 3.2.1 + '@fastify/error': 4.2.0 + fastify-plugin: 5.1.0 + secure-json-parse: 4.1.0 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.3.0 + + '@fastify/send@4.1.0': + dependencies: + '@lukeed/ms': 2.0.2 + escape-html: 1.0.3 + fast-decode-uri-component: 1.0.1 + http-errors: 2.0.1 + mime: 3.0.0 + + '@fastify/static@9.1.0': + dependencies: + '@fastify/accept-negotiator': 2.0.1 + '@fastify/send': 4.1.0 + content-disposition: 1.1.0 + fastify-plugin: 5.1.0 + fastq: 1.20.1 + glob: 13.0.6 + + '@fastify/view@11.1.1': + dependencies: + fastify-plugin: 5.1.0 + toad-cache: 3.7.0 + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2(@types/node@20.19.39)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.39) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/confirm@5.1.21(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/type': 3.0.10(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/core@10.3.2(@types/node@20.19.39)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.39) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/editor@4.2.23(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/external-editor': 1.0.3(@types/node@20.19.39) + '@inquirer/type': 3.0.10(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/expand@4.0.23(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/type': 3.0.10(@types/node@20.19.39) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/external-editor@1.0.3(@types/node@20.19.39)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.3.1(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/type': 3.0.10(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/number@3.0.23(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/type': 3.0.10(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/password@4.0.23(@types/node@20.19.39)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/type': 3.0.10(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/prompts@7.10.1(@types/node@20.19.39)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@20.19.39) + '@inquirer/confirm': 5.1.21(@types/node@20.19.39) + '@inquirer/editor': 4.2.23(@types/node@20.19.39) + '@inquirer/expand': 4.0.23(@types/node@20.19.39) + '@inquirer/input': 4.3.1(@types/node@20.19.39) + '@inquirer/number': 3.0.23(@types/node@20.19.39) + '@inquirer/password': 4.0.23(@types/node@20.19.39) + '@inquirer/rawlist': 4.1.11(@types/node@20.19.39) + '@inquirer/search': 3.2.2(@types/node@20.19.39) + '@inquirer/select': 4.4.2(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/prompts@7.3.2(@types/node@20.19.39)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@20.19.39) + '@inquirer/confirm': 5.1.21(@types/node@20.19.39) + '@inquirer/editor': 4.2.23(@types/node@20.19.39) + '@inquirer/expand': 4.0.23(@types/node@20.19.39) + '@inquirer/input': 4.3.1(@types/node@20.19.39) + '@inquirer/number': 3.0.23(@types/node@20.19.39) + '@inquirer/password': 4.0.23(@types/node@20.19.39) + '@inquirer/rawlist': 4.1.11(@types/node@20.19.39) + '@inquirer/search': 3.2.2(@types/node@20.19.39) + '@inquirer/select': 4.4.2(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/rawlist@4.1.11(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/type': 3.0.10(@types/node@20.19.39) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/search@3.2.2(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.39) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/select@4.4.2(@types/node@20.19.39)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.39) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/type@3.0.10(@types/node@20.19.39)': + optionalDependencies: + '@types/node': 20.19.39 + + '@ioredis/commands@1.5.1': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + '@jridgewell/resolve-uri@3.1.2': {} - write-file-atomic@4.0.2: - resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} + '@jridgewell/sourcemap-codec@1.5.5': {} - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + '@lukeed/csprng@1.1.0': {} - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} + '@lukeed/ms@2.0.2': {} - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} + '@microsoft/tsdoc@0.16.0': {} - yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true -snapshots: + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true - '@angular-devkit/core@17.3.11(chokidar@3.6.0)': - dependencies: - ajv: 8.12.0 - ajv-formats: 2.1.1(ajv@8.12.0) - jsonc-parser: 3.2.1 - picomatch: 4.0.1 - rxjs: 7.8.1 - source-map: 0.7.4 - optionalDependencies: - chokidar: 3.6.0 + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true - '@angular-devkit/schematics-cli@17.3.11(chokidar@3.6.0)': - dependencies: - '@angular-devkit/core': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) - ansi-colors: 4.1.3 - inquirer: 9.2.15 - symbol-observable: 4.0.0 - yargs-parser: 21.1.1 - transitivePeerDependencies: - - chokidar + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true - '@angular-devkit/schematics@17.3.11(chokidar@3.6.0)': - dependencies: - '@angular-devkit/core': 17.3.11(chokidar@3.6.0) - jsonc-parser: 3.2.1 - magic-string: 0.30.8 - ora: 5.4.1 - rxjs: 7.8.1 - transitivePeerDependencies: - - chokidar + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true - '@babel/code-frame@7.29.0': + '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.29.0': {} + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true - '@babel/core@7.29.0': + '@nestjs-modules/ioredis@2.2.1(@nestjs/axios@4.0.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.16.0)(rxjs@7.8.2))(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(ioredis@5.10.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.2 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + ioredis: 5.10.1 + optionalDependencies: + '@nestjs/terminus': 11.1.1(@nestjs/axios@4.0.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.16.0)(rxjs@7.8.2))(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) transitivePeerDependencies: - - supports-color + - '@grpc/grpc-js' + - '@grpc/proto-loader' + - '@mikro-orm/core' + - '@mikro-orm/nestjs' + - '@nestjs/axios' + - '@nestjs/microservices' + - '@nestjs/mongoose' + - '@nestjs/sequelize' + - '@nestjs/typeorm' + - '@prisma/client' + - mongoose + - reflect-metadata + - rxjs + - sequelize + - typeorm + + '@nestjs/axios@4.0.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.16.0)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + axios: 1.16.0 + rxjs: 7.8.2 - '@babel/generator@7.29.1': + '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 - '@babel/helper-compilation-targets@7.28.6': + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(bullmq@5.73.4)': dependencies: - '@babel/compat-data': 7.29.0 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.2 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.28.0': {} + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + bullmq: 5.73.4 + tslib: 2.8.1 - '@babel/helper-module-imports@7.28.6': + '@nestjs/cli@11.0.19(@swc/core@1.15.24)(@types/node@20.19.39)(esbuild@0.27.7)': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics-cli': 19.2.24(@types/node@20.19.39)(chokidar@4.0.3) + '@inquirer/prompts': 7.10.1(@types/node@20.19.39) + '@nestjs/schematics': 11.0.10(chokidar@4.0.3)(typescript@5.9.3) + ansis: 4.2.0 + chokidar: 4.0.3 + cli-table3: 0.6.5 + commander: 4.1.1 + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7)) + glob: 13.0.6 + node-emoji: 1.11.0 + ora: 5.4.1 + tsconfig-paths: 4.2.0 + tsconfig-paths-webpack-plugin: 4.2.0 + typescript: 5.9.3 + webpack: 5.106.0(@swc/core@1.15.24)(esbuild@0.27.7) + webpack-node-externals: 3.0.0 + optionalDependencies: + '@swc/core': 1.15.24 transitivePeerDependencies: - - supports-color + - '@types/node' + - esbuild + - uglify-js + - webpack-cli - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + '@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + file-type: 21.3.4 + iterare: 1.2.1 + load-esm: 1.0.3 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.28.6': {} - - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.28.5': {} + '@nestjs/config@4.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + dotenv: 17.4.1 + dotenv-expand: 12.0.3 + lodash: 4.18.1 + rxjs: 7.8.2 - '@babel/helper-validator-option@7.27.1': {} + '@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nuxt/opencollective': 0.4.1 + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 - '@babel/helpers@7.29.2': + '@nestjs/event-emitter@3.1.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + eventemitter2: 6.4.9 - '@babel/parser@7.29.2': + '@nestjs/jwt@11.0.2(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: - '@babel/types': 7.29.0 + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@types/jsonwebtoken': 9.0.10 + jsonwebtoken: 9.0.3 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': + '@nestjs/passport@11.0.5(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + passport: 0.7.0 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': + '@nestjs/platform-fastify@11.1.18(@fastify/static@9.1.0)(@fastify/view@11.1.1)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@fastify/cors': 11.2.0 + '@fastify/formbody': 8.0.2 + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + fast-querystring: 1.1.2 + fastify: 5.8.4 + fastify-plugin: 5.1.0 + find-my-way: 9.5.0 + light-my-request: 6.6.0 + path-to-regexp: 8.4.2 + reusify: 1.1.0 + tslib: 2.8.1 + optionalDependencies: + '@fastify/static': 9.1.0 + '@fastify/view': 11.1.1 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': + '@nestjs/schematics@11.0.10(chokidar@4.0.3)(typescript@5.9.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@angular-devkit/core': 19.2.23(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.23(chokidar@4.0.3) + comment-json: 4.6.2 + jsonc-parser: 3.3.1 + pluralize: 8.0.0 + typescript: 5.9.3 + transitivePeerDependencies: + - chokidar - '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + '@nestjs/swagger@11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@microsoft/tsdoc': 0.16.0 + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + js-yaml: 4.1.1 + lodash: 4.18.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.32.2 + optionalDependencies: + '@fastify/static': 9.1.0 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + '@nestjs/terminus@11.1.1(@nestjs/axios@4.0.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.16.0)(rxjs@7.8.2))(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + boxen: 5.1.2 + check-disk-space: 3.4.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + optionalDependencies: + '@nestjs/axios': 4.0.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.16.0)(rxjs@7.8.2) + optional: true - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': + '@nestjs/testing@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 - '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + '@nestjs/throttler@6.5.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': + '@noble/hashes@2.0.1': {} + + '@nodelib/fs.scandir@2.1.5': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': + '@nuxt/opencollective@0.4.1': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + consola: 3.4.2 + + '@opentelemetry/api@1.9.1': {} - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': + '@otplib/core@13.4.0': {} + + '@otplib/hotp@13.4.0': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@otplib/core': 13.4.0 + '@otplib/uri': 13.4.0 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': + '@otplib/plugin-base32-scure@13.4.0': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@otplib/core': 13.4.0 + '@scure/base': 2.0.0 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + '@otplib/plugin-crypto-noble@13.4.0': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@noble/hashes': 2.0.1 + '@otplib/core': 13.4.0 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': + '@otplib/totp@13.4.0': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@otplib/core': 13.4.0 + '@otplib/hotp': 13.4.0 + '@otplib/uri': 13.4.0 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': + '@otplib/uri@13.4.0': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@otplib/core': 13.4.0 + + '@oxc-project/types@0.124.0': {} - '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + '@paralleldrive/cuid2@3.3.0': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@noble/hashes': 2.0.1 + bignumber.js: 9.3.1 + error-causes: 3.0.2 + + '@phc/format@1.0.0': {} + + '@pinojs/redact@0.4.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + optional: true - '@babel/template@7.28.6': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + optional: true - '@babel/traverse@7.29.0': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.15': {} + + '@scarf/scarf@1.4.0': {} + + '@scure/base@2.0.0': {} + + '@simple-libs/child-process-utils@1.0.2': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.2 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color + '@simple-libs/stream-utils': 1.2.0 - '@babel/types@7.29.0': + '@simple-libs/stream-utils@1.2.0': {} + + '@smithy/chunked-blob-reader-native@4.2.3': dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 - '@bcoe/v8-coverage@0.2.3': {} + '@smithy/chunked-blob-reader@5.2.2': + dependencies: + tslib: 2.8.1 - '@borewit/text-codec@0.2.2': {} + '@smithy/config-resolver@4.4.14': + dependencies: + '@smithy/node-config-provider': 4.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-endpoints': 3.3.4 + '@smithy/util-middleware': 4.2.13 + tslib: 2.8.1 - '@colors/colors@1.5.0': - optional: true + '@smithy/core@3.23.14': + dependencies: + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-stream': 4.5.22 + '@smithy/util-utf8': 4.2.2 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 - '@cspotcode/source-map-support@0.8.1': + '@smithy/credential-provider-imds@4.2.13': dependencies: - '@jridgewell/trace-mapping': 0.3.9 + '@smithy/node-config-provider': 4.3.13 + '@smithy/property-provider': 4.2.13 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + tslib: 2.8.1 - '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + '@smithy/eventstream-codec@4.2.13': dependencies: - eslint: 8.57.1 - eslint-visitor-keys: 3.4.3 + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.0 + '@smithy/util-hex-encoding': 4.2.2 + tslib: 2.8.1 - '@eslint-community/regexpp@4.12.2': {} + '@smithy/eventstream-serde-browser@4.2.13': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@eslint/eslintrc@2.1.4': + '@smithy/eventstream-serde-config-resolver@4.3.13': dependencies: - ajv: 6.14.0 - debug: 4.4.3 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@eslint/js@8.57.1': {} + '@smithy/eventstream-serde-node@4.2.13': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@humanwhocodes/config-array@0.13.0': + '@smithy/eventstream-serde-universal@4.2.13': dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.3 - minimatch: 3.1.5 - transitivePeerDependencies: - - supports-color + '@smithy/eventstream-codec': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@humanwhocodes/module-importer@1.0.1': {} + '@smithy/fetch-http-handler@5.3.16': + dependencies: + '@smithy/protocol-http': 5.3.13 + '@smithy/querystring-builder': 4.2.13 + '@smithy/types': 4.14.0 + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 - '@humanwhocodes/object-schema@2.0.3': {} + '@smithy/hash-blob-browser@4.2.14': + dependencies: + '@smithy/chunked-blob-reader': 5.2.2 + '@smithy/chunked-blob-reader-native': 4.2.3 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@isaacs/cliui@8.0.2': + '@smithy/hash-node@4.2.13': dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.2.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 + '@smithy/types': 4.14.0 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 - '@istanbuljs/load-nyc-config@1.1.0': + '@smithy/hash-stream-node@4.2.13': dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.2 - resolve-from: 5.0.0 + '@smithy/types': 4.14.0 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 - '@istanbuljs/schema@0.1.3': {} + '@smithy/invalid-dependency@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@jest/console@29.7.0': + '@smithy/is-array-buffer@2.2.0': dependencies: - '@jest/types': 29.6.3 - '@types/node': 20.19.39 - chalk: 4.1.2 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 + tslib: 2.8.1 - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3))': + '@smithy/is-array-buffer@4.2.2': dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.19.39 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node + tslib: 2.8.1 - '@jest/environment@29.7.0': + '@smithy/md5-js@4.2.13': dependencies: - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.19.39 - jest-mock: 29.7.0 + '@smithy/types': 4.14.0 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 - '@jest/expect-utils@29.7.0': + '@smithy/middleware-content-length@4.2.13': dependencies: - jest-get-type: 29.6.3 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@jest/expect@29.7.0': + '@smithy/middleware-endpoint@4.4.29': dependencies: - expect: 29.7.0 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color + '@smithy/core': 3.23.14 + '@smithy/middleware-serde': 4.2.17 + '@smithy/node-config-provider': 4.3.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-middleware': 4.2.13 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.5.1': + dependencies: + '@smithy/core': 3.23.14 + '@smithy/node-config-provider': 4.3.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/service-error-classification': 4.2.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-retry': 4.3.1 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 - '@jest/fake-timers@29.7.0': + '@smithy/middleware-serde@4.2.17': dependencies: - '@jest/types': 29.6.3 - '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.19.39 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-util: 29.7.0 + '@smithy/core': 3.23.14 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@jest/globals@29.7.0': + '@smithy/middleware-stack@4.2.13': dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/types': 29.6.3 - jest-mock: 29.7.0 - transitivePeerDependencies: - - supports-color + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@jest/reporters@29.7.0': + '@smithy/node-config-provider@4.3.13': dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 20.19.39 - chalk: 4.1.2 - collect-v8-coverage: 1.0.3 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-instrument: 6.0.3 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.2.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - jest-worker: 29.7.0 - slash: 3.0.0 - string-length: 4.0.2 - strip-ansi: 6.0.1 - v8-to-istanbul: 9.3.0 - transitivePeerDependencies: - - supports-color + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@jest/schemas@29.6.3': + '@smithy/node-http-handler@4.5.2': dependencies: - '@sinclair/typebox': 0.27.10 + '@smithy/protocol-http': 5.3.13 + '@smithy/querystring-builder': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@jest/source-map@29.6.3': + '@smithy/property-provider@4.2.13': dependencies: - '@jridgewell/trace-mapping': 0.3.31 - callsites: 3.1.0 - graceful-fs: 4.2.11 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@jest/test-result@29.7.0': + '@smithy/protocol-http@5.3.13': dependencies: - '@jest/console': 29.7.0 - '@jest/types': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - collect-v8-coverage: 1.0.3 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@jest/test-sequencer@29.7.0': + '@smithy/querystring-builder@4.2.13': dependencies: - '@jest/test-result': 29.7.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - slash: 3.0.0 + '@smithy/types': 4.14.0 + '@smithy/util-uri-escape': 4.2.2 + tslib: 2.8.1 - '@jest/transform@29.7.0': + '@smithy/querystring-parser@4.2.13': dependencies: - '@babel/core': 7.29.0 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - babel-plugin-istanbul: 6.1.1 - chalk: 4.1.2 - convert-source-map: 2.0.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - micromatch: 4.0.8 - pirates: 4.0.7 - slash: 3.0.0 - write-file-atomic: 4.0.2 - transitivePeerDependencies: - - supports-color + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@jest/types@29.6.3': + '@smithy/service-error-classification@4.2.13': dependencies: - '@jest/schemas': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 20.19.39 - '@types/yargs': 17.0.35 - chalk: 4.1.2 + '@smithy/types': 4.14.0 - '@jridgewell/gen-mapping@0.3.13': + '@smithy/shared-ini-file-loader@4.4.8': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@jridgewell/remapping@2.3.5': + '@smithy/signature-v4@5.3.13': dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + '@smithy/is-array-buffer': 4.2.2 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-uri-escape': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 - '@jridgewell/resolve-uri@3.1.2': {} + '@smithy/smithy-client@4.12.9': + dependencies: + '@smithy/core': 3.23.14 + '@smithy/middleware-endpoint': 4.4.29 + '@smithy/middleware-stack': 4.2.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-stream': 4.5.22 + tslib: 2.8.1 - '@jridgewell/source-map@0.3.11': + '@smithy/types@4.14.0': dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + tslib: 2.8.1 - '@jridgewell/sourcemap-codec@1.5.5': {} + '@smithy/url-parser@4.2.13': + dependencies: + '@smithy/querystring-parser': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@jridgewell/trace-mapping@0.3.31': + '@smithy/util-base64@4.3.2': dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 - '@jridgewell/trace-mapping@0.3.9': + '@smithy/util-body-length-browser@4.2.2': dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + tslib: 2.8.1 - '@ljharb/through@2.3.14': + '@smithy/util-body-length-node@4.2.3': dependencies: - call-bind: 1.0.9 + tslib: 2.8.1 - '@lukeed/csprng@1.1.0': {} + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 - '@nestjs/cli@10.4.9': + '@smithy/util-buffer-from@4.2.2': dependencies: - '@angular-devkit/core': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics-cli': 17.3.11(chokidar@3.6.0) - '@nestjs/schematics': 10.2.3(chokidar@3.6.0)(typescript@5.7.2) - chalk: 4.1.2 - chokidar: 3.6.0 - cli-table3: 0.6.5 - commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.7.2)(webpack@5.97.1) - glob: 10.4.5 - inquirer: 8.2.6 - node-emoji: 1.11.0 - ora: 5.4.1 - tree-kill: 1.2.2 - tsconfig-paths: 4.2.0 - tsconfig-paths-webpack-plugin: 4.2.0 - typescript: 5.7.2 - webpack: 5.97.1 - webpack-node-externals: 3.0.0 - transitivePeerDependencies: - - esbuild - - uglify-js - - webpack-cli + '@smithy/is-array-buffer': 4.2.2 + tslib: 2.8.1 - '@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@smithy/util-config-provider@4.2.2': dependencies: - file-type: 20.4.1 - iterare: 1.2.1 - reflect-metadata: 0.2.2 - rxjs: 7.8.2 tslib: 2.8.1 - uid: 2.0.2 - transitivePeerDependencies: - - supports-color - '@nestjs/core@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@smithy/util-defaults-mode-browser@4.3.45': dependencies: - '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nuxtjs/opencollective': 0.3.2 - fast-safe-stringify: 2.1.1 - iterare: 1.2.1 - path-to-regexp: 3.3.0 - reflect-metadata: 0.2.2 - rxjs: 7.8.2 + '@smithy/property-provider': 4.2.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 tslib: 2.8.1 - uid: 2.0.2 - optionalDependencies: - '@nestjs/platform-express': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) - transitivePeerDependencies: - - encoding - '@nestjs/platform-express@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)': + '@smithy/util-defaults-mode-node@4.2.49': dependencies: - '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) - body-parser: 1.20.4 - cors: 2.8.5 - express: 4.22.1 - multer: 2.0.2 + '@smithy/config-resolver': 4.4.14 + '@smithy/credential-provider-imds': 4.2.13 + '@smithy/node-config-provider': 4.3.13 + '@smithy/property-provider': 4.2.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - '@nestjs/schematics@10.2.3(chokidar@3.6.0)(typescript@5.7.2)': + '@smithy/util-endpoints@3.3.4': dependencies: - '@angular-devkit/core': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) - comment-json: 4.2.5 - jsonc-parser: 3.3.1 - pluralize: 8.0.0 - typescript: 5.7.2 - transitivePeerDependencies: - - chokidar + '@smithy/node-config-provider': 4.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@nestjs/schematics@10.2.3(chokidar@3.6.0)(typescript@5.9.3)': + '@smithy/util-hex-encoding@4.2.2': dependencies: - '@angular-devkit/core': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) - comment-json: 4.2.5 - jsonc-parser: 3.3.1 - pluralize: 8.0.0 - typescript: 5.9.3 - transitivePeerDependencies: - - chokidar + tslib: 2.8.1 - '@nestjs/testing@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/platform-express@10.4.22)': + '@smithy/util-middleware@4.2.13': dependencies: - '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@smithy/types': 4.14.0 tslib: 2.8.1 - optionalDependencies: - '@nestjs/platform-express': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) - '@noble/hashes@1.8.0': {} + '@smithy/util-retry@4.3.1': + dependencies: + '@smithy/service-error-classification': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@nodelib/fs.scandir@2.1.5': + '@smithy/util-stream@4.5.22': dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 + '@smithy/fetch-http-handler': 5.3.16 + '@smithy/node-http-handler': 4.5.2 + '@smithy/types': 4.14.0 + '@smithy/util-base64': 4.3.2 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 - '@nodelib/fs.stat@2.0.5': {} + '@smithy/util-uri-escape@4.2.2': + dependencies: + tslib: 2.8.1 - '@nodelib/fs.walk@1.2.8': + '@smithy/util-utf8@2.3.0': dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 - '@nuxtjs/opencollective@0.3.2': + '@smithy/util-utf8@4.2.2': dependencies: - chalk: 4.1.2 - consola: 2.15.3 - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding + '@smithy/util-buffer-from': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-waiter@4.2.15': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/uuid@1.1.2': + dependencies: + tslib: 2.8.1 - '@paralleldrive/cuid2@2.3.1': + '@so-ric/colorspace@1.1.6': dependencies: - '@noble/hashes': 1.8.0 + color: 5.0.3 + text-hex: 1.0.0 + + '@standard-schema/spec@1.1.0': {} - '@pkgjs/parseargs@0.11.0': + '@swc/core-darwin-arm64@1.15.24': optional: true - '@pkgr/core@0.2.9': {} + '@swc/core-darwin-x64@1.15.24': + optional: true - '@sinclair/typebox@0.27.10': {} + '@swc/core-linux-arm-gnueabihf@1.15.24': + optional: true - '@sinonjs/commons@3.0.1': - dependencies: - type-detect: 4.0.8 + '@swc/core-linux-arm64-gnu@1.15.24': + optional: true - '@sinonjs/fake-timers@10.3.0': - dependencies: - '@sinonjs/commons': 3.0.1 + '@swc/core-linux-arm64-musl@1.15.24': + optional: true - '@tokenizer/inflate@0.2.7': - dependencies: - debug: 4.4.3 - fflate: 0.8.2 - token-types: 6.1.2 - transitivePeerDependencies: - - supports-color + '@swc/core-linux-ppc64-gnu@1.15.24': + optional: true - '@tokenizer/token@0.3.0': {} + '@swc/core-linux-s390x-gnu@1.15.24': + optional: true - '@tsconfig/node10@1.0.12': {} + '@swc/core-linux-x64-gnu@1.15.24': + optional: true + + '@swc/core-linux-x64-musl@1.15.24': + optional: true - '@tsconfig/node12@1.0.11': {} + '@swc/core-win32-arm64-msvc@1.15.24': + optional: true - '@tsconfig/node14@1.0.3': {} + '@swc/core-win32-ia32-msvc@1.15.24': + optional: true - '@tsconfig/node16@1.0.4': {} + '@swc/core-win32-x64-msvc@1.15.24': + optional: true - '@types/babel__core@7.20.5': + '@swc/core@1.15.24': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 + '@swc/counter': 0.1.3 + '@swc/types': 0.1.26 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.24 + '@swc/core-darwin-x64': 1.15.24 + '@swc/core-linux-arm-gnueabihf': 1.15.24 + '@swc/core-linux-arm64-gnu': 1.15.24 + '@swc/core-linux-arm64-musl': 1.15.24 + '@swc/core-linux-ppc64-gnu': 1.15.24 + '@swc/core-linux-s390x-gnu': 1.15.24 + '@swc/core-linux-x64-gnu': 1.15.24 + '@swc/core-linux-x64-musl': 1.15.24 + '@swc/core-win32-arm64-msvc': 1.15.24 + '@swc/core-win32-ia32-msvc': 1.15.24 + '@swc/core-win32-x64-msvc': 1.15.24 + optional: true + + '@swc/counter@0.1.3': + optional: true - '@types/babel__generator@7.27.0': + '@swc/types@0.1.26': dependencies: - '@babel/types': 7.29.0 + '@swc/counter': 0.1.3 + optional: true - '@types/babel__template@7.4.4': + '@tokenizer/inflate@0.4.1': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color - '@types/babel__traverse@7.28.0': + '@tokenizer/token@0.3.0': {} + + '@tybys/wasm-util@0.10.1': dependencies: - '@babel/types': 7.29.0 + tslib: 2.8.1 + optional: true '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 '@types/node': 20.19.39 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/connect@3.4.38': dependencies: '@types/node': 20.19.39 - '@types/cookiejar@2.1.5': {} + '@types/deep-eql@4.0.2': {} '@types/eslint-scope@3.7.7': dependencies: @@ -3487,50 +6582,58 @@ snapshots: '@types/estree@1.0.8': {} - '@types/express-serve-static-core@4.19.8': + '@types/express-serve-static-core@5.1.1': dependencies: '@types/node': 20.19.39 '@types/qs': 6.15.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 - '@types/express@4.17.25': + '@types/express@5.0.6': dependencies: '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 4.19.8 - '@types/qs': 6.15.0 - '@types/serve-static': 1.15.10 - - '@types/graceful-fs@4.1.9': - dependencies: - '@types/node': 20.19.39 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 '@types/http-errors@2.0.5': {} - '@types/istanbul-lib-coverage@2.0.6': {} + '@types/json-schema@7.0.15': {} - '@types/istanbul-lib-report@3.0.3': + '@types/jsonwebtoken@9.0.10': dependencies: - '@types/istanbul-lib-coverage': 2.0.6 + '@types/ms': 2.1.0 + '@types/node': 20.19.39 + + '@types/ms@2.1.0': {} - '@types/istanbul-reports@3.0.4': + '@types/node@20.19.39': dependencies: - '@types/istanbul-lib-report': 3.0.3 + undici-types: 6.21.0 - '@types/jest@29.5.14': + '@types/nodemailer@8.0.0': dependencies: - expect: 29.7.0 - pretty-format: 29.7.0 + '@types/node': 20.19.39 - '@types/json-schema@7.0.15': {} + '@types/passport-jwt@4.0.1': + dependencies: + '@types/jsonwebtoken': 9.0.10 + '@types/passport-strategy': 0.2.38 - '@types/methods@1.1.4': {} + '@types/passport-strategy@0.2.38': + dependencies: + '@types/express': 5.0.6 + '@types/passport': 1.0.17 - '@types/mime@1.3.5': {} + '@types/passport@1.0.17': + dependencies: + '@types/express': 5.0.6 - '@types/node@20.19.39': + '@types/pg@8.20.0': dependencies: - undici-types: 6.21.0 + '@types/node': 20.19.39 + pg-protocol: 1.13.0 + pg-types: 2.2.0 + optional: true '@types/qs@6.15.0': {} @@ -3538,40 +6641,18 @@ snapshots: '@types/semver@7.7.1': {} - '@types/send@0.17.6': - dependencies: - '@types/mime': 1.3.5 - '@types/node': 20.19.39 - '@types/send@1.2.1': dependencies: '@types/node': 20.19.39 - '@types/serve-static@1.15.10': + '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 '@types/node': 20.19.39 - '@types/send': 0.17.6 - '@types/stack-utils@2.0.3': {} + '@types/triple-beam@1.3.5': {} - '@types/superagent@8.1.9': - dependencies: - '@types/cookiejar': 2.1.5 - '@types/methods': 1.1.4 - '@types/node': 20.19.39 - form-data: 4.0.5 - - '@types/supertest@6.0.3': - dependencies: - '@types/methods': 1.1.4 - '@types/superagent': 8.1.9 - - '@types/yargs-parser@21.0.3': {} - - '@types/yargs@17.0.35': - dependencies: - '@types/yargs-parser': 21.0.3 + '@types/ua-parser-js@0.7.39': {} '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': dependencies: @@ -3661,6 +6742,61 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@vitest/coverage-v8@4.1.4(vitest@4.1.4)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.4 + ast-v8-to-istanbul: 1.0.0 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.2 + obug: 2.1.1 + std-env: 4.0.0 + tinyrainbow: 3.1.0 + vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + + '@vitest/expect@4.1.4': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + + '@vitest/pretty-format@4.1.4': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.4': + dependencies: + '@vitest/utils': 4.1.4 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.4': + dependencies: + '@vitest/pretty-format': 4.1.4 + '@vitest/utils': 4.1.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.4': {} + + '@vitest/utils@4.1.4': + dependencies: + '@vitest/pretty-format': 4.1.4 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -3737,30 +6873,36 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 + '@willsoto/nestjs-prometheus@6.1.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(prom-client@15.1.3)': + dependencies: + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + prom-client: 15.1.3 + '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} - accepts@1.3.8: + abort-controller@3.0.0: dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 + event-target-shim: 5.0.1 - acorn-jsx@5.3.2(acorn@8.16.0): + abstract-logging@2.0.1: {} + + acorn-import-phases@1.0.4(acorn@8.16.0): dependencies: acorn: 8.16.0 - acorn-walk@8.3.5: + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 acorn@8.16.0: {} - ajv-formats@2.1.1(ajv@8.12.0): + ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: - ajv: 8.12.0 + ajv: 8.18.0 - ajv-formats@2.1.1(ajv@8.18.0): + ajv-formats@3.0.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -3780,13 +6922,6 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.12.0: - dependencies: - fast-deep-equal: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - uri-js: 4.4.1 - ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 @@ -3794,11 +6929,16 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + optional: true + ansi-colors@4.1.3: {} - ansi-escapes@4.3.2: + ansi-escapes@7.3.0: dependencies: - type-fest: 0.21.3 + environment: 1.1.0 ansi-regex@5.0.1: {} @@ -3808,97 +6948,63 @@ snapshots: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: {} - ansi-styles@6.2.3: {} - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.2 - - append-field@1.0.0: {} - - arg@4.1.3: {} + ansis@4.2.0: {} - argparse@1.0.10: + argon2@0.44.0: dependencies: - sprintf-js: 1.0.3 + '@phc/format': 1.0.0 + cross-env: 10.1.0 + node-addon-api: 8.7.0 + node-gyp-build: 4.8.4 argparse@2.0.1: {} - array-flatten@1.1.1: {} + array-ify@1.0.0: {} array-timsort@1.0.3: {} array-union@2.1.0: {} - asap@2.0.6: {} + assertion-error@2.0.1: {} - asynckit@0.4.0: {} - - babel-jest@29.7.0(@babel/core@7.29.0): + ast-v8-to-istanbul@1.0.0: dependencies: - '@babel/core': 7.29.0 - '@jest/transform': 29.7.0 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.29.0) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 - babel-plugin-istanbul@6.1.1: + async@3.2.6: {} + + asynckit@0.4.0: {} + + atomic-sleep@1.0.0: {} + + avvio@9.2.0: dependencies: - '@babel/helper-plugin-utils': 7.28.6 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 5.2.1 - test-exclude: 6.0.0 - transitivePeerDependencies: - - supports-color + '@fastify/error': 4.2.0 + fastq: 1.20.1 - babel-plugin-jest-hoist@29.6.3: + axios@1.16.0: dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.28.0 - - babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) - - babel-preset-jest@29.6.3(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + follow-redirects: 1.16.0 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.10.17: {} - binary-extensions@2.3.0: {} + bignumber.js@9.3.1: {} + + bintrees@1.0.2: {} bl@4.1.0: dependencies: @@ -3906,22 +7012,19 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - body-parser@1.20.4: + bowser@2.14.1: {} + + boxen@5.1.2: dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.1 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.14.2 - raw-body: 2.5.3 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + string-width: 4.2.3 + type-fest: 0.20.2 + widest-line: 3.1.0 + wrap-ansi: 7.0.0 + optional: true brace-expansion@1.1.13: dependencies: @@ -3932,6 +7035,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -3944,13 +7051,7 @@ snapshots: node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) - bs-logger@0.2.6: - dependencies: - fast-json-stable-stringify: 2.1.0 - - bser@2.1.1: - dependencies: - node-int64: 0.4.0 + buffer-equal-constant-time@1.0.1: {} buffer-from@1.1.2: {} @@ -3959,70 +7060,64 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - busboy@1.6.0: + buffer@6.0.3: dependencies: - streamsearch: 1.1.0 + base64-js: 1.5.1 + ieee754: 1.2.1 - bytes@3.1.2: {} + bullmq@5.73.4: + dependencies: + cron-parser: 4.9.0 + ioredis: 5.10.1 + msgpackr: 1.11.5 + node-abort-controller: 3.1.1 + semver: 7.7.4 + tslib: 2.8.1 + uuid: 11.1.0 + transitivePeerDependencies: + - supports-color call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 function-bind: 1.1.2 - call-bind@1.0.9: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - callsites@3.1.0: {} - camelcase@5.3.1: {} - - camelcase@6.3.0: {} + camelcase@6.3.0: + optional: true caniuse-lite@1.0.30001787: {} + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.6.2: {} + chardet@2.1.1: {} - char-regex@1.0.2: {} - - chardet@0.7.0: {} + check-disk-space@3.4.0: + optional: true - chokidar@3.6.0: + chokidar@4.0.3: dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 + readdirp: 4.1.2 chrome-trace-event@1.0.4: {} - ci-info@3.9.0: {} - - cjs-module-lexer@1.4.3: {} + cli-boxes@2.2.1: + optional: true cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + cli-spinners@2.9.2: {} cli-table3@0.6.5: @@ -4031,7 +7126,10 @@ snapshots: optionalDependencies: '@colors/colors': 1.5.0 - cli-width@3.0.0: {} + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.0 cli-width@4.1.0: {} @@ -4043,91 +7141,109 @@ snapshots: clone@1.0.4: {} - co@4.6.0: {} - - collect-v8-coverage@1.0.3: {} + cluster-key-slot@1.1.2: {} color-convert@2.0.1: dependencies: color-name: 1.1.4 + color-convert@3.1.3: + dependencies: + color-name: 2.1.0 + color-name@1.1.4: {} + color-name@2.1.0: {} + + color-string@2.1.4: + dependencies: + color-name: 2.1.0 + + color@5.0.3: + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 + commander@14.0.3: {} + commander@2.20.3: {} commander@4.1.1: {} - comment-json@4.2.5: + comment-json@4.6.2: dependencies: array-timsort: 1.0.3 - core-util-is: 1.0.3 esprima: 4.0.1 - has-own-prop: 2.0.0 - repeat-string: 1.6.1 - component-emitter@1.3.1: {} + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 concat-map@0.0.1: {} - concat-stream@2.0.0: - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 3.6.2 - typedarray: 0.0.6 + consola@3.4.2: {} - consola@2.15.3: {} + content-disposition@1.1.0: {} - content-disposition@0.5.4: + conventional-changelog-angular@8.3.1: dependencies: - safe-buffer: 5.2.1 - - content-type@1.0.5: {} + compare-func: 2.0.0 - convert-source-map@2.0.0: {} + conventional-changelog-conventionalcommits@9.3.1: + dependencies: + compare-func: 2.0.0 - cookie-signature@1.0.7: {} + conventional-commits-parser@6.4.0: + dependencies: + '@simple-libs/stream-utils': 1.2.0 + meow: 13.2.0 - cookie@0.7.2: {} + convert-source-map@2.0.0: {} - cookiejar@2.1.4: {} + cookie@1.1.1: {} core-util-is@1.0.3: {} - cors@2.8.5: + cosmiconfig-typescript-loader@6.3.0(@types/node@20.19.39)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3): dependencies: - object-assign: 4.1.1 - vary: 1.1.2 + '@types/node': 20.19.39 + cosmiconfig: 9.0.1(typescript@5.9.3) + jiti: 2.6.1 + typescript: 5.9.3 - cosmiconfig@8.3.6(typescript@5.7.2): + cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 js-yaml: 4.1.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 5.7.2 + typescript: 5.9.3 - create-jest@29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)): + cosmiconfig@9.0.1(typescript@5.9.3): dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + + cron-parser@4.9.0: + dependencies: + luxon: 3.7.2 - create-require@1.1.1: {} + cross-env@10.1.0: + dependencies: + '@epic-web/invariant': 1.0.0 + cross-spawn: 7.0.6 cross-spawn@7.0.6: dependencies: @@ -4135,16 +7251,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - debug@2.6.9: - dependencies: - ms: 2.0.0 - debug@4.4.3: dependencies: ms: 2.1.3 - dedent@1.7.2: {} - deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -4153,28 +7263,17 @@ snapshots: dependencies: clone: 1.0.4 - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - delayed-stream@1.0.0: {} - depd@2.0.0: {} - - destroy@1.2.0: {} + denque@2.1.0: {} - detect-newline@3.1.0: {} + depd@2.0.0: {} - dezalgo@1.0.4: - dependencies: - asap: 2.0.6 - wrappy: 1.0.2 + dequal@2.0.3: {} - diff-sequences@29.6.3: {} + detect-europe-js@0.1.2: {} - diff@4.0.4: {} + detect-libc@2.1.2: {} dir-glob@3.0.1: dependencies: @@ -4184,31 +7283,90 @@ snapshots: dependencies: esutils: 2.0.3 + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + + dotenv@17.4.1: {} + + dotenv@17.4.2: {} + + drizzle-kit@0.31.10: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + tsx: 4.21.0 + + drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0)(postgres@3.4.9): + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/pg': 8.20.0 + pg: 8.20.0 + postgres: 3.4.9 + + drizzle-zod@0.8.3(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0)(postgres@3.4.9))(zod@4.3.6): + dependencies: + drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0)(postgres@3.4.9) + zod: 4.3.6 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 - eastasianwidth@0.2.0: {} + duplexify@3.7.1: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 2.3.8 + stream-shift: 1.0.3 + + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 - ee-first@1.1.1: {} + ejs@3.1.10: + dependencies: + jake: 10.9.4 electron-to-chromium@1.5.334: {} - emittery@0.13.1: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} - emoji-regex@9.2.2: {} + enabled@2.0.0: {} - encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 enhanced-resolve@5.20.1: dependencies: graceful-fs: 4.2.11 tapable: 2.3.2 + env-paths@2.2.1: {} + + environment@1.1.0: {} + + error-causes@3.0.2: {} + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -4217,7 +7375,7 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} es-object-atoms@1.1.1: dependencies: @@ -4228,32 +7386,97 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.2 + hasown: 2.0.3 + + 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 + + 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 + + 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 escalade@3.2.0: {} escape-html@1.0.3: {} - escape-string-regexp@1.0.5: {} - - escape-string-regexp@2.0.0: {} - escape-string-regexp@4.0.0: {} - eslint-config-prettier@9.1.2(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - - eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.8.2): - dependencies: - eslint: 8.57.1 - prettier: 3.8.2 - prettier-linter-helpers: 1.0.1 - synckit: 0.11.12 - optionalDependencies: - '@types/eslint': 9.6.1 - eslint-config-prettier: 9.1.2(eslint@8.57.1) - eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -4329,80 +7552,26 @@ snapshots: estraverse@5.3.0: {} - esutils@2.0.3: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 - etag@1.8.1: {} + esutils@2.0.3: {} - events@3.3.0: {} + event-target-shim@5.0.1: {} - execa@5.1.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 + eventemitter2@6.4.9: {} - exit@0.1.2: {} + eventemitter3@5.0.4: {} - expect@29.7.0: - dependencies: - '@jest/expect-utils': 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 + events@3.3.0: {} - express@4.22.1: - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.4 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.0.7 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.3.2 - fresh: 0.5.2 - http-errors: 2.0.1 - merge-descriptors: 1.0.3 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.13 - proxy-addr: 2.0.7 - qs: 6.14.2 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.2 - serve-static: 1.16.3 - setprototypeof: 1.2.0 - statuses: 2.0.2 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color + expect-type@1.3.0: {} - external-editor@3.1.0: - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 + fast-decode-uri-component@1.0.1: {} fast-deep-equal@3.1.3: {} - fast-diff@1.3.0: {} - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4413,59 +7582,91 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@6.3.0: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + fast-safe-stringify@2.1.1: {} fast-uri@3.1.0: {} - fastq@1.20.1: + fast-xml-builder@1.1.4: dependencies: - reusify: 1.1.0 + path-expression-matcher: 1.5.0 - fb-watchman@2.0.2: + fast-xml-parser@5.5.8: dependencies: - bser: 2.1.1 + fast-xml-builder: 1.1.4 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 + + fastify-plugin@5.1.0: {} - fflate@0.8.2: {} + fastify@5.8.4: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.2.0 + fast-json-stringify: 6.3.0 + find-my-way: 9.5.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.4 + toad-cache: 3.7.0 - figures@3.2.0: + fastq@1.20.1: dependencies: - escape-string-regexp: 1.0.5 + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fecha@4.2.3: {} file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 - file-type@20.4.1: + file-type@21.3.4: dependencies: - '@tokenizer/inflate': 0.2.7 + '@tokenizer/inflate': 0.4.1 strtok3: 10.3.5 token-types: 6.1.2 uint8array-extras: 1.5.0 transitivePeerDependencies: - supports-color - fill-range@7.1.1: + filelist@1.0.6: dependencies: - to-regex-range: 5.0.1 + minimatch: 5.1.9 - finalhandler@1.3.2: + fill-range@7.1.1: dependencies: - debug: 2.6.9 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color + to-regex-range: 5.0.1 - find-up@4.1.0: + find-my-way@9.5.0: dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.0 find-up@5.0.0: dependencies: @@ -4480,17 +7681,16 @@ snapshots: flatted@3.4.2: {} - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 + fn.name@1.1.0: {} - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1): + follow-redirects@1.16.0: {} + + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7)): dependencies: '@babel/code-frame': 7.29.0 chalk: 4.1.2 - chokidar: 3.6.0 - cosmiconfig: 8.3.6(typescript@5.7.2) + chokidar: 4.0.3 + cosmiconfig: 8.3.6(typescript@5.9.3) deepmerge: 4.3.1 fs-extra: 10.1.0 memfs: 3.5.3 @@ -4499,28 +7699,17 @@ snapshots: schema-utils: 3.3.0 semver: 7.7.4 tapable: 2.3.2 - typescript: 5.7.2 - webpack: 5.97.1 + typescript: 5.9.3 + webpack: 5.106.0(@swc/core@1.15.24)(esbuild@0.27.7) form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.2 + hasown: 2.0.3 mime-types: 2.1.35 - formidable@2.1.5: - dependencies: - '@paralleldrive/cuid2': 2.3.1 - dezalgo: 1.0.4 - once: 1.4.0 - qs: 6.15.1 - - forwarded@0.2.0: {} - - fresh@0.5.2: {} - fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 @@ -4536,10 +7725,10 @@ snapshots: function-bind@1.1.2: {} - gensync@1.0.0-beta.2: {} - get-caller-file@2.0.5: {} + get-east-asian-width@1.5.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4550,17 +7739,25 @@ snapshots: get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.3 math-intrinsics: 1.1.0 - get-package-type@0.1.0: {} - get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stream@6.0.1: {} + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + + git-raw-commits@5.0.1(conventional-commits-parser@6.4.0): + dependencies: + '@conventional-changelog/git-client': 2.7.0(conventional-commits-parser@6.4.0) + meow: 13.2.0 + transitivePeerDependencies: + - conventional-commits-filter + - conventional-commits-parser glob-parent@5.1.2: dependencies: @@ -4572,14 +7769,11 @@ snapshots: glob-to-regexp@0.4.1: {} - glob@10.4.5: + glob@13.0.6: dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.9 + minimatch: 10.2.5 minipass: 7.1.3 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 + path-scurry: 2.0.2 glob@7.2.3: dependencies: @@ -4590,6 +7784,10 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + globals@13.24.0: dependencies: type-fest: 0.20.2 @@ -4620,19 +7818,13 @@ snapshots: has-flag@4.0.0: {} - has-own-prop@2.0.0: {} - - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - has-symbols@1.1.0: {} has-tostringtag@1.0.2: dependencies: has-symbols: 1.1.0 - hasown@2.0.2: + hasown@2.0.3: dependencies: function-bind: 1.1.2 @@ -4646,9 +7838,9 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - human-signals@2.1.0: {} + husky@9.1.7: {} - iconv-lite@0.4.24: + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -4661,73 +7853,44 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-local@3.2.0: - dependencies: - pkg-dir: 4.2.0 - resolve-cwd: 3.0.0 + import-meta-resolve@4.2.0: {} imurmurhash@0.1.4: {} inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - inquirer@8.2.6: - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.18.1 - mute-stream: 0.0.8 - ora: 5.4.1 - run-async: 2.4.1 - rxjs: 7.8.2 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - wrap-ansi: 6.2.0 - - inquirer@9.2.15: - dependencies: - '@ljharb/through': 2.3.14 - ansi-escapes: 4.3.2 - chalk: 5.6.2 - cli-cursor: 3.1.0 - cli-width: 4.1.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.18.1 - mute-stream: 1.0.0 - ora: 5.4.1 - run-async: 3.0.0 - rxjs: 7.8.2 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 + dependencies: + once: 1.4.0 + wrappy: 1.0.2 - ipaddr.js@1.9.1: {} + inherits@2.0.4: {} - is-arrayish@0.2.1: {} + ini@4.1.1: {} - is-binary-path@2.1.0: + ioredis@5.10.1: dependencies: - binary-extensions: 2.3.0 + '@ioredis/commands': 1.5.1 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 + ipaddr.js@2.3.0: {} + + is-arrayish@0.2.1: {} is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} - is-generator-fn@2.1.0: {} + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.5.0 is-glob@4.0.3: dependencies: @@ -4737,50 +7900,30 @@ snapshots: is-number@7.0.0: {} + is-obj@2.0.0: {} + is-path-inside@3.0.3: {} + is-plain-obj@4.1.0: {} + + is-standalone-pwa@0.1.1: {} + is-stream@2.0.1: {} is-unicode-supported@0.1.0: {} + isarray@1.0.0: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} - istanbul-lib-instrument@5.2.1: - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.2 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - istanbul-lib-instrument@6.0.3: - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.2 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 7.7.4 - transitivePeerDependencies: - - supports-color - istanbul-lib-report@3.0.1: dependencies: istanbul-lib-coverage: 3.2.2 make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@4.0.1: - dependencies: - debug: 4.4.3 - istanbul-lib-coverage: 3.2.2 - source-map: 0.6.1 - transitivePeerDependencies: - - supports-color - istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 @@ -4788,391 +7931,202 @@ snapshots: iterare@1.2.1: {} - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - jest-changed-files@29.7.0: - dependencies: - execa: 5.1.1 - jest-util: 29.7.0 - p-limit: 3.1.0 - - jest-circus@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.19.39 - chalk: 4.1.2 - co: 4.6.0 - dedent: 1.7.2 - is-generator-fn: 2.1.0 - jest-each: 29.7.0 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - p-limit: 3.1.0 - pretty-format: 29.7.0 - pure-rand: 6.1.0 - slash: 3.0.0 - stack-utils: 2.0.6 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-cli@29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)): + jake@10.9.4: dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) - exit: 0.1.2 - import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node + async: 3.2.6 + filelist: 1.0.6 + picocolors: 1.1.1 - jest-config@29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)): + jest-worker@27.5.1: dependencies: - '@babel/core': 7.29.0 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: '@types/node': 20.19.39 - ts-node: 10.9.2(@types/node@20.19.39)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-diff@29.7.0: - dependencies: - chalk: 4.1.2 - diff-sequences: 29.6.3 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-docblock@29.7.0: - dependencies: - detect-newline: 3.1.0 - - jest-each@29.7.0: - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - jest-get-type: 29.6.3 - jest-util: 29.7.0 - pretty-format: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 - jest-environment-node@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.19.39 - jest-mock: 29.7.0 - jest-util: 29.7.0 + jiti@2.6.1: {} - jest-get-type@29.6.3: {} + js-tokens@10.0.0: {} - jest-haste-map@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/graceful-fs': 4.1.9 - '@types/node': 20.19.39 - anymatch: 3.1.3 - fb-watchman: 2.0.2 - graceful-fs: 4.2.11 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - jest-worker: 29.7.0 - micromatch: 4.0.8 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.3 + js-tokens@4.0.0: {} - jest-leak-detector@29.7.0: + js-yaml@4.1.1: dependencies: - jest-get-type: 29.6.3 - pretty-format: 29.7.0 + argparse: 2.0.1 - jest-matcher-utils@29.7.0: - dependencies: - chalk: 4.1.2 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 + json-buffer@3.0.1: {} - jest-message-util@29.7.0: - dependencies: - '@babel/code-frame': 7.29.0 - '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - stack-utils: 2.0.6 + json-parse-even-better-errors@2.3.1: {} - jest-mock@29.7.0: + json-schema-ref-resolver@3.0.0: dependencies: - '@jest/types': 29.6.3 - '@types/node': 20.19.39 - jest-util: 29.7.0 + dequal: 2.0.3 - jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - optionalDependencies: - jest-resolve: 29.7.0 + json-schema-traverse@0.4.1: {} - jest-regex-util@29.6.3: {} + json-schema-traverse@1.0.0: {} - jest-resolve-dependencies@29.7.0: - dependencies: - jest-regex-util: 29.6.3 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color + json-stable-stringify-without-jsonify@1.0.1: {} - jest-resolve@29.7.0: - dependencies: - chalk: 4.1.2 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) - jest-util: 29.7.0 - jest-validate: 29.7.0 - resolve: 1.22.11 - resolve.exports: 2.0.3 - slash: 3.0.0 + json5@2.2.3: {} - jest-runner@29.7.0: - dependencies: - '@jest/console': 29.7.0 - '@jest/environment': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.19.39 - chalk: 4.1.2 - emittery: 0.13.1 - graceful-fs: 4.2.11 - jest-docblock: 29.7.0 - jest-environment-node: 29.7.0 - jest-haste-map: 29.7.0 - jest-leak-detector: 29.7.0 - jest-message-util: 29.7.0 - jest-resolve: 29.7.0 - jest-runtime: 29.7.0 - jest-util: 29.7.0 - jest-watcher: 29.7.0 - jest-worker: 29.7.0 - p-limit: 3.1.0 - source-map-support: 0.5.13 - transitivePeerDependencies: - - supports-color + jsonc-parser@3.3.1: {} - jest-runtime@29.7.0: + jsonfile@6.2.0: dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/globals': 29.7.0 - '@jest/source-map': 29.6.3 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.19.39 - chalk: 4.1.2 - cjs-module-lexer: 1.4.3 - collect-v8-coverage: 1.0.3 - glob: 7.2.3 + universalify: 2.0.1 + optionalDependencies: graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - strip-bom: 4.0.0 - transitivePeerDependencies: - - supports-color - jest-snapshot@29.7.0: + jsonwebtoken@9.0.3: dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/types': 7.29.0 - '@jest/expect-utils': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) - chalk: 4.1.2 - expect: 29.7.0 - graceful-fs: 4.2.11 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - natural-compare: 1.4.0 - pretty-format: 29.7.0 + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 semver: 7.7.4 - transitivePeerDependencies: - - supports-color - jest-util@29.7.0: + jwa@2.0.1: dependencies: - '@jest/types': 29.6.3 - '@types/node': 20.19.39 - chalk: 4.1.2 - ci-info: 3.9.0 - graceful-fs: 4.2.11 - picomatch: 2.3.2 + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 - jest-validate@29.7.0: + jws@4.0.1: dependencies: - '@jest/types': 29.6.3 - camelcase: 6.3.0 - chalk: 4.1.2 - jest-get-type: 29.6.3 - leven: 3.1.0 - pretty-format: 29.7.0 + jwa: 2.0.1 + safe-buffer: 5.2.1 - jest-watcher@29.7.0: + keyv@4.5.4: dependencies: - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.19.39 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.13.1 - jest-util: 29.7.0 - string-length: 4.0.2 + json-buffer: 3.0.1 - jest-worker@27.5.1: - dependencies: - '@types/node': 20.19.39 - merge-stream: 2.0.0 - supports-color: 8.1.1 + kuler@2.0.0: {} - jest-worker@29.7.0: + levn@0.4.1: dependencies: - '@types/node': 20.19.39 - jest-util: 29.7.0 - merge-stream: 2.0.0 - supports-color: 8.1.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 - jest@29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)): + light-my-request@6.6.0: dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - js-tokens@4.0.0: {} + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 - js-yaml@3.14.2: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 + lightningcss-android-arm64@1.32.0: + optional: true - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 + lightningcss-darwin-arm64@1.32.0: + optional: true - jsesc@3.1.0: {} + lightningcss-darwin-x64@1.32.0: + optional: true - json-buffer@3.0.1: {} + lightningcss-freebsd-x64@1.32.0: + optional: true - json-parse-even-better-errors@2.3.1: {} + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true - json-schema-traverse@0.4.1: {} + lightningcss-linux-arm64-gnu@1.32.0: + optional: true - json-schema-traverse@1.0.0: {} + lightningcss-linux-arm64-musl@1.32.0: + optional: true - json-stable-stringify-without-jsonify@1.0.1: {} + lightningcss-linux-x64-gnu@1.32.0: + optional: true - json5@2.2.3: {} + lightningcss-linux-x64-musl@1.32.0: + optional: true - jsonc-parser@3.2.1: {} + lightningcss-win32-arm64-msvc@1.32.0: + optional: true - jsonc-parser@3.3.1: {} + lightningcss-win32-x64-msvc@1.32.0: + optional: true - jsonfile@6.2.0: + lightningcss@1.32.0: dependencies: - universalify: 2.0.1 + detect-libc: 2.1.2 optionalDependencies: - graceful-fs: 4.2.11 - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 - kleur@3.0.3: {} + lines-and-columns@1.2.4: {} - leven@3.1.0: {} + lint-staged@16.4.0: + dependencies: + commander: 14.0.3 + listr2: 9.0.5 + picomatch: 4.0.4 + string-argv: 0.3.2 + tinyexec: 1.1.1 + yaml: 2.8.3 - levn@0.4.1: + listr2@9.0.5: dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 + cli-truncate: 5.2.0 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 - lines-and-columns@1.2.4: {} + load-esm@1.0.3: {} loader-runner@4.3.1: {} - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - locate-path@6.0.0: dependencies: p-locate: 5.0.0 - lodash.memoize@4.1.2: {} + lodash.camelcase@4.3.0: {} + + lodash.defaults@4.2.0: {} + + lodash.includes@4.3.0: {} + + lodash.isarguments@3.1.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.kebabcase@4.1.1: {} lodash.merge@4.6.2: {} + lodash.mergewith@4.6.2: {} + + lodash.once@4.1.1: {} + + lodash.snakecase@4.1.1: {} + + lodash.startcase@4.4.0: {} + + lodash.upperfirst@4.3.1: {} + lodash@4.18.1: {} log-symbols@4.1.0: @@ -5180,42 +8134,57 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 - lru-cache@10.4.3: {} + log-update@6.1.0: + dependencies: + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 - lru-cache@5.1.1: + logform@2.7.0: dependencies: - yallist: 3.1.1 + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + lru-cache@11.3.3: {} - magic-string@0.30.8: + luxon@3.7.2: {} + + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - make-dir@4.0.0: + magic-string@0.30.21: dependencies: - semver: 7.7.4 + '@jridgewell/sourcemap-codec': 1.5.5 - make-error@1.3.6: {} + magicast@0.5.2: + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 - makeerror@1.0.12: + make-dir@4.0.0: dependencies: - tmpl: 1.0.5 + semver: 7.7.4 math-intrinsics@1.1.0: {} - media-typer@0.3.0: {} - memfs@3.5.3: dependencies: fs-monkey: 1.1.0 - merge-descriptors@1.0.3: {} + meow@13.2.0: {} merge-stream@2.0.0: {} merge2@1.4.1: {} - methods@1.1.2: {} - micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -5227,21 +8196,25 @@ snapshots: dependencies: mime-db: 1.52.0 - mime@1.6.0: {} - - mime@2.6.0: {} + mime@3.0.0: {} mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + minimatch@3.1.5: dependencies: brace-expansion: 1.1.13 - minimatch@9.0.3: + minimatch@5.1.9: dependencies: brace-expansion: 2.0.3 - minimatch@9.0.9: + minimatch@9.0.3: dependencies: brace-expansion: 2.0.3 @@ -5249,70 +8222,86 @@ snapshots: minipass@7.1.3: {} - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 - - ms@2.0.0: {} - ms@2.1.3: {} - multer@2.0.2: + msgpackr-extract@3.0.3: dependencies: - append-field: 1.0.0 - busboy: 1.6.0 - concat-stream: 2.0.0 - mkdirp: 0.5.6 - object-assign: 4.1.1 - type-is: 1.6.18 - xtend: 4.0.2 + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true - mute-stream@0.0.8: {} + msgpackr@1.11.5: + optionalDependencies: + msgpackr-extract: 3.0.3 - mute-stream@1.0.0: {} + mute-stream@2.0.0: {} - natural-compare@1.4.0: {} + nanoid@3.3.11: {} - negotiator@0.6.3: {} + natural-compare@1.4.0: {} neo-async@2.6.2: {} + nest-winston@1.10.2(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(winston@3.19.0): + dependencies: + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + fast-safe-stringify: 2.1.1 + winston: 3.19.0 + + nestjs-zod@5.3.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6): + dependencies: + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + deepmerge: 4.3.1 + rxjs: 7.8.2 + zod: 4.3.6 + optionalDependencies: + '@nestjs/swagger': 11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + node-abort-controller@3.1.1: {} + node-addon-api@8.7.0: {} + node-emoji@1.11.0: dependencies: lodash: 4.18.1 - node-fetch@2.7.0: + node-gyp-build-optional-packages@5.2.2: dependencies: - whatwg-url: 5.0.0 + detect-libc: 2.1.2 + optional: true - node-int64@0.4.0: {} + node-gyp-build@4.8.4: {} node-releases@2.0.37: {} - normalize-path@3.0.0: {} - - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - - object-assign@4.1.1: {} + nodemailer@8.0.5: {} - object-inspect@1.13.4: {} + obug@2.1.1: {} - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 + on-exit-leak-free@2.1.2: {} once@1.4.0: dependencies: wrappy: 1.0.2 + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -5334,28 +8323,23 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 - os-tmpdir@1.0.2: {} - - p-limit@2.3.0: + otplib@13.4.0: dependencies: - p-try: 2.2.0 + '@otplib/core': 13.4.0 + '@otplib/hotp': 13.4.0 + '@otplib/plugin-base32-scure': 13.4.0 + '@otplib/plugin-crypto-noble': 13.4.0 + '@otplib/totp': 13.4.0 + '@otplib/uri': 13.4.0 p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - p-locate@5.0.0: dependencies: p-limit: 3.1.0 - p-try@2.2.0: {} - - package-json-from-dist@1.0.1: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -5367,89 +8351,183 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - parseurl@1.3.3: {} + passport-jwt@4.0.1: + dependencies: + jsonwebtoken: 9.0.3 + passport-strategy: 1.0.0 + + passport-strategy@1.0.0: {} + + passport@0.7.0: + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 path-exists@4.0.0: {} + path-expression-matcher@1.5.0: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} - path-parse@1.0.7: {} - - path-scurry@1.11.1: + path-scurry@2.0.2: dependencies: - lru-cache: 10.4.3 + lru-cache: 11.3.3 minipass: 7.1.3 - path-to-regexp@0.1.13: {} - - path-to-regexp@3.3.0: {} + path-to-regexp@8.4.2: {} path-type@4.0.0: {} + pathe@2.0.3: {} + + pause@0.0.1: {} + + peek-stream@1.1.3: + dependencies: + buffer-from: 1.1.2 + duplexify: 3.7.1 + through2: 2.0.5 + + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.12.0: + optional: true + + pg-int8@1.0.1: + optional: true + + pg-pool@3.13.0(pg@8.20.0): + dependencies: + pg: 8.20.0 + optional: true + + pg-protocol@1.13.0: + optional: true + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + optional: true + + pg@8.20.0: + dependencies: + pg-connection-string: 2.12.0 + pg-pool: 3.13.0(pg@8.20.0) + pg-protocol: 1.13.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + optional: true + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + optional: true + picocolors@1.1.1: {} picomatch@2.3.2: {} - picomatch@4.0.1: {} + picomatch@4.0.4: {} + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 - pirates@4.0.7: {} + pino-std-serializers@7.1.0: {} - pkg-dir@4.2.0: + pino@10.3.1: dependencies: - find-up: 4.1.0 + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.0.0 pluralize@8.0.0: {} - prelude-ls@1.2.1: {} + postcss@8.5.9: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: + optional: true - prettier-linter-helpers@1.0.1: + postgres-bytea@1.0.1: + optional: true + + postgres-date@1.0.7: + optional: true + + postgres-interval@1.2.0: dependencies: - fast-diff: 1.3.0 + xtend: 4.0.2 + optional: true + + postgres@3.4.9: {} + + prelude-ls@1.2.1: {} prettier@3.8.2: {} - pretty-format@29.7.0: - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 + process-nextick-args@2.0.1: {} - prompts@2.4.2: - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 + process-warning@4.0.1: {} - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 + process-warning@5.0.0: {} - punycode@2.3.1: {} + process@0.11.10: {} + + prom-client@15.1.3: + dependencies: + '@opentelemetry/api': 1.9.1 + tdigest: 0.1.2 - pure-rand@6.1.0: {} + proxy-from-env@2.1.0: {} - qs@6.14.2: + pump@3.0.4: dependencies: - side-channel: 1.1.0 + end-of-stream: 1.4.5 + once: 1.4.0 - qs@6.15.1: + pumpify@2.0.1: dependencies: - side-channel: 1.1.0 + duplexify: 4.1.3 + inherits: 2.0.4 + pump: 3.0.4 + + punycode@2.3.1: {} queue-microtask@1.2.3: {} - range-parser@1.2.1: {} + quick-format-unescaped@4.0.4: {} - raw-body@2.5.3: + readable-stream@2.3.8: dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - - react-is@18.3.1: {} + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 readable-stream@3.6.2: dependencies: @@ -5457,48 +8535,80 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 - readdirp@3.6.0: + readable-stream@4.7.0: dependencies: - picomatch: 2.3.2 + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 - reflect-metadata@0.2.2: {} + readdirp@4.1.2: {} - repeat-string@1.6.1: {} + real-require@0.2.0: {} - require-directory@2.1.1: {} + redis-errors@1.2.0: {} - require-from-string@2.0.2: {} + redis-info@3.1.0: + dependencies: + lodash: 4.18.1 - resolve-cwd@3.0.0: + redis-parser@3.0.0: dependencies: - resolve-from: 5.0.0 + redis-errors: 1.2.0 + + reflect-metadata@0.2.2: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} resolve-from@4.0.0: {} resolve-from@5.0.0: {} - resolve.exports@2.0.3: {} - - resolve@1.22.11: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 + resolve-pkg-maps@1.0.0: {} restore-cursor@3.1.0: dependencies: onetime: 5.1.2 signal-exit: 3.0.7 + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + ret@0.5.0: {} + reusify@1.1.0: {} + rfdc@1.4.1: {} + rimraf@3.0.2: dependencies: glob: 7.2.3 - run-async@2.4.1: {} - - run-async@3.0.0: {} + rolldown@1.0.0-rc.15: + dependencies: + '@oxc-project/types': 0.124.0 + '@rolldown/pluginutils': 1.0.0-rc.15 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-x64': 1.0.0-rc.15 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 run-parallel@1.2.0: dependencies: @@ -5512,8 +8622,16 @@ snapshots: dependencies: tslib: 2.8.1 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} + safe-regex2@5.1.0: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} schema-utils@3.3.0: @@ -5529,45 +8647,11 @@ snapshots: ajv-formats: 2.1.1(ajv@8.18.0) ajv-keywords: 5.1.0(ajv@8.18.0) - semver@6.3.1: {} + secure-json-parse@4.1.0: {} semver@7.7.4: {} - send@0.19.2: - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.1 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - serve-static@1.16.3: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.19.2 - transitivePeerDependencies: - - supports-color - - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 + set-cookie-parser@2.7.2: {} setprototypeof@1.2.0: {} @@ -5577,46 +8661,29 @@ snapshots: shebang-regex@3.0.0: {} - side-channel-list@1.0.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.1 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} signal-exit@4.1.0: {} - sisteransi@1.0.5: {} - slash@3.0.0: {} - source-map-support@0.5.13: + slice-ansi@7.1.2: dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} source-map-support@0.5.21: dependencies: @@ -5629,20 +8696,21 @@ snapshots: source-map@0.7.6: {} - sprintf-js@1.0.3: {} + split2@4.2.0: {} - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 + stack-trace@0.0.10: {} + + stackback@0.0.2: {} + + standard-as-callback@2.1.0: {} statuses@2.0.2: {} - streamsearch@1.1.0: {} + std-env@4.0.0: {} - string-length@4.0.2: - dependencies: - char-regex: 1.0.2 - strip-ansi: 6.0.1 + stream-shift@1.0.3: {} + + string-argv@0.3.2: {} string-width@4.2.3: dependencies: @@ -5650,12 +8718,21 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - string-width@5.1.2: + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + string-width@8.2.0: dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 + get-east-asian-width: 1.5.0 strip-ansi: 7.2.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -5670,38 +8747,14 @@ snapshots: strip-bom@3.0.0: {} - strip-bom@4.0.0: {} - - strip-final-newline@2.0.0: {} - strip-json-comments@3.1.1: {} + strnum@2.2.3: {} + strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 - superagent@8.1.2: - dependencies: - component-emitter: 1.3.1 - cookiejar: 2.1.4 - debug: 4.4.3 - fast-safe-stringify: 2.1.1 - form-data: 4.0.5 - formidable: 2.1.5 - methods: 1.1.2 - mime: 2.6.0 - qs: 6.15.1 - semver: 7.7.4 - transitivePeerDependencies: - - supports-color - - supertest@6.3.4: - dependencies: - methods: 1.1.2 - superagent: 8.1.2 - transitivePeerDependencies: - - supports-color - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -5710,23 +8763,28 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-preserve-symlinks-flag@1.0.0: {} + swagger-ui-dist@5.32.2: + dependencies: + '@scarf/scarf': 1.4.0 symbol-observable@4.0.0: {} - synckit@0.11.12: - dependencies: - '@pkgr/core': 0.2.9 - tapable@2.3.2: {} - terser-webpack-plugin@5.4.0(webpack@5.97.1): + tdigest@0.1.2: + dependencies: + bintrees: 1.0.2 + + terser-webpack-plugin@5.4.0(@swc/core@1.15.24)(esbuild@0.27.7)(webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.46.1 - webpack: 5.97.1 + webpack: 5.106.0(@swc/core@1.15.24)(esbuild@0.27.7) + optionalDependencies: + '@swc/core': 1.15.24 + esbuild: 0.27.7 terser@5.46.1: dependencies: @@ -5735,26 +8793,36 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 - test-exclude@6.0.0: - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.5 + text-hex@1.0.0: {} text-table@0.2.0: {} - through@2.3.8: {} + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + + through2@2.0.5: + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + + tinybench@2.9.0: {} + + tinyexec@1.1.1: {} - tmp@0.0.33: + tinyglobby@0.2.16: dependencies: - os-tmpdir: 1.0.2 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 - tmpl@1.0.5: {} + tinyrainbow@3.1.0: {} to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toidentifier@1.0.1: {} token-types@6.1.2: @@ -5763,35 +8831,15 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - tr46@0.0.3: {} + transliteration@2.6.1: {} - tree-kill@1.2.2: {} + triple-beam@1.4.1: {} ts-api-utils@1.4.3(typescript@5.9.3): dependencies: typescript: 5.9.3 - ts-jest@29.4.9(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)))(typescript@5.9.3): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.9 - jest: 29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.7.4 - type-fest: 4.41.0 - typescript: 5.9.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.29.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) - jest-util: 29.7.0 - - ts-loader@9.5.7(typescript@5.9.3)(webpack@5.97.1): + ts-loader@9.5.7(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7)): dependencies: chalk: 4.1.2 enhanced-resolve: 5.20.1 @@ -5799,25 +8847,7 @@ snapshots: semver: 7.7.4 source-map: 0.7.6 typescript: 5.9.3 - webpack: 5.97.1 - - ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.12 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 20.19.39 - acorn: 8.16.0 - acorn-walk: 8.3.5 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.4 - make-error: 1.3.6 - typescript: 5.9.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 + webpack: 5.106.0(@swc/core@1.15.24)(esbuild@0.27.7) tsconfig-paths-webpack-plugin@4.2.0: dependencies: @@ -5834,28 +8864,28 @@ snapshots: tslib@2.8.1: {} + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - type-detect@4.0.8: {} - type-fest@0.20.2: {} - type-fest@0.21.3: {} + typescript@5.9.3: {} - type-fest@4.41.0: {} + ua-is-frozen@0.1.2: {} - type-is@1.6.18: + ua-parser-js@2.0.9: dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - - typedarray@0.0.6: {} - - typescript@5.7.2: {} - - typescript@5.9.3: {} + detect-europe-js: 0.1.2 + is-standalone-pwa: 0.1.1 + ua-is-frozen: 0.1.2 uglify-js@3.19.3: optional: true @@ -5870,8 +8900,6 @@ snapshots: universalify@2.0.1: {} - unpipe@1.0.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -5886,19 +8914,52 @@ snapshots: utils-merge@1.0.1: {} - v8-compile-cache-lib@3.0.1: {} - - v8-to-istanbul@9.3.0: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - '@types/istanbul-lib-coverage': 2.0.6 - convert-source-map: 2.0.0 - - vary@1.1.2: {} + uuid@11.1.0: {} - walker@1.0.8: + vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: - makeerror: 1.0.12 + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.9 + rolldown: 1.0.0-rc.15 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 20.19.39 + esbuild: 0.27.7 + fsevents: 2.3.3 + jiti: 2.6.1 + terser: 5.46.1 + tsx: 4.21.0 + yaml: 2.8.3 + + vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.4 + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.4 + '@vitest/runner': 4.1.4 + '@vitest/snapshot': 4.1.4 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 20.19.39 + '@vitest/coverage-v8': 4.1.4(vitest@4.1.4) + transitivePeerDependencies: + - msw watchpack@2.5.1: dependencies: @@ -5909,24 +8970,24 @@ snapshots: dependencies: defaults: 1.0.4 - webidl-conversions@3.0.1: {} - webpack-node-externals@3.0.0: {} webpack-sources@3.3.4: {} - webpack@5.97.1: + webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) browserslist: 4.28.2 chrome-trace-event: 1.0.4 enhanced-resolve: 5.20.1 - es-module-lexer: 1.7.0 + es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -5935,9 +8996,9 @@ snapshots: loader-runner: 4.3.1 mime-types: 2.1.35 neo-async: 2.6.2 - schema-utils: 3.3.0 + schema-utils: 4.3.3 tapable: 2.3.2 - terser-webpack-plugin: 5.4.0(webpack@5.97.1) + terser-webpack-plugin: 5.4.0(@swc/core@1.15.24)(esbuild@0.27.7)(webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7)) watchpack: 2.5.1 webpack-sources: 3.3.4 transitivePeerDependencies: @@ -5945,15 +9006,40 @@ snapshots: - esbuild - uglify-js - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - which@2.0.2: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + optional: true + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.19.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + word-wrap@1.2.5: {} wordwrap@1.0.0: {} @@ -5970,24 +9056,19 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrap-ansi@8.1.0: + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 - string-width: 5.1.2 + string-width: 7.2.0 strip-ansi: 7.2.0 wrappy@1.0.2: {} - write-file-atomic@4.0.2: - dependencies: - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - xtend@4.0.2: {} y18n@5.0.8: {} - yallist@3.1.1: {} + yaml@2.8.3: {} yargs-parser@21.1.1: {} @@ -6001,6 +9082,8 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yn@3.1.1: {} - yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} + + zod@4.3.6: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..7d1b24d --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - '.' + - 'infra/k6' diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts deleted file mode 100644 index d22f389..0000000 --- a/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/src/app.controller.ts b/src/app.controller.ts deleted file mode 100644 index cce879e..0000000 --- a/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/src/app.module.ts b/src/app.module.ts index 8662803..a787bb1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,10 +1,105 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; +import { ConfigModule } from '@libs/config'; +import { DatabaseModule } from '@libs/database'; +import { ConfigService } from '@nestjs/config'; +import * as schema from './shared/entities'; +import { APP_FILTER, APP_PIPE } from '@nestjs/core'; +import { ZodValidationPipe } from 'nestjs-zod'; +import { PrometheusModule } from '@willsoto/nestjs-prometheus'; +import { HealthModule } from '@libs/health'; +import { UserModule } from './user'; +import { GlobalExceptionFilter } from '@shared/error'; +import { AuthModule } from './auth/auth.module'; +import { BullBoardModule } from '@bull-board/nestjs'; +import { FastifyAdapter } from '@bull-board/fastify'; +import { BullModule } from '@nestjs/bullmq'; +import { MailModule } from '@shared/adapters/mail'; +import { TeamsModule } from './teams'; +import { ProjectsModule } from './projects'; +import { HttpModule } from '@nestjs/axios'; +import { MediaModule } from '@shared/media'; +import { version } from '../package.json'; +import { CacheModule } from '@shared/adapters/cache/module'; +import { S3Service } from '@libs/s3'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; +import { DatabaseHealthcheckService } from '@libs/database/database-healthcheck.service'; @Module({ - imports: [], - controllers: [AppController], - providers: [AppService], + imports: [ + ConfigModule, + PrometheusModule.registerAsync({ + useFactory: () => ({ + path: 'dump', + defaultMetrics: { + enabled: process.env.NODE_ENV !== 'test', + }, + }), + }), + DatabaseModule.registerAsync({ + global: true, + inject: [ConfigService], + useFactory: (cfg: ConfigService) => { + return { + schema, + schemaName: cfg.getOrThrow('DB_SCHEMA'), + logging: true, + }; + }, + }), + BullModule.forRootAsync({ + inject: [ConfigService], + useFactory: (cfg: ConfigService) => ({ + connection: { + password: cfg.get('REDIS_PASSWORD'), + host: cfg.getOrThrow('REDIS_HOST'), + port: cfg.get('REDIS_PORT'), + }, + }), + }), + CacheModule, + MediaModule, + HttpModule.register({ global: true }), + MailModule, + AuthModule, + UserModule, + TeamsModule, + ProjectsModule, + BullBoardModule.forRoot({ + route: '/queues', + boardOptions: { + uiConfig: { + sortQueues: true, + pollingInterval: { forceInterval: 10, showSetting: false }, + hideRedisDetails: true, + }, + }, + adapter: FastifyAdapter, + }), + HealthModule.registerAsync({ + inject: [DatabaseHealthcheckService, S3Service, CACHE_SERVICE], + useFactory: (db: DatabaseHealthcheckService, s3: S3Service, cache: ICacheService) => { + return { + serviceName: 'gateway', + version, + indicators: { + database: async () => db.isAlive(), + cache: async () => cache.isAlive(), + storage: async () => s3.isAlive(), + }, + }; + }, + }), + ], + providers: [ + { + provide: APP_PIPE, + useClass: ZodValidationPipe, + }, + { + provide: APP_FILTER, + useClass: GlobalExceptionFilter, + }, + ], }) export class AppModule {} diff --git a/src/app.service.ts b/src/app.service.ts deleted file mode 100644 index 927d7cc..0000000 --- a/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/src/auth/application/auth.facade.ts b/src/auth/application/auth.facade.ts new file mode 100644 index 0000000..e2aac1f --- /dev/null +++ b/src/auth/application/auth.facade.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@nestjs/common'; +import { + SignInUseCase, + SignUpUseCase, + SignOutUseCase, + SignUpVerifyUseCase, + RefreshTokensUseCase, + ResetPasswordUseCase, + VerifyResetPasswordUseCase, + ConfirmResetPasswordUseCase, +} from './use-cases'; +import { + PasswordResetConfirmDto, + ResetPasswordDto, + SignInDto, + SignUpDto, + VerifyDto, + VerifyResetCodeDto, +} from './dtos'; +import type { DeviceMetadata } from '../infrastructure/utils/get-device-meta'; + +@Injectable() +export class AuthFacade { + constructor( + private readonly signInUseCase: SignInUseCase, + private readonly signUpUseCase: SignUpUseCase, + private readonly signOutUseCase: SignOutUseCase, + private readonly signUpVerifyUseCase: SignUpVerifyUseCase, + private readonly refreshTokensUseCase: RefreshTokensUseCase, + private readonly resetPasswordUseCase: ResetPasswordUseCase, + private readonly verifyResetPasswordUseCase: VerifyResetPasswordUseCase, + private readonly confirmResetPasswordUseCase: ConfirmResetPasswordUseCase, + ) {} + + async signIn(dto: SignInDto, device: DeviceMetadata) { + return this.signInUseCase.execute(dto, device); + } + + async signUp(dto: SignUpDto) { + return this.signUpUseCase.execute(dto); + } + + async verifySignUp(dto: VerifyDto, device: DeviceMetadata) { + return this.signUpVerifyUseCase.execute(dto, device); + } + + async signOut(userId: string) { + return this.signOutUseCase.execute(userId); + } + + async refreshTokens(token: string, device: DeviceMetadata) { + return this.refreshTokensUseCase.execute(token, device); + } + + async sendResetCode(dto: ResetPasswordDto) { + return this.resetPasswordUseCase.execute(dto); + } + + async verifyResetCode(dto: VerifyResetCodeDto) { + return this.verifyResetPasswordUseCase.execute(dto); + } + + async confirmNewPassword(dto: PasswordResetConfirmDto) { + return this.confirmResetPasswordUseCase.execute(dto); + } +} diff --git a/src/auth/application/controller/auth/controller.ts b/src/auth/application/controller/auth/controller.ts new file mode 100644 index 0000000..f74bc1f --- /dev/null +++ b/src/auth/application/controller/auth/controller.ts @@ -0,0 +1,108 @@ +import { Body, HttpCode, HttpStatus, Post, Req, Res, UseGuards } from '@nestjs/common'; +import { + PostLoginSwagger, + PostLogoutSwagger, + PostRefreshSwagger, + PostRegisterSwagger, + PostSignUpConfirmSwagger, +} from './swagger'; +import { SignInDto, SignUpDto, VerifyDto } from '../../dtos'; +import type { FastifyReply, FastifyRequest } from 'fastify'; +import { BearerAuthGuard, CookieAuthGuard } from '@shared/guards'; +import { AuthFacade } from '../../auth.facade'; +import { getDeviceMeta } from '@core/auth/infrastructure/utils/get-device-meta'; +import { ApiBaseController } from '@shared/decorators'; +import { ConfigService } from '@nestjs/config'; + +@ApiBaseController('auth', 'Auth') +export class AuthController { + private readonly isProduction: boolean = false; + private readonly domain: string | null = null; + + constructor( + private readonly facade: AuthFacade, + private cfg: ConfigService, + ) { + this.isProduction = this.cfg.get('NODE_ENV') === 'production'; + this.domain = this.cfg.get('DOMAIN'); + } + + @Post('sign-up') + @PostRegisterSwagger() + @HttpCode(202) + async signUp(@Body() dto: SignUpDto) { + return this.facade.signUp(dto); + } + + @Post('sign-up/confirm') + @PostSignUpConfirmSwagger() + @HttpCode(201) + async verifySignUp( + @Res({ passthrough: true }) res: FastifyReply, + @Req() req: FastifyRequest, + @Body() dto: VerifyDto, + ) { + const meta = getDeviceMeta(req); + const { tokens, expiresAt, ...response } = await this.facade.verifySignUp(dto, meta); + + this.setRefreshCookie(res, tokens.refresh, expiresAt); + + return { ...response, token: tokens.access }; + } + + @Post('sign-in') + @PostLoginSwagger() + async signIn( + @Res({ passthrough: true }) res: FastifyReply, + @Req() req: FastifyRequest, + @Body() dto: SignInDto, + ) { + const meta = getDeviceMeta(req); + const { tokens, expiresAt, ...response } = await this.facade.signIn(dto, meta); + + this.setRefreshCookie(res, tokens.refresh, expiresAt); + + return { ...response, token: tokens.access }; + } + + @Post('sign-out') + @HttpCode(HttpStatus.OK) + @UseGuards(BearerAuthGuard) + @PostLogoutSwagger() + async logout(@Res({ passthrough: true }) res: FastifyReply, @Req() req: FastifyRequest) { + const session = req.cookies?.['refresh']; + const response = await this.facade.signOut(session); + + res.clearCookie('refresh', { + path: '/', + domain: this.domain ? `.${this.domain}` : undefined, + }); + + return response; + } + + @Post('refresh') + @UseGuards(CookieAuthGuard) + @PostRefreshSwagger() + @HttpCode(200) + async refresh(@Res({ passthrough: true }) res: FastifyReply, @Req() req: FastifyRequest) { + const meta = getDeviceMeta(req); + const session = req.cookies?.['refresh']; + const { tokens, expiresAt, ...response } = await this.facade.refreshTokens(session, meta); + + this.setRefreshCookie(res, tokens.refresh, expiresAt); + + return { token: tokens.access, ...response }; + } + + private setRefreshCookie(res: FastifyReply, refreshToken: string, expires: Date) { + res.setCookie('refresh', refreshToken, { + httpOnly: true, + secure: this.isProduction, + path: '/', + expires, + sameSite: 'lax', + domain: this.domain ? `.${this.domain}` : undefined, + }); + } +} diff --git a/src/auth/application/controller/auth/swagger.ts b/src/auth/application/controller/auth/swagger.ts new file mode 100644 index 0000000..bf429b9 --- /dev/null +++ b/src/auth/application/controller/auth/swagger.ts @@ -0,0 +1,125 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { + ApiBadRequest, + ApiConflict, + ApiForbidden, + ApiNotFound, + ApiUnauthorized, + ApiValidationError, +} from '@shared/error'; +import { SignInDto, SignResponse, SignUpDto, VerifyDto } from '../../dtos'; +import { ActionResponse } from '@shared/dtos'; + +export const PostRegisterSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Регистрация нового пользователя', + description: 'Создает пользователя, базовые настройки безопасности и уведомлений.', + }), + ApiBody({ type: SignUpDto.Output }), + ApiResponse({ + status: 201, + description: 'Пользователь успешно зарегистрирован.', + type: ActionResponse.Output, + }), + ApiValidationError('Ошибка валидации данных (например, неверный формат email)'), + ApiConflict('Пользователь с таким email уже существует'), + ); + +export const PostLoginSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Вход в систему', + description: + 'Возвращает Access/Refresh токены. Если у пользователя включена 2FA, вернет временный токен.', + }), + ApiBody({ type: SignInDto.Output }), + ApiResponse({ + status: 200, + description: 'Успешный вход.', + type: SignResponse.Output, + }), + ApiBadRequest('Неверный формат email'), + ApiUnauthorized('Неверный email или пароль'), + ); + +export const PostRefreshSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Обновление токенов', + description: 'Выдает новую пару Access и Refresh токенов.', + }), + ApiResponse({ + status: 200, + description: 'Токены успешно обновлены.', + type: SignResponse.Output, + }), + ApiBadRequest('Ошибка валидации (не передан refresh токен)'), + ApiUnauthorized('Refresh токен недействителен, истек или отозван'), + ); + +export const PostLogoutSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Выход из системы', + description: 'Удаляет текущую сессию пользователя из Redis.', + }), + ApiResponse({ status: 200, description: 'Успешный выход.', type: ActionResponse.Output }), + ApiUnauthorized(), + ); + +export const PostSignUpConfirmSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Подтверждение регистрации по коду', + description: + 'Проверяет OTP из письма, создаёт аккаунт, выдаёт access-токен в теле ответа и устанавливает refresh в httpOnly cookie.', + }), + ApiBody({ type: VerifyDto.Output }), + ApiResponse({ + status: 201, + description: 'Аккаунт подтверждён, сессия создана.', + type: SignResponse.Output, + }), + ApiValidationError('Ошибка валидации (неверный формат email или длина кода)'), + ApiBadRequest('Срок регистрации истёк или сессия не найдена'), + ApiBadRequest('Неверный или истёкший код подтверждения'), + ); + +export const GetSessionsSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Получить активные сессии', + description: 'Возвращает список всех активных устройств/сессий пользователя.', + }), + ApiResponse({ + status: 200, + description: 'Список сессий успешно получен.', + schema: { + example: [ + { + id: 'clj1xyz990000abc1', + device: 'Chrome on macOS', + ip: '192.168.1.1', + lastActive: '2026-04-11T14:30:00.000Z', + isCurrent: true, + }, + ], + }, + }), + ApiUnauthorized(), + ); + +export const DeleteSessionSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Завершить чужую сессию', + description: 'Принудительно удаляет указанную сессию из Redis.', + }), + ApiParam({ name: 'cuid', description: 'ID сессии, которую нужно завершить' }), + ApiResponse({ status: 200, description: 'Сессия успешно завершена.' }), + ApiUnauthorized(), + ApiForbidden(), + ApiNotFound('Сессия не найдена или уже истекла'), + ); diff --git a/src/auth/application/controller/index.ts b/src/auth/application/controller/index.ts new file mode 100644 index 0000000..c2c6838 --- /dev/null +++ b/src/auth/application/controller/index.ts @@ -0,0 +1,2 @@ +export { AuthController } from './auth/controller'; +export { AuthRecoveryController } from './recovery/controller'; diff --git a/src/auth/application/controller/recovery/controller.ts b/src/auth/application/controller/recovery/controller.ts new file mode 100644 index 0000000..e274427 --- /dev/null +++ b/src/auth/application/controller/recovery/controller.ts @@ -0,0 +1,32 @@ +import { ApiBaseController } from '@shared/decorators'; +import { Body, Post } from '@nestjs/common'; +import { + PostPasswordResetConfirmSwagger, + PostPasswordResetSwagger, + PostPasswordResetVerifySwagger, +} from './swagger'; +import { PasswordResetConfirmDto, ResetPasswordDto, VerifyResetCodeDto } from '../../dtos'; +import { AuthFacade } from '../../auth.facade'; + +@ApiBaseController('auth', 'Auth Recovery') +export class AuthRecoveryController { + constructor(private readonly facade: AuthFacade) {} + + @Post('password/reset') + @PostPasswordResetSwagger() + async sendResetCode(@Body() dto: ResetPasswordDto) { + return this.facade.sendResetCode(dto); + } + + @Post('password/reset/verify') + @PostPasswordResetVerifySwagger() + async verifyResetCode(@Body() dto: VerifyResetCodeDto) { + return this.facade.verifyResetCode(dto); + } + + @Post('password/reset/confirm') + @PostPasswordResetConfirmSwagger() + async confirmNewPassword(@Body() dto: PasswordResetConfirmDto) { + return this.facade.confirmNewPassword(dto); + } +} diff --git a/src/auth/application/controller/recovery/swagger.ts b/src/auth/application/controller/recovery/swagger.ts new file mode 100644 index 0000000..3e10cdc --- /dev/null +++ b/src/auth/application/controller/recovery/swagger.ts @@ -0,0 +1,173 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { + ApiBadRequest, + ApiErrorResponse, + ApiForbidden, + ApiNotFound, + ApiUnauthorized, + ApiValidationError, +} from '@shared/error'; +import { + ChangePasswordDto, + Confirm2FaDto, + Disable2FaDto, + PasswordResetConfirmDto, + ResetPasswordDto, + VerifyResetCodeDto, +} from '../../dtos'; +import { ActionResponse } from '@shared/dtos'; + +export const PostPasswordResetSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Запрос кода восстановления пароля', + description: 'Отправляет одноразовый код на email, если пользователь существует.', + }), + ApiBody({ type: ResetPasswordDto.Output }), + ApiResponse({ + status: 201, + description: 'Код отправлен на почту (при успешной обработке запроса).', + type: ActionResponse.Output, + }), + ApiValidationError('Некорректный формат email'), + ApiErrorResponse( + 422, + 'INVALID_EMAIL_FORMAT', + 'Указанный email адрес имеет некорректный формат', + ), + ApiNotFound('Пользователь с таким email не найден'), + ); + +export const PostPasswordResetVerifySwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Проверка кода восстановления пароля', + description: 'Проверяет код из письма и помечает сессию сброса как подтверждённую.', + }), + ApiBody({ type: VerifyResetCodeDto.Output }), + ApiResponse({ + status: 201, + description: 'Код подтверждён, можно задать новый пароль.', + type: ActionResponse.Output, + }), + ApiValidationError('Ошибка валидации (email или формат кода)'), + ApiBadRequest('Время подтверждения истекло или запрос не найден'), + ApiBadRequest('Неверный или истёкший код подтверждения'), + ); + +export const PostPasswordResetConfirmSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Установка нового пароля после сброса', + description: 'Доступно только после успешной проверки кода на шаге verify.', + }), + ApiBody({ type: PasswordResetConfirmDto.Output }), + ApiResponse({ + status: 201, + description: 'Пароль успешно изменён.', + type: ActionResponse.Output, + }), + ApiValidationError('Ошибка валидации (пароли не совпадают или неверная длина)'), + ApiBadRequest('Сессия восстановления не найдена или истекла'), + ApiForbidden(), + ApiErrorResponse( + 500, + 'PASSWORD_UPDATE_FAILED', + 'Не удалось обновить пароль. Попробуйте позже.', + ), + ); + +export const GetSessionsSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Получить активные сессии', + description: 'Возвращает список всех активных устройств/сессий пользователя.', + }), + ApiResponse({ + status: 200, + description: 'Список сессий успешно получен.', + schema: { + example: [ + { + id: 'clj1xyz990000abc1', + device: 'Chrome on macOS', + ip: '192.168.1.1', + lastActive: '2026-04-11T14:30:00.000Z', + isCurrent: true, + }, + ], + }, + }), + ApiUnauthorized(), + ); + +export const DeleteSessionSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Завершить чужую сессию', + description: 'Принудительно удаляет указанную сессию из Redis.', + }), + ApiParam({ name: 'cuid', description: 'ID сессии, которую нужно завершить' }), + ApiResponse({ status: 200, description: 'Сессия успешно завершена.' }), + ApiUnauthorized(), + ApiForbidden(), + ApiNotFound('Сессия не найдена или уже истекла'), + ); + +export const PostChangePasswordSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Смена пароля', + description: 'Требует текущий и новый пароль. Инвалидирует все остальные сессии.', + }), + ApiBody({ type: ChangePasswordDto.Output }), + ApiResponse({ status: 200, description: 'Пароль успешно изменен.' }), + ApiBadRequest('Неверный старый пароль'), + ApiUnauthorized(), + ); + +export const PostEnable2faSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Генерация QR-кода для 2FA', + description: 'Создает секрет и возвращает ссылку (otpauth) для Google Authenticator.', + }), + ApiResponse({ + status: 200, + description: 'QR-код сгенерирован.', + schema: { + example: { + secret: 'JBSWY3DPEHPK3PXP', + qrCodeUrl: + 'otpauth://totp/TaskTracker:alexey?secret=JBSWY3DPEHPK3PXP&issuer=TaskTracker', + }, + }, + }), + ApiUnauthorized(), + ); + +export const PostDisable2faSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Подтверждение включения 2FA', + description: 'Проверяет первый код из приложения для окончательной активации 2FA.', + }), + ApiBody({ type: Confirm2FaDto.Output }), + ApiResponse({ status: 200, description: 'Двухфакторная аутентификация успешно включена.' }), + ApiBadRequest('Неверный код подтверждения'), + ApiUnauthorized(), + ); + +export const PostConfirm2faSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Отключение 2FA', + description: + 'Отключает двухфакторную аутентификацию (требует подтверждения паролем или текущим кодом).', + }), + ApiBody({ type: Disable2FaDto.Output }), + ApiResponse({ status: 200, description: '2FA успешно отключена.' }), + ApiBadRequest('Неверный код или пароль для отключения'), + ApiUnauthorized(), + ); diff --git a/src/auth/application/dtos/2fa.dto.ts b/src/auth/application/dtos/2fa.dto.ts new file mode 100644 index 0000000..8d10068 --- /dev/null +++ b/src/auth/application/dtos/2fa.dto.ts @@ -0,0 +1,25 @@ +import z from 'zod/v4'; +import { createZodDto } from 'nestjs-zod'; + +export const Confirm2FaSchema = z + .object({ + code: z + .string() + .length(6, 'Код должен состоять ровно из 6 символов') + .describe('6-значный код из Google Authenticator'), + }) + .describe('Схема подтверждения 2FA'); + +export class Confirm2FaDto extends createZodDto(Confirm2FaSchema) {} + +export const Disable2FaSchema = z + .object({ + password: z.string().optional().describe('Текущий пароль для подтверждения (опционально)'), + code: z.string().optional().describe('Код из приложения (опционально)'), + }) + .refine((data) => data.password || data.code, { + message: 'Нужно передать либо пароль, либо код', + }) + .describe('Схема отключения 2FA'); + +export class Disable2FaDto extends createZodDto(Disable2FaSchema) {} diff --git a/src/auth/application/dtos/auth.dto.ts b/src/auth/application/dtos/auth.dto.ts new file mode 100644 index 0000000..0f3059a --- /dev/null +++ b/src/auth/application/dtos/auth.dto.ts @@ -0,0 +1,65 @@ +import { ActionResponseSchema } from '@shared/dtos'; +import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; + +export const SignInSchema = z + .object({ + email: z.email('Некорректный формат email').describe('Email пользователя'), + password: z.string().describe('Пароль пользователя'), + }) + .describe('Схема входа в систему'); + +export class SignInDto extends createZodDto(SignInSchema) {} + +export const SignUpSchema = z + .object({ + email: z.email('Некорректный формат email').describe('Email пользователя'), + password: z + .string() + .min(8, 'Пароль должен содержать минимум 8 символов') + .max(32, 'Пароль должен содержать максимум 32 символа') + .describe('Пароль (минимум 8 символов)'), + firstName: z + .string() + .min(2, 'Имя должно содержать минимум 2 символа') + .max(50) + .trim() + .describe('Имя'), + lastName: z + .string() + .min(2, 'Фамилия должна содержать минимум 2 символа') + .max(50) + .trim() + .describe('Фамилия'), + middleName: z + .string() + .max(50) + .trim() + .optional() + .or(z.literal('')) + .describe('Отчество (опционально)'), + }) + .describe('Схема регистрации пользователя'); + +export class SignUpDto extends createZodDto(SignUpSchema) {} + +export const VerifySchema = z + .object({ + email: z + .string() + .email('Некорректный формат email') + .describe('Email пользователя, на который был отправлен код'), + code: z + .string() + .length(6, 'Код должен содержать ровно 6 символов') + .describe('6-значный OTP код подтверждения'), + }) + .describe('Схема верификации OTP кода'); + +export class VerifyDto extends createZodDto(VerifySchema) {} + +export const SignResponseSchema = ActionResponseSchema.extend({ + token: z.string().describe('JWT токен доступа пользователя'), +}); + +export class SignResponse extends createZodDto(SignResponseSchema) {} diff --git a/src/auth/application/dtos/index.ts b/src/auth/application/dtos/index.ts new file mode 100644 index 0000000..6a0829f --- /dev/null +++ b/src/auth/application/dtos/index.ts @@ -0,0 +1,3 @@ +export * from './auth.dto'; +export * from './2fa.dto'; +export * from './password.dto'; diff --git a/src/auth/application/dtos/password.dto.ts b/src/auth/application/dtos/password.dto.ts new file mode 100644 index 0000000..e0a260f --- /dev/null +++ b/src/auth/application/dtos/password.dto.ts @@ -0,0 +1,45 @@ +import { createZodDto } from 'nestjs-zod'; +import z from 'zod/v4'; + +export const ChangePasswordSchema = z + .object({ + oldPassword: z.string().describe('Текущий пароль'), + newPassword: z + .string() + .min(8, 'Новый пароль должен содержать минимум 8 символов') + .max(32, 'Новый пароль должен содержать максимум 32 символа') + .describe('Новый пароль (минимум 8 символов)'), + }) + .describe('Схема смены пароля'); + +export class ChangePasswordDto extends createZodDto(ChangePasswordSchema) {} + +export const ResetPasswordSchema = z.object({ + email: z.string().email('Некорректный формат email').describe('Email для восстановления'), +}); + +export class ResetPasswordDto extends createZodDto(ResetPasswordSchema) {} + +export const VerifyResetCodeSchema = z.object({ + email: z.string().email(), + code: z.string().length(6, 'Код должен содержать 6 цифр').describe('Код из письма'), +}); + +export class VerifyResetCodeDto extends createZodDto(VerifyResetCodeSchema) {} + +export const PasswordResetConfirmSchema = z + .object({ + email: z.string().email(), + password: z + .string() + .min(8, 'Минимум 8 символов') + .max(32, 'Максимум 32 символа') + .describe('Новый пароль'), + confirmPassword: z.string().describe('Повторите новый пароль'), + }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Пароли не совпадают', + path: ['confirmPassword'], + }); + +export class PasswordResetConfirmDto extends createZodDto(PasswordResetConfirmSchema) {} diff --git a/src/auth/application/use-cases/confirm-reset-password.use-case.ts b/src/auth/application/use-cases/confirm-reset-password.use-case.ts new file mode 100644 index 0000000..5545b79 --- /dev/null +++ b/src/auth/application/use-cases/confirm-reset-password.use-case.ts @@ -0,0 +1,65 @@ +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import * as argon from 'argon2'; +import { BaseException } from '@shared/error'; +import { PasswordResetConfirmDto } from '../dtos'; +import { UpdatePasswordUseCase } from '@core/user'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; + +@Injectable() +export class ConfirmResetPasswordUseCase { + constructor( + @Inject(CACHE_SERVICE) + private readonly cacheService: ICacheService, + private readonly updatePasswordUserUseCase: UpdatePasswordUseCase, + ) {} + + async execute(dto: PasswordResetConfirmDto) { + const redisKey = `pass:reset:${dto.email}`; + const cachedData = await this.cacheService.getOne(redisKey); + + if (!cachedData) { + throw new BaseException( + { + code: 'RESET_SESSION_NOT_FOUND', + message: + 'Сессия восстановления не найдена или истекла. Начните процесс заново.', + }, + HttpStatus.BAD_REQUEST, + ); + } + + const resetSession = JSON.parse(cachedData); + + if (!resetSession.isVerified) { + throw new BaseException( + { + code: 'CODE_NOT_VERIFIED', + message: 'Код подтверждения еще не был верифицирован.', + details: [{ target: 'isVerified', value: false }], + }, + HttpStatus.FORBIDDEN, + ); + } + + const hashed = await argon.hash(dto.password); + const isUpdated = await this.updatePasswordUserUseCase.execute(dto.email, hashed); + + if (!isUpdated) { + throw new BaseException( + { + code: 'PASSWORD_UPDATE_FAILED', + message: 'Не удалось обновить пароль. Попробуйте позже.', + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + await this.cacheService.removeOne(redisKey); + + return { + success: true, + message: 'Пароль успешно изменен. Теперь вы можете войти в аккаунт.', + }; + } +} diff --git a/src/auth/application/use-cases/index.ts b/src/auth/application/use-cases/index.ts new file mode 100644 index 0000000..450edfb --- /dev/null +++ b/src/auth/application/use-cases/index.ts @@ -0,0 +1,30 @@ +import { ConfirmResetPasswordUseCase } from './confirm-reset-password.use-case'; +import { VerifyResetPasswordUseCase } from './verify-reset-password.use-case'; +import { RefreshTokensUseCase } from './refresh-tokens.use-case'; +import { ResetPasswordUseCase } from './reset-password.use-case'; +import { SignUpVerifyUseCase } from './sign-up-verify.use-case'; +import { SignInUseCase } from './sign-in.use-case'; +import { SignOutUseCase } from './sign-out.use-case'; +import { SignUpUseCase } from './sign-up.use-case'; + +export { + ConfirmResetPasswordUseCase, + VerifyResetPasswordUseCase, + RefreshTokensUseCase, + ResetPasswordUseCase, + SignUpVerifyUseCase, + SignInUseCase, + SignOutUseCase, + SignUpUseCase, +}; + +export const AuthUseCases = [ + ConfirmResetPasswordUseCase, + VerifyResetPasswordUseCase, + RefreshTokensUseCase, + ResetPasswordUseCase, + SignUpVerifyUseCase, + SignInUseCase, + SignOutUseCase, + SignUpUseCase, +]; diff --git a/src/auth/application/use-cases/refresh-tokens.use-case.ts b/src/auth/application/use-cases/refresh-tokens.use-case.ts new file mode 100644 index 0000000..4e3ad3a --- /dev/null +++ b/src/auth/application/use-cases/refresh-tokens.use-case.ts @@ -0,0 +1,78 @@ +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { BaseException } from '@shared/error'; +import { ISessionRepository } from '../../domain/repository'; +import { TokenService } from '../../infrastructure/security'; +import { DeviceMetadata } from '../../infrastructure/utils/get-device-meta'; +import { FindUserQuery } from '@core/user'; +import { createId } from '@paralleldrive/cuid2'; + +@Injectable() +export class RefreshTokensUseCase { + constructor( + @Inject('ISessionRepository') + private readonly sessionRepo: ISessionRepository, + private readonly tokenService: TokenService, + private readonly findUserQuery: FindUserQuery, + ) {} + + async execute(token: string, metadata: DeviceMetadata) { + const payload = await this.tokenService.validateToken(token, 'refresh'); + + if (!payload?.jti) { + throw new BaseException( + { + code: 'INVALID_TOKEN', + message: 'Сессия недействительна или истекла', + }, + HttpStatus.UNAUTHORIZED, + ); + } + + const session = await this.sessionRepo.findById(payload.jti); + + if (!session || session?.isRevoked) { + throw new BaseException( + { + code: 'SESSION_REVOKED', + message: 'Ваша сессия была отозвана или завершена', + }, + HttpStatus.UNAUTHORIZED, + ); + } + + const entity = await this.findUserQuery.execute({ id: session.userId }); + + if (!entity?.user) { + await this.sessionRepo.revoke(session.id); + throw new BaseException( + { + code: 'USER_NOT_FOUND', + message: 'Аккаунт пользователя не найден', + }, + HttpStatus.UNAUTHORIZED, + ); + } + + await this.sessionRepo.revoke(session.id); + + const sessionId = createId(); + const { access, refresh, expiresAt } = await this.tokenService.generateTokens( + entity.user, + sessionId, + ); + + await this.sessionRepo.create({ + id: sessionId, + userId: entity.user.id, + ...metadata, + expiresAt, + }); + + return { + tokens: { access, refresh }, + success: true, + expiresAt, + message: 'Токены успешно обновлены', + }; + } +} diff --git a/src/auth/application/use-cases/reset-password.use-case.ts b/src/auth/application/use-cases/reset-password.use-case.ts new file mode 100644 index 0000000..b265a05 --- /dev/null +++ b/src/auth/application/use-cases/reset-password.use-case.ts @@ -0,0 +1,87 @@ +import { InjectQueue } from '@nestjs/bullmq'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { Queue } from 'bullmq'; +import { generate, generateSecret } from 'otplib'; +import { BaseException } from '@shared/error'; +import { AuthMailJobs, AuthQueues } from '../../domain/enums'; +import { ResetPasswordEvent } from '../../domain/events'; +import { ResetPasswordDto } from '../dtos'; +import { FindUserQuery } from '@core/user'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; + +@Injectable() +export class ResetPasswordUseCase { + constructor( + @Inject(CACHE_SERVICE) + private readonly cacheService: ICacheService, + @InjectQueue(AuthQueues.AUTH_MAIL) + private readonly mailQueue: Queue, + private readonly findUserQuery: FindUserQuery, + ) {} + + async execute(dto: ResetPasswordDto) { + const redisKey = `pass:reset:${dto.email}`; + const isExistsAttempt = await this.cacheService.getOne(redisKey); + + if (isExistsAttempt) { + throw new BaseException( + { + code: 'PASS_RESET_ATTEMPT_ACTIVE', + message: + 'Запрос на сброс пароля уже активен. Проверьте почту или попробуйте позже.', + details: [ + { + target: 'email', + value: dto.email, + }, + ], + }, + HttpStatus.CONFLICT, + ); + } + + const entity = await this.findUserQuery.execute({ email: dto.email }); + + if (!entity?.user) { + throw new BaseException( + { + code: 'USER_NOT_FOUND', + message: 'Пользователь с таким email не найден', + details: [{ target: 'email', value: dto.email }], + }, + HttpStatus.NOT_FOUND, + ); + } + + const secret = generateSecret(); + const token = await generate({ + secret, + digits: 6, + period: 900, + strategy: 'totp', + }); + + const resetPayload = { + email: entity.user.email, + otp: { secret, token }, + isVerified: false, + }; + + await this.cacheService.setOne(redisKey, JSON.stringify(resetPayload), 900); + + const event = new ResetPasswordEvent(dto.email, token); + await this.mailQueue.add(AuthMailJobs.SEND_RESET_PASSWORD, event, { + attempts: 3, + backoff: { + type: 'exponential', + delay: 5000, + }, + }); + + return { + success: true, + message: 'Код для восстановления пароля отправлен на вашу почту', + }; + } +} diff --git a/src/auth/application/use-cases/sign-in.use-case.ts b/src/auth/application/use-cases/sign-in.use-case.ts new file mode 100644 index 0000000..2c53dad --- /dev/null +++ b/src/auth/application/use-cases/sign-in.use-case.ts @@ -0,0 +1,68 @@ +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import * as argon from 'argon2'; +import { BaseException } from '@shared/error'; +import { ISessionRepository } from '../../domain/repository'; +import { TokenService } from '../../infrastructure/security'; +import { DeviceMetadata } from '../../infrastructure/utils/get-device-meta'; +import { SignInDto } from '../dtos'; +import { FindUserQuery } from '@core/user'; +import { createId } from '@paralleldrive/cuid2'; + +@Injectable() +export class SignInUseCase { + constructor( + @Inject('ISessionRepository') + private readonly sessionRepo: ISessionRepository, + private readonly tokenService: TokenService, + private readonly findUserQuery: FindUserQuery, + ) {} + + async execute(dto: SignInDto, meta: DeviceMetadata) { + const entities = await this.findUserQuery.execute({ email: dto.email }); + + if (!entities?.user || !entities?.security) { + throw new BaseException( + { + code: 'INVALID_CREDENTIALS', + message: 'Неверный email или пароль', + }, + HttpStatus.UNAUTHORIZED, + ); + } + + const { security, user } = entities; + const isPasswordValid = await argon.verify(security.passwordHash, dto.password); + + if (!isPasswordValid) { + throw new BaseException( + { + code: 'INVALID_CREDENTIALS', + message: 'Неверный email или пароль', + }, + HttpStatus.UNAUTHORIZED, + ); + } + const sessionId = createId(); + const { access, refresh, expiresAt } = await this.tokenService.generateTokens( + user, + sessionId, + ); + + await this.sessionRepo.create({ + id: sessionId, + userId: user.id, + expiresAt, + ...meta, + }); + + return { + success: true, + tokens: { + access, + refresh, + }, + expiresAt, + message: 'Вы успешно вошли в систему', + }; + } +} diff --git a/src/auth/application/use-cases/sign-out.use-case.ts b/src/auth/application/use-cases/sign-out.use-case.ts new file mode 100644 index 0000000..23d85fc --- /dev/null +++ b/src/auth/application/use-cases/sign-out.use-case.ts @@ -0,0 +1,46 @@ +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { BaseException } from '@shared/error'; +import { ISessionRepository } from '../../domain/repository'; +import { TokenService } from '../../infrastructure/security'; + +@Injectable() +export class SignOutUseCase { + constructor( + @Inject('ISessionRepository') + private readonly sessionRepo: ISessionRepository, + private readonly tokenService: TokenService, + ) {} + + async execute(token: string) { + const payload = await this.tokenService.validateToken(token, 'refresh'); + + if (!payload?.jti) { + throw new BaseException( + { + code: 'SESSION_EXPIRED', + message: 'Сессия уже истекла', + }, + HttpStatus.UNAUTHORIZED, + ); + } + + const session = await this.sessionRepo.findById(payload.jti); + + if (session) { + const isRevoked = await this.sessionRepo.revoke(session.id); + + if (!isRevoked) { + throw new BaseException( + { + code: 'SIGNOUT_FAILED', + message: 'Не удалось завершить сессию на сервере. Попробуйте позже.', + details: [{ target: 'database', message: 'Session revocation failed' }], + }, + HttpStatus.SERVICE_UNAVAILABLE, + ); + } + } + + return { success: true, message: 'Успешно вышли из системы!' }; + } +} diff --git a/src/auth/application/use-cases/sign-up-verify.use-case.ts b/src/auth/application/use-cases/sign-up-verify.use-case.ts new file mode 100644 index 0000000..2d1ad52 --- /dev/null +++ b/src/auth/application/use-cases/sign-up-verify.use-case.ts @@ -0,0 +1,98 @@ +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { verify as verifyOTP } from 'otplib'; +import { RegisterUserUseCase } from '@core/user'; +import { BaseException } from '@shared/error'; +import { ISessionRepository } from '../../domain/repository'; +import { TokenService } from '../../infrastructure/security'; +import { DeviceMetadata } from '../../infrastructure/utils/get-device-meta'; +import { VerifyDto } from '../dtos'; +import { createId } from '@paralleldrive/cuid2'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; + +@Injectable() +export class SignUpVerifyUseCase { + constructor( + @Inject(CACHE_SERVICE) + private readonly cacheService: ICacheService, + @Inject('ISessionRepository') + private readonly sessionRepo: ISessionRepository, + private readonly tokenService: TokenService, + private readonly registerUserUseCase: RegisterUserUseCase, + ) {} + + async execute(dto: VerifyDto, meta: DeviceMetadata) { + const redisKey = `reg:${dto.email}`; + const cachedData = await this.cacheService.getOne(redisKey); + + if (!cachedData) { + throw new BaseException( + { + code: 'REGISTRATION_EXPIRED', + message: 'Срок регистрации истек или email не найден. Попробуйте снова.', + }, + HttpStatus.GONE, + ); + } + + const userData = JSON.parse(cachedData); + + if (!userData) { + throw new BaseException( + { + code: 'INTERNAL_DATA_CORRUPTION', + message: 'Ошибка целостности данных. Попробуйте начать регистрацию заново.', + }, + HttpStatus.UNPROCESSABLE_ENTITY, + ); + } + + const verifyResult = await verifyOTP({ + token: dto.code, + secret: userData.otp.secret, + algorithm: 'sha256', + digits: 6, + period: 900, + strategy: 'totp', + afterTimeStep: 1, + }); + + if (!verifyResult.valid) { + throw new BaseException( + { + code: 'INVALID_OTP', + message: 'Неверный или истекший код подтверждения', + details: [{ target: 'code', message: 'OTP code is invalid or expired' }], + }, + HttpStatus.BAD_REQUEST, + ); + } + + const user = await this.registerUserUseCase.execute({ + ...userData.user, + password: userData.password, + }); + + const sessionId = createId(); + const { access, refresh, expiresAt } = await this.tokenService.generateTokens( + user, + sessionId, + ); + + await this.sessionRepo.create({ + id: sessionId, + userId: user.id, + ...meta, + expiresAt, + }); + + await this.cacheService.removeOne(redisKey); + + return { + success: true, + tokens: { access, refresh }, + expiresAt, + message: 'Аккаунт успешно подтвержден', + }; + } +} diff --git a/src/auth/application/use-cases/sign-up.use-case.ts b/src/auth/application/use-cases/sign-up.use-case.ts new file mode 100644 index 0000000..ccc4d03 --- /dev/null +++ b/src/auth/application/use-cases/sign-up.use-case.ts @@ -0,0 +1,85 @@ +import { InjectQueue } from '@nestjs/bullmq'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import * as argon from 'argon2'; +import { Queue } from 'bullmq'; +import { generate, generateSecret } from 'otplib'; +import { FindUserQuery } from '@core/user'; +import { BaseException } from '@shared/error'; +import { AuthQueues, AuthMailJobs } from '../../domain/enums'; +import { RegisterCodeEvent } from '../../domain/events'; +import { SignUpDto } from '../dtos'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; + +@Injectable() +export class SignUpUseCase { + constructor( + @Inject(CACHE_SERVICE) + private readonly cacheService: ICacheService, + @InjectQueue(AuthQueues.AUTH_MAIL) + private readonly mailQueue: Queue, + private readonly findUserQuery: FindUserQuery, + ) {} + + async execute(dto: SignUpDto) { + const redisKey = `reg:${dto.email}`; + const cachedData = await this.cacheService.getOne(redisKey); + + if (cachedData) { + throw new BaseException( + { + code: 'REGISTRATION_IN_PROGRESS', + message: 'Код уже был отправлен. Проверьте почту или подождите 15 минут.', + details: [{ target: 'email', message: 'Verification code already sent' }], + }, + HttpStatus.BAD_REQUEST, + ); + } + + const isExists = await this.findUserQuery.execute({ email: dto.email }); + + if (isExists) { + throw new BaseException( + { + code: 'USER_ALREADY_EXISTS', + message: 'Email уже занят другим аккаунтом', + details: [{ target: 'email', value: dto.email }], + }, + HttpStatus.CONFLICT, + ); + } + + const hashPass = await argon.hash(dto.password); + + const secret = generateSecret(); + const token = await generate({ + secret, + algorithm: 'sha256', + digits: 6, + period: 900, + strategy: 'totp', + }); + + const data = { + user: dto, + password: hashPass, + otp: { token, secret }, + }; + + await this.cacheService.setOne(`reg:${dto.email}`, JSON.stringify(data), 900); + + const event = new RegisterCodeEvent(dto.email, dto.firstName, token); + await this.mailQueue.add(AuthMailJobs.SEND_REGISTER_CODE, event, { + attempts: 3, + backoff: { + type: 'exponential', + delay: 5000, + }, + }); + + return { + success: true, + message: 'Код подтверждения отправлен на вашу почту', + }; + } +} diff --git a/src/auth/application/use-cases/verify-reset-password.use-case.ts b/src/auth/application/use-cases/verify-reset-password.use-case.ts new file mode 100644 index 0000000..a17a7e6 --- /dev/null +++ b/src/auth/application/use-cases/verify-reset-password.use-case.ts @@ -0,0 +1,62 @@ +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { verify as verifyOTP } from 'otplib'; +import { BaseException } from '@shared/error'; +import { VerifyResetCodeDto } from '../dtos'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; + +@Injectable() +export class VerifyResetPasswordUseCase { + constructor( + @Inject(CACHE_SERVICE) + private readonly cacheService: ICacheService, + ) {} + + async execute(dto: VerifyResetCodeDto) { + const redisKey = `pass:reset:${dto.email}`; + const cachedData = await this.cacheService.getOne(redisKey); + + if (!cachedData) { + throw new BaseException( + { + code: 'RESET_SESSION_EXPIRED', + message: + 'Время подтверждения истекло или запрос не найден. Запросите код снова.', + }, + HttpStatus.GONE, + ); + } + + const resetSession = JSON.parse(cachedData); + + const verifyResult = await verifyOTP({ + token: dto.code, + secret: resetSession.otp.secret, + digits: 6, + period: 900, + strategy: 'totp', + }); + + if (!verifyResult.valid) { + throw new BaseException( + { + code: 'INVALID_VERIFICATION_CODE', + message: 'Неверный или истекший код подтверждения', + details: [{ target: 'code', message: 'The provided OTP is incorrect' }], + }, + HttpStatus.BAD_REQUEST, + ); + } + + await this.cacheService.setOne( + redisKey, + JSON.stringify({ ...resetSession, isVerified: true }), + 600, + ); + + return { + success: true, + message: 'Код успешно подтвержден. Теперь вы можете установить новый пароль.', + }; + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..3bf25b6 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,73 @@ +import { BullBoardModule } from '@bull-board/nestjs'; +import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; +import { BullModule } from '@nestjs/bullmq'; +import { Module, forwardRef } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; +import { UserModule } from '@core/user'; +import { AuthController, AuthRecoveryController } from './application/controller'; +import { AuthFacade } from './application/auth.facade'; +import { AuthUseCases } from './application/use-cases'; +import { AuthQueues } from './domain/enums'; +import { SessionRepository } from './infrastructure/persistence/repositories'; +import { TokenService } from './infrastructure/security'; +import { BearerStrategy, CookieStrategy } from './infrastructure/strategies'; +import { MailProcessor } from './infrastructure/workers'; +import { MailAdapter } from '@shared/adapters/mail'; + +const WORKERS = [MailProcessor]; + +const REPOSITORY = { + provide: 'ISessionRepository', + useClass: SessionRepository, +}; + +@Module({ + imports: [ + JwtModule.registerAsync({ + inject: [ConfigService], + useFactory: async (cfg: ConfigService) => ({ + secret: cfg.get('JWT_ACCESS_SECRET'), + signOptions: { + /** + * Использование 'any' здесь необходимо, так как Zod гарантирует + * формат строки (напр. '15m', '30d') через regex в ConfigSchema, но внутренний тип + * 'StringValue' из библиотеки 'ms' слишком строг для обычного string. + */ + expiresIn: cfg.get('JWT_ACCESS_EXPIRES_IN'), + algorithm: 'HS256', + }, + verifyOptions: { + algorithms: ['HS256'], + ignoreExpiration: false, + clockTolerance: 10, + }, + }), + }), + BullModule.registerQueue({ + name: AuthQueues.AUTH_MAIL, + }), + BullBoardModule.forFeature({ + name: AuthQueues.AUTH_MAIL, + adapter: BullMQAdapter, + }), + forwardRef(() => UserModule), + ], + controllers: [AuthController, AuthRecoveryController], + providers: [ + // TOOD: FIX PROVIDER + { + provide: 'IMailPort', + useClass: MailAdapter, + }, + ...WORKERS, + TokenService, + CookieStrategy, + ...AuthUseCases, + BearerStrategy, + REPOSITORY, + AuthFacade, + ], + exports: [], +}) +export class AuthModule {} diff --git a/src/auth/domain/domain/.gitkeep b/src/auth/domain/domain/.gitkeep new file mode 100644 index 0000000..42e6429 --- /dev/null +++ b/src/auth/domain/domain/.gitkeep @@ -0,0 +1 @@ +# feature added entity class \ No newline at end of file diff --git a/src/auth/domain/enums/index.ts b/src/auth/domain/enums/index.ts new file mode 100644 index 0000000..a2f814f --- /dev/null +++ b/src/auth/domain/enums/index.ts @@ -0,0 +1 @@ +export { AuthMailJobs, AuthQueues } from './mail-jobs.enum'; diff --git a/src/auth/domain/enums/mail-jobs.enum.ts b/src/auth/domain/enums/mail-jobs.enum.ts new file mode 100644 index 0000000..e9ff5ab --- /dev/null +++ b/src/auth/domain/enums/mail-jobs.enum.ts @@ -0,0 +1,9 @@ +export enum AuthQueues { + AUTH_MAIL = 'AUTH_MAIL_QUEUE', +} + +export enum AuthMailJobs { + SEND_REGISTER_CODE = 'AUTH_SEND_REGISTER_CODE', + SEND_RESET_PASSWORD = 'AUTH_SEND_RESET_PASSWORD', + SEND_CHANGE_EMAIL = 'AUTH_SEND_CHANGE_EMAIL', +} diff --git a/src/auth/domain/events/index.ts b/src/auth/domain/events/index.ts new file mode 100644 index 0000000..61a6360 --- /dev/null +++ b/src/auth/domain/events/index.ts @@ -0,0 +1,2 @@ +export { RegisterCodeEvent } from './register-code.event'; +export { ResetPasswordEvent } from './reset-password.event'; diff --git a/src/auth/domain/events/register-code.event.ts b/src/auth/domain/events/register-code.event.ts new file mode 100644 index 0000000..df87ca8 --- /dev/null +++ b/src/auth/domain/events/register-code.event.ts @@ -0,0 +1,7 @@ +export class RegisterCodeEvent { + constructor( + public email: string, + public name: string, + public otp: string, + ) {} +} diff --git a/src/auth/domain/events/reset-password.event.ts b/src/auth/domain/events/reset-password.event.ts new file mode 100644 index 0000000..1f50e09 --- /dev/null +++ b/src/auth/domain/events/reset-password.event.ts @@ -0,0 +1,6 @@ +export class ResetPasswordEvent { + constructor( + public email: string, + public otp: string, + ) {} +} diff --git a/src/auth/domain/repository/index.ts b/src/auth/domain/repository/index.ts new file mode 100644 index 0000000..298c188 --- /dev/null +++ b/src/auth/domain/repository/index.ts @@ -0,0 +1 @@ +export * from './session.repository.interface'; diff --git a/src/auth/domain/repository/session.repository.interface.ts b/src/auth/domain/repository/session.repository.interface.ts new file mode 100644 index 0000000..e83a682 --- /dev/null +++ b/src/auth/domain/repository/session.repository.interface.ts @@ -0,0 +1,13 @@ +import { sessions } from '../../infrastructure/persistence/models/session.model'; + +export type SessionInsert = typeof sessions.$inferInsert; +export type SessionSelect = typeof sessions.$inferSelect; + +export interface ISessionRepository { + create(data: SessionInsert): Promise; + findById(id: string): Promise; + findAllByUserId(userId: string): Promise; + revoke(id: string): Promise; + revokeAllByUserId(userId: string, exceptSessionId?: string): Promise; + deleteExpired(): Promise; +} diff --git a/src/auth/infrastructure/persistence/models/index.ts b/src/auth/infrastructure/persistence/models/index.ts new file mode 100644 index 0000000..9b52ede --- /dev/null +++ b/src/auth/infrastructure/persistence/models/index.ts @@ -0,0 +1 @@ +export { sessions } from './session.model'; diff --git a/src/auth/infrastructure/persistence/models/session.model.ts b/src/auth/infrastructure/persistence/models/session.model.ts new file mode 100644 index 0000000..db788ae --- /dev/null +++ b/src/auth/infrastructure/persistence/models/session.model.ts @@ -0,0 +1,24 @@ +import { createId } from '@paralleldrive/cuid2'; +import { text, timestamp, varchar } from 'drizzle-orm/pg-core'; +import { boolean } from 'drizzle-orm/pg-core'; +import { baseSchema, users } from '@shared/entities'; + +export const sessions = baseSchema.table('sessions', { + id: text('id') + .primaryKey() + .$defaultFn(() => createId()), + userId: text('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + deviceType: varchar('device_type', { length: 20 }).$type<'mobile' | 'desktop' | 'tablet'>(), + browser: varchar('browser', { length: 50 }), + os: varchar('os', { length: 50 }), + userAgent: text('user_agent').notNull(), + ip: varchar('ip', { length: 45 }).notNull(), + city: varchar('city', { length: 100 }), + countryCode: varchar('country_code', { length: 5 }), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + isRevoked: boolean('is_revoked').default(false).notNull(), +}); diff --git a/src/auth/infrastructure/persistence/repositories/index.ts b/src/auth/infrastructure/persistence/repositories/index.ts new file mode 100644 index 0000000..d223cdb --- /dev/null +++ b/src/auth/infrastructure/persistence/repositories/index.ts @@ -0,0 +1 @@ +export { SessionRepository } from './session.repository'; diff --git a/src/auth/infrastructure/persistence/repositories/session.repository.ts b/src/auth/infrastructure/persistence/repositories/session.repository.ts new file mode 100644 index 0000000..8e38f4a --- /dev/null +++ b/src/auth/infrastructure/persistence/repositories/session.repository.ts @@ -0,0 +1,66 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { eq, and, ne, lt, desc } from 'drizzle-orm'; +import * as schema from '../models'; +import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; +import { ISessionRepository, type SessionInsert } from '../../../domain/repository'; + +@Injectable() +export class SessionRepository implements ISessionRepository { + constructor( + @Inject(DATABASE_SERVICE) + private readonly db: DatabaseService, + ) {} + + async create(data: SessionInsert) { + const [result] = await this.db.insert(schema.sessions).values(data).returning(); + return result; + } + + async findById(id: string) { + const [result] = await this.db + .select() + .from(schema.sessions) + .where(and(eq(schema.sessions.id, id), eq(schema.sessions.isRevoked, false))) + .limit(1); + + return result || null; + } + + async findAllByUserId(userId: string) { + return this.db + .select() + .from(schema.sessions) + .where(and(eq(schema.sessions.userId, userId), eq(schema.sessions.isRevoked, false))) + .orderBy(desc(schema.sessions.createdAt)); + } + + async revoke(id: string) { + const result = await this.db + .update(schema.sessions) + .set({ isRevoked: true, updatedAt: new Date() }) + .where(eq(schema.sessions.id, id)); + + return (result?.count ?? 0) > 0; + } + + async revokeAllByUserId(userId: string, exceptSessionId?: string) { + const filters = [eq(schema.sessions.userId, userId)]; + + if (exceptSessionId) { + filters.push(ne(schema.sessions.id, exceptSessionId)); + } + + await this.db + .update(schema.sessions) + .set({ isRevoked: true, updatedAt: new Date() }) + .where(and(...filters)); + } + + async deleteExpired() { + const result = await this.db + .delete(schema.sessions) + .where(lt(schema.sessions.expiresAt, new Date())); + + return result?.count ?? 0; + } +} diff --git a/src/auth/infrastructure/security/index.ts b/src/auth/infrastructure/security/index.ts new file mode 100644 index 0000000..0b27e01 --- /dev/null +++ b/src/auth/infrastructure/security/index.ts @@ -0,0 +1 @@ +export { TokenService } from './token.service'; diff --git a/src/auth/infrastructure/security/token.service.ts b/src/auth/infrastructure/security/token.service.ts new file mode 100644 index 0000000..78ae7c0 --- /dev/null +++ b/src/auth/infrastructure/security/token.service.ts @@ -0,0 +1,57 @@ +import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; +import type { JwtPayload } from '@shared/types'; +import type { User } from '@core/user'; + +@Injectable() +export class TokenService { + constructor( + private readonly jwtService: JwtService, + private readonly cfg: ConfigService, + ) {} + + async generateTokens(user: User, sessionId: string) { + const domain = this.cfg.get('DOMAIN'); + const audConstraint = this.cfg.getOrThrow('JWT_AUDIENCE'); + + const payload = { + jti: sessionId, + sub: user.id, + email: user.email, + iss: btoa(domain), + aud: btoa(audConstraint), + }; + + const accessExp = this.cfg.get('JWT_ACCESS_EXPIRES_IN'); + const refreshExp = this.cfg.get('JWT_REFRESH_EXPIRES_IN'); + + const [access, refresh] = await Promise.all([ + this.jwtService.signAsync(payload, { + secret: this.cfg.get('JWT_ACCESS_SECRET'), + expiresIn: accessExp, + }), + this.jwtService.signAsync(payload, { + secret: this.cfg.get('JWT_REFRESH_SECRET'), + expiresIn: refreshExp, + }), + ]); + + const refreshDecodedData = this.jwtService.decode(refresh); + + return { access, refresh, expiresAt: new Date(refreshDecodedData?.exp * 1000) }; + } + + async validateToken(token: string, type: 'access' | 'refresh'): Promise { + try { + const accessSecret = this.cfg.get('JWT_ACCESS_SECRET'); + const refreshSecret = this.cfg.get('JWT_REFRESH_SECRET'); + + const secret = type === 'access' ? accessSecret : refreshSecret; + + return this.jwtService.verifyAsync(token, { secret }); + } catch (e) { + return null; + } + } +} diff --git a/src/auth/infrastructure/strategies/bearer.strategy.ts b/src/auth/infrastructure/strategies/bearer.strategy.ts new file mode 100644 index 0000000..c14ed7d --- /dev/null +++ b/src/auth/infrastructure/strategies/bearer.strategy.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import type { JwtPayload } from '@shared/types'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy, ExtractJwt } from 'passport-jwt'; + +@Injectable() +export class BearerStrategy extends PassportStrategy(Strategy, 'bearer') { + constructor(cfg: ConfigService) { + const audConstraint = cfg.getOrThrow('JWT_AUDIENCE'); + const audience = btoa(audConstraint); + + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: cfg.get('JWT_ACCESS_SECRET'), + issuer: cfg.get('JWT_ISSUER'), + audience, + }); + } + + validate(payload: JwtPayload) { + return payload; + } +} diff --git a/src/auth/infrastructure/strategies/cookie.strategy.ts b/src/auth/infrastructure/strategies/cookie.strategy.ts new file mode 100644 index 0000000..d9334eb --- /dev/null +++ b/src/auth/infrastructure/strategies/cookie.strategy.ts @@ -0,0 +1,38 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { HttpStatus, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import type { FastifyRequest } from 'fastify'; +import type { JwtPayload } from '@shared/types'; +import { BaseException } from '@shared/error'; + +@Injectable() +export class CookieStrategy extends PassportStrategy(Strategy, 'cookie') { + constructor(configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromExtractors([ + (request: FastifyRequest) => { + const token = request?.cookies?.['refresh']; + return token; + }, + ]), + secretOrKey: configService.get('JWT_REFRESH_SECRET'), + passReqToCallback: true, + }); + } + + validate(_req: FastifyRequest, payload: JwtPayload) { + if (!payload || !payload.jti) { + throw new BaseException( + { + code: 'INVALID_REFRESH_TOKEN', + message: 'Refresh токен невалиден или протух', + details: [{ target: 'auth', reason: 'Payload is missing or jti is invalid' }], + }, + HttpStatus.UNAUTHORIZED, + ); + } + + return payload; + } +} diff --git a/src/auth/infrastructure/strategies/index.ts b/src/auth/infrastructure/strategies/index.ts new file mode 100644 index 0000000..4ea10ce --- /dev/null +++ b/src/auth/infrastructure/strategies/index.ts @@ -0,0 +1,2 @@ +export { BearerStrategy } from './bearer.strategy'; +export { CookieStrategy } from './cookie.strategy'; diff --git a/src/auth/infrastructure/utils/get-device-meta.ts b/src/auth/infrastructure/utils/get-device-meta.ts new file mode 100644 index 0000000..b37f69e --- /dev/null +++ b/src/auth/infrastructure/utils/get-device-meta.ts @@ -0,0 +1,30 @@ +import type { FastifyRequest } from 'fastify'; +import { UAParser } from 'ua-parser-js'; + +export interface DeviceMetadata { + ip: string; + userAgent: string; + browser: string; + os: string; + deviceType: 'mobile' | 'desktop' | 'tablet'; +} + +export function getDeviceMeta(req: FastifyRequest): DeviceMetadata { + const uaString = req.headers['user-agent'] || ''; + const parser = new UAParser(uaString); + const res = parser.getResult(); + + const ip = (req.headers['x-forwarded-for'] as string)?.split(',')[0] || req.ip || '0.0.0.0'; + + let deviceType: 'mobile' | 'desktop' | 'tablet' = 'desktop'; + if (res.device.type === 'mobile') deviceType = 'mobile'; + if (res.device.type === 'tablet') deviceType = 'tablet'; + + return { + ip, + userAgent: uaString, + browser: `${res.browser.name || 'Unknown'} ${res.browser.version || ''}`.trim(), + os: `${res.os.name || 'Unknown'} ${res.os.version || ''}`.trim(), + deviceType, + }; +} diff --git a/src/auth/infrastructure/workers/index.ts b/src/auth/infrastructure/workers/index.ts new file mode 100644 index 0000000..d20e25d --- /dev/null +++ b/src/auth/infrastructure/workers/index.ts @@ -0,0 +1 @@ +export { MailProcessor } from './mail.processor'; diff --git a/src/auth/infrastructure/workers/mail.processor.ts b/src/auth/infrastructure/workers/mail.processor.ts new file mode 100644 index 0000000..3e4a926 --- /dev/null +++ b/src/auth/infrastructure/workers/mail.processor.ts @@ -0,0 +1,72 @@ +import { Processor, WorkerHost } from '@nestjs/bullmq'; +import type { Job } from 'bullmq'; +import { IMailPort } from '@shared/adapters/mail'; +import { Inject } from '@nestjs/common'; +import { RegisterCodeEvent, ResetPasswordEvent } from '../../domain/events'; +import { AuthMailJobs, AuthQueues } from '../../domain/enums'; + +@Processor(AuthQueues.AUTH_MAIL) +export class MailProcessor extends WorkerHost { + constructor( + @Inject('IMailPort') + private readonly mailAdapter: IMailPort, + ) { + super(); + } + + async process(job: Job): Promise; + async process(job: Job): Promise; + async process(job: Job): Promise { + await job.log(`[START] Job ID: ${job.id} | Type: ${job.name}`); + + try { + switch (job.name) { + case AuthMailJobs.SEND_REGISTER_CODE: + await this.sendRegisterCode(job); + break; + case AuthMailJobs.SEND_RESET_PASSWORD: + await this.sendResetPassCode(job); + break; + default: + await job.log(`[WRN] No handler for job: ${job.name}`); + await job.updateProgress(100); + } + + await job.log(`[DONE] Job ${job.id} processed`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : ''; + + await job.log(`[FAIL] ${errorMessage}`); + if (errorStack) { + await job.log(errorStack); + } + + throw error; + } + } + + private sendRegisterCode = async (job: Job) => { + const { email, name, otp } = job.data; + + await job.log(`Sending registration code to: ${email}`); + await job.updateProgress(20); + + await this.mailAdapter.sendRegistrationCode(email, name, otp); + + await job.log(`Successfully sent to ${email}`); + await job.updateProgress(100); + }; + + private sendResetPassCode = async (job: Job) => { + const { email, otp } = job.data; + + await job.log(`Sending password reset to: ${email}`); + await job.updateProgress(30); + + await this.mailAdapter.sendResetPasswordCode(email, otp); + + await job.log(`Reset link delivered to ${email}`); + await job.updateProgress(100); + }; +} diff --git a/src/main.ts b/src/main.ts index 13cad38..009bef9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,25 @@ -import { NestFactory } from '@nestjs/core'; +import { bootstrapApp } from '@libs/bootstrap'; import { AppModule } from './app.module'; -async function bootstrap() { - const app = await NestFactory.create(AppModule); - await app.listen(3000); -} -bootstrap(); +bootstrapApp({ + serviceName: 'Tracker Monolit', + appModule: AppModule, + version: 'v1', + defaultPort: 2000, + portEnvKey: 'PORT', + swaggerOptions: { + title: 'Task Tracker API', + description: ` +### Описание +RESTful API сервиса управления задачами (Task Tracker). + +### Поддержка +Для доступа к закрытым методам используйте заголовок Authorization: Bearer token. +По вопросам интеграции обращаться к команде разработки. + `.trim(), + version: '0.1.0', + path: 'docs', + }, + useCors: true, + useCookieParser: true, +}); diff --git a/src/projects/application/controller/index.ts b/src/projects/application/controller/index.ts new file mode 100644 index 0000000..4c96d63 --- /dev/null +++ b/src/projects/application/controller/index.ts @@ -0,0 +1 @@ +export { ProjectsController } from './projects/controller'; diff --git a/src/projects/application/controller/projects/controller.ts b/src/projects/application/controller/projects/controller.ts new file mode 100644 index 0000000..390a31f --- /dev/null +++ b/src/projects/application/controller/projects/controller.ts @@ -0,0 +1,89 @@ +import { ApiBaseController, GetUserId, Public } from '@shared/decorators'; +import { Body, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { + ArchiveProjectSwagger, + CreateProjectSwagger, + CreateShareTokenSwagger, + FindAllProjectsSwagger, + FindOneProjectSwagger, + RemoveProjectSwagger, + UpdateProjectSwagger, +} from './swagger'; +import { CreateProjectDto, CreateShareTokenDto, UpdateProjectDto } from '../../dtos'; +import { ProjectStatus } from '@core/projects/domain/entities'; +import { ProjectsFacade } from '../../projects.facade'; + +@ApiBaseController('teams/:slug/projects', 'Team Projects', true) +export class ProjectsController { + constructor(private readonly facade: ProjectsFacade) {} + + @Get() + @FindAllProjectsSwagger() + async findAll(@Param('slug') slug: string, @GetUserId() userId: string) { + return this.facade.getTeamProjects(slug, userId); + } + + @Get(':id') + @Public() + @FindOneProjectSwagger() + async getOne( + @Param('id') id: string, + @Param('slug') slug: string, + @GetUserId() userId?: string, + @Query('token') token?: string, + ) { + return this.facade.getDetail(id, slug, userId, token); + } + + @Post(':id/share') + @CreateShareTokenSwagger() + async generateShareToken( + @Param('id') id: string, + @Param('slug') slug: string, + @GetUserId() userId: string, + @Body() dto: CreateShareTokenDto, + ) { + return this.facade.generateShareToken(id, slug, userId, dto); + } + + @Post(':id/archive') + @ArchiveProjectSwagger() + async archive( + @Param('id') id: string, + @Param('slug') slug: string, + @GetUserId() userId: string, + ) { + return this.facade.setStatus(id, slug, userId, ProjectStatus.Archived); + } + + @Post() + @CreateProjectSwagger() + async create( + @Param('slug') slug: string, + @GetUserId() userId: string, + @Body() dto: CreateProjectDto, + ) { + return this.facade.create(userId, slug, dto); + } + + @Patch(':id') + @UpdateProjectSwagger() + async update( + @Param('id') id: string, + @Param('slug') slug: string, + @GetUserId() userId: string, + @Body() dto: UpdateProjectDto, + ) { + return this.facade.update(id, slug, userId, dto); + } + + @Delete(':id') + @RemoveProjectSwagger() + async remove( + @Param('id') id: string, + @Param('slug') slug: string, + @GetUserId() userId: string, + ) { + return this.facade.delete(id, slug, userId); + } +} diff --git a/src/projects/application/controller/projects/swagger.ts b/src/projects/application/controller/projects/swagger.ts new file mode 100644 index 0000000..55e1e1a --- /dev/null +++ b/src/projects/application/controller/projects/swagger.ts @@ -0,0 +1,135 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiOperation, ApiBody, ApiResponse, ApiParam } from '@nestjs/swagger'; +import { ActionResponse } from '@shared/dtos'; +import { ApiValidationError, ApiUnauthorized, ApiForbidden, ApiNotFound } from '@shared/error'; +import { + CreateProjectDto, + CreateProjectResponse, + CreateShareTokenDto, + UpdateProjectDto, +} from '../../dtos'; + +export const CreateProjectSwagger = () => + applyDecorators( + ApiOperation({ summary: 'Создать новый проект в команде' }), + ApiParam({ name: 'slug', type: 'string' }), + ApiBody({ type: CreateProjectDto.Output }), + ApiResponse({ + status: 201, + description: 'Проект успешно создан', + type: CreateProjectResponse.Output, + }), + ApiValidationError(), + ApiUnauthorized(), + ApiForbidden(), + ); + +export const FindAllProjectsSwagger = () => + applyDecorators( + ApiOperation({ summary: 'Получить список всех проектов команды' }), + ApiParam({ name: 'slug', type: 'string' }), + ApiResponse({ + status: 200, + description: 'Список проектов получен', + type: [Object], + }), + ApiUnauthorized(), + ); + +export const FindOneProjectSwagger = () => + applyDecorators( + ApiOperation({ summary: 'Получить детальную информацию о проекте' }), + ApiParam({ + name: 'id', + description: 'CUID проекта', + type: 'string', + example: 'clv123456', + }), + ApiResponse({ status: 200, type: Object }), + ApiNotFound('Проект не найден'), + ApiUnauthorized(), + ); + +export const UpdateProjectSwagger = () => + applyDecorators( + ApiOperation({ summary: 'Обновить информацию о проекте' }), + ApiParam({ + name: 'id', + description: 'CUID проекта', + type: 'string', + example: 'clv123456', + }), + ApiBody({ type: UpdateProjectDto.Output }), + ApiResponse({ status: 200, description: 'Проект обновлен', type: ActionResponse.Output }), + ApiValidationError(), + ApiNotFound(), + ApiUnauthorized(), + ); + +export const RemoveProjectSwagger = () => + applyDecorators( + ApiOperation({ summary: 'Архивировать (удалить) проект' }), + ApiParam({ + name: 'id', + description: 'CUID проекта', + type: 'string', + example: 'clv123456', + }), + ApiResponse({ status: 200, description: 'Проект удален', type: ActionResponse.Output }), + ApiNotFound(), + ApiUnauthorized(), + ); + +export const ArchiveProjectSwagger = () => + applyDecorators( + ApiOperation({ summary: 'Перевести проект в статус архива' }), + ApiParam({ + name: 'id', + description: 'CUID проекта', + type: 'string', + example: 'clv123456', + }), + ApiResponse({ status: 200, description: 'Статус обновлен', type: ActionResponse.Output }), + ApiUnauthorized(), + ); + +export const GetProjectByTokenSwagger = () => + applyDecorators( + ApiOperation({ summary: 'Получить проект по публичному токену' }), + ApiParam({ name: 'token', description: 'Токен доступа', type: 'string' }), + ApiResponse({ status: 200, type: Object }), + ApiNotFound('Токен недействителен'), + ); + +export const CreateShareTokenSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Сгенерировать публичную ссылку', + description: + 'Создает защищенный токен доступа к проекту. Если expiresAt не указан, по умолчанию ставится доступ на 3 месяца.', + }), + ApiParam({ + name: 'slug', + description: 'Slug команды', + type: 'string', + }), + ApiParam({ + name: 'id', + description: 'CUID проекта', + type: 'string', + example: 'clv123456', + }), + ApiBody({ + type: CreateShareTokenDto.Output, + description: 'Настройки срока действия ссылки', + }), + ApiResponse({ + status: 201, + description: 'Токен успешно создан', + type: ActionResponse.Output, + }), + ApiNotFound('Проект не найден в этой команде'), + ApiValidationError('Некорректная дата или параметры'), + ApiUnauthorized(), + ApiForbidden('У вас нет прав для создания ссылки для этого проекта'), + ); diff --git a/src/projects/application/dtos/index.ts b/src/projects/application/dtos/index.ts new file mode 100644 index 0000000..359d3f9 --- /dev/null +++ b/src/projects/application/dtos/index.ts @@ -0,0 +1,6 @@ +export { + CreateProjectDto, + UpdateProjectDto, + CreateProjectResponse, + CreateShareTokenDto, +} from './projects.dto'; diff --git a/src/projects/application/dtos/projects.dto.ts b/src/projects/application/dtos/projects.dto.ts new file mode 100644 index 0000000..82375e8 --- /dev/null +++ b/src/projects/application/dtos/projects.dto.ts @@ -0,0 +1,54 @@ +import { z } from 'zod/v4'; +import { createZodDto } from 'nestjs-zod'; +import { ActionResponseSchema } from '@shared/dtos'; +import { ProjectStatus } from '@core/projects/domain/entities'; + +export const CreateProjectSchema = z.object({ + name: z + .string() + .min(1, 'Название проекта не может быть пустым') + .max(100, 'Название не должно превышать 100 символов'), + key: z + .string() + .min(2, 'Ключ проекта должен быть от 2 до 10 символов') + .max(10) + .regex(/^[A-Z0-9]+$/, 'Ключ должен содержать только заглавные латинские буквы и цифры'), + description: z.string().max(2000, 'Описание слишком длинное').optional().nullable(), + icon: z.string().optional().nullable(), + color: z + .string() + .regex(/^#[A-Fa-f0-9]{6}$/, 'Цвет должен быть в формате HEX (например, #FFFFFF)') + .optional(), + visibility: z.enum(['public', 'private']).default('public'), +}); + +export class CreateProjectDto extends createZodDto(CreateProjectSchema) {} + +export const UpdateProjectSchema = CreateProjectSchema.extend({ + status: z.enum([ProjectStatus.Active, ProjectStatus.Archived]).optional(), + isPublic: z.boolean().optional(), +}) + .partial() + .refine((data) => Object.keys(data).length > 0, { + error: 'Необходимо передать хотя бы одно поле для обновления', + abort: true, + }); + +export class UpdateProjectDto extends createZodDto(UpdateProjectSchema) {} + +const CreateProjectsResponseSchema = ActionResponseSchema.extend({ + projectId: z.string().describe('Уникальный идентификатор проекта в системе'), +}); + +export class CreateProjectResponse extends createZodDto(CreateProjectsResponseSchema) {} + +export const CreateShareTokenSchema = z.object({ + ttl: z + .string() + .datetime() + .optional() + .nullable() + .describe('Дата истечения ссылки. Если не указана — ставится дефолт 3 месяца'), +}); + +export class CreateShareTokenDto extends createZodDto(CreateShareTokenSchema) {} diff --git a/src/projects/application/mappers/index.ts b/src/projects/application/mappers/index.ts new file mode 100644 index 0000000..7f5f566 --- /dev/null +++ b/src/projects/application/mappers/index.ts @@ -0,0 +1 @@ +export { ProjectsMapper } from './projects.mapper'; diff --git a/src/projects/application/mappers/projects.mapper.ts b/src/projects/application/mappers/projects.mapper.ts new file mode 100644 index 0000000..4aa1a12 --- /dev/null +++ b/src/projects/application/mappers/projects.mapper.ts @@ -0,0 +1,63 @@ +import { ROLE_PRIORITY } from '@shared/constants'; +import { RawMemberRow } from '@core/teams/domain/repository'; +import { Project } from '@core/projects/domain/entities'; + +export class ProjectsMapper { + public static toDetailResponse(project: Project, member?: RawMemberRow, token?: string) { + const { + id, + key, + name, + status, + description, + color, + icon, + taskSequence, + createdAt, + updatedAt, + visibility, + settings, + } = project; + + const rolePriority = member ? ROLE_PRIORITY[member.role] : -1; + + return { + id, + key, + name, + status, + description, + visuals: { + color: color ?? '#3b82f6', + icon, + }, + meta: { + taskSequence, + createdAt, + updatedAt, + }, + access: { + visibility, + canEdit: rolePriority >= ROLE_PRIORITY.moderator, + canDelete: rolePriority >= ROLE_PRIORITY.admin, + shareUrl: visibility === 'public' && token ? `/share/${token}` : null, + }, + settings: settings || {}, + }; + } + + public static toListResponse(project: Project, member: RawMemberRow) { + const { id, key, name, status, color, icon, createdAt } = project; + + return { + id, + key, + name, + status, + color: color ?? '#3b82f6', + icon, + createdAt, + canEdit: ROLE_PRIORITY[member.role] >= ROLE_PRIORITY.moderator, + }; + } +} diff --git a/src/projects/application/projects.facade.ts b/src/projects/application/projects.facade.ts new file mode 100644 index 0000000..3fb6dc9 --- /dev/null +++ b/src/projects/application/projects.facade.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@nestjs/common'; +import { ProjectStatus } from '../domain/entities'; +import type { CreateProjectDto, CreateShareTokenDto, UpdateProjectDto } from './dtos'; +import { + CreateProjectUseCase, + DeleteProjectUseCase, + GenerateShareTokenUseCase, + SetProjectStatusUseCase, + UpdateProjectUseCase, + FindProjectsByTeamQuery, + GetProjectDetailQuery, +} from './use-cases'; + +@Injectable() +export class ProjectsFacade { + constructor( + private readonly createProjectUC: CreateProjectUseCase, + private readonly updateProjectUC: UpdateProjectUseCase, + private readonly deleteProjectUC: DeleteProjectUseCase, + private readonly setStatusUC: SetProjectStatusUseCase, + private readonly generateTokenUC: GenerateShareTokenUseCase, + private readonly getDetailQ: GetProjectDetailQuery, + private readonly findByTeamQ: FindProjectsByTeamQuery, + ) {} + + public async create(userId: string, slug: string, dto: CreateProjectDto) { + return this.createProjectUC.execute(userId, slug, dto); + } + + public async update(id: string, slug: string, userId: string, dto: UpdateProjectDto) { + return this.updateProjectUC.execute(id, slug, userId, dto); + } + + public async delete(id: string, slug: string, userId: string) { + return this.deleteProjectUC.execute(id, slug, userId); + } + + public async setStatus(id: string, slug: string, userId: string, status: ProjectStatus) { + return this.setStatusUC.execute(id, slug, userId, status); + } + + public async generateShareToken( + id: string, + slug: string, + userId: string, + dto: CreateShareTokenDto, + ) { + return this.generateTokenUC.execute(id, slug, userId, dto); + } + + public async getDetail(id: string, slug: string, userId?: string, token?: string) { + return this.getDetailQ.execute(id, slug, userId, token); + } + + public async getTeamProjects(slug: string, userId: string) { + return this.findByTeamQ.execute(slug, userId); + } +} diff --git a/src/projects/application/use-cases/create-project.use-case.ts b/src/projects/application/use-cases/create-project.use-case.ts new file mode 100644 index 0000000..a2cc6de --- /dev/null +++ b/src/projects/application/use-cases/create-project.use-case.ts @@ -0,0 +1,34 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { CreateProjectDto } from '../dtos'; +import { IProjectsRepository } from '@core/projects/domain/repository'; +import { ProjectStatus } from '@core/projects/domain/entities'; +import { ProjectAccessPolicy } from '@core/projects/domain/policy'; + +@Injectable() +export class CreateProjectUseCase { + constructor( + @Inject('IProjectsRepository') + private readonly projectsRepo: IProjectsRepository, + private readonly policy: ProjectAccessPolicy, + ) {} + + public async execute(userId: string, slug: string, dto: CreateProjectDto) { + const { team } = await this.policy.ensureTeamAccess(slug, userId, 'admin'); + + const data = { + ...dto, + teamId: team.id, + ownerId: userId, + key: dto.key.toUpperCase(), + status: ProjectStatus.Active, + }; + + const { result, id } = await this.projectsRepo.create(data); + + return { + success: result, + message: `Проект ${dto.name} успешно создан`, + projectId: id, + }; + } +} diff --git a/src/projects/application/use-cases/delete-project.use-case.ts b/src/projects/application/use-cases/delete-project.use-case.ts new file mode 100644 index 0000000..b5d3e71 --- /dev/null +++ b/src/projects/application/use-cases/delete-project.use-case.ts @@ -0,0 +1,35 @@ +import { ProjectAccessPolicy } from '@core/projects/domain/policy'; +import { IProjectsRepository } from '@core/projects/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { BaseException } from '@shared/error'; + +@Injectable() +export class DeleteProjectUseCase { + constructor( + @Inject('IProjectsRepository') + private readonly projectsRepo: IProjectsRepository, + private readonly policy: ProjectAccessPolicy, + ) {} + + public async execute(id: string, slug: string, userId: string) { + const { project } = await this.policy.validateProjectAccess(id, slug, userId, 'admin'); + const result = await this.projectsRepo.delete(project.id); + + if (!result) { + throw new BaseException( + { + code: 'DELETE_FAILED', + message: 'Не удалось удалить проект', + }, + HttpStatus.SERVICE_UNAVAILABLE, + ); + } + + return { + success: true, + message: result + ? `Проект ${project.name} успешно перемещен в корзину` + : 'Не удалось удалить проект, попробуйте позже', + }; + } +} diff --git a/src/projects/application/use-cases/find-project.query.ts b/src/projects/application/use-cases/find-project.query.ts new file mode 100644 index 0000000..c5b41d9 --- /dev/null +++ b/src/projects/application/use-cases/find-project.query.ts @@ -0,0 +1,116 @@ +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { FindTeamMemberQuery, FindTeamQuery } from '@core/teams'; +import { createHash } from 'crypto'; +import { BaseException } from '@shared/error'; +import { ROLE_PRIORITY } from '@shared/constants'; +import { IProjectsRepository } from '@core/projects/domain/repository'; +import type { Project } from '@core/projects/domain/entities'; + +@Injectable() +export class FindProjectQuery { + constructor( + @Inject('IProjectsRepository') + private readonly projectsRepo: IProjectsRepository, + private readonly findTeamQ: FindTeamQuery, + private readonly findTeamMemberQ: FindTeamMemberQuery, + ) {} + + /** + * Точка входа для получения проекта с проверкой прав. + */ + public async execute( + projectId: string, + slug: string, + userId?: string, + shareToken?: string, + minRole: keyof typeof ROLE_PRIORITY = 'viewer', + ) { + const project = await this.projectsRepo.findOne(projectId); + + if (!project) { + throw new BaseException( + { + code: 'PROJECT_NOT_FOUND', + message: 'Проект не найден', + details: [{ target: 'projectId', value: projectId }], + }, + HttpStatus.NOT_FOUND, + ); + } + + if (shareToken) { + return this.findPublic(project, shareToken); + } + + return this.findPrivate(project, slug, userId, minRole); + } + + private findPrivate = async ( + project: Project, + slug: string, + userId?: string, + minRole: keyof typeof ROLE_PRIORITY = 'viewer', + ) => { + if (!userId) { + throw new BaseException( + { + code: 'AUTH_REQUIRED', + message: 'Требуется авторизация для доступа к приватному проекту', + }, + HttpStatus.UNAUTHORIZED, + ); + } + + const team = await this.findTeamQ.execute(slug); + if (!team || team.id !== project.teamId) { + throw new BaseException( + { + code: 'PROJECT_TEAM_MISMATCH', + message: 'Проект не принадлежит указанной команде', + }, + HttpStatus.BAD_REQUEST, + ); + } + + const member = await this.findTeamMemberQ.execute(team.id, userId); + if (!member) { + throw new BaseException( + { code: 'ACCESS_DENIED', message: 'Вы не являетесь участником этой команды' }, + HttpStatus.FORBIDDEN, + ); + } + + if (ROLE_PRIORITY[member.role] < ROLE_PRIORITY[minRole]) { + throw new BaseException( + { + code: 'INSUFFICIENT_PERMISSIONS', + message: `Для этого действия необходимы права: ${minRole}`, + }, + HttpStatus.FORBIDDEN, + ); + } + + return { project, member, team }; + }; + + private findPublic = async (project: Project, token: string) => { + if (project.visibility !== 'public') { + throw new BaseException( + { code: 'PROJECT_NOT_PUBLIC', message: 'Публичный доступ к проекту ограничен' }, + HttpStatus.FORBIDDEN, + ); + } + + const hashedToken = createHash('sha256').update(token).digest('hex'); + const isValidToken = await this.projectsRepo.hasValidShareToken(project.id, hashedToken); + + if (!isValidToken) { + throw new BaseException( + { code: 'SHARE_LINK_INVALID', message: 'Ссылка недействительна или истекла' }, + HttpStatus.GONE, + ); + } + + return { project, member: null, team: null }; + }; +} diff --git a/src/projects/application/use-cases/find-projects-by-team.query.ts b/src/projects/application/use-cases/find-projects-by-team.query.ts new file mode 100644 index 0000000..7229508 --- /dev/null +++ b/src/projects/application/use-cases/find-projects-by-team.query.ts @@ -0,0 +1,31 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ProjectsMapper } from '../mappers'; +import { IProjectsRepository } from '@core/projects/domain/repository'; +import { ProjectAccessPolicy } from '@core/projects/domain/policy'; + +@Injectable() +export class FindProjectsByTeamQuery { + constructor( + @Inject('IProjectsRepository') + private readonly projectsRepo: IProjectsRepository, + private readonly policy: ProjectAccessPolicy, + ) {} + + public async execute(slug: string, userId: string) { + const { team, member } = await this.policy.ensureTeamAccess(slug, userId, 'viewer'); + const projects = await this.projectsRepo.findByTeam(team.id); + + return { + team: { + id: team.id, + name: team.name, + slug: team.slug, + role: member.role, + }, + items: projects.map((p) => ProjectsMapper.toListResponse(p, member)), + meta: { + total: projects.length, + }, + }; + } +} diff --git a/src/projects/application/use-cases/generate-share-token.use-case.ts b/src/projects/application/use-cases/generate-share-token.use-case.ts new file mode 100644 index 0000000..deb68da --- /dev/null +++ b/src/projects/application/use-cases/generate-share-token.use-case.ts @@ -0,0 +1,82 @@ +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import type { CreateShareTokenDto } from '../dtos'; +import { createHash, randomBytes } from 'crypto'; +import { BaseException } from '@shared/error'; +import { ProjectAccessPolicy } from '@core/projects/domain/policy'; +import { IProjectsRepository } from '@core/projects/domain/repository'; + +@Injectable() +export class GenerateShareTokenUseCase { + constructor( + @Inject('IProjectsRepository') + private readonly projectsRepo: IProjectsRepository, + private readonly policy: ProjectAccessPolicy, + ) {} + + public async execute(id: string, slug: string, userId: string, dto: CreateShareTokenDto) { + const { project } = await this.policy.validateProjectAccess(id, slug, userId); + + let expiresAt: Date; + + if (dto.ttl) { + expiresAt = new Date(dto.ttl); + + if (expiresAt <= new Date()) { + throw new BaseException( + { + code: 'INVALID_EXPIRATION', + message: 'Дата истечения не может быть в прошлом', + details: [ + { target: 'ttl', message: 'Expiration date is behind current time' }, + ], + }, + HttpStatus.BAD_REQUEST, + ); + } + } else { + expiresAt = new Date(); + expiresAt.setMonth(expiresAt.getMonth() + 3); + } + + const rawToken = this.generateSecureToken(); + + const isSaved = await this.projectsRepo.createShare({ + projectId: project.id, + token: this.hash(rawToken), + expiresAt, + createdBy: userId, + }); + + if (!isSaved) { + throw new BaseException( + { + code: 'SHARE_CREATE_FAILED', + message: 'Не удалось сгенерировать ссылку доступа', + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const durationMsg = dto.ttl + ? `закроется ${expiresAt.toLocaleDateString('ru-RU')}` + : 'бессрочна (на 3 месяца по умолчанию)'; + + return { + success: true, + message: `Ссылка для проекта «${project.name}» создана и ${durationMsg}`, + payload: { + token: rawToken, + isYourself: !!dto, + expiresAt: expiresAt.toISOString(), + }, + }; + } + + private generateSecureToken(): string { + return `st_${randomBytes(32).toString('hex')}`; + } + + private hash(token: string): string { + return createHash('sha256').update(token).digest('hex'); + } +} diff --git a/src/projects/application/use-cases/get-project-detail.query.ts b/src/projects/application/use-cases/get-project-detail.query.ts new file mode 100644 index 0000000..d69ec4e --- /dev/null +++ b/src/projects/application/use-cases/get-project-detail.query.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@nestjs/common'; +import { ProjectsMapper } from '../mappers'; +import { FindProjectQuery } from './find-project.query'; + +@Injectable() +export class GetProjectDetailQuery { + constructor(private readonly findProjectQuery: FindProjectQuery) {} + + public async execute(id: string, slug: string, userId?: string, token?: string) { + const { project, member } = await this.findProjectQuery.execute( + id, + slug, + userId, + token, + 'viewer', + ); + + return ProjectsMapper.toDetailResponse(project, member); + } +} diff --git a/src/projects/application/use-cases/index.ts b/src/projects/application/use-cases/index.ts new file mode 100644 index 0000000..e7fb41c --- /dev/null +++ b/src/projects/application/use-cases/index.ts @@ -0,0 +1,27 @@ +import { CreateProjectUseCase } from './create-project.use-case'; +import { DeleteProjectUseCase } from './delete-project.use-case'; +import { GenerateShareTokenUseCase } from './generate-share-token.use-case'; +import { SetProjectStatusUseCase } from './set-project-status.use-case'; +import { UpdateProjectUseCase } from './update-project.use-case'; +import { FindProjectsByTeamQuery } from './find-projects-by-team.query'; +import { GetProjectDetailQuery } from './get-project-detail.query'; +import { FindProjectQuery } from './find-project.query'; + +export * from './create-project.use-case'; +export * from './delete-project.use-case'; +export * from './generate-share-token.use-case'; +export * from './set-project-status.use-case'; +export * from './update-project.use-case'; +export * from './find-projects-by-team.query'; +export * from './get-project-detail.query'; +export * from './find-project.query'; + +export const ProjectUseCases = [ + CreateProjectUseCase, + DeleteProjectUseCase, + GenerateShareTokenUseCase, + SetProjectStatusUseCase, + UpdateProjectUseCase, +]; + +export const ProjectQueries = [FindProjectsByTeamQuery, GetProjectDetailQuery, FindProjectQuery]; diff --git a/src/projects/application/use-cases/set-project-status.use-case.ts b/src/projects/application/use-cases/set-project-status.use-case.ts new file mode 100644 index 0000000..9e5240e --- /dev/null +++ b/src/projects/application/use-cases/set-project-status.use-case.ts @@ -0,0 +1,41 @@ +import { ProjectStatus } from '@core/projects/domain/entities'; +import { ProjectAccessPolicy } from '@core/projects/domain/policy'; +import { IProjectsRepository } from '@core/projects/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { BaseException } from '@shared/error'; + +@Injectable() +export class SetProjectStatusUseCase { + constructor( + @Inject('IProjectsRepository') + private readonly projectsRepo: IProjectsRepository, + private readonly policy: ProjectAccessPolicy, + ) {} + + public async execute(id: string, slug: string, userId: string, status: ProjectStatus) { + const { project } = await this.policy.validateProjectAccess(id, slug, userId); + const result = await this.projectsRepo.update(project.id, { status }); + + if (!result) { + throw new BaseException( + { + code: 'STATUS_UPDATE_FAILED', + message: 'Не удалось обновить статус проекта', + details: [{ target: 'status', value: status }], + }, + HttpStatus.SERVICE_UNAVAILABLE, + ); + } + + const messages: Record = { + archived: `Проект «${project.name}» успешно архивирован`, + active: `Проект «${project.name}» теперь активен`, + template: `Проект «${project.name}» успешно сохранен как шаблон`, + }; + + return { + success: result, + message: messages[status] || `Статус проекта «${project.name}» изменен`, + }; + } +} diff --git a/src/projects/application/use-cases/update-project.use-case.ts b/src/projects/application/use-cases/update-project.use-case.ts new file mode 100644 index 0000000..eaf8a7b --- /dev/null +++ b/src/projects/application/use-cases/update-project.use-case.ts @@ -0,0 +1,43 @@ +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import type { UpdateProjectDto } from '../dtos'; +import { BaseException } from '@shared/error'; +import { IProjectsRepository } from '@core/projects/domain/repository'; +import { ProjectAccessPolicy } from '@core/projects/domain/policy'; + +@Injectable() +export class UpdateProjectUseCase { + constructor( + @Inject('IProjectsRepository') + private readonly projectsRepo: IProjectsRepository, + private readonly policy: ProjectAccessPolicy, + ) {} + + public async execute(id: string, slug: string, userId: string, dto: UpdateProjectDto) { + const { project } = await this.policy.validateProjectAccess(id, slug, userId); + const { isPublic, key, ...data } = dto; + + const result = await this.projectsRepo.update(project.id, { + ...data, + ...(key && { key: key.toUpperCase() }), + ...(typeof isPublic === 'boolean' && { + visibility: isPublic ? 'public' : 'private', + }), + }); + + if (!result) { + throw new BaseException( + { + code: 'UPDATE_FAILED', + message: + 'Изменения не были применены. Возможно, данные идентичны текущим или проект недоступен', + }, + HttpStatus.BAD_REQUEST, + ); + } + + return { + success: result, + message: result ? 'Настройки проекта успешно обновлены' : 'Изменения не были применены', + }; + } +} diff --git a/src/projects/domain/entities/entities.domain.ts b/src/projects/domain/entities/entities.domain.ts new file mode 100644 index 0000000..4df40c2 --- /dev/null +++ b/src/projects/domain/entities/entities.domain.ts @@ -0,0 +1,28 @@ +import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'; +import { projects, projectShares } from '../../infrastructure/persistence/models/projects.model'; + +export enum ProjectStatus { + Active = 'active', + Archived = 'archived', + Template = 'template', +} + +export enum ProjectVisibility { + Public = 'public', + Private = 'private', +} + +export type Project = InferSelectModel; +export type NewProject = InferInsertModel; +export interface ProjectSettings { + allowGuestComments?: boolean; + defaultAssigneeId?: string; + showTaskNumbers?: boolean; +} + +export type ProjectWithTypedSettings = Omit & { + settings: ProjectSettings; +}; + +export type ProjectShare = InferSelectModel; +export type NewProjectShare = InferInsertModel; diff --git a/src/projects/domain/entities/index.ts b/src/projects/domain/entities/index.ts new file mode 100644 index 0000000..1481834 --- /dev/null +++ b/src/projects/domain/entities/index.ts @@ -0,0 +1 @@ +export * from './entities.domain'; diff --git a/src/projects/domain/policy/index.ts b/src/projects/domain/policy/index.ts new file mode 100644 index 0000000..cc90b6c --- /dev/null +++ b/src/projects/domain/policy/index.ts @@ -0,0 +1,5 @@ +import { ProjectAccessPolicy } from './project-access.policy'; + +export * from './project-access.policy'; + +export const POLICIES = [ProjectAccessPolicy]; diff --git a/src/projects/domain/policy/project-access.policy.ts b/src/projects/domain/policy/project-access.policy.ts new file mode 100644 index 0000000..7001ebf --- /dev/null +++ b/src/projects/domain/policy/project-access.policy.ts @@ -0,0 +1,78 @@ +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { IProjectsRepository } from '../repository'; +import { BaseException } from '@shared/error'; +import { FindTeamMemberQuery, FindTeamQuery } from '@core/teams'; +import { ROLE_PRIORITY } from '@shared/constants'; + +@Injectable() +export class ProjectAccessPolicy { + constructor( + @Inject('IProjectsRepository') + private readonly projectsRepo: IProjectsRepository, + private readonly findTeamQ: FindTeamQuery, + private readonly findTeamMemberQ: FindTeamMemberQuery, + ) {} + + /** + * Проверка доступа к команде (используется, например, при создании проекта) + */ + public async ensureTeamAccess( + slug: string, + userId: string, + minRole: keyof typeof ROLE_PRIORITY = 'viewer', + ) { + const team = await this.findTeamQ.execute(slug); + if (!team) { + throw new BaseException( + { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, + HttpStatus.NOT_FOUND, + ); + } + + const member = await this.findTeamMemberQ.execute(team.id, userId); + if (!member) { + throw new BaseException( + { code: 'NOT_TEAM_MEMBER', message: 'Вы не участник команды' }, + HttpStatus.FORBIDDEN, + ); + } + + if (ROLE_PRIORITY[member.role] < ROLE_PRIORITY[minRole]) { + throw new BaseException( + { + code: 'INSUFFICIENT_PERMISSIONS', + message: `Требуется роль ${minRole} или выше`, + details: [{ target: 'role', current: member.role, required: minRole }], + }, + HttpStatus.FORBIDDEN, + ); + } + + return { team, member }; + } + + /** + * Полная проверка доступа к конкретному проекту внутри команды + */ + public async validateProjectAccess( + projectId: string, + slug: string, + userId: string, + minRole: keyof typeof ROLE_PRIORITY = 'admin', + ) { + const { team, member } = await this.ensureTeamAccess(slug, userId, minRole); + + const project = await this.projectsRepo.findOne(projectId); + if (!project || project.teamId !== team.id) { + throw new BaseException( + { + code: 'PROJECT_NOT_FOUND', + message: 'Проект не найден в этой команде', + }, + HttpStatus.NOT_FOUND, + ); + } + + return { project, member, team }; + } +} diff --git a/src/projects/domain/repository/index.ts b/src/projects/domain/repository/index.ts new file mode 100644 index 0000000..aea7492 --- /dev/null +++ b/src/projects/domain/repository/index.ts @@ -0,0 +1 @@ +export { IProjectsRepository } from './projects.repository.interface'; diff --git a/src/projects/domain/repository/projects.repository.interface.ts b/src/projects/domain/repository/projects.repository.interface.ts new file mode 100644 index 0000000..58fc8cf --- /dev/null +++ b/src/projects/domain/repository/projects.repository.interface.ts @@ -0,0 +1,12 @@ +import type { NewProject, NewProjectShare, Project } from '../entities'; + +export interface IProjectsRepository { + create(data: NewProject): Promise<{ result: boolean; id: string }>; + update(id: string, data: Partial): Promise; + delete(id: string): Promise; + findOne(id: string): Promise; + findByTeam(teamId: string): Promise; + createShare(data: NewProjectShare): Promise; + hasValidShareToken(id: string, token: string): Promise; + revokeAllShares(projectId: string): Promise; +} diff --git a/src/projects/index.ts b/src/projects/index.ts new file mode 100644 index 0000000..f17852e --- /dev/null +++ b/src/projects/index.ts @@ -0,0 +1 @@ +export { ProjectsModule } from './projects.module'; diff --git a/src/projects/infrastructure/persistence/models/enums.ts b/src/projects/infrastructure/persistence/models/enums.ts new file mode 100644 index 0000000..5cfc624 --- /dev/null +++ b/src/projects/infrastructure/persistence/models/enums.ts @@ -0,0 +1,8 @@ +import { baseSchema } from '@shared/entities'; + +export const projectStatusEnum = baseSchema.enum('project_status', [ + 'active', + 'archived', + 'template', +]); +export const projectVisibilityEnum = baseSchema.enum('project_visibility', ['public', 'private']); diff --git a/src/projects/infrastructure/persistence/models/index.ts b/src/projects/infrastructure/persistence/models/index.ts new file mode 100644 index 0000000..ed46b14 --- /dev/null +++ b/src/projects/infrastructure/persistence/models/index.ts @@ -0,0 +1,2 @@ +export { projectStatusEnum, projectVisibilityEnum } from './enums'; +export { projectShares, projects } from './projects.model'; diff --git a/src/projects/infrastructure/persistence/models/projects.model.ts b/src/projects/infrastructure/persistence/models/projects.model.ts new file mode 100644 index 0000000..2eccc18 --- /dev/null +++ b/src/projects/infrastructure/persistence/models/projects.model.ts @@ -0,0 +1,60 @@ +import { text, varchar, timestamp, jsonb, integer, uniqueIndex, index } from 'drizzle-orm/pg-core'; +import { baseSchema, teams, users } from '@shared/entities'; +import { createId } from '@paralleldrive/cuid2'; +import { isNull } from 'drizzle-orm'; +import { projectStatusEnum, projectVisibilityEnum } from './enums'; + +export const projects = baseSchema.table( + 'projects', + { + id: text('id') + .primaryKey() + .$defaultFn(() => createId()), + teamId: text('team_id') + .references(() => teams.id, { onDelete: 'cascade' }) + .notNull(), + key: varchar('key', { length: 10 }).notNull(), + name: varchar('name', { length: 100 }).notNull(), + description: text('description'), + icon: varchar('icon', { length: 255 }), + color: varchar('color', { length: 7 }), + status: projectStatusEnum('status').default('active').notNull(), + taskSequence: integer('task_sequence').default(0).notNull(), + ownerId: text('owner_id').references(() => users.id, { onDelete: 'set null' }), + visibility: projectVisibilityEnum('visibility').default('public').notNull(), + settings: jsonb('settings').default({}), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), + deletedAt: timestamp('deleted_at'), + }, + (t) => ({ + uniqueTeamKey: uniqueIndex('project_team_key_idx') + .on(t.teamId, t.key) + .where(isNull(t.deletedAt)), + uniqueTeamName: uniqueIndex('project_team_name_idx') + .on(t.teamId, t.name) + .where(isNull(t.deletedAt)), + ownerIdx: index('project_owner_id_idx').on(t.ownerId), + teamIdx: index('project_team_id_idx').on(t.teamId), + }), +); + +export const projectShares = baseSchema.table( + 'project_shares', + { + id: text('id') + .primaryKey() + .$defaultFn(() => createId()), + projectId: text('project_id') + .notNull() + .references(() => projects.id, { onDelete: 'cascade' }), + token: text('token').notNull().unique(), + expiresAt: timestamp('expires_at', { withTimezone: true }), + createdBy: text('created_by').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + }, + (table) => ({ + tokenIdx: index('token_idx').on(table.token), + projectIdx: index('project_share_project_id_idx').on(table.projectId), + }), +); diff --git a/src/projects/infrastructure/persistence/repositories/index.ts b/src/projects/infrastructure/persistence/repositories/index.ts new file mode 100644 index 0000000..5c64093 --- /dev/null +++ b/src/projects/infrastructure/persistence/repositories/index.ts @@ -0,0 +1,2 @@ +export { ProjectsRepository } from './projects.repository'; +export { IProjectsRepository } from '../../../domain/repository/projects.repository.interface'; diff --git a/src/projects/infrastructure/persistence/repositories/projects.repository.ts b/src/projects/infrastructure/persistence/repositories/projects.repository.ts new file mode 100644 index 0000000..69d8c33 --- /dev/null +++ b/src/projects/infrastructure/persistence/repositories/projects.repository.ts @@ -0,0 +1,105 @@ +import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; +import { Injectable, Inject } from '@nestjs/common'; +import * as schema from '../models'; +import { IProjectsRepository } from '../../../domain/repository'; +import { and, eq, gt, isNull, or } from 'drizzle-orm'; +import type { NewProject, NewProjectShare } from '@core/projects/domain/entities'; + +@Injectable() +export class ProjectsRepository implements IProjectsRepository { + constructor( + @Inject(DATABASE_SERVICE) + private readonly db: DatabaseService, + ) {} + + public create = async (data: NewProject) => { + const result = await this.db + .insert(schema.projects) + .values(data) + .returning({ id: schema.projects.id }); + + return { result: result.length > 0, id: result[0].id }; + }; + + public update = async (id: string, data: Partial) => { + const result = await this.db + .update(schema.projects) + .set({ ...data, updatedAt: new Date() }) + .where(eq(schema.projects.id, id)) + .returning({ id: schema.projects.id }); + + return result.length > 0; + }; + + public delete = async (id: string) => { + const result = await this.db + .update(schema.projects) + .set({ deletedAt: new Date() }) + .where(eq(schema.projects.id, id)) + .returning({ id: schema.projects.id }); + + return result.length > 0; + }; + + public findOne = async (id: string) => { + const [project] = await this.db + .select() + .from(schema.projects) + .where(and(eq(schema.projects.id, id), isNull(schema.projects.deletedAt))); + + if (!project) return null; + + return project; + }; + + public findByTeam = async (teamId: string) => { + return this.db + .select() + .from(schema.projects) + .where(and(eq(schema.projects.teamId, teamId), isNull(schema.projects.deletedAt))); + }; + + public createShare = async (data: NewProjectShare) => { + const [result] = await this.db + .insert(schema.projectShares) + .values(data) + .onConflictDoUpdate({ + target: schema.projectShares.token, + set: { + expiresAt: data.expiresAt, + token: data.token, + }, + }) + .returning({ id: schema.projectShares.id }); + + return !!result; + }; + + public hasValidShareToken = async (id: string, token: string) => { + const [result] = await this.db + .select() + .from(schema.projectShares) + .where( + and( + eq(schema.projectShares.projectId, id), + eq(schema.projectShares.token, token), + or( + isNull(schema.projectShares.expiresAt), + gt(schema.projectShares.expiresAt, new Date()), + ), + ), + ) + .limit(1); + + return !!result; + }; + + public revokeAllShares = async (projectId: string) => { + const result = await this.db + .delete(schema.projectShares) + .where(eq(schema.projectShares.projectId, projectId)) + .returning({ id: schema.projectShares.id }); + + return result.length > 0; + }; +} diff --git a/src/projects/projects.module.ts b/src/projects/projects.module.ts new file mode 100644 index 0000000..4a74316 --- /dev/null +++ b/src/projects/projects.module.ts @@ -0,0 +1,20 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { ProjectsRepository } from './infrastructure/persistence/repositories'; +import { TeamsModule } from '@core/teams'; +import { ProjectsController } from './application/controller'; +import { FindProjectQuery, ProjectQueries, ProjectUseCases } from './application/use-cases'; +import { POLICIES } from './domain/policy'; +import { ProjectsFacade } from './application/projects.facade'; + +const REPOSITORY = { + provide: 'IProjectsRepository', + useClass: ProjectsRepository, +}; + +@Module({ + imports: [forwardRef(() => TeamsModule)], + controllers: [ProjectsController], + providers: [REPOSITORY, ...POLICIES, ...ProjectUseCases, ...ProjectQueries, ProjectsFacade], + exports: [FindProjectQuery], +}) +export class ProjectsModule {} diff --git a/src/shared/adapters/cache/adapters/index.ts b/src/shared/adapters/cache/adapters/index.ts new file mode 100644 index 0000000..d75652f --- /dev/null +++ b/src/shared/adapters/cache/adapters/index.ts @@ -0,0 +1 @@ +export * from './redis-cache.adapter'; diff --git a/src/shared/adapters/cache/adapters/redis-cache.adapter.ts b/src/shared/adapters/cache/adapters/redis-cache.adapter.ts new file mode 100644 index 0000000..89f72cf --- /dev/null +++ b/src/shared/adapters/cache/adapters/redis-cache.adapter.ts @@ -0,0 +1,158 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRedis } from '@nestjs-modules/ioredis'; +import Redis, { ChainableCommander } from 'ioredis'; +import { ICacheService, ICacheTransaction } from '../ports'; + +@Injectable() +export class RedisCacheAdapter implements ICacheService { + private readonly defaultTtl = 259200; // 3 days in seconds + + constructor(@InjectRedis() private readonly redis: Redis) {} + + async getOne(key: string) { + return this.redis.get(key); + } + + async getMany(keys: string[]): Promise<(string | null)[]> { + if (keys.length === 0) return []; + return this.redis.mget(keys); + } + + async getCollection(key: string): Promise { + return this.redis.smembers(key); + } + + async setOne(key: string, value: string, ttlSeconds: number = this.defaultTtl) { + await this.redis.set(key, value, 'EX', ttlSeconds); + } + + async setMany(items: { key: string; value: string }[], ttlSeconds: number = this.defaultTtl) { + if (!items.length) return; + + const pipeline = this.redis.pipeline(); + + for (const item of items) { + pipeline.set(item.key, item.value, 'EX', ttlSeconds); + } + + await pipeline.exec(); + } + + async addOneToCollection(key: string, value: string, ttlSeconds: number = this.defaultTtl) { + await this.redis.pipeline().sadd(key, value).expire(key, ttlSeconds).exec(); + } + + async addManyToCollection(key: string, values: string[], ttlSeconds: number = this.defaultTtl) { + if (!values.length) return; + + await this.redis + .pipeline() + .sadd(key, ...values) + .expire(key, ttlSeconds) + .exec(); + } + + async removeOne(key: string) { + await this.redis.del(key); + } + + async removeMany(keys: string[]) { + if (!keys.length) return; + await this.redis.del(keys); + } + + async removeOneFromCollection(key: string, value: string) { + await this.redis.srem(key, value); + } + + async removeManyFromCollection(key: string, values: string[]) { + if (!values.length) return; + await this.redis.srem(key, ...values); + } + + async getTtl(key: string): Promise { + const ttl = await this.redis.ttl(key); + return ttl > 0 ? ttl : 0; + } + + async getOneWithTtl(key: string) { + const [[, value], [, ttl]] = (await this.redis.pipeline().get(key).ttl(key).exec()) as [ + [Error | null, string | null], + [Error | null, number], + ]; + + return { + value, + ttlSeconds: ttl && ttl > 0 ? ttl : 0, + }; + } + + transaction(): ICacheTransaction { + return new RedisTransaction(this.redis.multi()); + } + + async isAlive() { + try { + return (await this.redis.ping()) === 'PONG'; + } catch { + return false; + } + } +} + +class RedisTransaction implements ICacheTransaction { + private readonly defaultTtl = 259200; // 3 days in seconds + + constructor(private readonly multi: ChainableCommander) {} + + setOne(key: string, value: string, ttlSeconds: number = this.defaultTtl): this { + this.multi.set(key, value, 'EX', ttlSeconds); + return this; + } + + setMany(items: { key: string; value: string }[], ttlSeconds: number = this.defaultTtl): this { + for (const item of items) { + this.multi.set(item.key, item.value, 'EX', ttlSeconds); + } + return this; + } + + addOneToCollection(key: string, value: string, ttlSeconds: number = this.defaultTtl): this { + this.multi.sadd(key, value); + this.multi.expire(key, ttlSeconds); + return this; + } + + addManyToCollection(key: string, values: string[], ttlSeconds: number = this.defaultTtl): this { + if (!values.length) return this; + this.multi.sadd(key, ...values); + this.multi.expire(key, ttlSeconds); + return this; + } + + removeOne(key: string): this { + this.multi.del(key); + return this; + } + + removeMany(keys: string[]): this { + if (!keys.length) return this; + this.multi.del(keys); + return this; + } + + removeOneFromCollection(collectionKey: string, value: string): this { + this.multi.srem(collectionKey, value); + return this; + } + + removeManyFromCollection(collectionKey: string, values: string[]): this { + if (!values.length) return this; + this.multi.srem(collectionKey, ...values); + return this; + } + + async execute(): Promise { + await this.multi.exec(); + } +} diff --git a/src/shared/adapters/cache/constants.ts b/src/shared/adapters/cache/constants.ts new file mode 100644 index 0000000..b6f0485 --- /dev/null +++ b/src/shared/adapters/cache/constants.ts @@ -0,0 +1 @@ +export const CACHE_SERVICE = 'ICacheService'; diff --git a/src/shared/adapters/cache/module.ts b/src/shared/adapters/cache/module.ts new file mode 100644 index 0000000..de8c581 --- /dev/null +++ b/src/shared/adapters/cache/module.ts @@ -0,0 +1,40 @@ +import { Global, Module } from '@nestjs/common'; +import { RedisModule } from '@nestjs-modules/ioredis'; +import { ConfigService } from '@nestjs/config'; +import { RedisCacheAdapter } from './adapters'; +import { CACHE_SERVICE } from './constants'; + +@Global() +@Module({ + imports: [ + RedisModule.forRootAsync({ + inject: [ConfigService], + useFactory: async (cfg: ConfigService) => { + const host = cfg.getOrThrow('REDIS_HOST'); + const port = cfg.get('REDIS_PORT'); + const password = cfg.get('REDIS_PASSWORD'); + const url = `redis://${host}${port ? `:${port}` : ''}`; + + return { + type: 'single', + url, + options: { + password, + retryStrategy(times) { + return Math.min(times * 50, 2000); + }, + commandTimeout: 3000, + }, + }; + }, + }), + ], + providers: [ + { + provide: CACHE_SERVICE, + useClass: RedisCacheAdapter, + }, + ], + exports: [CACHE_SERVICE], +}) +export class CacheModule {} diff --git a/src/shared/adapters/cache/ports/index.ts b/src/shared/adapters/cache/ports/index.ts new file mode 100644 index 0000000..454366b --- /dev/null +++ b/src/shared/adapters/cache/ports/index.ts @@ -0,0 +1 @@ +export { ICacheService, ICacheTransaction } from './static-cache.port'; diff --git a/src/shared/adapters/cache/ports/static-cache.port.ts b/src/shared/adapters/cache/ports/static-cache.port.ts new file mode 100644 index 0000000..e982fbb --- /dev/null +++ b/src/shared/adapters/cache/ports/static-cache.port.ts @@ -0,0 +1,40 @@ +export interface ICacheService { + getOne(key: string): Promise; + getMany(keys: string[]): Promise<(string | null)[]>; + getCollection(collectionKey: string): Promise; + + setOne(key: string, value: string, ttlSeconds?: number): Promise; + setMany(items: { key: string; value: string }[], ttlSeconds?: number): Promise; + + addOneToCollection(key: string, value: string, ttlSeconds?: number): Promise; + addManyToCollection(key: string, values: string[], ttlSeconds?: number): Promise; + + removeOne(key: string): Promise; + removeMany(keys: string[]): Promise; + + removeOneFromCollection(key: string, value: string): Promise; + removeManyFromCollection(key: string, values: string[]): Promise; + + getTtl(key: string): Promise; + getOneWithTtl(key: string): Promise<{ value: string | null; ttlSeconds: number }>; + + transaction(): ICacheTransaction; + + isAlive(): Promise; +} + +export interface ICacheTransaction { + setOne(key: string, value: string, ttlSeconds?: number): this; + setMany(items: { key: string; value: string }[], ttlSeconds?: number): this; + + addOneToCollection(key: string, value: string, ttlSeconds?: number): this; + addManyToCollection(key: string, values: string[], ttlSeconds?: number): this; + + removeOne(key: string): this; + removeMany(keys: string[]): this; + + removeOneFromCollection(key: string, value: string): this; + removeManyFromCollection(key: string, values: string[]): this; + + execute(): Promise; +} diff --git a/src/shared/adapters/mail/adapter.ts b/src/shared/adapters/mail/adapter.ts new file mode 100644 index 0000000..6996131 --- /dev/null +++ b/src/shared/adapters/mail/adapter.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as nodemailer from 'nodemailer'; +import * as hbs from 'handlebars'; +import * as fs from 'fs'; +import * as path from 'path'; +import { IMailPort } from './port'; + +@Injectable() +export class MailAdapter implements IMailPort { + private transporter: nodemailer.Transporter; + + constructor(private cfg: ConfigService) { + const port = this.cfg.get('MAIL_PORT'); + const mode = this.cfg.get('NODE_ENV'); + + this.transporter = nodemailer.createTransport({ + host: this.cfg.get('MAIL_HOST'), + port: +port, + secure: port === 465, + auth: { + user: this.cfg.get('MAIL_USER'), + pass: this.cfg.get('MAIL_PASSWORD'), + }, + pool: true, + connectionTimeout: 10000, + tls: { + rejectUnauthorized: mode === 'production', + servername: 'smtp.gmail.com', + }, + }); + } + + private async sendMail(to: string, subject: string, templateName: string, context: any) { + const templatePath = path.join(process.cwd(), 'templates', `${templateName}.hbs`); + const templateSource = fs.readFileSync(templatePath, 'utf8'); + + const contextWithYear = { + ...context, + year: new Date().getFullYear(), + }; + + const template = hbs.compile(templateSource); + const html = template(contextWithYear); + + return await this.transporter.sendMail({ + from: `"${this.cfg.get('MAIL_FROM_NAME')}" <${this.cfg.get('MAIL_FROM_EMAIL')}>`, + to, + subject, + html, + }); + } + + async sendRegistrationCode(email: string, name: string, code: string) { + const codeArray = code.toString().split(''); + + return this.sendMail(email, 'Код подтверждения регистрации', 'confirmation', { + name, + codeArray, + }); + } + + async sendResetPasswordCode(email: string, code: string) { + const codeArray = code.toString().split(''); + + return this.sendMail(email, 'Восстановление пароля', 'reset-password', { + codeArray, + }); + } + + async sendTeamInvitation(email: string, teamName: string, inviteUrl: string) { + return this.sendMail(email, `Приглашение в команду ${teamName}`, 'team-invitation', { + teamName, + inviteUrl, + }); + } +} diff --git a/src/shared/adapters/mail/index.ts b/src/shared/adapters/mail/index.ts new file mode 100644 index 0000000..e132652 --- /dev/null +++ b/src/shared/adapters/mail/index.ts @@ -0,0 +1,3 @@ +export { MailAdapter } from './adapter'; +export { IMailPort } from './port'; +export { MailModule } from './module'; diff --git a/src/shared/adapters/mail/module.ts b/src/shared/adapters/mail/module.ts new file mode 100644 index 0000000..d70c47c --- /dev/null +++ b/src/shared/adapters/mail/module.ts @@ -0,0 +1,14 @@ +import { Global, Module } from '@nestjs/common'; +import { MailAdapter } from './adapter'; + +@Global() +@Module({ + providers: [ + { + provide: 'IMailPort', + useClass: MailAdapter, + }, + ], + exports: ['IMailPort'], +}) +export class MailModule {} diff --git a/src/shared/adapters/mail/port.ts b/src/shared/adapters/mail/port.ts new file mode 100644 index 0000000..0ae1a57 --- /dev/null +++ b/src/shared/adapters/mail/port.ts @@ -0,0 +1,5 @@ +export interface IMailPort { + sendRegistrationCode(email: string, name: string, code: string): Promise; + sendResetPasswordCode(email: string, code: string): Promise; + sendTeamInvitation(email: string, teamName: string, inviteUrl: string): Promise; +} diff --git a/src/shared/constants/file.constants.ts b/src/shared/constants/file.constants.ts new file mode 100644 index 0000000..be950f2 --- /dev/null +++ b/src/shared/constants/file.constants.ts @@ -0,0 +1 @@ +export const IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/jpg']; diff --git a/src/shared/constants/index.ts b/src/shared/constants/index.ts new file mode 100644 index 0000000..8a4ea9d --- /dev/null +++ b/src/shared/constants/index.ts @@ -0,0 +1,2 @@ +export * from './file.constants'; +export * from './roles.constant'; diff --git a/src/shared/constants/roles.constant.ts b/src/shared/constants/roles.constant.ts new file mode 100644 index 0000000..1da5f2c --- /dev/null +++ b/src/shared/constants/roles.constant.ts @@ -0,0 +1,7 @@ +export const ROLE_PRIORITY: Record = { + owner: 4, + admin: 3, + moderator: 2, + member: 1, + viewer: 0, +}; diff --git a/src/shared/decorators/api-controller.decorator.ts b/src/shared/decorators/api-controller.decorator.ts new file mode 100644 index 0000000..bcbb4af --- /dev/null +++ b/src/shared/decorators/api-controller.decorator.ts @@ -0,0 +1,19 @@ +import { Controller, UseGuards, applyDecorators } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ApiErrorResponse } from '@shared/error'; +import { BearerAuthGuard } from '../guards'; + +export const ApiBaseController = (path: string, tag: string, hasJWTGuard?: boolean) => { + const decorators = [ + ApiTags(tag), + Controller(path), + hasJWTGuard ? UseGuards(BearerAuthGuard) : null, + ApiErrorResponse( + 500, + 'INTERNAL_SERVER_ERROR', + 'Произошла критическая ошибка на стороне сервера', + ), + ].filter(Boolean); + + return applyDecorators(...decorators); +}; diff --git a/src/shared/decorators/index.ts b/src/shared/decorators/index.ts new file mode 100644 index 0000000..baf933f --- /dev/null +++ b/src/shared/decorators/index.ts @@ -0,0 +1,3 @@ +export { ApiBaseController } from './api-controller.decorator'; +export { IS_PUBLIC_KEY, Public } from './public.decorator'; +export * from './user.decorator'; diff --git a/src/shared/decorators/public.decorator.ts b/src/shared/decorators/public.decorator.ts new file mode 100644 index 0000000..b3845e1 --- /dev/null +++ b/src/shared/decorators/public.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/src/shared/decorators/user.decorator.ts b/src/shared/decorators/user.decorator.ts new file mode 100644 index 0000000..938bc37 --- /dev/null +++ b/src/shared/decorators/user.decorator.ts @@ -0,0 +1,20 @@ +import { createParamDecorator, type ExecutionContext } from '@nestjs/common'; +import type { FastifyRequest } from 'fastify'; +import type { JwtPayload } from '@shared/types'; + +export const GetUser = createParamDecorator( + (data: keyof JwtPayload | undefined, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + const user = request.user; + if (!user) return null; + return data ? user[data] : user; + }, +); + +export const GetUserId = createParamDecorator( + (_data: unknown, ctx: ExecutionContext): string | undefined => { + const request = ctx.switchToHttp().getRequest(); + const user = request.user; + return user?.sub; + }, +); diff --git a/src/shared/dtos/index.ts b/src/shared/dtos/index.ts new file mode 100644 index 0000000..5a8e94b --- /dev/null +++ b/src/shared/dtos/index.ts @@ -0,0 +1,2 @@ +export * from './pagination.dto'; +export * from './response.dto'; diff --git a/src/shared/dtos/pagination.dto.ts b/src/shared/dtos/pagination.dto.ts new file mode 100644 index 0000000..d0e8d38 --- /dev/null +++ b/src/shared/dtos/pagination.dto.ts @@ -0,0 +1,9 @@ +import { z } from 'zod/v4'; +import { createZodDto } from 'nestjs-zod'; + +export const PaginationSchema = z.object({ + page: z.coerce.number().int().min(1).default(1), + limit: z.coerce.number().int().min(1).max(100).default(20), +}); + +export class PaginationDto extends createZodDto(PaginationSchema) {} diff --git a/src/shared/dtos/response.dto.ts b/src/shared/dtos/response.dto.ts new file mode 100644 index 0000000..325f719 --- /dev/null +++ b/src/shared/dtos/response.dto.ts @@ -0,0 +1,9 @@ +import { z } from 'zod/v4'; +import { createZodDto } from 'nestjs-zod'; + +export const ActionResponseSchema = z.object({ + success: z.boolean().describe('Статус операции'), + message: z.string().optional().describe('Сообщение для пользователя'), +}); + +export class ActionResponse extends createZodDto(ActionResponseSchema) {} diff --git a/src/shared/entities/index.ts b/src/shared/entities/index.ts new file mode 100644 index 0000000..b50a6a2 --- /dev/null +++ b/src/shared/entities/index.ts @@ -0,0 +1,5 @@ +export { baseSchema } from './schema'; +export * from '../../user/infrastructure/persistence/models'; +export * from '../../auth/infrastructure/persistence/models'; +export * from '../../teams/infrastructure/persistence/models'; +export * from '../../projects/infrastructure/persistence/models'; diff --git a/src/shared/entities/schema.ts b/src/shared/entities/schema.ts new file mode 100644 index 0000000..8a9bcc9 --- /dev/null +++ b/src/shared/entities/schema.ts @@ -0,0 +1,3 @@ +import { pgSchema, type PgSchema } from 'drizzle-orm/pg-core'; + +export const baseSchema: PgSchema = pgSchema('base'); diff --git a/src/shared/error/exception.ts b/src/shared/error/exception.ts new file mode 100644 index 0000000..640645f --- /dev/null +++ b/src/shared/error/exception.ts @@ -0,0 +1,18 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; + +interface IDetailsOptions { + target?: string; + [key: string]: any; +} + +export interface IErrorOptions { + code: string; + message: string; + details?: IDetailsOptions[]; +} + +export class BaseException extends HttpException { + constructor(options: IErrorOptions, status: HttpStatus) { + super(options, status); + } +} diff --git a/src/shared/error/filter.ts b/src/shared/error/filter.ts new file mode 100644 index 0000000..b06dc7f --- /dev/null +++ b/src/shared/error/filter.ts @@ -0,0 +1,244 @@ +import { + type ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; +import { ZodValidationException } from 'nestjs-zod'; +import type { FastifyReply, FastifyRequest } from 'fastify'; +import { PostgresError } from 'postgres'; +import { BaseException, type IErrorOptions } from './exception'; +import { DrizzleQueryError } from 'drizzle-orm'; +import type { ZodError, ZodIssue } from 'zod/v4'; +import { DATABASE_ERRORS } from './swagger'; + +@Catch() +export class GlobalExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(GlobalExceptionFilter.name); + private isDev = process.env.NODE_ENV === 'development'; + + catch(exception: unknown, host: ArgumentsHost) { + if (exception instanceof ZodValidationException) { + return this.parseZodValidation(exception, host); + } + + if (exception instanceof BaseException) { + return this.parseHttp(exception, host); + } + + if (exception instanceof HttpException) { + return this.parseNestHttp(exception, host); + } + + if (exception instanceof DrizzleQueryError) { + return this.parseDatabase(exception, host); + } + + return this.handleUnknownError(exception, host); + } + + private parseZodValidation = async (exception: ZodValidationException, host: ArgumentsHost) => { + const { request, response } = this.getCtxBase(host); + const status = exception.getStatus(); + + const zodError = exception.getZodError() as ZodError; + const issues: ZodIssue[] = zodError.issues || []; + + this.log(exception, host, status, { + validationIssues: issues, + body: request.body, + }); + + return response.status(status).send( + this.formatErrorResponse(request, status, { + code: 'VALIDATION_FAILED', + message: 'Переданные данные не прошли валидацию', + details: issues, + stack: exception.stack, + }), + ); + }; + + private parseDatabase = async (exception: DrizzleQueryError, host: ArgumentsHost) => { + const { request, response } = this.getCtxBase(host); + + const error = + exception.cause instanceof PostgresError + ? exception.cause + : exception instanceof PostgresError + ? exception + : null; + + let status = 500; + let message = exception.message || 'Database operation failed'; + const errorCode = 'DATABASE_ERROR'; + + if (error) { + const mapping = DATABASE_ERRORS[error.code]; + if (mapping) { + status = mapping.code; + message = mapping.msg; + } + } + + this.log(exception, host, status, { + dbCode: error?.code, + dbTable: error?.table_name, + dbDetail: error?.detail, + query: this.isDev ? exception.query : undefined, + }); + + return response.status(status).send( + this.formatErrorResponse(request, status, { + code: errorCode, + message, + details: error?.detail ? [{ target: error.detail }] : [], + stack: exception.stack, + service: 'postgres', + }), + ); + }; + + private parseHttp = async (exception: BaseException, host: ArgumentsHost) => { + const { request, response } = this.getCtxBase(host); + const status = exception.getStatus(); + + const error = exception.getResponse() as IErrorOptions; + + this.log(exception, host, status, { + errorCode: error.code, + details: error.details, + type: 'BUSINESS_EXCEPTION', + }); + + return response.status(status).send( + this.formatErrorResponse(request, status, { + code: error.code, + message: error.message || exception.message, + details: error.details || [], + stack: exception.stack, + }), + ); + }; + + private parseNestHttp = async (exception: HttpException, host: ArgumentsHost) => { + const { request, response } = this.getCtxBase(host); + const status = exception.getStatus(); + const res = exception.getResponse(); + + const message = + typeof res === 'object' && res['message'] ? res['message'] : exception.message; + + const code = + typeof res === 'object' && res['error'] + ? res['error'].toUpperCase().replace(/\s+/g, '_') + : 'HTTP_EXCEPTION'; + + this.log(exception, host, status, { + httpCode: code, + nestResponse: res, + type: 'NEST_HTTP_EXCEPTION', + }); + + return response.status(status).send( + this.formatErrorResponse(request, status, { + code, + message, + stack: exception.stack, + details: [], + }), + ); + }; + + private handleUnknownError(exception: any, host: ArgumentsHost) { + const { request, response } = this.getCtxBase(host); + const status = HttpStatus.INTERNAL_SERVER_ERROR; + + this.log(exception, host, status, { type: 'UNKNOWN_SERVER_ERROR' }); + + return response.status(status).send( + this.formatErrorResponse(request, status, { + code: 'INTERNAL_SERVER_ERROR', + message: 'Произошла непредвиденная ошибка на сервере', + details: [], + stack: exception?.stack, + }), + ); + } + + private formatErrorResponse( + request: FastifyRequest, + status: number, + data: { code: string; message: string; details: any[]; stack?: string; service?: string }, + ) { + const requestId = request.id ?? request.headers['x-request-id']; + + return { + success: false, + error: { + code: data.code, + message: data.message, + retryable: status >= 500, + }, + details: data.details, + meta: { + service: data.service ?? 'gateway', + request: { + requestId, + path: request.url, + method: request.method, + ip: request.ip, + }, + timestamp: new Date().toISOString(), + ...(this.isDev && { + debug: { + stack: data.stack, + }, + }), + }, + }; + } + + private getCtxBase(host: ArgumentsHost) { + const ctx = host.switchToHttp(); + return { + response: ctx.getResponse(), + request: ctx.getRequest(), + }; + } + + private log( + exception: any, + host: ArgumentsHost, + status: number, + extraData: Record = {}, + ) { + const { request } = this.getCtxBase(host); + + const logData = { + request_id: request.id || request.headers['x-request-id'] || 'unknown', + triggered_by: 'filter_exception', + type: 'error', + method: request.method ?? 'Unknown', + url: request.url, + path: request.url.split('?')[0], + status_code: status, + ip: request.ip, + user_agent: request.headers['user-agent'] || 'unknown', + controller: 'Unknown', + handler: 'Unknown', + stack: exception instanceof Error ? exception.stack : undefined, + error_details: extraData, + }; + + const message = `Exception Filter: ${logData.method} ${logData.path} | ${status} | ${exception?.message || 'Unknown Error'}`; + + if (status >= 500) { + this.logger.error(message, logData); + } else { + this.logger.warn(message, logData); + } + } +} diff --git a/src/shared/error/index.ts b/src/shared/error/index.ts new file mode 100644 index 0000000..9ddc922 --- /dev/null +++ b/src/shared/error/index.ts @@ -0,0 +1,3 @@ +export * from './swagger'; +export * from './filter'; +export * from './exception'; diff --git a/src/shared/error/schema.ts b/src/shared/error/schema.ts new file mode 100644 index 0000000..e064c5c --- /dev/null +++ b/src/shared/error/schema.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { createZodDto } from 'nestjs-zod'; + +const ErrorDetailSchema = z.object({ + field: z.string().describe('Путь к полю (например, "user.email")'), + message: z.string().describe('Сообщение об ошибке'), + code: z.string().describe('Машиночитаемый код (например, "too_short")'), +}); + +const ErrorMetaSchema = z.object({ + service: z.string().default('gateway').describe('Имя микросервиса'), + request: z.object({ + requestId: z.string().describe('Trace ID для логов'), + path: z.string().describe('URL эндпоинта'), + method: z.string().describe('HTTP метод'), + ip: z.string().optional().describe('IP клиента'), + }), + timestamp: z.string().datetime().describe('Время ошибки ISO 8601'), + debug: z + .object({ + stack: z.string().optional().describe('Стек вызовов (только в Dev)'), + }) + .optional(), +}); + +export const GlobalErrorSchema = z.object({ + success: z.literal(false).default(false), + error: z.object({ + code: z.string().describe('Бизнес-код ошибки'), + message: z.string().describe('Описание для пользователя'), + retryable: z.boolean().describe('Флаг возможности повтора'), + }), + details: z.array(ErrorDetailSchema).optional(), + meta: ErrorMetaSchema, +}); + +export class GlobalErrorResponse extends createZodDto(GlobalErrorSchema) {} diff --git a/src/shared/error/swagger.ts b/src/shared/error/swagger.ts new file mode 100644 index 0000000..57489c9 --- /dev/null +++ b/src/shared/error/swagger.ts @@ -0,0 +1,66 @@ +import { ApiResponse, getSchemaPath } from '@nestjs/swagger'; +import { GlobalErrorResponse } from './schema'; +import { applyDecorators } from '@nestjs/common'; + +export const ApiErrorResponse = ( + status: number, + bizCode: string, + description: string, + details?: { field: string; message: string; code: string }[], +) => + ApiResponse({ + status, + description, + schema: { + allOf: [{ $ref: getSchemaPath(GlobalErrorResponse.Output) }], + example: { + success: false, + error: { + code: bizCode, + message: description, + retryable: status >= 500, + }, + details: details || [], + meta: { + request: { + requestId: 'req-clj1abc230000jk78', + path: '/api/v1/...', + method: 'POST', + ip: '127.0.0.1', + }, + timestamp: new Date().toISOString(), + service: 'main-backend', + }, + }, + }, + }); + +export const ApiBadRequest = (description: string = 'Некорректный запрос') => + applyDecorators(ApiErrorResponse(400, 'BAD_REQUEST', description)); + +export const ApiUnauthorized = (description: string = 'Сессия истекла или токен не валиден') => + applyDecorators(ApiErrorResponse(401, 'AUTH_REQUIRED', description)); + +export const ApiForbidden = (description: string = 'У вас недостаточно прав для этого действия') => + applyDecorators(ApiErrorResponse(403, 'ACCESS_DENIED', description)); + +export const ApiNotFound = (description: string = 'Ресурс не найден') => + applyDecorators(ApiErrorResponse(404, 'NOT_FOUND', description)); + +export const ApiValidationError = ( + description: string = 'Ошибка валидации входных данных', + fields: any[] = [], +) => applyDecorators(ApiErrorResponse(400, 'VALIDATION_FAILED', description, fields)); + +export const ApiConflict = (description: string = 'Ресурс уже существует') => + applyDecorators(ApiErrorResponse(409, 'CONFLICT', description)); + +export const DATABASE_ERRORS: Record = { + '23505': { code: 409, msg: 'Запись с таким значением уже существует (дубликат).' }, + '23503': { code: 409, msg: 'Ошибка внешнего ключа: связанная запись не найдена.' }, + '22P02': { code: 400, msg: 'Неверный формат данных (например, некорректный UUID).' }, + '23514': { code: 400, msg: 'Нарушено ограничение проверки (check constraint).' }, + '23502': { code: 400, msg: 'Отсутствует обязательное поле.' }, + '08006': { code: 500, msg: 'Ошибка соединения с базой данных.' }, + '40001': { code: 500, msg: 'Конфликт транзакции. Пожалуйста, повторите попытку.' }, +}; diff --git a/src/shared/guards/bearer.guard.ts b/src/shared/guards/bearer.guard.ts new file mode 100644 index 0000000..a7b2b02 --- /dev/null +++ b/src/shared/guards/bearer.guard.ts @@ -0,0 +1,69 @@ +import { type ExecutionContext, HttpStatus, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; +import { IS_PUBLIC_KEY } from '@shared/decorators'; +import { BaseException } from '@shared/error'; +import type { JwtPayload } from '@shared/types'; +import type { FastifyRequest } from 'fastify'; + +@Injectable() +export class BearerAuthGuard extends AuthGuard('bearer') { + constructor(private reflector: Reflector) { + super(); + } + + async canActivate(context: ExecutionContext): Promise { + try { + return super.canActivate(context) as Promise; + } catch (e) { + if (this.isPublicOrHasToken(context)) { + return true; + } + + throw e; + } + } + + handleRequest( + err: unknown, + user: TUser, + info: unknown, + context: ExecutionContext, + ): TUser { + if (user) { + return user; + } + + if (this.isPublicOrHasToken(context)) { + return null; + } + + throw new BaseException( + { + code: 'AUTH_FAILED', + message: 'Доступ запрещен: требуется валидный токен авторизации', + details: this.getAuthDetails(err, info), + }, + HttpStatus.UNAUTHORIZED, + ); + } + + private isPublicOrHasToken(context: ExecutionContext): boolean { + const { query } = context + .switchToHttp() + .getRequest>(); + + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + return !!(isPublic || query.token); + } + + private getAuthDetails(err: unknown, info: any) { + const message = info?.message || (err instanceof Error ? err.message : null); + + return message ? [{ target: 'auth', reason: message }] : []; + } +} diff --git a/src/shared/guards/cookie.guard.ts b/src/shared/guards/cookie.guard.ts new file mode 100644 index 0000000..89300ab --- /dev/null +++ b/src/shared/guards/cookie.guard.ts @@ -0,0 +1,27 @@ +import { HttpStatus, Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { BaseException } from '@shared/error'; +import type { JwtPayload } from '@shared/types'; + +@Injectable() +export class CookieAuthGuard extends AuthGuard('cookie') { + handleRequest(err: unknown, user: TUser, info: any): TUser { + if (err || !user) { + throw new BaseException( + { + code: 'INVALID_REFRESH_TOKEN', + message: 'Refresh токен невалиден или отсутствует', + details: [ + { + target: 'auth', + reason: info?.message || 'Token verification failed', + }, + ], + }, + HttpStatus.UNAUTHORIZED, + ); + } + + return user; + } +} diff --git a/src/shared/guards/index.ts b/src/shared/guards/index.ts new file mode 100644 index 0000000..20ada34 --- /dev/null +++ b/src/shared/guards/index.ts @@ -0,0 +1,2 @@ +export { BearerAuthGuard } from './bearer.guard'; +export { CookieAuthGuard } from './cookie.guard'; diff --git a/src/shared/media/controller/index.ts b/src/shared/media/controller/index.ts new file mode 100644 index 0000000..e61a17a --- /dev/null +++ b/src/shared/media/controller/index.ts @@ -0,0 +1,17 @@ +import { Post } from '@nestjs/common'; +import { ApiBaseController, GetUserId } from '@shared/decorators'; +import { UploadMediaDto } from '../dtos'; +import { MediaService } from '../media.service'; +import { UploadMediaSwagger } from './swagger'; +import { ExtractMediaReq } from '../decorators'; + +@ApiBaseController('upload', 'Upload Media', true) +export class MediaController { + constructor(private readonly service: MediaService) {} + + @Post() + @UploadMediaSwagger() + async upload(@ExtractMediaReq() dto: UploadMediaDto, @GetUserId() userId: string) { + return this.service.upload(dto, userId); + } +} diff --git a/src/shared/media/controller/swagger.ts b/src/shared/media/controller/swagger.ts new file mode 100644 index 0000000..175364c --- /dev/null +++ b/src/shared/media/controller/swagger.ts @@ -0,0 +1,26 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiBody, ApiConsumes, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { UploadMediaDto } from '../dtos'; +import { ApiUnauthorized, ApiValidationError } from '@shared/error'; +import { ActionResponse } from '@shared/dtos'; + +export const UploadMediaSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Загрузить медиа-файл', + description: + 'Загружает файл в S3 и инициирует фоновую задачу по обновлению ссылки в БД.', + }), + ApiConsumes('multipart/form-data'), + ApiBody({ + description: 'Файл для загрузки и метаданные', + type: UploadMediaDto.Output, + }), + ApiResponse({ + status: 201, + description: 'Файл успешно загружен и принят в обработку', + type: ActionResponse.Output, + }), + ApiValidationError('Неверный формат файла или отсутствуют обязательные поля'), + ApiUnauthorized(), + ); diff --git a/src/shared/media/decorators/extract-media-req.decorator.ts b/src/shared/media/decorators/extract-media-req.decorator.ts new file mode 100644 index 0000000..d06bfbc --- /dev/null +++ b/src/shared/media/decorators/extract-media-req.decorator.ts @@ -0,0 +1,66 @@ +import { createParamDecorator, type ExecutionContext, HttpStatus } from '@nestjs/common'; +import type { FastifyRequest } from 'fastify'; +import { IMAGE_MIME_TYPES } from '../../constants'; +import { BaseException } from '@shared/error'; + +export const ExtractMediaReq = createParamDecorator( + async ( + { allowedMimetypes = IMAGE_MIME_TYPES }: { allowedMimetypes?: string[] } = {}, + ctx: ExecutionContext, + ) => { + const req = ctx.switchToHttp().getRequest(); + + if (!req.isMultipart()) { + throw new BaseException( + { + code: 'INVALID_CONTENT_TYPE', + message: 'Ожидался multipart/form-data запрос', + details: [ + { target: 'header', message: 'Content-Type must be multipart/form-data' }, + ], + }, + HttpStatus.UNPROCESSABLE_ENTITY, + ); + } + + const file = await req.file(); + if (!file) { + throw new BaseException( + { + code: 'FILE_NOT_FOUND', + message: 'Файл не был передан в запросе', + }, + HttpStatus.BAD_REQUEST, + ); + } + + const buffer = await file.toBuffer(); + + if (allowedMimetypes?.length && !allowedMimetypes.includes(file.mimetype)) { + throw new BaseException( + { code: 'INVALID_FILE_TYPE', message: 'Недопустимый формат файла' }, + HttpStatus.UNSUPPORTED_MEDIA_TYPE, + ); + } + + const fields: Record = {}; + + for (const key in file.fields) { + if (key === 'file') continue; + + const field = file.fields[key]; + if (field && !Array.isArray(field) && 'value' in field) { + fields[key] = String(field.value); + } + } + + return { + file: { + filename: file.filename, + mimetype: file.mimetype, + buffer, + }, + ...fields, + }; + }, +); diff --git a/src/shared/media/decorators/index.ts b/src/shared/media/decorators/index.ts new file mode 100644 index 0000000..2ed4b48 --- /dev/null +++ b/src/shared/media/decorators/index.ts @@ -0,0 +1 @@ +export { ExtractMediaReq } from './extract-media-req.decorator'; diff --git a/src/shared/media/dtos/index.ts b/src/shared/media/dtos/index.ts new file mode 100644 index 0000000..f49c3fe --- /dev/null +++ b/src/shared/media/dtos/index.ts @@ -0,0 +1 @@ +export * from './upload-file.dto'; diff --git a/src/shared/media/dtos/upload-file.dto.ts b/src/shared/media/dtos/upload-file.dto.ts new file mode 100644 index 0000000..72ae1bb --- /dev/null +++ b/src/shared/media/dtos/upload-file.dto.ts @@ -0,0 +1,31 @@ +import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; + +const FileSchema = z + .object({ + buffer: z.any().describe('Бинарные данные файла'), + filename: z + .string() + .regex(/\.(jpg|jpeg|png|webp)$/i, 'Допустимы только изображения') + .describe('Имя файла с расширением'), + mimetype: z.enum(['image/jpeg', 'image/png', 'image/webp']).describe('MIME-тип файла'), + }) + .describe('Объект загруженного файла'); + +export const UploadMediaSchema = z.object({ + context: z + .enum(['user.avatar', 'team.avatar', 'team.banner'], { + error: 'Выберите корректный контекст: user.avatar, team.avatar или team.banner', + }) + .describe('Контекст загрузки (тип сущности и тип медиа)'), + file: FileSchema, + slug: z + .string({ + error: 'Slug должен быть строкой', + }) + .min(1, 'Slug не может быть пустым') + .optional() + .describe('Уникальный идентификатор (slug) команды. Обязателен для контекстов team.*'), +}); + +export class UploadMediaDto extends createZodDto(UploadMediaSchema) {} diff --git a/src/shared/media/index.ts b/src/shared/media/index.ts new file mode 100644 index 0000000..9e463f3 --- /dev/null +++ b/src/shared/media/index.ts @@ -0,0 +1,3 @@ +export { MediaModule } from './media.module'; +export * from './interfaces/media.interface'; +export * from './media.constant'; diff --git a/src/shared/media/interfaces/media.interface.ts b/src/shared/media/interfaces/media.interface.ts new file mode 100644 index 0000000..471f7df --- /dev/null +++ b/src/shared/media/interfaces/media.interface.ts @@ -0,0 +1,19 @@ +export type MediaEntityType = 'team' | 'user'; + +export interface UpdateMediaUser { + entity: { + type: 'user'; + id: string; + }; + path: string; +} + +export interface UpdateMediaTeam { + entity: { + type: 'team'; + slug: string; + }; + path: string; + type: 'avatar' | 'banner'; + initiatorId: string; +} diff --git a/src/shared/media/media.constant.ts b/src/shared/media/media.constant.ts new file mode 100644 index 0000000..04efa24 --- /dev/null +++ b/src/shared/media/media.constant.ts @@ -0,0 +1,24 @@ +export const MEDIA_QUEUES = { + RESIZE: 'RESIZE', + SAVE_ENTITY: 'SAVE_ENTITY', +}; + +export const MEDIA_JOBS = { + RESIZE_IMAGES: 'RESIZE_IMAGES', + UPDATE_USER_AVATAR: 'UPDATE_USER_AVATAR', + UPDATE_TEAM_MEDIA: 'UPDATE_TEAM_MEDIA', +}; +export const MEDIA_FLOW = 'MEDIA_FLOW'; + +export const MEDIA_SPECS = { + avatar: [ + { name: 'sm', width: 64, height: 64, quality: 80 }, + { name: 'md', width: 256, height: 256, quality: 85 }, + { name: 'lg', width: 512, height: 512, quality: 90 }, + ], + banner: [ + { name: 'sm', width: 640, height: 360, fit: 'fit-in' }, + { name: 'md', width: 1280, height: 720, fit: 'fit-in' }, + { name: 'lg', width: 1920, height: 1080, fit: 'fit-in' }, + ], +} as const; diff --git a/src/shared/media/media.module.ts b/src/shared/media/media.module.ts new file mode 100644 index 0000000..79eea26 --- /dev/null +++ b/src/shared/media/media.module.ts @@ -0,0 +1,63 @@ +import { Module } from '@nestjs/common'; +import { MediaService } from './media.service'; +import { S3Module } from '@libs/s3'; +import { ConfigService } from '@nestjs/config'; +import { MediaController } from './controller'; +import { MEDIA_FLOW, MEDIA_QUEUES } from './media.constant'; +import { BullModule } from '@nestjs/bullmq'; +import { ImagorModule } from '@libs/imagor'; +import { BullBoardModule } from '@bull-board/nestjs'; +import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; +import { MediaProcessor } from './workers/media.worker'; + +@Module({ + imports: [ + S3Module.registerAsync({ + inject: [ConfigService], + global: true, + useFactory: (cfg: ConfigService) => ({ + bucket: cfg.getOrThrow('S3_BUCKET_NAME'), + connection: { + endpoint: cfg.getOrThrow('S3_ENDPOINT'), + region: cfg.getOrThrow('S3_REGION'), + credentials: { + accessKeyId: cfg.getOrThrow('S3_ACCESS_KEY'), + secretAccessKey: cfg.getOrThrow('S3_SECRET_KEY'), + }, + }, + config: { + forcePathStyle: true, + connectTimeout: 2000, + requestTimeout: 5000, + maxAttempts: 3, + }, + }), + }), + ImagorModule.forRootAsync({ + inject: [ConfigService], + useFactory: (cfg: ConfigService) => ({ + url: cfg.getOrThrow('IMAGOR_URL'), + secret: cfg.getOrThrow('IMAGOR_SECRET'), + debug: true, + filters: { format: 'webp', smart: true, strip_icc: true }, + }), + }), + BullModule.registerQueue({ name: MEDIA_QUEUES.RESIZE }, { name: MEDIA_QUEUES.SAVE_ENTITY }), + BullModule.registerFlowProducer({ + name: MEDIA_FLOW, + }), + BullBoardModule.forFeature( + { + name: MEDIA_QUEUES.RESIZE, + adapter: BullMQAdapter, + }, + { + name: MEDIA_QUEUES.SAVE_ENTITY, + adapter: BullMQAdapter, + }, + ), + ], + controllers: [MediaController], + providers: [MediaProcessor, MediaService], +}) +export class MediaModule {} diff --git a/src/shared/media/media.service.ts b/src/shared/media/media.service.ts new file mode 100644 index 0000000..c2f011b --- /dev/null +++ b/src/shared/media/media.service.ts @@ -0,0 +1,110 @@ +import { HttpStatus, Injectable } from '@nestjs/common'; +import { S3Service } from '@libs/s3'; +import type { UploadMediaDto } from './dtos'; +import { BaseException } from '@shared/error'; +import { FlowProducer } from 'bullmq'; +import { InjectFlowProducer } from '@nestjs/bullmq'; +import { MEDIA_STRATEGIES } from './strategies'; +import { MEDIA_FLOW, MEDIA_JOBS, MEDIA_QUEUES } from './media.constant'; +import { MediaDispatchStrategy } from './strategies/media.strategy'; +import { extname } from 'path'; + +@Injectable() +export class MediaService { + constructor( + @InjectFlowProducer(MEDIA_FLOW) + private readonly flow: FlowProducer, + private readonly s3: S3Service, + ) {} + + public upload = async (dto: UploadMediaDto, userId: string) => { + const { context, file } = dto; + + const strategy = this.getStrategy(context); + const { folder, fileName } = this.generateStoragePath(context, userId, file.filename); + + try { + const originalUrl = await this.s3.upload(file.buffer, { + mimetype: file.mimetype, + original: file.filename, + path: { folder, key: fileName }, + }); + + await this.enqueueMediaFlow(strategy, dto, userId, originalUrl); + + return { + success: true, + message: 'Изменения вступят в силу после завершения фоновой обработки', + }; + } catch (error) { + this.handleError(error); + } + }; + + private generateStoragePath(context: string, userId: string, originalName: string) { + const contextPath = context.replace(/\./g, '/'); + const extension = extname(originalName); + + return { + folder: `${contextPath}/${Date.now()}-${userId}`, + fileName: `original${extension}`, + }; + } + + private async enqueueMediaFlow( + strategy: MediaDispatchStrategy, + dto: UploadMediaDto, + userId: string, + url: string, + ) { + const payload = strategy.createPayload(dto, userId, url); + + return this.flow.add({ + name: MEDIA_JOBS.RESIZE_IMAGES, + queueName: MEDIA_QUEUES.RESIZE, + data: { userId, original: url, context: dto.context }, + opts: this.getJobOptions(5, 'fixed'), + children: [ + { + name: strategy.jobName, + queueName: MEDIA_QUEUES.SAVE_ENTITY, + data: payload, + opts: { ...this.getJobOptions(3, 'exponential'), failParentOnFailure: true }, + }, + ], + }); + } + + private getJobOptions(attempts: number, backoffType: 'fixed' | 'exponential') { + return { + attempts, + backoff: { type: backoffType, delay: backoffType === 'fixed' ? 2000 : 1000 }, + // removeOnComplete: true, + removeOnFail: false, + }; + } + + private getStrategy(context: string) { + const strategy = MEDIA_STRATEGIES[context]; + if (!strategy) { + throw new BaseException( + { code: 'STRATEGY_NOT_FOUND', message: `No strategy for ${context}` }, + HttpStatus.BAD_REQUEST, + ); + } + return strategy; + } + + private handleError(error: unknown): never { + if (error instanceof BaseException) throw error; + + throw new BaseException( + { + code: 'MEDIA_SAVE_FAILED', + message: 'Ошибка при сохранении медиа-данных', + details: [{ reason: error instanceof Error ? error.message : 'Unknown error' }], + }, + HttpStatus.BAD_REQUEST, + ); + } +} diff --git a/src/shared/media/strategies/index.ts b/src/shared/media/strategies/index.ts new file mode 100644 index 0000000..4ee05c9 --- /dev/null +++ b/src/shared/media/strategies/index.ts @@ -0,0 +1,8 @@ +import { UserAvatarStrategy } from './user-avatar.strategy'; +import { TeamMediaStrategy } from './team-media.strategy'; + +export const MEDIA_STRATEGIES = { + 'user.avatar': new UserAvatarStrategy(), + 'team.avatar': new TeamMediaStrategy(), + 'team.banner': new TeamMediaStrategy(), +} as const; diff --git a/src/shared/media/strategies/media.strategy.ts b/src/shared/media/strategies/media.strategy.ts new file mode 100644 index 0000000..c94c1ab --- /dev/null +++ b/src/shared/media/strategies/media.strategy.ts @@ -0,0 +1,4 @@ +export abstract class MediaDispatchStrategy { + abstract readonly jobName: string; + abstract createPayload(dto: any, userId: string, path: string): any; +} diff --git a/src/shared/media/strategies/team-media.strategy.ts b/src/shared/media/strategies/team-media.strategy.ts new file mode 100644 index 0000000..6f7bcf2 --- /dev/null +++ b/src/shared/media/strategies/team-media.strategy.ts @@ -0,0 +1,17 @@ +import type { UploadMediaDto } from '../dtos'; +import type { UpdateMediaTeam } from '../interfaces/media.interface'; +import { MEDIA_JOBS } from '../media.constant'; +import { MediaDispatchStrategy } from './media.strategy'; + +export class TeamMediaStrategy implements MediaDispatchStrategy { + jobName: string = MEDIA_JOBS.UPDATE_TEAM_MEDIA; + + createPayload(dto: UploadMediaDto, userId: string, path: string): UpdateMediaTeam { + return { + entity: { type: 'team', slug: dto.slug! }, + type: dto.context.split('.').pop() as 'avatar' | 'banner', + initiatorId: userId, + path, + }; + } +} diff --git a/src/shared/media/strategies/user-avatar.strategy.ts b/src/shared/media/strategies/user-avatar.strategy.ts new file mode 100644 index 0000000..20ccdc7 --- /dev/null +++ b/src/shared/media/strategies/user-avatar.strategy.ts @@ -0,0 +1,15 @@ +import type { UploadMediaDto } from '../dtos'; +import type { UpdateMediaUser } from '../interfaces/media.interface'; +import { MEDIA_JOBS } from '../media.constant'; +import { MediaDispatchStrategy } from './media.strategy'; + +export class UserAvatarStrategy implements MediaDispatchStrategy { + jobName: string = MEDIA_JOBS.UPDATE_USER_AVATAR; + + createPayload(_d: UploadMediaDto, userId: string, path: string): UpdateMediaUser { + return { + entity: { type: 'user', id: userId }, + path, + }; + } +} diff --git a/src/shared/media/workers/index.ts b/src/shared/media/workers/index.ts new file mode 100644 index 0000000..4a62243 --- /dev/null +++ b/src/shared/media/workers/index.ts @@ -0,0 +1 @@ +export { MediaProcessor } from './media.worker'; diff --git a/src/shared/media/workers/media.worker.ts b/src/shared/media/workers/media.worker.ts new file mode 100644 index 0000000..3f5f8b0 --- /dev/null +++ b/src/shared/media/workers/media.worker.ts @@ -0,0 +1,63 @@ +import { ImagorService } from '@libs/imagor'; +import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { MEDIA_JOBS, MEDIA_QUEUES, MEDIA_SPECS } from '../media.constant'; +import { Job } from 'bullmq'; +import { S3Service } from '@libs/s3'; +import { dirname } from 'path'; + +@Processor(MEDIA_QUEUES.RESIZE) +export class MediaProcessor extends WorkerHost { + constructor( + private readonly imagor: ImagorService, + private readonly s3: S3Service, + ) { + super(); + } + + async process(job: Job<{ original: string; context: string; userId: string; slug?: string }>) { + if (job.name !== MEDIA_JOBS.RESIZE_IMAGES) return; + + const { original: originalFilePath, context } = job.data; + + try { + await job.updateProgress(5); + + const type = context.includes('banner') ? 'banner' : 'avatar'; + const resizeSpecs = MEDIA_SPECS[type]; + const targetFolder = dirname(originalFilePath); + + const progressStep = Math.floor(90 / resizeSpecs.length); + + for (let i = 0; i < resizeSpecs.length; i++) { + const { name, ...dimensions } = resizeSpecs[i]; + const targetFileName = `${name}.webp`; + + const processedImage = await this.imagor.get(`/${originalFilePath}`, dimensions); + + const uploadedPath = await this.s3.upload(processedImage, { + original: targetFileName, + mimetype: 'image/webp', + path: { + folder: targetFolder, + key: targetFileName, + }, + }); + + await job.log(`[Variant:${name}] Saved to: ${uploadedPath}`); + await job.updateProgress(5 + progressStep * (i + 1)); + } + + await job.updateProgress(100); + + return { + original: originalFilePath, + folder: targetFolder, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + await job.log(`Error during resizing: ${errorMessage}`); + + throw error; + } + } +} diff --git a/src/shared/schemas/index.ts b/src/shared/schemas/index.ts new file mode 100644 index 0000000..b3c8aa4 --- /dev/null +++ b/src/shared/schemas/index.ts @@ -0,0 +1 @@ +export * from './pagination-response.schema'; diff --git a/src/shared/schemas/pagination-response.schema.ts b/src/shared/schemas/pagination-response.schema.ts new file mode 100644 index 0000000..0d3fcca --- /dev/null +++ b/src/shared/schemas/pagination-response.schema.ts @@ -0,0 +1,29 @@ +import { z } from 'zod/v4'; + +export const paginationResponseSchema = z.object({ + hasNextPage: z + .boolean() + .describe('Флаг наличия следующей страницы. True, если текущая страница не последняя.'), + hasPrevPage: z + .boolean() + .describe('Флаг наличия предыдущей страницы. True, если текущая страница больше первой.'), + total: z + .number() + .int() + .nonnegative() + .describe('Общее количество записей, соответствующих поисковому запросу/фильтрам.'), + totalPages: z + .number() + .int() + .nonnegative() + .describe('Общее количество страниц, рассчитанное на основе limit.'), + page: z.number().int().positive().describe('Номер текущей страницы (начиная с 1).'), + limit: z.number().int().positive().describe('Количество элементов на одну страницу.'), +}); + +export const createPaginationSchema = (itemSchema: T) => { + return z.object({ + data: z.array(itemSchema), + meta: paginationResponseSchema, + }); +}; diff --git a/src/shared/types/fastify.d.ts b/src/shared/types/fastify.d.ts new file mode 100644 index 0000000..9c77358 --- /dev/null +++ b/src/shared/types/fastify.d.ts @@ -0,0 +1,7 @@ +import type { JwtPayload } from './jwt-payload'; + +declare module 'fastify' { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts new file mode 100644 index 0000000..9a3c79a --- /dev/null +++ b/src/shared/types/index.ts @@ -0,0 +1 @@ +export type { JwtPayload } from './jwt-payload'; diff --git a/src/shared/types/jwt-payload.ts b/src/shared/types/jwt-payload.ts new file mode 100644 index 0000000..c788698 --- /dev/null +++ b/src/shared/types/jwt-payload.ts @@ -0,0 +1,8 @@ +export interface JwtPayload { + sub: string; + email: string; + role: string; + iss: string; + aud: string; + jti: string; +} diff --git a/src/shared/utils/image-builder.util.ts b/src/shared/utils/image-builder.util.ts new file mode 100644 index 0000000..77e83ce --- /dev/null +++ b/src/shared/utils/image-builder.util.ts @@ -0,0 +1,17 @@ +import { dirname } from 'path'; + +export class ImageHelper { + public static buildResponsiveUrls(cdn: string, path?: string | null) { + if (!path) return null; + + const folder = dirname(path); + const base = `${cdn}/${folder}`; + + return { + small: `${base}/sm.webp`, + medium: `${base}/md.webp`, + large: `${base}/lg.webp`, + original: `${cdn}/${path}`, + }; + } +} diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts new file mode 100644 index 0000000..a946ec6 --- /dev/null +++ b/src/shared/utils/index.ts @@ -0,0 +1 @@ +export { ImageHelper } from './image-builder.util'; diff --git a/src/teams/application/controller/index.ts b/src/teams/application/controller/index.ts new file mode 100644 index 0000000..5e1b219 --- /dev/null +++ b/src/teams/application/controller/index.ts @@ -0,0 +1,5 @@ +export { MeController } from './me/controller'; +export { TeamsController } from './teams/controller'; +export { TeamsMembersController } from './members/controller'; +export { TeamsSettingsController } from './settings/controller'; +export { TeamsInvitationsController } from './invitations/controller'; diff --git a/src/teams/application/controller/invitations/controller.ts b/src/teams/application/controller/invitations/controller.ts new file mode 100644 index 0000000..f653a70 --- /dev/null +++ b/src/teams/application/controller/invitations/controller.ts @@ -0,0 +1,71 @@ +import { Body, Get, Param, Delete, Patch, Post } from '@nestjs/common'; +import { ApiBaseController, GetUser, GetUserId } from '@shared/decorators'; +import { + AcceptInviteSwagger, + DeleteTeamInvitationSwagger, + GetTeamInvitationSwagger, + GetTeamInvitationsSwagger, + InviteMemberSwagger, + UpdateTeamInvitationSwagger, +} from './swagger'; +import type { JwtPayload } from '@shared/types'; +import { InviteMemberDto, UpdateInvitationDto } from '../../dtos'; +import { TeamsFacade } from '../../team.facade'; + +@ApiBaseController('teams/:slug/invitations', 'Teams Invitations', true) +export class TeamsInvitationsController { + constructor(private readonly facade: TeamsFacade) {} + + @Get() + @GetTeamInvitationsSwagger() + async getAll(@Param('slug') slug: string, @GetUserId() userId: string) { + return this.facade.getInvitations(slug, userId); + } + + @Get(':code') + @GetTeamInvitationSwagger() + async getOne( + @Param('slug') slug: string, + @Param('code') code: string, + @GetUser() user: JwtPayload, + ) { + return this.facade.getInvitation(slug, code, user.sub, user.email); + } + + @Post() + @InviteMemberSwagger() + async invite( + @Param('slug') slug: string, + @GetUserId() inviterId: string, + @Body() dto: InviteMemberDto, + ) { + return this.facade.invite(slug, inviterId, dto); + } + + @Post(':code/accept') + @AcceptInviteSwagger() + async accept(@Param('code') code: string, @GetUser() user: JwtPayload) { + return this.facade.acceptInvite(code, user.sub, user.email); + } + + @Patch(':code') + @UpdateTeamInvitationSwagger() + async update( + @Param('slug') slug: string, + @Param('code') code: string, + @GetUserId() userId: string, + @Body() dto: UpdateInvitationDto, + ) { + return this.facade.updateInvitation(slug, code, userId, dto); + } + + @Delete(':code') + @DeleteTeamInvitationSwagger() + async decline( + @Param('slug') slug: string, + @Param('code') code: string, + @GetUser() user: JwtPayload, + ) { + return this.facade.declineInvitation(slug, code, user.sub, user.email); + } +} diff --git a/src/teams/application/controller/invitations/swagger.ts b/src/teams/application/controller/invitations/swagger.ts new file mode 100644 index 0000000..30f5dca --- /dev/null +++ b/src/teams/application/controller/invitations/swagger.ts @@ -0,0 +1,154 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { ActionResponse } from '@shared/dtos'; +import { + ApiBadRequest, + ApiConflict, + ApiForbidden, + ApiNotFound, + ApiUnauthorized, + ApiValidationError, +} from '@shared/error'; +import { + InviteMemberDto, + TeamInvitationResponse, + UpdateInvitationDto, + UserInviteResponse, +} from '../../dtos'; + +export const FindInvitesSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Получить список входящих приглашений', + description: + 'Возвращает все активные приглашения в команды, отправленные на email текущего пользователя.', + }), + ApiResponse({ + status: 200, + description: 'Список приглашений успешно получен', + type: [UserInviteResponse.Output], + }), + ApiUnauthorized(), + ); + +export const InviteMemberSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Пригласить пользователя в команду по Email', + description: + 'Создает запись об участнике со статусом "pending".' + + ' Если пользователь уже зарегистрирован — он увидит приглашение в разделе "my/invites".' + + ' Если нет — ему уйдет письмо на указанный Email.', + }), + ApiBody({ type: InviteMemberDto.Output }), + ApiParam({ name: 'slug', description: 'Слаг команды, в которую приглашаем' }), + ApiResponse({ + status: 201, + description: 'Инвайт создан и отправлен', + type: ActionResponse.Output, + }), + ApiValidationError('Некорректный формат Email или роль не поддерживается'), + ApiUnauthorized(), + ApiForbidden(), + ); + +export const AcceptInviteSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Принять приглашение в команду', + description: + 'Активирует участие пользователя в команде по уникальному коду приглашения.' + + ' После успешного принятия статус участника меняется с "pending" на "active".' + + ' Система автоматически связывает текущего авторизованного пользователя с инвайтом через Email.', + }), + ApiParam({ + name: 'code', + description: 'Уникальный код/токен приглашения (из ссылки или письма)', + example: '7df1-4a2b-9e8c', + }), + ApiResponse({ + status: 200, + description: 'Приглашение успешно принято. Пользователь теперь участник команды.', + type: ActionResponse.Output, + }), + ApiBadRequest('Невалидный код, срок действия приглашения истек или оно уже использовано'), + ApiNotFound('Приглашение с таким кодом не найдено'), + ApiConflict('Пользователь уже является участником этой команды'), + ApiUnauthorized(), + ); + +export const GetTeamInvitationsSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Получить список всех приглашений в команду', + description: 'Возвращает все активные инвайты команды. Доступно только owner/admin.', + }), + ApiParam({ name: 'slug', description: 'Слаг команды' }), + ApiResponse({ + status: 200, + description: 'Список приглашений команды', + type: [TeamInvitationResponse.Output], + }), + ApiNotFound('Команда не найдена'), + ApiForbidden('Недостаточно прав (только owner/admin)'), + ApiUnauthorized(), + ); + +export const GetTeamInvitationSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Получить приглашение по коду', + description: + 'Возвращает данные инвайта по коду в рамках команды. Доступно только owner/admin.', + }), + ApiParam({ name: 'slug', description: 'Слаг команды' }), + ApiParam({ name: 'code', description: 'Код инвайта' }), + ApiResponse({ + status: 200, + description: 'Инвайт найден', + type: TeamInvitationResponse.Output, + }), + ApiNotFound('Инвайт или команда не найдены'), + ApiForbidden('Недостаточно прав (только owner/admin)'), + ApiUnauthorized(), + ); + +export const UpdateTeamInvitationSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Обновить приглашение (только роль)', + description: + 'Позволяет изменить только поле role у существующего инвайта. TTL сохраняется.', + }), + ApiParam({ name: 'slug', description: 'Слаг команды' }), + ApiParam({ name: 'code', description: 'Код инвайта' }), + ApiBody({ type: UpdateInvitationDto.Output }), + ApiResponse({ + status: 200, + description: 'Инвайт обновлён', + type: TeamInvitationResponse.Output, + }), + ApiValidationError(), + ApiNotFound('Инвайт или команда не найдены'), + ApiForbidden('Недостаточно прав (только owner/admin)'), + ApiUnauthorized(), + ); + +export const DeleteTeamInvitationSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Удалить приглашение', + description: + 'Удаляет инвайт и чистит индексы в Redis (team:invites и user:invites). Доступно только owner/admin.', + }), + ApiParam({ name: 'slug', description: 'Слаг команды' }), + ApiParam({ name: 'code', description: 'Код инвайта' }), + ApiResponse({ + status: 200, + description: 'Инвайт удалён', + type: ActionResponse.Output, + }), + ApiNotFound('Инвайт или команда не найдены'), + ApiForbidden('Недостаточно прав (только owner/admin)'), + ApiUnauthorized(), + ); diff --git a/src/teams/application/controller/me/controller.ts b/src/teams/application/controller/me/controller.ts new file mode 100644 index 0000000..7c1098b --- /dev/null +++ b/src/teams/application/controller/me/controller.ts @@ -0,0 +1,23 @@ +import { ApiBaseController, GetUser, GetUserId } from '@shared/decorators'; +import { Get, Query } from '@nestjs/common'; +import { FindInvitesSwagger, FindTeamsSwagger } from './swagger'; +import type { JwtPayload } from '@shared/types'; +import { TeamsFacade } from '../../team.facade'; + +@ApiBaseController('users/me', 'Account Teams', true) +export class MeController { + constructor(private readonly facade: TeamsFacade) {} + + @Get('teams') + @FindTeamsSwagger() + // TODO: ADD TO QUERY DTO + async findMyTeams(@GetUserId() userId: string, @Query() query: any) { + return this.facade.getMyTeams(userId, query); + } + + @Get('invites') + @FindInvitesSwagger() + async findMyInvites(@GetUser() user: JwtPayload) { + return this.facade.getMyInvites(user.email); + } +} diff --git a/src/teams/application/controller/me/swagger.ts b/src/teams/application/controller/me/swagger.ts new file mode 100644 index 0000000..8081737 --- /dev/null +++ b/src/teams/application/controller/me/swagger.ts @@ -0,0 +1,34 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { ApiUnauthorized } from '@shared/error'; +import { UserTeamResponse, UserInviteResponse } from '../../dtos'; + +export const FindTeamsSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Получить список команд пользователя', + description: + 'Возвращает все команды, в которых текущий пользователь является участником или владельцем.', + }), + ApiResponse({ + status: 200, + description: 'Список команд получен', + type: [UserTeamResponse.Output], + }), + ApiUnauthorized(), + ); + +export const FindInvitesSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Получить список входящих приглашений', + description: + 'Возвращает все активные приглашения в команды, отправленные на email текущего пользователя.', + }), + ApiResponse({ + status: 200, + description: 'Список приглашений успешно получен', + type: [UserInviteResponse.Output], + }), + ApiUnauthorized(), + ); diff --git a/src/teams/application/controller/members/controller.ts b/src/teams/application/controller/members/controller.ts new file mode 100644 index 0000000..b4e9b22 --- /dev/null +++ b/src/teams/application/controller/members/controller.ts @@ -0,0 +1,37 @@ +import { Body, Delete, Get, Param, Patch } from '@nestjs/common'; +import { ApiBaseController, GetUserId } from '@shared/decorators'; +import { GetMembersSwagger, RemoveMemberSwagger, UpdateMemberSwagger } from './swagger'; +import type { UpdateMemberDto } from '../../dtos/member.dto'; +import { TeamsFacade } from '../../team.facade'; + +@ApiBaseController('teams/:slug', 'Teams Members', true) +export class TeamsMembersController { + constructor(private readonly facade: TeamsFacade) {} + + @Get('members') + @GetMembersSwagger() + async getMembers(@Param('slug') slug: string) { + return this.facade.getMembers(slug); + } + + @Patch('members/:userId') + @UpdateMemberSwagger() + async updateMember( + @Param('slug') slug: string, + @Param('userId') targetUserId: string, + @GetUserId() currentUserId: string, + @Body() dto: UpdateMemberDto, + ) { + return this.facade.updateMember(slug, currentUserId, targetUserId, dto); + } + + @Delete('members/:userId') + @RemoveMemberSwagger() + async removeMember( + @Param('slug') slug: string, + @Param('userId') targerUserId: string, + @GetUserId() currentUserId: string, + ) { + return this.facade.removeMember(slug, currentUserId, targerUserId); + } +} diff --git a/src/teams/application/controller/members/swagger.ts b/src/teams/application/controller/members/swagger.ts new file mode 100644 index 0000000..92c1dcf --- /dev/null +++ b/src/teams/application/controller/members/swagger.ts @@ -0,0 +1,89 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { ActionResponse } from '@shared/dtos'; +import { ApiForbidden, ApiNotFound, ApiUnauthorized } from '@shared/error'; +import { + TeamMemberResponse, + UpdateMemberDto, + UserTeamResponse, + UserInviteResponse, +} from '../../dtos'; + +export const FindTeamsSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Получить список команд пользователя', + description: + 'Возвращает все команды, в которых текущий пользователь является участником или владельцем.', + }), + ApiResponse({ + status: 200, + description: 'Список команд получен', + type: [UserTeamResponse.Output], + }), + ApiUnauthorized(), + ); + +export const FindInvitesSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Получить список входящих приглашений', + description: + 'Возвращает все активные приглашения в команды, отправленные на email текущего пользователя.', + }), + ApiResponse({ + status: 200, + description: 'Список приглашений успешно получен', + type: [UserInviteResponse.Output], + }), + ApiUnauthorized(), + ); + +export const GetMembersSwagger = () => + applyDecorators( + ApiOperation({ summary: 'Получить список всех участников команды' }), + ApiParam({ name: 'slug', description: 'Слаг команды' }), + ApiResponse({ + status: 200, + description: 'Список участников получен', + type: [TeamMemberResponse.Output], + }), + ApiUnauthorized(), + ApiForbidden(), + ); + +export const UpdateMemberSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Изменить роль или статус участника', + description: + 'Позволяет изменить роль участника (member -> admin) или вручную изменить его статус.' + + ' Владелец команды (Owner) не может понизить свою роль через этот эндпоинт.', + }), + ApiBody({ type: UpdateMemberDto.Output }), + ApiParam({ name: 'slug', description: 'Слаг команды' }), + ApiParam({ name: 'userId', description: 'ID пользователя, чьи права редактируются' }), + ApiResponse({ + status: 200, + description: 'Данные участника обновлены', + type: ActionResponse.Output, + }), + ApiNotFound('Участник или команда не найдены'), + ApiUnauthorized(), + ApiForbidden(), + ); + +export const RemoveMemberSwagger = () => + applyDecorators( + ApiOperation({ summary: 'Удалить участника из команды' }), + ApiParam({ name: 'slug', description: 'Слаг команды' }), + ApiParam({ name: 'userId', description: 'ID пользователя' }), + ApiResponse({ + status: 200, + type: ActionResponse.Output, + description: 'Участник успешно удален', + }), + ApiNotFound(), + ApiUnauthorized(), + ApiForbidden(), + ); diff --git a/src/teams/application/controller/settings/controller.ts b/src/teams/application/controller/settings/controller.ts new file mode 100644 index 0000000..4f691e2 --- /dev/null +++ b/src/teams/application/controller/settings/controller.ts @@ -0,0 +1,16 @@ +import { Body, Param, Put } from '@nestjs/common'; +import { ApiBaseController } from '@shared/decorators'; +import { SyncTeamTagsSwagger } from './swagger'; +import { SyncTagsDto } from '../../dtos'; +import { TeamsFacade } from '../../team.facade'; + +@ApiBaseController('teams/:slug', 'Teams Settings', true) +export class TeamsSettingsController { + constructor(private readonly facade: TeamsFacade) {} + + @Put('tags') + @SyncTeamTagsSwagger() + async syncTags(@Param('slug') slug: string, @Body() dto: SyncTagsDto) { + return this.facade.syncTags(slug, dto.tags); + } +} diff --git a/src/teams/application/controller/settings/swagger.ts b/src/teams/application/controller/settings/swagger.ts new file mode 100644 index 0000000..46328c7 --- /dev/null +++ b/src/teams/application/controller/settings/swagger.ts @@ -0,0 +1,15 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { ActionResponse } from '@shared/dtos'; +import { ApiForbidden, ApiNotFound, ApiUnauthorized } from '@shared/error'; +import { SyncTagsDto } from '../../dtos'; + +export const SyncTeamTagsSwagger = () => + applyDecorators( + ApiOperation({ summary: 'Синхронизировать теги команды' }), + ApiBody({ type: SyncTagsDto.Output }), + ApiResponse({ status: 200, description: 'Теги обновлены', type: ActionResponse.Output }), + ApiForbidden(), + ApiNotFound(), + ApiUnauthorized(), + ); diff --git a/src/teams/application/controller/teams/controller.ts b/src/teams/application/controller/teams/controller.ts new file mode 100644 index 0000000..b4546e0 --- /dev/null +++ b/src/teams/application/controller/teams/controller.ts @@ -0,0 +1,51 @@ +import { Body, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post } from '@nestjs/common'; +import { ApiBaseController, GetUserId } from '@shared/decorators'; +import { + CreateTeamSwagger, + FindOneTeamSwagger, + RemoveTeamSwagger, + UpdateTeamSwagger, + CheckSlugSwagger, +} from './swagger'; +import { CreateTeamDto, UpdateTeamDto } from '../../dtos'; +import { TeamsFacade } from '../../team.facade'; + +@ApiBaseController('teams', 'Teams', true) +export class TeamsController { + constructor(private readonly facade: TeamsFacade) {} + + @Post() + @CreateTeamSwagger() + async create(@GetUserId() userId: string, @Body() dto: CreateTeamDto) { + return this.facade.createTeam(userId, dto); + } + + @Get('check-slug/:slug') + @CheckSlugSwagger() + async checkSlug(@Param('slug') slug: string) { + return this.facade.checkSlug(slug); + } + + @Get(':slug') + @FindOneTeamSwagger() + async findOne(@Param('slug') slug: string) { + return this.facade.getTeamBySlug(slug); + } + + @Patch(':slug') + @UpdateTeamSwagger() + async update( + @Param('slug') slug: string, + @GetUserId() userId: string, + @Body() dto: UpdateTeamDto, + ) { + return this.facade.updateTeam(slug, userId, dto); + } + + @Delete(':slug') + @RemoveTeamSwagger() + @HttpCode(HttpStatus.OK) + async remove(@Param('slug') slug: string, @GetUserId() userId: string) { + return this.facade.deleteTeam(slug, userId); + } +} diff --git a/src/teams/application/controller/teams/swagger.ts b/src/teams/application/controller/teams/swagger.ts new file mode 100644 index 0000000..fac00e5 --- /dev/null +++ b/src/teams/application/controller/teams/swagger.ts @@ -0,0 +1,87 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { ActionResponse } from '@shared/dtos'; +import { + ApiConflict, + ApiForbidden, + ApiNotFound, + ApiUnauthorized, + ApiValidationError, +} from '@shared/error'; +import { CreateTeamDto, UpdateTeamDto, CheckSlugResponse } from '../../dtos'; + +export const CreateTeamSwagger = () => + applyDecorators( + ApiOperation({ summary: 'Создать новую команду' }), + ApiBody({ type: CreateTeamDto.Output }), + ApiResponse({ + status: 201, + description: 'Команда успешно создана', + type: ActionResponse.Output, + }), + ApiConflict('Команда с таким slug уже существует'), + ApiValidationError(), + ApiUnauthorized(), + ); + +export const CheckSlugSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Проверить доступность слага', + description: 'Проверяет, свободен ли уникальный адрес команды для использования.', + }), + ApiParam({ + name: 'slug', + description: 'Желаемый слаг команды', + example: 'my-super-team', + }), + ApiResponse({ + status: 200, + description: 'Результат проверки доступности', + type: CheckSlugResponse.Output, + }), + ApiUnauthorized(), + ); + +export const FindOneTeamSwagger = () => + applyDecorators( + ApiOperation({ summary: 'Получить детальную информацию о команде по slug' }), + ApiParam({ name: 'slug', description: 'Уникальный идентификатор (слаг) команды' }), + ApiResponse({ + status: 200, + description: 'Данные команды получены', + type: Object, + }), + ApiNotFound('Команда не найдена'), + ApiUnauthorized(), + ); + +export const UpdateTeamSwagger = () => + applyDecorators( + ApiOperation({ summary: 'Обновить данные команды' }), + ApiBody({ type: UpdateTeamDto.Output }), + ApiParam({ name: 'slug', description: 'Слаг команды для редактирования' }), + ApiResponse({ + status: 200, + description: 'Команда успешно обновлена', + type: ActionResponse.Output, + }), + ApiForbidden(), + ApiNotFound(), + ApiValidationError(), + ApiUnauthorized(), + ); + +export const RemoveTeamSwagger = () => + applyDecorators( + ApiOperation({ summary: 'Удалить команду' }), + ApiParam({ name: 'slug', description: 'Слаг команды для удаления' }), + ApiResponse({ + status: 200, + description: 'Команда успешно удалена', + type: ActionResponse.Output, + }), + ApiForbidden(), + ApiNotFound(), + ApiUnauthorized(), + ); diff --git a/src/teams/application/dtos/index.ts b/src/teams/application/dtos/index.ts new file mode 100644 index 0000000..f87edb5 --- /dev/null +++ b/src/teams/application/dtos/index.ts @@ -0,0 +1,16 @@ +export { + InviteMemberDto, + UpdateMemberDto, + TeamMemberResponse, + UserInviteResponse, +} from './member.dto'; +export { UpdateInvitationDto, TeamInvitationResponse } from './invitation.dto'; +export { + CreateTeamDto, + UpdateTeamDto, + FindTagsQuery, + SyncTagsDto, + UserTeamResponse, + TagResponse, + CheckSlugResponse, +} from './team.dto'; diff --git a/src/teams/application/dtos/invitation.dto.ts b/src/teams/application/dtos/invitation.dto.ts new file mode 100644 index 0000000..9d7c2b8 --- /dev/null +++ b/src/teams/application/dtos/invitation.dto.ts @@ -0,0 +1,38 @@ +import { z } from 'zod/v4'; +import { createZodDto } from 'nestjs-zod'; +import { roleEnum, TeamRole } from '../../infrastructure/persistence/models/enums'; + +export const UpdateInvitationSchema = z.object({ + role: z + .enum(roleEnum.enumValues) + .describe('Новая роль, которая будет назначена пользователю после принятия инвайта'), +}); + +export class UpdateInvitationDto extends createZodDto(UpdateInvitationSchema) {} + +export const TeamInvitationSchema = z.object({ + code: z.string().describe('Код инвайта'), + teamId: z.string().describe('ID команды'), + teamName: z.string().describe('Название команды'), + teamAvatar: z.string().nullable().describe('Аватар команды'), + email: z.string().email().describe('Email приглашённого пользователя'), + role: z.string().describe('Роль, которая будет назначена после принятия инвайта'), + inviterId: z.string().describe('ID пользователя, отправившего приглашение'), + inviterName: z.string().describe('Имя пригласившего'), + createdAt: z.string().datetime().describe('Дата создания инвайта (ISO 8601)'), + expiresAt: z.string().datetime().describe('Дата истечения инвайта (ISO 8601)'), +}); + +export class TeamInvitationResponse extends createZodDto(TeamInvitationSchema) {} + +export interface TeamInvite { + teamId: string; + teamName: string; + teamAvatar: string | null; + email: string; + role: TeamRole; + inviterId: string; + inviterName: string; + createdAt: string; + expiresAt: string; +} diff --git a/src/teams/application/dtos/member.dto.ts b/src/teams/application/dtos/member.dto.ts new file mode 100644 index 0000000..1e9fe35 --- /dev/null +++ b/src/teams/application/dtos/member.dto.ts @@ -0,0 +1,62 @@ +import { z } from 'zod/v4'; +import { createZodDto } from 'nestjs-zod'; +import { roleEnum } from '@core/teams/infrastructure/persistence/models'; + +export const InviteMemberSchema = z.object({ + email: z.string().email().describe('Email пользователя, которого нужно пригласить'), + role: z + .enum(roleEnum.enumValues) + .default('member') + .describe('Роль, которая будет назначена пользователю после принятия инвайта'), +}); + +export class InviteMemberDto extends createZodDto(InviteMemberSchema) {} + +const UpdateMemberDtoSchema = z + .object({ + role: z.enum(roleEnum.enumValues).optional().describe('Новая роль участника'), + status: z.string().optional().describe('Новый статус (active, blocked и т.д.)'), + }) + .refine((data) => Object.keys(data).length > 0, { + error: 'Необходимо передать хотя бы одно поле для обновления', + abort: true, + }); + +export class UpdateMemberDto extends createZodDto(UpdateMemberDtoSchema) {} + +export const TeamMemberResponseSchema = z.object({ + id: z.string().describe('Уникальный ID пользователя (UUID или ULID)'), + role: z + .enum(['owner', 'admin', 'member']) + .describe('Роль участника в рамках конкретной команды'), + status: z + .enum(['active', 'pending', 'blocked']) + .describe('Текущий статус членства (активен, ожидает приглашения, заблокирован)'), + fullName: z.string().describe('Полное имя для отображения (Фамилия Имя Отчество)'), + firstName: z.string().describe('Имя пользователя'), + lastName: z.string().describe('Фамилия пользователя'), + avatarUrl: z + .string() + .url() + .nullable() + .describe('Прямая ссылка на изображение профиля или null, если не задано'), + + initials: z.string().max(2).describe('Две буквы для аватара-заглушки (например, "ИИ")'), + joinedAt: z + .string() + .datetime() + .describe('Дата и время вступления в команду в формате ISO 8601'), +}); + +export class TeamMemberResponse extends createZodDto(TeamMemberResponseSchema) {} + +export const UserInviteSchema = z.object({ + code: z.string().describe('Код инвайта'), + teamName: z.string().describe('Название команды'), + teamAvatar: z.string().nullable().describe('Аватар команды'), + role: z.string().describe('Роль'), + inviterName: z.string().describe('Имя пригласившего'), + expiresAt: z.string().datetime().describe('Дата истечения'), +}); + +export class UserInviteResponse extends createZodDto(UserInviteSchema) {} diff --git a/src/teams/application/dtos/team.dto.ts b/src/teams/application/dtos/team.dto.ts new file mode 100644 index 0000000..b96c9c4 --- /dev/null +++ b/src/teams/application/dtos/team.dto.ts @@ -0,0 +1,111 @@ +import { z } from 'zod/v4'; +import { createZodDto } from 'nestjs-zod'; +import { createPaginationSchema } from '../../../shared/schemas'; + +export const CreateTeamSchema = z.object({ + name: z.string().min(2).max(100).describe('Название команды, отображаемое в интерфейсе'), + description: z + .string() + .min(10) + .max(500) + .describe('Краткое описание деятельности или целей команды'), + slug: z.string().optional().describe('Уникальная ссылка на изображение команду'), + tags: z + .array(z.string()) + .optional() + .superRefine((items, ctx) => { + if (!items) return; + const lowerItems = items.map((i) => i.toLowerCase()); + const hasDuplicates = new Set(lowerItems).size !== items.length; + + if (hasDuplicates) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Теги в списке не должны повторяться (регистр не важен)', + }); + } + }) + .describe('Список строковых названий тегов для классификации'), +}); + +export class CreateTeamDto extends createZodDto(CreateTeamSchema) {} +export class UpdateTeamDto extends createZodDto( + CreateTeamSchema.partial().refine((data) => Object.keys(data).length > 0, { + error: 'Необходимо передать хотя бы одно поле для обновления', + abort: true, + }), +) {} + +export const TagSchema = z.object({ + id: z.string().describe('Уникальный идентификатор тега (CUID2)'), + name: z.string().min(1).max(50).describe('Название тега (например, "Backend", "Design")'), +}); + +export const SyncTagsSchema = z.object({ + tags: z + .array(z.string()) + .min(1, 'Список тегов не может быть пустым') + .max(15, 'Нельзя добавить более 15 тегов за раз') + .superRefine((items, ctx) => { + if (!items) return; + const lowerItems = items.map((i) => i.toLowerCase()); + const hasDuplicates = new Set(lowerItems).size !== items.length; + + if (hasDuplicates) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Теги в списке не должны повторяться (регистр не важен)', + }); + } + }) + .describe( + 'Массив названий тегов для привязки к команде. Если тега нет в базе, он будет создан.', + ), +}); + +const FindTagsQuerySchema = z.object({ + search: z.string().optional().describe('Поисковый запрос для фильтрации тегов по названию'), + page: z.coerce.number().int().min(1).default(1).describe('Номер страницы (от 1)'), + limit: z.coerce + .number() + .int() + .min(1) + .max(100) + .default(20) + .describe('Количество возвращаемых результатов (1-100)'), +}); + +export class TagResponse extends createZodDto(createPaginationSchema(TagSchema)) {} +export class SyncTagsDto extends createZodDto(SyncTagsSchema) {} +export class FindTagsQuery extends createZodDto(FindTagsQuerySchema) {} + +export const CheckSlugResponseSchema = z.object({ + available: z + .boolean() + .describe('Флаг доступности: true — адрес свободен, false — уже занят или некорректен'), +}); + +export class CheckSlugResponse extends createZodDto(CheckSlugResponseSchema) {} + +export const TeamPermissionsSchema = z.object({ + canEdit: z.boolean().describe('Разрешено ли редактировать настройки и профиль команды'), + canDelete: z + .boolean() + .describe('Разрешено ли полностью удалить команду (только для владельца)'), + canManageMembers: z.boolean().describe('Разрешено ли менять роли и исключать участников'), + canInvite: z.boolean().describe('Разрешено ли приглашать новых участников'), + isOwner: z.boolean().describe('Является ли текущий пользователь владельцем (Owner)'), +}); + +export const UserTeamSchema = z.object({ + id: z.string().uuid().describe('Уникальный ID команды'), + name: z.string().describe('Название команды'), + slug: z.string().describe('Уникальный URL-путь команды'), + description: z.string().nullable().describe('Краткое описание команды'), + avatarUrl: z.string().nullable().describe('URL изображения профиля команды'), + role: z.string().describe('Системное название роли пользователя'), + joinedAt: z.string().datetime().describe('Дата, когда пользователь вступил в команду'), + permissions: TeamPermissionsSchema.describe('Объект прав доступа текущего пользователя'), +}); + +export class UserTeamResponse extends createZodDto(UserTeamSchema) {} diff --git a/src/teams/application/mappers/index.ts b/src/teams/application/mappers/index.ts new file mode 100644 index 0000000..f09718a --- /dev/null +++ b/src/teams/application/mappers/index.ts @@ -0,0 +1 @@ +export { TeamMemberMapper } from './member.mapper'; diff --git a/src/teams/application/mappers/member.mapper.ts b/src/teams/application/mappers/member.mapper.ts new file mode 100644 index 0000000..5765213 --- /dev/null +++ b/src/teams/application/mappers/member.mapper.ts @@ -0,0 +1,74 @@ +import type { RawMemberRow, RawMemberTeams } from '../../domain/repository'; +import { ImageHelper } from '@shared/utils'; + +export class TeamMemberMapper { + public static toDetail(row: RawMemberRow, cdn: string) { + const { firstName, lastName, middleName, avatarUrl, userId, ...rest } = row; + + const fullName = + [lastName, firstName, middleName].filter(Boolean).join(' ') || 'Unknown User'; + + const avatar = ImageHelper.buildResponsiveUrls(cdn, avatarUrl); + + return { + id: userId, + ...rest, + firstName, + lastName, + middleName, + fullName, + avatar, + initials: this.getInitials(firstName, lastName), + }; + } + + public static toList(rows: RawMemberRow[], cdn: string) { + return rows.map((row) => this.toDetail(row, cdn)); + } + + public static toUserTeam(data: RawMemberTeams, cdn: string) { + const { role, avatarUrl, ...row } = data; + + const avatar = ImageHelper.buildResponsiveUrls(cdn, avatarUrl); + + return { + id: row.id, + name: row.name, + slug: row.slug, + description: row.description, + avatar, + role: role, + joinedAt: row.joinedAt, + permissions: { + canEdit: ['owner', 'admin'].includes(role), + canDelete: role === 'owner', + canManageMembers: ['owner', 'admin'].includes(role), + canInvite: ['owner', 'admin'].includes(role), + isOwner: role === 'owner', + }, + }; + } + + public static toPublicInvite(raw: string | null, code: string) { + if (!raw) return null; + try { + const p = JSON.parse(raw); + return { + code, + teamName: p.teamName, + teamAvatar: p.teamAvatar ?? null, + inviterName: p.inviterName, + role: p.role, + expiresAt: p.expiresAt, + }; + } catch { + return null; + } + } + + private static getInitials(fName: string | null, lName: string | null): string { + const first = fName?.[0] ?? ''; + const last = lName?.[0] ?? ''; + return (first + last).toUpperCase() || '?'; + } +} diff --git a/src/teams/application/team.facade.ts b/src/teams/application/team.facade.ts new file mode 100644 index 0000000..bc96eca --- /dev/null +++ b/src/teams/application/team.facade.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@nestjs/common'; +import * as UC from './use-cases'; +import type { + CreateTeamDto, + InviteMemberDto, + UpdateInvitationDto, + UpdateMemberDto, + UpdateTeamDto, +} from './dtos'; + +@Injectable() +export class TeamsFacade { + constructor( + private readonly findTeamQ: UC.FindTeamQuery, + private readonly getInvitationQ: UC.GetInvitationQuery, + private readonly getInvitationsQ: UC.GetInvitationsQuery, + private readonly getTeamMembersQ: UC.GetTeamMembersQuery, + private readonly checkSlugQ: UC.CheckTeamSlugQuery, + + private readonly createTeamUc: UC.CreateTeamUseCase, + private readonly deleteTeamUc: UC.DeleteTeamUseCase, + private readonly updateTeamUc: UC.UpdateTeamUseCase, + private readonly syncTagsUc: UC.SyncTeamTagsUseCase, + + private readonly updateMemberUc: UC.UpdateTeamMemberUseCase, + private readonly removeMemberUc: UC.RemoveTeamMemberUseCase, + private readonly sendInviteUc: UC.SendInvitationUseCase, + private readonly acceptInviteUc: UC.AcceptInvitationUseCase, + private readonly updateInvitationUc: UC.UpdateInvitationUseCase, + private readonly declineInvitationUc: UC.DeclineInvitationUseCase, + + private readonly getMyTeamsUc: UC.GetMyTeamsUseCase, + private readonly getMyInvitesUc: UC.GetMyInvitesUseCase, + ) {} + + public checkSlug = (slug: string) => this.checkSlugQ.execute(slug); + + public getTeamBySlug = (slug: string) => this.findTeamQ.execute(slug); + + public getInvitation = (slug: string, code: string, userId: string, userEmail: string) => + this.getInvitationQ.execute(slug, code, userId, userEmail); + + public createTeam = (ownerId: string, dto: CreateTeamDto) => + this.createTeamUc.execute(ownerId, dto); + + public updateTeam = (slug: string, userId: string, dto: UpdateTeamDto) => + this.updateTeamUc.execute(slug, userId, dto); + + public deleteTeam = (slug: string, userId: string) => this.deleteTeamUc.execute(slug, userId); + + public getMembers = (slug: string) => this.getTeamMembersQ.execute(slug); + + public updateMember = (slug: string, curr: string, target: string, dto: UpdateMemberDto) => + this.updateMemberUc.execute(slug, curr, target, dto); + + public removeMember = (slug: string, curr: string, target: string) => + this.removeMemberUc.execute(slug, curr, target); + + public getInvitations = (slug: string, userId?: string) => + this.getInvitationsQ.execute(slug, userId); + + public invite = (slug: string, inviterId: string, dto: InviteMemberDto) => + this.sendInviteUc.execute(slug, inviterId, dto); + + public acceptInvite = (code: string, userId: string, email: string) => + this.acceptInviteUc.execute(code, userId, email); + + public declineInvitation = (slug: string, code: string, userId: string, userEmail: string) => + this.declineInvitationUc.execute(slug, code, userId, userEmail); + + public updateInvitation = ( + slug: string, + code: string, + userId: string, + dto: UpdateInvitationDto, + ) => this.updateInvitationUc.execute(slug, code, userId, dto); + + public syncTags = (slug: string, tags: string[]) => this.syncTagsUc.execute(slug, tags); + + public getMyTeams = (userId: string, pagination: any) => + this.getMyTeamsUc.execute(userId, pagination); + + public getMyInvites = (email: string) => this.getMyInvitesUc.execute(email); +} diff --git a/src/teams/application/use-cases/base/check-team-slug.query.ts b/src/teams/application/use-cases/base/check-team-slug.query.ts new file mode 100644 index 0000000..46a3083 --- /dev/null +++ b/src/teams/application/use-cases/base/check-team-slug.query.ts @@ -0,0 +1,20 @@ +import { ITeamsRepository } from '@core/teams/domain/repository'; +import { Inject, Injectable } from '@nestjs/common'; + +@Injectable() +export class CheckTeamSlugQuery { + constructor(@Inject('ITeamsRepository') private readonly teamsRepo: ITeamsRepository) {} + + async execute(slug: string) { + const normalizedSlug = slug.trim().toLowerCase(); + + const available = await this.teamsRepo.isSlugAvailable(normalizedSlug); + + return { + available, + message: available + ? `Slug ${normalizedSlug} доступен для использования` + : `Slug ${normalizedSlug} уже занят`, + }; + } +} diff --git a/src/teams/application/use-cases/base/create-team.use-case.ts b/src/teams/application/use-cases/base/create-team.use-case.ts new file mode 100644 index 0000000..c6b8cc7 --- /dev/null +++ b/src/teams/application/use-cases/base/create-team.use-case.ts @@ -0,0 +1,60 @@ +import { ITeamsRepository } from '@core/teams/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { CreateTeamDto } from '../../dtos'; +import { BaseException } from '@shared/error'; +import { slugify } from 'transliteration'; + +@Injectable() +export class CreateTeamUseCase { + constructor( + @Inject('ITeamsRepository') + private readonly teamsRepo: ITeamsRepository, + ) {} + + async execute(userId: string, dto: CreateTeamDto) { + const baseSlug = slugify(dto.slug || dto.name, { lowercase: true, separator: '-' }); + const existingTeam = await this.teamsRepo.findBySlug(baseSlug); + + if (existingTeam) { + throw new BaseException( + { + code: 'SLUG_ALREADY_EXISTS', + message: `Ссылка "${baseSlug}" уже занята другой командой`, + details: [{ target: 'slug', value: baseSlug }], + }, + HttpStatus.CONFLICT, + ); + } + + const { tags, ...teamData } = dto; + const uniqueTags = tags ? [...new Set(tags.map((tag) => tag.toLowerCase()))] : []; + + try { + const result = await this.teamsRepo.create( + userId, + { + ...teamData, + slug: baseSlug, + }, + uniqueTags, + ); + + return { + ...result, + slug: baseSlug, + message: 'Команда успешно создана', + }; + } catch (error) { + if (error instanceof BaseException) throw error; + + throw new BaseException( + { + code: 'TEAM_CREATE_FAILED', + message: 'Не удалось создать команду', + details: [{ reason: error instanceof Error ? error.message : 'Unknown error' }], + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/teams/application/use-cases/base/delete-team.use-case.ts b/src/teams/application/use-cases/base/delete-team.use-case.ts new file mode 100644 index 0000000..134bdf6 --- /dev/null +++ b/src/teams/application/use-cases/base/delete-team.use-case.ts @@ -0,0 +1,58 @@ +import { ITeamsRepository } from '@core/teams/domain/repository'; +import { Inject, Injectable, HttpStatus } from '@nestjs/common'; +import { BaseException } from '@shared/error'; + +@Injectable() +export class DeleteTeamUseCase { + constructor( + @Inject('ITeamsRepository') + private readonly teamsRepo: ITeamsRepository, + ) {} + + async execute(slug: string, userId: string) { + const team = await this.teamsRepo.findBySlug(slug); + + if (!team) { + throw new BaseException( + { + code: 'TEAM_NOT_FOUND', + message: `Команда ${slug} не найдена`, + }, + HttpStatus.NOT_FOUND, + ); + } + + const member = await this.teamsRepo.findMember(team.id, userId); + const isOwner = team.ownerId === userId || member?.role === 'owner'; + + if (!isOwner) { + throw new BaseException( + { + code: 'ONLY_OWNER_CAN_DELETE', + message: 'Только владелец может удалить команду', + }, + HttpStatus.FORBIDDEN, + ); + } + + try { + const result = await this.teamsRepo.remove(team.id, userId); + + return { + success: result, + message: 'Команда успешно удалена', + }; + } catch (error) { + if (error instanceof BaseException) throw error; + + throw new BaseException( + { + code: 'TEAM_DELETE_FAILED', + message: 'Не удалось удалить команду', + details: [{ reason: error instanceof Error ? error.message : 'Unknown error' }], + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/teams/application/use-cases/base/find-team.query.ts b/src/teams/application/use-cases/base/find-team.query.ts new file mode 100644 index 0000000..58bae3c --- /dev/null +++ b/src/teams/application/use-cases/base/find-team.query.ts @@ -0,0 +1,14 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ITeamsRepository } from '../../../domain/repository'; + +@Injectable() +export class FindTeamQuery { + constructor( + @Inject('ITeamsRepository') + private readonly repository: ITeamsRepository, + ) {} + + async execute(slug: string) { + return this.repository.findBySlug(slug); + } +} diff --git a/src/teams/application/use-cases/base/get-all-tags.use-case.ts b/src/teams/application/use-cases/base/get-all-tags.use-case.ts new file mode 100644 index 0000000..de51e75 --- /dev/null +++ b/src/teams/application/use-cases/base/get-all-tags.use-case.ts @@ -0,0 +1,37 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { FindTagsQuery } from '../../dtos'; +import { ITeamsRepository } from '@core/teams/domain/repository'; + +@Injectable() +export class GetAllTagsUseCase { + constructor( + @Inject('ITeamsRepository') + private readonly teamsRepo: ITeamsRepository, + ) {} + + async execute(query: FindTagsQuery) { + const safePage = Math.max(query.page ?? 1, 1); + const safeLimit = Math.min(Math.max(query.limit ?? 20, 1), 50); + const offset = (safePage - 1) * safeLimit; + + const { data, total } = await this.teamsRepo.findAllTags({ + search: query.search, + limit: safeLimit, + offset, + }); + + const totalPages = total === 0 ? 0 : Math.ceil(total / safeLimit); + + return { + data, + meta: { + hasNextPage: safePage < totalPages, + hasPrevPage: safePage > 1, + total, + totalPages, + page: safePage, + limit: safeLimit, + }, + }; + } +} diff --git a/src/teams/application/use-cases/base/get-my-teams.use-case.ts b/src/teams/application/use-cases/base/get-my-teams.use-case.ts new file mode 100644 index 0000000..7315ca8 --- /dev/null +++ b/src/teams/application/use-cases/base/get-my-teams.use-case.ts @@ -0,0 +1,27 @@ +import { ITeamsRepository } from '@core/teams/domain/repository'; +import { TeamMemberMapper } from '@core/teams/application/mappers'; +import { Inject, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class GetMyTeamsUseCase { + constructor( + @Inject('ITeamsRepository') + private readonly teamsRepo: ITeamsRepository, + private readonly cfg: ConfigService, + ) {} + + async execute(userId: string, pagination: Record) { + const teams = await this.teamsRepo.findByUser(userId, pagination); + const cdn = this.getCdnBaseUrl(); + return teams.map((t) => TeamMemberMapper.toUserTeam(t, cdn)); + } + + private getCdnBaseUrl(): string { + const domain = this.cfg.get('DOMAIN'); + const bucket = this.cfg.get('S3_BUCKET_NAME'); + const endpoint = this.cfg.get('S3_ENDPOINT'); + + return domain ? `https://cdn.${domain}/${bucket}` : `${endpoint}/${bucket}`; + } +} diff --git a/src/teams/application/use-cases/base/sync-team-tags.use-case.ts b/src/teams/application/use-cases/base/sync-team-tags.use-case.ts new file mode 100644 index 0000000..5199ad0 --- /dev/null +++ b/src/teams/application/use-cases/base/sync-team-tags.use-case.ts @@ -0,0 +1,45 @@ +import { ITeamsRepository } from '@core/teams/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { BaseException } from '@shared/error'; + +@Injectable() +export class SyncTeamTagsUseCase { + constructor( + @Inject('ITeamsRepository') + private readonly teamsRepo: ITeamsRepository, + ) {} + + async execute(slug: string, tags: string[]) { + const team = await this.teamsRepo.findBySlug(slug); + + if (!team) { + throw new BaseException( + { + code: 'TEAM_NOT_FOUND', + message: 'Команда не найдена', + }, + HttpStatus.NOT_FOUND, + ); + } + + const normalizedTags = [...new Set(tags.map((t) => t.trim()).filter(Boolean))]; + + const isSynced = await this.teamsRepo.syncTags(team.id, normalizedTags); + + if (!isSynced) { + throw new BaseException( + { + code: 'TAGS_SYNC_FAILED', + message: 'Не удалось обновить теги команды', + details: [{ target: 'tags', count: normalizedTags.length }], + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + return { + success: true, + message: 'Теги команды успешно обновлены', + }; + } +} diff --git a/src/teams/application/use-cases/base/update-team.use-case.ts b/src/teams/application/use-cases/base/update-team.use-case.ts new file mode 100644 index 0000000..745d438 --- /dev/null +++ b/src/teams/application/use-cases/base/update-team.use-case.ts @@ -0,0 +1,62 @@ +import { Inject, Injectable, HttpStatus } from '@nestjs/common'; +import { ITeamsRepository } from '../../../domain/repository'; +import type { UpdateTeamDto } from '../../dtos'; +import { BaseException } from '@shared/error'; + +@Injectable() +export class UpdateTeamUseCase { + constructor( + @Inject('ITeamsRepository') + private readonly teamsRepo: ITeamsRepository, + ) {} + + async execute(slug: string, userId: string, dto: UpdateTeamDto) { + const team = await this.teamsRepo.findBySlug(slug); + + if (!team?.id) { + throw new BaseException( + { + code: 'TEAM_NOT_FOUND', + message: `Команда ${slug} не найдена`, + }, + HttpStatus.NOT_FOUND, + ); + } + + const member = await this.teamsRepo.findMember(team.id, userId); + const canEdit = member?.role === 'admin' || member?.role === 'owner'; + + if (!canEdit) { + throw new BaseException( + { + code: 'INSUFFICIENT_PERMISSIONS', + message: 'У вас нет прав для редактирования этой команды', + details: [{ target: 'role', value: member?.role }], + }, + HttpStatus.FORBIDDEN, + ); + } + + const { tags, ...data } = dto; + + try { + const result = await this.teamsRepo.update(team.id, data, tags); + + return { + ...result, + message: 'Данные команды успешно обновлены', + }; + } catch (error) { + if (error instanceof BaseException) throw error; + + throw new BaseException( + { + code: 'TEAM_UPDATE_FAILED', + message: 'Ошибка при обновлении данных команды', + details: [{ reason: error instanceof Error ? error.message : 'Unknown error' }], + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/teams/application/use-cases/index.ts b/src/teams/application/use-cases/index.ts new file mode 100644 index 0000000..a359b89 --- /dev/null +++ b/src/teams/application/use-cases/index.ts @@ -0,0 +1,69 @@ +import { CheckTeamSlugQuery } from './base/check-team-slug.query'; +import { FindTeamQuery } from './base/find-team.query'; +import { FindTeamMemberQuery } from './members/find-team-member.query'; +import { GetInvitationQuery } from './invitions/get-invitation.query'; +import { GetInvitationsQuery } from './invitions/get-invitations.query'; +import { GetTeamMembersQuery } from './members/get-team-members.query'; +import { GetAllTagsUseCase } from './base/get-all-tags.use-case'; +import { GetMyInvitesUseCase } from './invitions/get-my-invites.use-case'; +import { GetMyTeamsUseCase } from './base/get-my-teams.use-case'; + +import { AcceptInvitationUseCase } from './invitions/accept-invitation.use-case'; +import { CreateTeamUseCase } from './base/create-team.use-case'; +import { DeleteTeamUseCase } from './base/delete-team.use-case'; +import { RemoveTeamMemberUseCase } from './members/remove-team-member.use-case'; +import { SendInvitationUseCase } from './invitions/send-invitation.use-case'; +import { SyncTeamTagsUseCase } from './base/sync-team-tags.use-case'; +import { UpdateTeamUseCase } from './base/update-team.use-case'; +import { UpdateTeamMemberUseCase } from './members/update-team-member.use-case'; +import { UpdateInvitationUseCase } from './invitions/update-invitation.use-case'; +import { DeclineInvitationUseCase } from './invitions/decline-invitation.use-case'; + +export { + CheckTeamSlugQuery, + FindTeamQuery, + FindTeamMemberQuery, + GetInvitationQuery, + GetInvitationsQuery, + GetTeamMembersQuery, + GetAllTagsUseCase, + GetMyInvitesUseCase, + GetMyTeamsUseCase, + AcceptInvitationUseCase, + CreateTeamUseCase, + DeleteTeamUseCase, + RemoveTeamMemberUseCase, + SendInvitationUseCase, + SyncTeamTagsUseCase, + UpdateTeamUseCase, + UpdateTeamMemberUseCase, + UpdateInvitationUseCase, + DeclineInvitationUseCase, +}; + +export const TeamQueries = [ + CheckTeamSlugQuery, + FindTeamQuery, + FindTeamMemberQuery, + GetInvitationQuery, + GetInvitationsQuery, + GetTeamMembersQuery, + GetAllTagsUseCase, + GetMyInvitesUseCase, + GetMyTeamsUseCase, +]; + +export const TeamUseCases = [ + AcceptInvitationUseCase, + CreateTeamUseCase, + DeleteTeamUseCase, + RemoveTeamMemberUseCase, + SendInvitationUseCase, + SyncTeamTagsUseCase, + UpdateTeamUseCase, + UpdateTeamMemberUseCase, + UpdateInvitationUseCase, + DeclineInvitationUseCase, +]; + +export const TEAM_EXTERNAL_QUERIES = [FindTeamQuery, FindTeamMemberQuery]; diff --git a/src/teams/application/use-cases/invitions/accept-invitation.use-case.ts b/src/teams/application/use-cases/invitions/accept-invitation.use-case.ts new file mode 100644 index 0000000..b45a4b6 --- /dev/null +++ b/src/teams/application/use-cases/invitions/accept-invitation.use-case.ts @@ -0,0 +1,94 @@ +import { ITeamsRepository } from '@core/teams/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { BaseException } from '@shared/error'; +import type { TeamInvite } from '../../dtos/invitation.dto'; +import { ICacheService } from '@shared/adapters/cache/ports'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; + +@Injectable() +export class AcceptInvitationUseCase { + private readonly INVITES_KEY = (code: string) => `inv:code:${code}`; + private readonly TEAM_INVITES_KEY = (teamId: string) => `team:invites:${teamId}`; + private readonly USER_INVITES_KEY = (email: string) => `user:invites:${email.toLowerCase()}`; + + constructor( + @Inject('ITeamsRepository') private readonly teamsRepo: ITeamsRepository, + @Inject(CACHE_SERVICE) private readonly cacheService: ICacheService, + ) {} + + async execute(code: string, userId: string, email: string) { + const inviteRaw = await this.cacheService.getOne(this.INVITES_KEY(code)); + if (!inviteRaw) { + throw new BaseException( + { + code: 'INVITE_EXPIRED_OR_INVALID', + message: 'The invitation link has expired or is no longer valid.', + }, + HttpStatus.GONE, + ); + } + + const invite = JSON.parse(inviteRaw) as TeamInvite; + if (invite?.email?.toLowerCase() !== email.toLowerCase()) { + throw new BaseException( + { + code: 'INVITE_EMAIL_MISMATCH', + message: 'This invitation was sent to a different email address.', + }, + HttpStatus.FORBIDDEN, + ); + } + + const member = await this.teamsRepo.findMember(invite.teamId, userId); + if (member) { + if (member.status === 'banned') { + throw new BaseException( + { code: 'MEMBER_BANNED', message: 'You are banned from this team.' }, + HttpStatus.FORBIDDEN, + ); + } + if (member.status === 'active') { + await this.cleanupInvite(code, invite.teamId, email); + throw new BaseException( + { code: 'ALREADY_MEMBER', message: 'You are already a member of this team.' }, + HttpStatus.BAD_REQUEST, + ); + } + } + + await this.teamsRepo.addMember({ + teamId: invite.teamId, + userId, + role: invite.role, + status: 'active', + joinedAt: new Date(), + }); + + await this.cacheService + .transaction() + .removeOne(this.INVITES_KEY(code)) + .removeOneFromCollection(this.TEAM_INVITES_KEY(invite.teamId), code) + .removeOneFromCollection(this.USER_INVITES_KEY(email.toLowerCase()), code) + .execute(); + + return { success: true, message: 'Вы успешно присоединились к команде' }; + } + + private checkMemberStatus(member: any) { + if (member?.status === 'banned') { + // throw new BaseException({ code: 'MEMBER_BANNED' }, 403); + } + if (member?.status === 'active') { + // throw new BaseException({ code: 'ALREADY_MEMBER' }, 400); + } + } + + private async cleanupInvite(code: string, teamId: string, email: string) { + await this.cacheService + .transaction() + .removeOne(this.INVITES_KEY(code)) + .removeOneFromCollection(this.TEAM_INVITES_KEY(teamId), code) + .removeOneFromCollection(this.USER_INVITES_KEY(email.toLowerCase()), code) + .execute(); + } +} diff --git a/src/teams/application/use-cases/invitions/decline-invitation.use-case.ts b/src/teams/application/use-cases/invitions/decline-invitation.use-case.ts new file mode 100644 index 0000000..e13c696 --- /dev/null +++ b/src/teams/application/use-cases/invitions/decline-invitation.use-case.ts @@ -0,0 +1,97 @@ +import { ITeamsRepository } from '@core/teams/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { BaseException } from '@shared/error'; +import type { TeamInvite } from '../../dtos/invitation.dto'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; + +@Injectable() +export class DeclineInvitationUseCase { + private readonly INVITES_KEY = (code: string) => `inv:code:${code}`; + private readonly TEAM_INVITES_KEY = (teamId: string) => `team:invites:${teamId}`; + private readonly USER_INVITES_KEY = (email: string) => `user:invites:${email.toLowerCase()}`; + + constructor( + @Inject('ITeamsRepository') private readonly teamsRepo: ITeamsRepository, + @Inject(CACHE_SERVICE) private readonly cacheService: ICacheService, + ) {} + + async execute(slug: string, code: string, userId: string, userEmail: string) { + const team = await this.getTeamOrThrow(slug); + const invite = await this.getInviteOrThrow(code); + + this.validateInviteOwnership(invite, team.id); + + await this.validateAccess(team.id, userId, userEmail, invite.email); + + await this.cleanupInvite(code, team.id, invite.email); + + return { + success: true, + message: 'Приглашение успешно удалено', + }; + } + + private async validateAccess( + teamId: string, + userId: string, + currentUserEmail: string, + inviteEmail: string, + ) { + if (currentUserEmail.toLowerCase() === inviteEmail.toLowerCase()) { + return; + } + + const member = await this.teamsRepo.findMember(teamId, userId); + if (member && (member.role === 'owner' || member.role === 'admin')) { + return; + } + + throw new BaseException( + { + code: 'INSUFFICIENT_PERMISSIONS', + message: 'У вас нет прав для отмены этого приглашения', + }, + HttpStatus.FORBIDDEN, + ); + } + + private async getTeamOrThrow(slug: string) { + const team = await this.teamsRepo.findBySlug(slug); + if (!team) + throw new BaseException( + { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, + HttpStatus.NOT_FOUND, + ); + return team; + } + + private async getInviteOrThrow(code: string) { + const rawInvite = await this.cacheService.getOne(this.INVITES_KEY(code)); + if (!rawInvite) { + throw new BaseException( + { code: 'INVITE_NOT_FOUND', message: 'Приглашение не найдено' }, + HttpStatus.NOT_FOUND, + ); + } + return JSON.parse(rawInvite) as TeamInvite; + } + + private validateInviteOwnership(invite: TeamInvite, teamId: string) { + if (invite.teamId !== teamId) { + throw new BaseException( + { code: 'ACCESS_DENIED', message: 'Ошибка доступа' }, + HttpStatus.FORBIDDEN, + ); + } + } + + private async cleanupInvite(code: string, teamId: string, email: string) { + await this.cacheService + .transaction() + .removeOne(this.INVITES_KEY(code)) + .removeOneFromCollection(this.TEAM_INVITES_KEY(teamId), code) + .removeOneFromCollection(this.USER_INVITES_KEY(email), code) + .execute(); + } +} diff --git a/src/teams/application/use-cases/invitions/get-invitation.query.ts b/src/teams/application/use-cases/invitions/get-invitation.query.ts new file mode 100644 index 0000000..2336765 --- /dev/null +++ b/src/teams/application/use-cases/invitions/get-invitation.query.ts @@ -0,0 +1,72 @@ +import { ITeamsRepository } from '@core/teams/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { BaseException } from '@shared/error'; +import type { TeamInvite } from '../../dtos/invitation.dto'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; + +@Injectable() +export class GetInvitationQuery { + private readonly INVITES_KEY = (code: string) => `inv:code:${code}`; + + constructor( + @Inject('ITeamsRepository') private readonly teamsRepo: ITeamsRepository, + @Inject(CACHE_SERVICE) private readonly cacheService: ICacheService, + ) {} + + async execute(slug: string, code: string, userId: string, userEmail: string) { + const team = await this.getTeamOrThrow(slug); + const invite = await this.getInviteOrThrow(code); + + this.validateInviteOwnership(invite, team.id); + await this.validateAccess(team.id, userId, userEmail, invite.email); + + return { code, ...invite }; + } + + private async getTeamOrThrow(slug: string) { + const team = await this.teamsRepo.findBySlug(slug); + if (!team) + throw new BaseException( + { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, + HttpStatus.NOT_FOUND, + ); + return team; + } + + private async getInviteOrThrow(code: string) { + const raw = await this.cacheService.getOne(this.INVITES_KEY(code)); + if (!raw) + throw new BaseException( + { code: 'INVITE_EXPIRED', message: 'Срок действия приглашения истек' }, + HttpStatus.NOT_FOUND, + ); + return JSON.parse(raw) as TeamInvite; + } + + private validateInviteOwnership(invite: TeamInvite, teamId: string) { + if (invite.teamId !== teamId) { + throw new BaseException( + { code: 'INVITE_NOT_FOUND', message: 'Приглашение не найдено' }, + HttpStatus.NOT_FOUND, + ); + } + } + + private async validateAccess( + teamId: string, + userId: string, + currentUserEmail: string, + inviteEmail: string, + ) { + if (currentUserEmail.toLowerCase() === inviteEmail.toLowerCase()) return; + + const member = await this.teamsRepo.findMember(teamId, userId); + if (member && (member.role === 'owner' || member.role === 'admin')) return; + + throw new BaseException( + { code: 'INSUFFICIENT_PERMISSIONS', message: 'У вас нет прав просмотра' }, + HttpStatus.FORBIDDEN, + ); + } +} diff --git a/src/teams/application/use-cases/invitions/get-invitations.query.ts b/src/teams/application/use-cases/invitions/get-invitations.query.ts new file mode 100644 index 0000000..52d4f09 --- /dev/null +++ b/src/teams/application/use-cases/invitions/get-invitations.query.ts @@ -0,0 +1,67 @@ +import { ITeamsRepository } from '@core/teams/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { BaseException } from '@shared/error'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; + +@Injectable() +export class GetInvitationsQuery { + private readonly TEAM_INVITES_KEY = (teamId: string) => `team:invites:${teamId}`; + private readonly INVITES_KEY = (code: string) => `inv:code:${code}`; + + constructor( + @Inject('ITeamsRepository') private readonly teamsRepo: ITeamsRepository, + @Inject(CACHE_SERVICE) private readonly cacheService: ICacheService, + ) {} + + async execute(slug: string, userId: string) { + const team = await this.getTeamOrThrow(slug); + await this.ensureAdminPermissions(team.id, userId); + + const teamKey = this.TEAM_INVITES_KEY(team.id); + const codes = await this.cacheService.getCollection(teamKey); + if (!codes.length) return []; + + const results = await this.cacheService.getMany(codes.map(this.INVITES_KEY)); + + const { active, expired } = results.reduce( + (acc, raw, i) => { + if (raw) { + acc.active.push({ code: codes[i], ...JSON.parse(raw) }); + } else { + acc.expired.push(codes[i]); + } + return acc; + }, + { active: [], expired: [] }, + ); + + if (expired.length > 0) { + this.cacheService + .removeManyFromCollection(teamKey, expired) + .catch((e) => console.error('Cleanup error:', e)); + } + + return active; + } + + private async getTeamOrThrow(slug: string) { + const team = await this.teamsRepo.findBySlug(slug); + if (!team) + throw new BaseException( + { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, + HttpStatus.NOT_FOUND, + ); + return team; + } + + private async ensureAdminPermissions(teamId: string, userId: string) { + const member = await this.teamsRepo.findMember(teamId, userId); + if (!member || (member.role !== 'owner' && member.role !== 'admin')) { + throw new BaseException( + { code: 'INSUFFICIENT_PERMISSIONS', message: 'У вас нет прав' }, + HttpStatus.FORBIDDEN, + ); + } + } +} diff --git a/src/teams/application/use-cases/invitions/get-my-invites.use-case.ts b/src/teams/application/use-cases/invitions/get-my-invites.use-case.ts new file mode 100644 index 0000000..9c1c55a --- /dev/null +++ b/src/teams/application/use-cases/invitions/get-my-invites.use-case.ts @@ -0,0 +1,42 @@ +import { TeamMemberMapper } from '@core/teams/application/mappers'; +import { Inject, Injectable } from '@nestjs/common'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; + +@Injectable() +export class GetMyInvitesUseCase { + constructor( + @Inject(CACHE_SERVICE) + private readonly cacheService: ICacheService, + ) {} + + async execute(email: string) { + const userKey = `user:invites:${email.toLowerCase()}`; + const codes = await this.cacheService.getCollection(userKey); + + if (!codes.length) return []; + + const inviteKeys = codes.map((c) => `inv:code:${c}`); + const results = await this.cacheService.getMany(inviteKeys); + + const { activeInvites, expiredCodes } = results.reduce( + (acc, raw, i) => { + if (raw) { + acc.activeInvites.push(TeamMemberMapper.toPublicInvite(raw, codes[i])); + } else { + acc.expiredCodes.push(codes[i]); + } + return acc; + }, + { activeInvites: [], expiredCodes: [] }, + ); + + if (expiredCodes.length > 0) { + this.cacheService.removeManyFromCollection(userKey, expiredCodes).catch((err) => { + console.error('Failed to cleanup expired invites:', err); + }); + } + + return activeInvites; + } +} diff --git a/src/teams/application/use-cases/invitions/send-invitation.use-case.ts b/src/teams/application/use-cases/invitions/send-invitation.use-case.ts new file mode 100644 index 0000000..f970066 --- /dev/null +++ b/src/teams/application/use-cases/invitions/send-invitation.use-case.ts @@ -0,0 +1,141 @@ +import { TeamMailJobs, TeamQueues } from '@core/teams/domain/enums'; +import { ITeamsRepository } from '@core/teams/domain/repository'; +import { InjectQueue } from '@nestjs/bullmq'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Queue } from 'bullmq'; +import { InviteMemberDto } from '../../dtos'; +import { BaseException } from '@shared/error'; +import { generateSecret } from 'otplib'; +import type { TeamInvite } from '../../dtos/invitation.dto'; +import { TeamInvitationEvent } from '@core/teams/domain/events'; +import { TeamMemberPolicy } from '@core/teams/domain/policy'; +import type { TeamRole } from '@shared/entities'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; + +@Injectable() +export class SendInvitationUseCase { + private readonly INVITE_TTL = 86400; + private readonly INVITES_KEY = (code: string) => `inv:code:${code}`; + private readonly TEAM_INVITES_KEY = (teamId: string) => `team:invites:${teamId}`; + private readonly USER_INVITES_KEY = (email: string) => `user:invites:${email.toLowerCase()}`; + + constructor( + @Inject('ITeamsRepository') private readonly teamsRepo: ITeamsRepository, + @Inject(CACHE_SERVICE) private readonly cacheService: ICacheService, + @InjectQueue(TeamQueues.TEAM_MAIL) private readonly mailQueue: Queue, + private readonly cfg: ConfigService, + private readonly policy: TeamMemberPolicy, + ) {} + + async execute(slug: string, inviterId: string, dto: InviteMemberDto) { + const team = await this.getTeamOrThrow(slug); + const inviter = await this.getInviterOrThrow(team.id, inviterId); + + this.validatePermissions(inviter.role as TeamRole, dto.role as TeamRole); + await this.ensureNotAlreadyMember(team.id, dto.email); + await this.ensureNoPendingInvite(team.id, dto.email); + + const code = generateSecret({ length: 8 }); + const inviteData = this.buildInviteData(team, inviter, dto); + + await this.saveInviteToCache(code, inviteData); + + await this.sendEmailNotification(code, team.name, dto.email); + + return { success: true, message: `Приглашение отправлено на ${dto.email.toLowerCase()}` }; + } + + private async getTeamOrThrow(slug: string) { + const team = await this.teamsRepo.findBySlug(slug); + if (!team) + throw new BaseException( + { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, + HttpStatus.NOT_FOUND, + ); + return team; + } + + private async getInviterOrThrow(teamId: string, userId: string) { + const inviter = await this.teamsRepo.findMember(teamId, userId); + if (!inviter) + throw new BaseException( + { code: 'NOT_A_MEMBER', message: 'Вы не член команды' }, + HttpStatus.FORBIDDEN, + ); + return inviter; + } + + private validatePermissions(inviterRole: TeamRole, targetRole: TeamRole) { + if (!this.policy.canInvite(inviterRole, targetRole || 'member')) { + throw new BaseException( + { code: 'INSUFFICIENT_PERMISSIONS', message: 'Недостаточно прав' }, + HttpStatus.FORBIDDEN, + ); + } + } + + private async ensureNotAlreadyMember(teamId: string, email: string) { + const member = await this.teamsRepo.findMember(teamId, email); // Тут лучше искать по email в репо + if (member) + throw new BaseException( + { code: 'ALREADY_MEMBER', message: 'Уже в команде' }, + HttpStatus.BAD_REQUEST, + ); + } + + private async ensureNoPendingInvite(teamId: string, email: string) { + const activeCodes = await this.cacheService.getCollection(this.USER_INVITES_KEY(email)); + if (activeCodes.length === 0) return; + + const invitesData = await this.cacheService.getMany(activeCodes.map(this.INVITES_KEY)); + const hasDuplicate = invitesData + .filter((d): d is string => !!d) + .map((d) => JSON.parse(d) as TeamInvite) + .some((i) => i.teamId === teamId); + + if (hasDuplicate) { + throw new BaseException( + { code: 'INVITATION_ALREADY_SENT', message: 'Приглашение уже в пути' }, + HttpStatus.BAD_REQUEST, + ); + } + } + + private buildInviteData(team: any, inviter: any, dto: InviteMemberDto): TeamInvite { + const expiresAt = new Date(Date.now() + this.INVITE_TTL * 1000); + return { + teamId: team.id, + teamName: team.name, + teamAvatar: team.avatarUrl, + email: dto.email.toLowerCase(), + role: (dto.role || 'member') as TeamRole, + inviterId: inviter.userId, + inviterName: inviter.firstName, + createdAt: new Date().toISOString(), + expiresAt: expiresAt.toISOString(), + }; + } + + private async saveInviteToCache(code: string, data: TeamInvite) { + await this.cacheService + .transaction() + .setOne(this.INVITES_KEY(code), JSON.stringify(data), this.INVITE_TTL) + .addOneToCollection(this.TEAM_INVITES_KEY(data.teamId), code) + .addOneToCollection(this.USER_INVITES_KEY(data.email), code) + .execute(); + } + + private async sendEmailNotification(code: string, teamName: string, email: string) { + const origins = this.cfg.get('CORS_ALLOWED_ORIGINS') || []; + const url = `${origins[0]}/invites/accept?code=${code}`; + const event = new TeamInvitationEvent(email, teamName, url); + + await this.mailQueue.add(TeamMailJobs.SEND_TEAM_INVITATION, event, { + attempts: 3, + backoff: { type: 'exponential', delay: 5000 }, + removeOnComplete: true, + }); + } +} diff --git a/src/teams/application/use-cases/invitions/update-invitation.use-case.ts b/src/teams/application/use-cases/invitions/update-invitation.use-case.ts new file mode 100644 index 0000000..84fde3c --- /dev/null +++ b/src/teams/application/use-cases/invitions/update-invitation.use-case.ts @@ -0,0 +1,100 @@ +import { ITeamsRepository } from '@core/teams/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { UpdateInvitationDto } from '../../dtos'; +import { BaseException } from '@shared/error'; +import { TeamInvite } from '../../dtos/invitation.dto'; +import { TeamMemberPolicy } from '@core/teams/domain/policy'; +import { TeamRole } from '@shared/entities'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; + +@Injectable() +export class UpdateInvitationUseCase { + private readonly INVITES_KEY = (code: string) => `inv:code:${code}`; + + constructor( + @Inject('ITeamsRepository') private readonly teamsRepo: ITeamsRepository, + @Inject(CACHE_SERVICE) private readonly cacheService: ICacheService, + private readonly policy: TeamMemberPolicy, + ) {} + + async execute(slug: string, code: string, userId: string, dto: UpdateInvitationDto) { + const team = await this.getTeamOrThrow(slug); + const member = await this.getMemberOrThrow(team.id, userId); + + const key = this.INVITES_KEY(code); + const { invite, ttlSeconds } = await this.getInviteContextOrThrow(key); + + this.validateInviteOwnership(invite, team.id); + this.validatePolicy(member.role as TeamRole, invite.role as TeamRole, dto.role as TeamRole); + + invite.role = dto.role as TeamRole; + await this.cacheService.setOne(key, JSON.stringify(invite), ttlSeconds); + + return { + success: true, + message: 'Роль в приглашении успешно обновлена', + }; + } + + private async getTeamOrThrow(slug: string) { + const team = await this.teamsRepo.findBySlug(slug); + if (!team) { + throw new BaseException( + { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, + HttpStatus.NOT_FOUND, + ); + } + return team; + } + + private async getMemberOrThrow(teamId: string, userId: string) { + const member = await this.teamsRepo.findMember(teamId, userId); + if (!member) { + throw new BaseException( + { code: 'NOT_A_MEMBER', message: 'Вы не член команды' }, + HttpStatus.FORBIDDEN, + ); + } + return member; + } + + private async getInviteContextOrThrow(key: string) { + const { value, ttlSeconds } = await this.cacheService.getOneWithTtl(key); + + if (!value || ttlSeconds <= 0) { + throw new BaseException( + { + code: 'INVITE_NOT_FOUND_OR_EXPIRED', + message: 'Приглашение не найдено или истекло', + }, + HttpStatus.NOT_FOUND, + ); + } + + return { invite: JSON.parse(value) as TeamInvite, ttlSeconds }; + } + + private validateInviteOwnership(invite: TeamInvite, teamId: string) { + if (invite.teamId !== teamId) { + throw new BaseException( + { code: 'INVITE_TEAM_MISMATCH', message: 'Приглашение принадлежит другой команде' }, + HttpStatus.BAD_REQUEST, + ); + } + } + + private validatePolicy(issuerRole: TeamRole, currentTargetRole: TeamRole, newRole: TeamRole) { + const canUpdate = this.policy.canAssignRole(issuerRole, currentTargetRole, newRole); + + if (!canUpdate) { + throw new BaseException( + { + code: 'INSUFFICIENT_PERMISSIONS', + message: 'У вас недостаточно прав для назначения этой роли', + }, + HttpStatus.FORBIDDEN, + ); + } + } +} diff --git a/src/teams/application/use-cases/members/find-team-member.query.ts b/src/teams/application/use-cases/members/find-team-member.query.ts new file mode 100644 index 0000000..291ce9f --- /dev/null +++ b/src/teams/application/use-cases/members/find-team-member.query.ts @@ -0,0 +1,14 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ITeamsRepository } from '../../../domain/repository'; + +@Injectable() +export class FindTeamMemberQuery { + constructor( + @Inject('ITeamsRepository') + private readonly repository: ITeamsRepository, + ) {} + + async execute(teamId: string, userId: string) { + return this.repository.findMember(teamId, userId); + } +} diff --git a/src/teams/application/use-cases/members/get-team-members.query.ts b/src/teams/application/use-cases/members/get-team-members.query.ts new file mode 100644 index 0000000..f2cc552 --- /dev/null +++ b/src/teams/application/use-cases/members/get-team-members.query.ts @@ -0,0 +1,36 @@ +import { ITeamsRepository } from '@core/teams/domain/repository'; +import { TeamMemberMapper } from '@core/teams/application/mappers'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { BaseException } from '@shared/error'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class GetTeamMembersQuery { + constructor( + @Inject('ITeamsRepository') + private readonly teamsRepo: ITeamsRepository, + private readonly cfg: ConfigService, + ) {} + + async execute(slug: string) { + const team = await this.teamsRepo.findBySlug(slug); + + if (!team) { + throw new BaseException( + { code: 'TEAM_NOT_FOUND', message: `Команда ${slug} не найдена` }, + HttpStatus.NOT_FOUND, + ); + } + const cdn = this.getCdnBaseUrl(); + const members = await this.teamsRepo.findMembers(team.id); + return TeamMemberMapper.toList(members, cdn); + } + + private getCdnBaseUrl(): string { + const domain = this.cfg.get('DOMAIN'); + const bucket = this.cfg.get('S3_BUCKET_NAME'); + const endpoint = this.cfg.get('S3_ENDPOINT'); + + return domain ? `https://cdn.${domain}/${bucket}` : `${endpoint}/${bucket}`; + } +} diff --git a/src/teams/application/use-cases/members/remove-team-member.use-case.ts b/src/teams/application/use-cases/members/remove-team-member.use-case.ts new file mode 100644 index 0000000..a8c219f --- /dev/null +++ b/src/teams/application/use-cases/members/remove-team-member.use-case.ts @@ -0,0 +1,80 @@ +import { TeamMemberPolicy } from '@core/teams/domain/policy'; +import { ITeamsRepository } from '@core/teams/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import type { TeamRole } from '@shared/entities'; +import { BaseException } from '@shared/error'; + +@Injectable() +export class RemoveTeamMemberUseCase { + constructor( + @Inject('ITeamsRepository') + private readonly teamsRepo: ITeamsRepository, + private readonly policy: TeamMemberPolicy, + ) {} + + async execute(slug: string, currentUserId: string, targetUserId: string) { + const team = await this.teamsRepo.findBySlug(slug); + if (!team) { + throw new BaseException( + { code: 'TEAM_NOT_FOUND', message: `Команда ${slug} не найдена` }, + HttpStatus.NOT_FOUND, + ); + } + + const [currentUser, targetUser] = await Promise.all([ + this.teamsRepo.findMember(team.id, currentUserId), + this.teamsRepo.findMember(team.id, targetUserId), + ]); + + if (!targetUser) { + throw new BaseException( + { code: 'MEMBER_NOT_FOUND', message: 'Участник не найден' }, + HttpStatus.NOT_FOUND, + ); + } + + if (!currentUser) { + throw new BaseException( + { code: 'NOT_A_TEAM_MEMBER', message: 'Вы не состоите в этой команде' }, + HttpStatus.FORBIDDEN, + ); + } + + const isSelfRemoval = currentUserId === targetUserId; + + const canRemove = this.policy.canRemove( + currentUser.role as TeamRole, + targetUser.role as TeamRole, + isSelfRemoval, + ); + + if (!canRemove) { + const errorCode = isSelfRemoval ? 'OWNER_CANNOT_LEAVE' : 'KICK_FORBIDDEN'; + const errorMessage = isSelfRemoval + ? 'Владелец не может покинуть команду без передачи прав' + : 'У вас недостаточно прав, чтобы исключить этого участника'; + + throw new BaseException( + { code: errorCode, message: errorMessage }, + HttpStatus.FORBIDDEN, + ); + } + + try { + const result = await this.teamsRepo.removeMember(team.id, targetUserId); + return { + success: result, + message: isSelfRemoval + ? `Вы успешно покинули команду ${team.name}` + : `Участник успешно исключен из команды ${team.name}`, + }; + } catch (error) { + if (error instanceof BaseException) throw error; + + throw new BaseException( + { code: 'MEMBER_REMOVAL_FAILED', message: 'Ошибка при удалении участника' }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/teams/application/use-cases/members/update-team-member.use-case.ts b/src/teams/application/use-cases/members/update-team-member.use-case.ts new file mode 100644 index 0000000..84813dd --- /dev/null +++ b/src/teams/application/use-cases/members/update-team-member.use-case.ts @@ -0,0 +1,103 @@ +import { ITeamsRepository } from '@core/teams/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { UpdateMemberDto } from '../../dtos'; +import { BaseException } from '@shared/error'; +import { TeamMemberPolicy } from '@core/teams/domain/policy'; +import { TeamRole } from '@shared/entities'; + +@Injectable() +export class UpdateTeamMemberUseCase { + constructor( + @Inject('ITeamsRepository') + private readonly teamsRepo: ITeamsRepository, + private readonly teamMemberPolicy: TeamMemberPolicy, + ) {} + + async execute(slug: string, currentUserId: string, targetUserId: string, dto: UpdateMemberDto) { + if (currentUserId === targetUserId) { + throw new BaseException( + { code: 'SELF_EDIT_RESTRICTED', message: 'Вы не можете редактировать свои данные' }, + HttpStatus.BAD_REQUEST, + ); + } + + const team = await this.teamsRepo.findBySlug(slug); + if (!team) { + throw new BaseException( + { code: 'TEAM_NOT_FOUND', message: `Команда ${slug} не найдена` }, + HttpStatus.NOT_FOUND, + ); + } + + const [currentUser, targetUser] = await Promise.all([ + this.teamsRepo.findMember(team.id, currentUserId), + this.teamsRepo.findMember(team.id, targetUserId), + ]); + + if (!targetUser) { + throw new BaseException( + { code: 'MEMBER_NOT_FOUND', message: 'Участник не найден' }, + HttpStatus.NOT_FOUND, + ); + } + + if (!currentUser) { + throw new BaseException( + { code: 'NOT_A_TEAM_MEMBER', message: 'Вы не состоите в этой команде' }, + HttpStatus.FORBIDDEN, + ); + } + + const issuerRole = currentUser.role as TeamRole; + const targetRole = targetUser.role as TeamRole; + + if (!this.teamMemberPolicy.canManage(issuerRole, targetRole)) { + throw new BaseException( + { code: 'INSUFFICIENT_RANK', message: 'Ваш ранг должен быть выше ранга цели' }, + HttpStatus.FORBIDDEN, + ); + } + + if (dto.role) { + if (!this.teamMemberPolicy.canAssignRole(issuerRole, targetRole, dto.role)) { + throw new BaseException( + { + code: 'INVALID_ROLE_ASSIGNMENT', + message: 'У вас нет прав назначить выбранную роль', + }, + HttpStatus.FORBIDDEN, + ); + } + } + + if (dto.status) { + if (!this.teamMemberPolicy.canChangeStatus(issuerRole, targetRole)) { + throw new BaseException( + { + code: 'INVALID_STATUS_CHANGE', + message: 'Вы не можете менять статус этого участника', + }, + HttpStatus.FORBIDDEN, + ); + } + } + + try { + const result = await this.teamsRepo.updateMember(team.id, targetUserId, dto); + return { + success: result, + message: `Данные участника команды ${team.name} успешно обновлены`, + }; + } catch (error) { + if (error instanceof BaseException) throw error; + + throw new BaseException( + { + code: 'MEMBER_UPDATE_FAILED', + message: 'Ошибка при обновлении данных участника', + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/teams/domain/entities/index.ts b/src/teams/domain/entities/index.ts new file mode 100644 index 0000000..40d100b --- /dev/null +++ b/src/teams/domain/entities/index.ts @@ -0,0 +1 @@ +export * from './teams.domain'; diff --git a/src/teams/domain/entities/teams.domain.ts b/src/teams/domain/entities/teams.domain.ts new file mode 100644 index 0000000..9ffee16 --- /dev/null +++ b/src/teams/domain/entities/teams.domain.ts @@ -0,0 +1,22 @@ +import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'; +import { teams, teamMembers, tags, teamsToTags } from '../../infrastructure/persistence/models'; + +export type Team = InferSelectModel; +export type NewTeam = InferInsertModel; + +export type TeamMember = InferSelectModel; +export type NewTeamMember = InferInsertModel; + +export type Tag = InferSelectModel; +export type NewTag = InferInsertModel; + +export type TeamToTag = InferSelectModel; +export type NewTeamToTag = InferInsertModel; + +export type TeamWithMembers = Team & { + members: TeamMember[]; +}; + +export type TeamWithTags = Team & { + tags: Tag[]; +}; diff --git a/src/teams/domain/enums/index.ts b/src/teams/domain/enums/index.ts new file mode 100644 index 0000000..4a72780 --- /dev/null +++ b/src/teams/domain/enums/index.ts @@ -0,0 +1 @@ +export { TeamMailJobs, TeamQueues } from './mail-jobs.enum'; diff --git a/src/teams/domain/enums/mail-jobs.enum.ts b/src/teams/domain/enums/mail-jobs.enum.ts new file mode 100644 index 0000000..a46d334 --- /dev/null +++ b/src/teams/domain/enums/mail-jobs.enum.ts @@ -0,0 +1,7 @@ +export enum TeamQueues { + TEAM_MAIL = 'TEAM_MAIL_QUEUE', +} + +export enum TeamMailJobs { + SEND_TEAM_INVITATION = 'TEAM_SEND_TEAM_INVITATION', +} diff --git a/src/teams/domain/events/index.ts b/src/teams/domain/events/index.ts new file mode 100644 index 0000000..f0cfd4e --- /dev/null +++ b/src/teams/domain/events/index.ts @@ -0,0 +1 @@ +export { TeamInvitationEvent } from './team-invitation.event'; diff --git a/src/teams/domain/events/team-invitation.event.ts b/src/teams/domain/events/team-invitation.event.ts new file mode 100644 index 0000000..5dc9d67 --- /dev/null +++ b/src/teams/domain/events/team-invitation.event.ts @@ -0,0 +1,7 @@ +export class TeamInvitationEvent { + constructor( + public email: string, + public teamName: string, + public inviteUrl: string, + ) {} +} diff --git a/src/teams/domain/policy/index.ts b/src/teams/domain/policy/index.ts new file mode 100644 index 0000000..f076bce --- /dev/null +++ b/src/teams/domain/policy/index.ts @@ -0,0 +1 @@ +export { TeamMemberPolicy } from './team-member.policy'; diff --git a/src/teams/domain/policy/team-member.policy.ts b/src/teams/domain/policy/team-member.policy.ts new file mode 100644 index 0000000..3979e53 --- /dev/null +++ b/src/teams/domain/policy/team-member.policy.ts @@ -0,0 +1,105 @@ +import { Injectable } from '@nestjs/common'; +import { ROLE_PRIORITY } from '@shared/constants'; +import type { TeamRole } from '@shared/entities'; + +@Injectable() +export class TeamMemberPolicy { + constructor() {} + + private getPriority(role: TeamRole): number { + return ROLE_PRIORITY[role] ?? 0; + } + + /** + * Может ли Инициатор вообще редактировать Цель? + */ + public canManage(issuerRole: TeamRole, targetRole: TeamRole): boolean { + // Минимальный порог для управления — администратор + if (this.getPriority(issuerRole) < ROLE_PRIORITY.admin) return false; + + // Нельзя редактировать того, кто равен или выше по рангу + return this.getPriority(issuerRole) > this.getPriority(targetRole); + } + + /** + * Может ли Инициатор назначить Цели новую роль? + */ + public canAssignRole( + issuerRole: TeamRole, + targetCurrentRole: TeamRole, + newRole: TeamRole, + ): boolean { + // 1. Проверка прав на управление целью + if (!this.canManage(issuerRole, targetCurrentRole)) return false; + + // 2. Роль Owner неприкосновенна (нельзя снять и нельзя назначить через обычный Update) + if (targetCurrentRole === 'owner' || newRole === 'owner') return false; + + // 3. Нельзя назначить роль выше своей или равную своей (если ты не владелец) + if (issuerRole !== 'owner' && this.getPriority(newRole) >= this.getPriority(issuerRole)) { + return false; + } + + return true; + } + + /** + * Может ли Инициатор менять статус (ban/block/active) Цели? + */ + public canChangeStatus(issuerRole: TeamRole, targetRole: TeamRole): boolean { + // Владельца нельзя забанить или деактивировать + if (targetRole === 'owner') return false; + + // В остальном работают стандартные правила иерархии + return this.canManage(issuerRole, targetRole); + } + + /** + * Может ли Инициатор удалить Цель (или самого себя)? + */ + public canRemove(issuerRole: TeamRole, targetRole: TeamRole, isSelf: boolean): boolean { + if (isSelf) { + return issuerRole !== 'owner'; + } + + const issuerPrio = this.getPriority(issuerRole); + const targetPrio = this.getPriority(targetRole); + + return issuerPrio >= ROLE_PRIORITY.admin && issuerPrio > targetPrio; + } + + /** + * Может ли Инициатор приглашать новых участников с определенной ролью? + */ + public canInvite(issuerRole: TeamRole, newMemberRole: TeamRole): boolean { + const issuerPrio = this.getPriority(issuerRole); + const newRolePrio = this.getPriority(newMemberRole); + + // Только админы и выше могут приглашать + if (issuerPrio < ROLE_PRIORITY.admin) return false; + + // Нельзя пригласить кого-то на роль выше или равную своей (кроме owner) + if (issuerRole !== 'owner' && newRolePrio >= issuerPrio) return false; + + // Нельзя пригласить на роль owner через обычный инвайт + if (newMemberRole === 'owner') return false; + + return true; + } + + /** + * Проверяет, имеет ли участник право на обновление медиа-ресурсов (аватар, баннер) команды. + * + * @remarks + * Логика базируется на приоритете ролей. Минимально допустимая роль — Модератор. + * + * @param issuerRole - Роль участника, инициирующего обновление. + * @returns `true`, если приоритет роли равен или выше приоритета модератора, иначе `false`. + * + * @example + * const canUpdate = policy.canUpdateMedia('admin'); // true + */ + public canUpdateMedia(issuerRole: TeamRole): boolean { + return this.getPriority(issuerRole) >= ROLE_PRIORITY.moderator; + } +} diff --git a/src/teams/domain/repository/index.ts b/src/teams/domain/repository/index.ts new file mode 100644 index 0000000..0d97b36 --- /dev/null +++ b/src/teams/domain/repository/index.ts @@ -0,0 +1,5 @@ +export { + ITeamsRepository, + type RawMemberRow, + type RawMemberTeams, +} from './teams.repository.interface'; diff --git a/src/teams/domain/repository/teams.repository.interface.ts b/src/teams/domain/repository/teams.repository.interface.ts new file mode 100644 index 0000000..06b83a1 --- /dev/null +++ b/src/teams/domain/repository/teams.repository.interface.ts @@ -0,0 +1,60 @@ +import type { Team, NewTeam, NewTeamMember, Tag } from '../entities'; + +type TResponse = { success: boolean; teamId: string }; + +export type RawMemberRow = { + userId: string; + role: string; + status: string; + joinedAt: Date | string | null; + firstName: string | null; + lastName: string | null; + middleName: string | null; + avatarUrl: string | null; + email?: string; +}; + +export type RawMemberTeams = { + id: string; + name: string; + slug: string; + description: string | null; + avatarUrl: string | null; + role: string; + joinedAt: Date; +}; + +export interface ITeamsRepository { + create(ownerId: string, dto: NewTeam, tags?: string[]): Promise; + update(id: string, dto: Partial, tags?: string[]): Promise; + remove(id: string, userId: string): Promise; + + isSlugAvailable(slug: string): Promise; + + findMember(teamId: string, userId: string): Promise; + findMembers(teamId: string): Promise; + findBySlug(slug: string): Promise; + findByUser( + userId: string, + // TODO: ADD ZOD QUERY + pagination: { search?: string; limit?: number; offset?: number }, + ): Promise; + + findAllTags(options: { + search?: string; + limit?: number; + offset?: number; + }): Promise<{ data: Tag[]; total: number }>; + syncTags(teamId: string, tagNames: string[]): Promise; + + updateTeamAvatar(teamId: string, url: string): Promise; + updateTeamBanner(teamId: string, url: string): Promise; + + addMember(dto: NewTeamMember): Promise; + updateMember( + teamId: string, + userId: string, + dto: { role?: string; status?: string }, + ): Promise; + removeMember(teamId: string, userId: string): Promise; +} diff --git a/src/teams/index.ts b/src/teams/index.ts new file mode 100644 index 0000000..f4d6e9c --- /dev/null +++ b/src/teams/index.ts @@ -0,0 +1,2 @@ +export { TeamsModule } from './teams.module'; +export { FindTeamQuery, FindTeamMemberQuery } from './application/use-cases'; diff --git a/src/teams/infrastructure/listeners/index.ts b/src/teams/infrastructure/listeners/index.ts new file mode 100644 index 0000000..9c374ba --- /dev/null +++ b/src/teams/infrastructure/listeners/index.ts @@ -0,0 +1,3 @@ +import { UpdateTeamMediaListener } from './update-media.listener'; + +export const LISTENERS = [UpdateTeamMediaListener]; diff --git a/src/teams/infrastructure/listeners/update-media.listener.ts b/src/teams/infrastructure/listeners/update-media.listener.ts new file mode 100644 index 0000000..3a9f2fd --- /dev/null +++ b/src/teams/infrastructure/listeners/update-media.listener.ts @@ -0,0 +1,77 @@ +import { Inject } from '@nestjs/common'; +import { ITeamsRepository } from '@core/teams/domain/repository'; +import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { type Job, UnrecoverableError } from 'bullmq'; +import { MEDIA_JOBS, MEDIA_QUEUES, type UpdateMediaTeam } from '@shared/media'; +import { TeamMemberPolicy } from '@core/teams/domain/policy'; +import type { TeamRole } from '@shared/entities'; + +@Processor(MEDIA_QUEUES.SAVE_ENTITY) +export class UpdateTeamMediaListener extends WorkerHost { + constructor( + @Inject('ITeamsRepository') + private readonly repository: ITeamsRepository, + private readonly poilcy: TeamMemberPolicy, + ) { + super(); + } + + async process(job: Job): Promise { + if (job.name !== MEDIA_JOBS.UPDATE_TEAM_MEDIA) return; + + const { initiatorId, entity, type, path } = job.data; + + try { + const teamId = await this.validatePermissionsAndGetTeamId(entity.slug, initiatorId); + + await this.executeMediaUpdate(teamId, type, path); + + await job.log(`Successfully updated ${type} for team ${entity.slug}`); + } catch (error) { + await job.log( + `Failed to update ${type} for team ${entity.slug}: ${error instanceof Error ? error.message : String(error)}`, + ); + throw error; + } + } + + private async validatePermissionsAndGetTeamId(slug: string, userId: string): Promise { + const team = await this.repository.findBySlug(slug); + if (!team) { + throw new UnrecoverableError('Команда не найдена'); + } + + const member = await this.repository.findMember(team.id, userId); + + if (!member) { + throw new UnrecoverableError('Не состоит в этой команде'); + } + + const hasAccess = this.poilcy.canUpdateMedia(member.role as TeamRole); + + if (!hasAccess) { + throw new UnrecoverableError('Недостаточно прав для обновления медиа'); + } + + return team.id; + } + + private async executeMediaUpdate( + teamId: string, + type: 'banner' | 'avatar', + url: string, + ): Promise { + const updateActions: Record Promise> = { + banner: (id, path) => this.repository.updateTeamBanner(id, path), + avatar: (id, path) => this.repository.updateTeamAvatar(id, path), + }; + + const action = updateActions[type]; + + if (!action) { + throw new UnrecoverableError(`Unsupported media type: ${type}`); + } + + await action(teamId, url); + } +} diff --git a/src/teams/infrastructure/persistence/models/enums.ts b/src/teams/infrastructure/persistence/models/enums.ts new file mode 100644 index 0000000..2dba2b2 --- /dev/null +++ b/src/teams/infrastructure/persistence/models/enums.ts @@ -0,0 +1,17 @@ +import { baseSchema } from '@shared/entities'; + +export const roleEnum = baseSchema.enum('team_role', [ + 'owner', + 'admin', // управление юзерами, настройками + 'lead', // управление проектами + 'moderator', // чистка контента/сообщений + 'member', // обычный работяга + 'viewer', // просто смотрит +]); +export type TeamRole = (typeof roleEnum.enumValues)[number]; + +export const statusEnum = baseSchema.enum('member_status', [ + 'active', // Полноценный участник + 'banned', // Заблокирован не может вернуться по инвайту + 'inactive', // Доступ закрыт, но запись сохранена +]); diff --git a/src/teams/infrastructure/persistence/models/index.ts b/src/teams/infrastructure/persistence/models/index.ts new file mode 100644 index 0000000..f97c6e3 --- /dev/null +++ b/src/teams/infrastructure/persistence/models/index.ts @@ -0,0 +1,2 @@ +export { tags, teamMembers, teams, teamsToTags } from './teams.model'; +export { type TeamRole, roleEnum, statusEnum } from './enums'; diff --git a/src/teams/infrastructure/persistence/models/teams.model.ts b/src/teams/infrastructure/persistence/models/teams.model.ts new file mode 100644 index 0000000..44603e0 --- /dev/null +++ b/src/teams/infrastructure/persistence/models/teams.model.ts @@ -0,0 +1,74 @@ +import { primaryKey, timestamp, text, varchar, index } from 'drizzle-orm/pg-core'; +import { createId } from '@paralleldrive/cuid2'; +import { roleEnum, statusEnum } from './enums'; +import { baseSchema, users } from '@shared/entities'; +import { uniqueIndex } from 'drizzle-orm/pg-core'; +import { isNull } from 'drizzle-orm'; + +export const teams = baseSchema.table( + 'teams', + { + id: text('id') + .primaryKey() + .$defaultFn(() => createId()), + slug: varchar('slug', { length: 120 }).unique().notNull(), + name: varchar('name', { length: 100 }).notNull(), + description: text('description'), + avatarUrl: text('avatar_url'), + coverUrl: text('cover_url'), + ownerId: text('owner_id').references(() => users.id, { onDelete: 'set null' }), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), + deletedAt: timestamp('deleted_at'), + }, + (t) => ({ + uniqueActiveSlug: uniqueIndex('team_active_slug_idx').on(t.slug).where(isNull(t.deletedAt)), + slugIdx: index('team_slug_idx').on(t.slug), + ownerIdx: index('team_owner_idx').on(t.ownerId), + softDeleteIdx: index('team_deleted_at_idx').on(t.deletedAt), + }), +); + +export const teamMembers = baseSchema.table( + 'team_members', + { + teamId: text('team_id') + .references(() => teams.id, { onDelete: 'cascade' }) + .notNull(), + userId: text('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + role: roleEnum('role').default('member').notNull(), + status: statusEnum('status').default('inactive').notNull(), + joinedAt: timestamp('joined_at'), + createdAt: timestamp('created_at').defaultNow().notNull(), + }, + (t) => ({ + pk: primaryKey({ columns: [t.teamId, t.userId] }), + statusIdx: index('member_status_idx').on(t.status), + userRoleIdx: index('member_role_idx').on(t.userId, t.role), + }), +); + +export const tags = baseSchema.table('tags', { + id: text('id') + .primaryKey() + .$defaultFn(() => createId()), + name: varchar('name', { length: 50 }).unique().notNull(), +}); + +export const teamsToTags = baseSchema.table( + 'teams_to_tags', + { + teamId: text('team_id') + .references(() => teams.id, { onDelete: 'cascade' }) + .notNull(), + tagId: text('tag_id') + .references(() => tags.id, { onDelete: 'cascade' }) + .notNull(), + }, + (t) => ({ + pk: primaryKey({ columns: [t.teamId, t.tagId] }), + tagIdx: index('teams_to_tags_tag_id_idx').on(t.tagId), + }), +); diff --git a/src/teams/infrastructure/persistence/repositories/index.ts b/src/teams/infrastructure/persistence/repositories/index.ts new file mode 100644 index 0000000..259ca0a --- /dev/null +++ b/src/teams/infrastructure/persistence/repositories/index.ts @@ -0,0 +1 @@ +export { TeamsRepository } from './teams.repository'; diff --git a/src/teams/infrastructure/persistence/repositories/teams.repository.ts b/src/teams/infrastructure/persistence/repositories/teams.repository.ts new file mode 100644 index 0000000..845c376 --- /dev/null +++ b/src/teams/infrastructure/persistence/repositories/teams.repository.ts @@ -0,0 +1,281 @@ +import { Inject } from '@nestjs/common'; +import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; +import * as schema from '../models'; +import * as scUsers from '@core/user/infrastructure/persistence/models'; +import { and, asc, count, desc, eq, ilike, inArray, isNull, sql } from 'drizzle-orm'; +import type { NewTeam, NewTeamMember, Team, TeamMember } from '@core/teams/domain/entities'; +import { ITeamsRepository } from '@core/teams/domain/repository'; + +export class TeamsRepository implements ITeamsRepository { + constructor( + @Inject(DATABASE_SERVICE) + private readonly db: DatabaseService, + ) {} + + public isSlugAvailable = async (slug: string) => { + const result = await this.db + .select({ id: schema.teams.id }) + .from(schema.teams) + .where(eq(schema.teams.slug, slug)); + + return result.length === 0; + }; + + public addMember = async (dto: NewTeamMember) => { + const result = await this.db + .insert(schema.teamMembers) + .values(dto) + .onConflictDoNothing({ + target: [schema.teamMembers.teamId, schema.teamMembers.userId], + }); + + return (result?.count ?? 0) > 0; + }; + + public create = async (ownerId: string, dto: NewTeam, tags?: string[]) => { + return this.db.transaction(async (tx) => { + const [{ teamId }] = await tx + .insert(schema.teams) + .values({ ...dto, ownerId }) + .returning({ teamId: schema.teams.id }); + + if (tags?.length) { + const names = tags.map((n) => ({ name: n })); + + const insertedTags = await tx + .insert(schema.tags) + .values(names) + .onConflictDoUpdate({ + target: schema.tags.name, + set: { name: sql`${schema.tags.name}` }, + }) + .returning({ id: schema.tags.id }); + + if (insertedTags.length > 0) { + const tags = insertedTags.map((t) => ({ teamId, tagId: t.id })); + + await tx.insert(schema.teamsToTags).values(tags); + } + } + + await tx.insert(schema.teamMembers).values({ + teamId, + userId: ownerId, + role: 'owner', + status: 'active', + joinedAt: new Date(), + }); + + return { + success: true, + teamId, + }; + }); + }; + + public update = async (id: string, dto: Partial, tags?: string[]) => { + return this.db.transaction(async (tx) => { + const [{ teamId }] = await tx + .update(schema.teams) + .set(dto) + .where(eq(schema.teams.id, id)) + .returning({ teamId: schema.teams.id }); + + if (tags?.length) { + // TODO: FEAT AT FEATURE + } + + return { + success: true, + teamId, + }; + }); + }; + + public remove = async (teamId: string, userId) => { + const suffix = Date.now().toString(); + + const result = await this.db + .update(schema.teams) + .set({ + deletedAt: new Date(), + slug: sql`${schema.teams.slug} || '-' || ${suffix}`, + }) + .where(and(eq(schema.teams.id, teamId), eq(schema.teams.ownerId, userId))); + + return (result?.count ?? 0) > 0; + }; + + public findMember = async (teamId: string, userId: string) => { + const [member] = await this.membersQuery.where( + and(eq(schema.teamMembers.teamId, teamId), eq(schema.teamMembers.userId, userId)), + ); + + return member || null; + }; + + public findMembers = async (teamId: string) => { + return this.membersQuery + .where(eq(schema.teamMembers.teamId, teamId)) + .orderBy(desc(schema.teamMembers.joinedAt)); + }; + + public findByUser = async ( + userId: string, + pagination: { search?: string; limit?: number; offset?: number }, + ) => { + const { search, limit = 10, offset = 0 } = pagination; + + const filters = [ + eq(schema.teamMembers.userId, userId), + eq(schema.teamMembers.status, 'active'), + isNull(schema.teams.deletedAt), + ]; + + if (search) { + filters.push(ilike(schema.teams.name, `%${search}%`)); + } + + const query = this.db + .select({ + id: schema.teams.id, + name: schema.teams.name, + slug: schema.teams.slug, + description: schema.teams.description, + avatarUrl: schema.teams.avatarUrl, + role: schema.teamMembers.role, + joinedAt: schema.teamMembers.joinedAt, + }) + .from(schema.teamMembers) + .innerJoin(schema.teams, eq(schema.teams.id, schema.teamMembers.teamId)) + .where(and(...filters)) + .orderBy(desc(schema.teamMembers.joinedAt)) + .limit(limit) + .offset(offset); + + return query; + }; + + public findAllTags = async (options: { search?: string; limit?: number; offset?: number }) => { + const cleanSearch = options.search?.trim(); + const escapedSearch = cleanSearch?.replace(/([%_\\])/g, '\\$1'); + + const whereCondition = escapedSearch + ? ilike(schema.tags.name, `%${escapedSearch}%`) + : undefined; + + const [data, [{ total }]] = await Promise.all([ + this.db + .select() + .from(schema.tags) + .where(whereCondition) + .limit(options.limit) + .offset(options.offset) + .orderBy(asc(schema.tags.name)), + + this.db.select({ total: count() }).from(schema.tags).where(whereCondition), + ]); + + return { + data, + total: Number(total ?? 0), + }; + }; + + public findBySlug = async (slug: string) => { + const [team] = await this.db.select().from(schema.teams).where(eq(schema.teams.slug, slug)); + if (!team) return null; + return team; + }; + + public removeMember = async (teamId: string, userId: string) => { + const result = await this.db + .delete(schema.teamMembers) + .where( + and(eq(schema.teamMembers.teamId, teamId), eq(schema.teamMembers.userId, userId)), + ); + + return (result?.count ?? 0) > 0; + }; + + public syncTags = async (teamId: string, tagNames: string[]) => { + await this.db.transaction(async (tx) => { + await tx.delete(schema.teamsToTags).where(eq(schema.teamsToTags.teamId, teamId)); + + if (tagNames.length === 0) { + return; + } + + await tx + .insert(schema.tags) + .values(tagNames.map((name) => ({ name }))) + .onConflictDoNothing({ target: schema.tags.name }); + + const existingTags = await tx + .select({ id: schema.tags.id }) + .from(schema.tags) + .where(inArray(schema.tags.name, tagNames)); + + await tx + .insert(schema.teamsToTags) + .values(existingTags.map((tag) => ({ teamId, tagId: tag.id }))); + }); + + return true; + }; + + public updateMember = async (teamId: string, userId: string, dto: Partial) => { + const { role, status } = dto; + + const data = { + role, + ...(status === 'active' ? { joinedAt: new Date() } : {}), + }; + + const result = await this.db + .update(schema.teamMembers) + .set(data) + .where( + and(eq(schema.teamMembers.teamId, teamId), eq(schema.teamMembers.userId, userId)), + ); + + return (result?.count ?? 0) > 0; + }; + + public async updateTeamAvatar(teamId: string, url: string): Promise { + const result = await this.db + .update(schema.teams) + .set({ avatarUrl: url, updatedAt: new Date() }) + .where(eq(schema.teams.id, teamId)); + return (result?.count ?? 0) > 0; + } + + public async updateTeamBanner(teamId: string, url: string): Promise { + const result = await this.db + .update(schema.teams) + .set({ coverUrl: url, updatedAt: new Date() }) + .where(eq(schema.teams.id, teamId)); + return (result?.count ?? 0) > 0; + } + + private get memberSelection() { + return { + userId: schema.teamMembers.userId, + role: schema.teamMembers.role, + status: schema.teamMembers.status, + joinedAt: schema.teamMembers.joinedAt, + firstName: scUsers.users.firstName, + lastName: scUsers.users.lastName, + middleName: scUsers.users.middleName, + avatarUrl: scUsers.users.avatarUrl, + email: scUsers.users.email, + }; + } + + private get membersQuery() { + return this.db + .select(this.memberSelection) + .from(schema.teamMembers) + .innerJoin(scUsers.users, eq(schema.teamMembers.userId, scUsers.users.id)); + } +} diff --git a/src/teams/infrastructure/workers/index.ts b/src/teams/infrastructure/workers/index.ts new file mode 100644 index 0000000..d20e25d --- /dev/null +++ b/src/teams/infrastructure/workers/index.ts @@ -0,0 +1 @@ +export { MailProcessor } from './mail.processor'; diff --git a/src/teams/infrastructure/workers/mail.processor.ts b/src/teams/infrastructure/workers/mail.processor.ts new file mode 100644 index 0000000..34320b9 --- /dev/null +++ b/src/teams/infrastructure/workers/mail.processor.ts @@ -0,0 +1,49 @@ +import { Processor, WorkerHost } from '@nestjs/bullmq'; +import type { Job } from 'bullmq'; +import { IMailPort } from '@shared/adapters/mail'; +import { Inject } from '@nestjs/common'; +import { TeamInvitationEvent } from '@core/teams/domain/events'; +import { TeamQueues } from '@core/teams/domain/enums'; + +@Processor(TeamQueues.TEAM_MAIL) +export class MailProcessor extends WorkerHost { + constructor( + @Inject('IMailPort') + private readonly mailAdapter: IMailPort, + ) { + super(); + } + + async process(job: Job): Promise { + await job.log(`[START] Job ID: ${job.id} | Type: ${job.name}`); + + try { + await this.sendTeamInvitation(job); + + await job.log(`[DONE] Job ${job.id} processed`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : ''; + + await job.log(`[FAIL] ${errorMessage}`); + + if (errorStack) { + await job.log(errorStack); + } + + throw error; + } + } + + private sendTeamInvitation = async (job: Job) => { + const { email, teamName, inviteUrl } = job.data; + + await job.log(`Sending team(${teamName}) invitation link to: ${email}`); + await job.updateProgress(30); + + await this.mailAdapter.sendTeamInvitation(email, teamName, inviteUrl); + + await job.log(`Team invitation link delivered to ${email}`); + await job.updateProgress(100); + }; +} diff --git a/src/teams/teams.module.ts b/src/teams/teams.module.ts new file mode 100644 index 0000000..094595d --- /dev/null +++ b/src/teams/teams.module.ts @@ -0,0 +1,50 @@ +import { Module } from '@nestjs/common'; +import { + TeamsInvitationsController, + TeamsSettingsController, + TeamsMembersController, + TeamsController, + MeController, +} from './application/controller'; +import { BullModule } from '@nestjs/bullmq'; +import { BullBoardModule } from '@bull-board/nestjs'; +import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; +import { TeamsRepository } from './infrastructure/persistence/repositories'; +import { TeamQueues } from './domain/enums'; +import { TeamsFacade } from './application/team.facade'; +import { TeamQueries, TeamUseCases, TEAM_EXTERNAL_QUERIES } from './application/use-cases'; +import { TeamMemberPolicy } from './domain/policy'; +import { MailProcessor } from '@core/teams/infrastructure/workers'; +import { LISTENERS } from './infrastructure/listeners'; + +const REPOSITORY = { provide: 'ITeamsRepository', useClass: TeamsRepository }; + +@Module({ + imports: [ + BullModule.registerQueue({ + name: TeamQueues.TEAM_MAIL, + }), + BullBoardModule.forFeature({ + name: TeamQueues.TEAM_MAIL, + adapter: BullMQAdapter, + }), + ], + controllers: [ + TeamsInvitationsController, + TeamsSettingsController, + TeamsMembersController, + TeamsController, + MeController, + ], + providers: [ + TeamMemberPolicy, + REPOSITORY, + ...LISTENERS, + ...TeamUseCases, + ...TeamQueries, + TeamsFacade, + MailProcessor, + ], + exports: [...TEAM_EXTERNAL_QUERIES], +}) +export class TeamsModule {} diff --git a/src/user/application/controller/index.ts b/src/user/application/controller/index.ts new file mode 100644 index 0000000..0ea2e27 --- /dev/null +++ b/src/user/application/controller/index.ts @@ -0,0 +1,2 @@ +export { UserController } from './user/controller'; +export { UserSettingsController } from './settings/controller'; diff --git a/src/user/application/controller/settings/controller.ts b/src/user/application/controller/settings/controller.ts new file mode 100644 index 0000000..66a174c --- /dev/null +++ b/src/user/application/controller/settings/controller.ts @@ -0,0 +1,16 @@ +import { Body, Patch } from '@nestjs/common'; +import { UserFacade } from '../../user.facade'; +import { PatchMeNotificationsSwagger } from './swagger'; +import { ApiBaseController, GetUserId } from '@shared/decorators'; +import { UpdateNotificationsDto } from '../../dtos'; + +@ApiBaseController('users/me', 'Account Settings', true) +export class UserSettingsController { + constructor(private readonly facade: UserFacade) {} + + @Patch('notifications') + @PatchMeNotificationsSwagger() + async updateNotifications(@Body() settings: UpdateNotificationsDto, @GetUserId() id: string) { + return this.facade.updateNotifications(id, settings); + } +} diff --git a/src/user/application/controller/settings/swagger.ts b/src/user/application/controller/settings/swagger.ts new file mode 100644 index 0000000..956284f --- /dev/null +++ b/src/user/application/controller/settings/swagger.ts @@ -0,0 +1,23 @@ +import { ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { applyDecorators } from '@nestjs/common'; +import { ApiUnauthorized, ApiValidationError } from '@shared/error'; +import { ActionResponse } from '@shared/dtos'; +import { UpdateNotificationsDto } from '../../dtos'; + +export const PatchMeNotificationsSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Обновить настройки уведомлений', + description: 'Частичное обновление настроек email и push уведомлений.', + }), + ApiBody({ + type: UpdateNotificationsDto.Output, + }), + ApiResponse({ + status: 200, + description: 'Настройки успешно сохранены.', + type: ActionResponse.Output, + }), + ApiValidationError('Некорректный формат настроек'), + ApiUnauthorized(), + ); diff --git a/src/user/application/controller/user/controller.ts b/src/user/application/controller/user/controller.ts new file mode 100644 index 0000000..d60790e --- /dev/null +++ b/src/user/application/controller/user/controller.ts @@ -0,0 +1,29 @@ +import { Body, Get, Patch, Query } from '@nestjs/common'; +import { GetMeActivitySwagger, GetMeSwagger, PatchMeSwagger } from './swagger'; +import { UpdateProfileDto } from '../../dtos'; +import { ApiBaseController, GetUserId } from '@shared/decorators'; +import { UserFacade } from '../../user.facade'; +import { PaginationDto } from '@shared/dtos'; + +@ApiBaseController('users/me', 'Account Profile', true) +export class UserController { + constructor(private readonly facade: UserFacade) {} + + @Get() + @GetMeSwagger() + async getProfile(@GetUserId() id: string) { + return this.facade.getProfile(id); + } + + @Patch() + @PatchMeSwagger() + async updateProfile(@Body() dto: UpdateProfileDto, @GetUserId() id: string) { + return this.facade.updateProfile(id, dto); + } + + @Get('activity') + @GetMeActivitySwagger() + async getActivity(@Query() query: PaginationDto, @GetUserId() id: string) { + return this.facade.getActivity(id, query.page, query.limit); + } +} diff --git a/src/user/application/controller/user/swagger.ts b/src/user/application/controller/user/swagger.ts new file mode 100644 index 0000000..d5e4e5e --- /dev/null +++ b/src/user/application/controller/user/swagger.ts @@ -0,0 +1,82 @@ +import { ApiBody, ApiExtraModels, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; +import { UpdateProfileDto, UserResponse } from '../../dtos'; +import { applyDecorators } from '@nestjs/common'; +import { ApiUnauthorized, ApiValidationError } from '@shared/error'; +import { ActionResponse } from '@shared/dtos'; + +export const GetMeSwagger = () => + applyDecorators( + ApiExtraModels(UserResponse.Output), + ApiOperation({ + summary: 'Получить профиль текущего пользователя', + description: + 'Возвращает полную структуру профиля, включая вложенные объекты безопасности и настроек.', + }), + ApiResponse({ + status: 200, + description: 'Данные профиля успешно получены.', + type: UserResponse.Output, + }), + ApiUnauthorized(), + ); + +export const PatchMeSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Обновить данные профиля', + description: 'Позволяет точечно обновить имя, bio, часовой пояс и язык интерфейса.', + }), + ApiBody({ type: UpdateProfileDto.Output }), + ApiResponse({ + status: 200, + description: 'Профиль успешно обновлен.', + type: ActionResponse.Output, + }), + ApiValidationError('Ошибка валидации (например, слишком короткое имя)', [ + { + field: 'fullName', + message: 'Строка должна содержать минимум 2 символа', + code: 'too_small', + }, + ]), + ApiUnauthorized(), + ); + +export const GetMeActivitySwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Получить ленту активности пользователя', + description: 'Возвращает список последних действий пользователя (логи).', + }), + ApiQuery({ + name: 'limit', + required: false, + type: String, + description: 'Количество записей для вывода (по умолчанию 10)', + example: '15', + }), + ApiResponse({ + status: 200, + description: 'Список активностей успешно получен.', + schema: { + example: { + data: [ + { + id: 'clj1abc230000jk78', + eventType: 'TASK_COMPLETED', + description: 'Завершена задача "Обновить текст лендинга"', + createdAt: '2026-04-10T20:00:00.000Z', + metadata: { taskId: 'clj1xyz990000abc1' }, + }, + ], + meta: { + total: 45, + page: 1, + limit: 20, + totalPages: 3, + }, + }, + }, + }), + ApiUnauthorized(), + ); diff --git a/src/user/application/dtos/index.ts b/src/user/application/dtos/index.ts new file mode 100644 index 0000000..bdcec8b --- /dev/null +++ b/src/user/application/dtos/index.ts @@ -0,0 +1 @@ +export { UpdateProfileDto, UpdateNotificationsDto, UserResponse } from './user.dto'; diff --git a/src/user/application/dtos/user.dto.ts b/src/user/application/dtos/user.dto.ts new file mode 100644 index 0000000..4a9cdf9 --- /dev/null +++ b/src/user/application/dtos/user.dto.ts @@ -0,0 +1,94 @@ +import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; + +const NotificationsSchema = z + .object({ + email: z.object({ + task_assigned: z.boolean().describe('Уведомление на почту при назначении задачи'), + mentions: z.boolean().describe('Уведомление на почту при упоминании в комментариях'), + daily_summary: z.boolean().describe('Ежедневная сводка задач на почту'), + }), + push: z.object({ + task_assigned: z.boolean().describe('Push-уведомление при назначении задачи'), + reminders: z.boolean().describe('Push-уведомления о дедлайнах'), + }), + }) + .describe('Настройки уведомлений пользователя'); + +export const UpdateNotificationsSchema = NotificationsSchema.partial() + .refine((data) => Object.keys(data).length > 0, { + error: 'Необходимо передать хотя бы одно поле для обновления', + abort: true, + }) + .describe('Схема для частичного обновления настроек уведомлений'); + +export class UpdateNotificationsDto extends createZodDto(UpdateNotificationsSchema) {} + +const SecuritySchema = z + .object({ + is2faEnabled: z.boolean().describe('Статус двухфакторной аутентификации'), + lastPasswordChange: z.string().datetime().describe('Дата последнего изменения пароля'), + }) + .describe('Данные безопасности аккаунта'); + +const ProfileAvatarSchema = z + .object({ + small: z.string().url(), + medium: z.string().url(), + large: z.string().url(), + original: z.string().url(), + }) + .nullable() + .describe( + 'Аватар пользователя: объект с размерами (sm, md, lg, original) или null, если аватар отсутствует', + ); + +const ProfileSchema = z.object({ + firstName: z.string().describe('Имя пользователя'), + lastName: z.string().describe('Фамилия'), + middleName: z.string().nullable().describe('Отчество'), + bio: z.string().nullable().describe('О себе'), + avatar: ProfileAvatarSchema, + timezone: z.string().describe('Временная зона'), + language: z.string().describe('Язык интерфейса'), + createdAt: z.string().datetime().describe('Дата регистрации'), + updatedAt: z.string().datetime().describe('Дата последнего обновления профиля'), +}); + +export const UserSchema = z.object({ + id: z.string().describe('Уникальный идентификатор (CUID/UUID)'), + email: z.string().email().describe('Электронная почта'), + profile: ProfileSchema, + security: SecuritySchema, + notifications: NotificationsSchema, +}); + +export class UserResponse extends createZodDto(UserSchema) {} + +export const UpdateProfileSchema = z + .object({ + firstName: z + .string() + .min(1, 'Имя не может быть пустым') + .max(50, 'Имя слишком длинное') + .optional(), + lastName: z + .string() + .min(1, 'Фамилия не может быть пустой') + .max(50, 'Фамилия слишком длинная') + .optional(), + middleName: z.string().max(50, 'Отчество слишком длинное').nullish(), + bio: z.string().max(1000, 'О себе не более 1000 символов').nullish(), + timezone: z.string().max(50).optional(), + language: z + .string() + .length(2, 'Используйте формат ISO (например, "ru" или "en")') + .optional(), + }) + .refine((data) => Object.keys(data).length > 0, { + error: 'Необходимо передать хотя бы одно поле для обновления', + abort: true, + }) + .describe('Схема для частичного обновления данных профиля'); + +export class UpdateProfileDto extends createZodDto(UpdateProfileSchema) {} diff --git a/src/user/application/use-cases/find-profile.query.ts b/src/user/application/use-cases/find-profile.query.ts new file mode 100644 index 0000000..c840653 --- /dev/null +++ b/src/user/application/use-cases/find-profile.query.ts @@ -0,0 +1,49 @@ +import { IUserRepository } from '@core/user/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { BaseException } from '@shared/error'; +import { ImageHelper } from '@shared/utils'; + +@Injectable() +export class FindProfileQuery { + constructor( + @Inject('IUserRepository') + private readonly userRepo: IUserRepository, + private readonly cfg: ConfigService, + ) {} + + async execute(userId: string) { + const { user, notifications, security } = await this.userRepo.findProfile(userId); + + if (!user) { + throw new BaseException( + { code: 'USER_NOT_FOUND', message: 'Пользователь не найден' }, + HttpStatus.NOT_FOUND, + ); + } + const { id, email, avatarUrl, ...profile } = user; + + const cdn = this.getCdnBaseUrl(); + + const avatar = ImageHelper.buildResponsiveUrls(cdn, avatarUrl); + + return { + id, + email, + profile: { + ...profile, + avatar, + }, + security, + notifications, + }; + } + + private getCdnBaseUrl(): string { + const domain = this.cfg.get('DOMAIN'); + const bucket = this.cfg.get('S3_BUCKET_NAME'); + const endpoint = this.cfg.get('S3_ENDPOINT'); + + return domain ? `https://cdn.${domain}/${bucket}` : `${endpoint}/${bucket}`; + } +} diff --git a/src/user/application/use-cases/find-user.query.ts b/src/user/application/use-cases/find-user.query.ts new file mode 100644 index 0000000..a83ec65 --- /dev/null +++ b/src/user/application/use-cases/find-user.query.ts @@ -0,0 +1,24 @@ +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { IUserRepository } from '@core/user/domain/repository'; +import { BaseException } from '@shared/error'; + +@Injectable() +export class FindUserQuery { + constructor( + @Inject('IUserRepository') + private readonly repository: IUserRepository, + ) {} + + async execute(params: { email?: string; id?: string }) { + if (params.email) return this.repository.findByEmail(params.email); + if (params.id) return this.repository.findById(params.id); + + throw new BaseException( + { + code: 'QUERY_PARAMS_MISSING', + message: 'Не указаны параметры поиска', + }, + HttpStatus.BAD_REQUEST, + ); + } +} diff --git a/src/user/application/use-cases/get-activity.query.ts b/src/user/application/use-cases/get-activity.query.ts new file mode 100644 index 0000000..5921bd6 --- /dev/null +++ b/src/user/application/use-cases/get-activity.query.ts @@ -0,0 +1,30 @@ +import { IUserRepository } from '@core/user/domain/repository'; +import { Inject, Injectable } from '@nestjs/common'; + +@Injectable() +export class GetActivityQuery { + constructor( + @Inject('IUserRepository') + private readonly userRepo: IUserRepository, + ) {} + + async execute(id: string, page: number, limit: number) { + const safeLimit = Math.min(limit, 50); + const offset = (page - 1) * safeLimit; + + const { items, total } = await this.userRepo.findActivityByUser(id, { + limit: safeLimit, + offset, + }); + + return { + items, + meta: { + total, + page, + limit: safeLimit, + totalPages: Math.ceil(total / safeLimit), + }, + }; + } +} diff --git a/src/user/application/use-cases/index.ts b/src/user/application/use-cases/index.ts new file mode 100644 index 0000000..2939769 --- /dev/null +++ b/src/user/application/use-cases/index.ts @@ -0,0 +1,31 @@ +import { RegisterUserUseCase } from './register-user.use-case'; +import { UpdateNotificationsUseCase } from './update-notifications.use-case'; +import { UpdatePasswordUseCase } from './update-password.use-case'; +import { UpdateProfileUseCase } from './update-profile.use-case'; +import { UploadAvatarUseCase } from './upload-avatar.use-case'; + +import { FindProfileQuery } from './find-profile.query'; +import { FindUserQuery } from './find-user.query'; +import { GetActivityQuery } from './get-activity.query'; + +export * from './register-user.use-case'; +export * from './update-notifications.use-case'; +export * from './update-password.use-case'; +export * from './update-profile.use-case'; +export * from './upload-avatar.use-case'; + +export * from './find-profile.query'; +export * from './find-user.query'; +export * from './get-activity.query'; + +export const UserUseCases = [ + RegisterUserUseCase, + UpdateNotificationsUseCase, + UpdatePasswordUseCase, + UpdateProfileUseCase, + UploadAvatarUseCase, +]; + +export const UserQueries = [FindProfileQuery, FindUserQuery, GetActivityQuery]; + +export const USER_EXTERNAL_USE_CASES = [RegisterUserUseCase, UpdatePasswordUseCase, FindUserQuery]; diff --git a/src/user/application/use-cases/register-user.use-case.ts b/src/user/application/use-cases/register-user.use-case.ts new file mode 100644 index 0000000..88167ee --- /dev/null +++ b/src/user/application/use-cases/register-user.use-case.ts @@ -0,0 +1,56 @@ +import type { NewUser } from '@core/user/domain/entities'; +import { IUserRepository } from '@core/user/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { createId } from '@paralleldrive/cuid2'; +import { BaseException } from '@shared/error'; + +@Injectable() +export class RegisterUserUseCase { + constructor( + @Inject('IUserRepository') + private readonly repository: IUserRepository, + ) {} + + async execute(dto: NewUser & { password: string }) { + const existingUser = await this.repository.findByEmail(dto.email); + + if (existingUser?.user) { + throw new BaseException( + { + code: 'USER_ALREADY_EXISTS', + message: `Пользователь с email ${dto.email} уже зарегистрирован`, + details: [{ target: 'email', value: dto.email }], + }, + HttpStatus.CONFLICT, + ); + } + + try { + const user = await this.repository.create(dto); + + await Promise.all([ + this.repository.logActivity({ + eventType: 'registered', + userId: user.id, + id: createId(), + }), + this.repository.updatePasswordHash(user.id, dto.password), + ]); + + return user; + } catch (error) { + if (error instanceof BaseException) { + throw error; + } + + throw new BaseException( + { + code: 'USER_REGISTRATION_FAILED', + message: 'Не удалось завершить регистрацию', + details: [{ reason: error instanceof Error ? error.message : 'DB error' }], + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/user/application/use-cases/update-notifications.use-case.ts b/src/user/application/use-cases/update-notifications.use-case.ts new file mode 100644 index 0000000..022e251 --- /dev/null +++ b/src/user/application/use-cases/update-notifications.use-case.ts @@ -0,0 +1,67 @@ +import { IUserRepository } from '@core/user/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { BaseException } from '@shared/error'; +import { UpdateNotificationsDto } from '../dtos'; +import { createId } from '@paralleldrive/cuid2'; + +@Injectable() +export class UpdateNotificationsUseCase { + constructor( + @Inject('IUserRepository') + private readonly userRepo: IUserRepository, + ) {} + + async execute(id: string, dto: UpdateNotificationsDto) { + const user = await this.userRepo.findById(id); + + if (!user) { + throw new BaseException( + { code: 'USER_NOT_FOUND', message: 'Пользователь не найден' }, + HttpStatus.NOT_FOUND, + ); + } + + try { + const isUpdated = await this.userRepo.updateNotifications(id, { + email: dto.email, + push: dto.push, + }); + + if (!isUpdated) { + throw new BaseException( + { + code: 'NOTIFICATIONS_UPDATE_FAILED', + message: 'Не удалось обновить настройки уведомлений', + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + await this.userRepo.logActivity({ + id: createId(), + userId: id, + eventType: 'NOTIFICATIONS_UPDATED', + }); + + return { + success: true, + message: 'Настройки уведомлений обновлены', + }; + } catch (error) { + if (error instanceof BaseException) throw error; + + throw new BaseException( + { + code: 'USER_SETTINGS_ERROR', + message: 'Ошибка при сохранении настроек пользователя', + details: [ + { + reason: error instanceof Error ? error.message : 'Database error', + }, + ], + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/user/application/use-cases/update-password.use-case.ts b/src/user/application/use-cases/update-password.use-case.ts new file mode 100644 index 0000000..c718ae3 --- /dev/null +++ b/src/user/application/use-cases/update-password.use-case.ts @@ -0,0 +1,50 @@ +import { IUserRepository } from '@core/user/domain/repository'; +import { Injectable, Inject, HttpStatus } from '@nestjs/common'; +import { BaseException } from '@shared/error'; + +@Injectable() +export class UpdatePasswordUseCase { + constructor( + @Inject('IUserRepository') + private readonly repository: IUserRepository, + ) {} + + async execute(email: string, password: string) { + const result = await this.repository.findByEmail(email); + + if (!result?.user) { + throw new BaseException( + { + code: 'USER_NOT_FOUND', + message: 'Пользователь для обновления пароля не найден', + }, + HttpStatus.NOT_FOUND, + ); + } + + try { + const isUpdated = await this.repository.updatePasswordHash(result.user.id, password); + + if (!isUpdated) { + throw new BaseException( + { + code: 'PASSWORD_UPDATE_FAILED', + message: 'Запись не была изменена', + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + return isUpdated; + } catch (error) { + throw new BaseException( + { + code: 'DATABASE_ERROR', + message: 'Ошибка при работе с БД', + details: [{ reason: error instanceof Error ? error.message : 'Unknown' }], + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/user/application/use-cases/update-profile.use-case.ts b/src/user/application/use-cases/update-profile.use-case.ts new file mode 100644 index 0000000..9ce8056 --- /dev/null +++ b/src/user/application/use-cases/update-profile.use-case.ts @@ -0,0 +1,41 @@ +import { IUserRepository } from '@core/user/domain/repository'; +import { Injectable, Inject, HttpStatus } from '@nestjs/common'; +import { UpdateProfileDto } from '../dtos'; +import { BaseException } from '@shared/error'; +import { createId } from '@paralleldrive/cuid2'; + +@Injectable() +export class UpdateProfileUseCase { + constructor( + @Inject('IUserRepository') + private readonly userRepo: IUserRepository, + ) {} + + async execute(id: string, dto: UpdateProfileDto) { + const entity = await this.userRepo.findById(id); + + if (!entity.user) { + throw new BaseException( + { code: 'USER_NOT_FOUND', message: 'Пользователь не найден' }, + HttpStatus.NOT_FOUND, + ); + } + + const isUpdated = await this.userRepo.updateProfile(entity.user.id, dto); + + if (!isUpdated) { + throw new BaseException( + { code: 'PROFILE_UPDATE_FAILED', message: 'Не удалось обновить данные' }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + await this.userRepo.logActivity({ + id: createId(), + userId: id, + eventType: 'PROFILE_UPDATED', + }); + + return { success: true, message: 'Профиль успешно обновлен' }; + } +} diff --git a/src/user/application/use-cases/upload-avatar.use-case.ts b/src/user/application/use-cases/upload-avatar.use-case.ts new file mode 100644 index 0000000..8037f75 --- /dev/null +++ b/src/user/application/use-cases/upload-avatar.use-case.ts @@ -0,0 +1,37 @@ +import { IUserRepository } from '@core/user/domain/repository'; +import { Inject, Injectable } from '@nestjs/common'; +import { createId } from '@paralleldrive/cuid2'; + +@Injectable() +export class UploadAvatarUseCase { + constructor( + @Inject('IUserRepository') + private readonly userRepo: IUserRepository, + ) {} + + async execute(userId: string) { + // const result = await this.mediaService.uploadUserAvatar(userId, fileDto); + + // const [sm, md, lg] = await Promise.all([ + // this.imagor.get(result, 'small'), + // this.imagor.get(result, 'medium'), + // this.imagor.get(result, 'large'), + // ]); + + const result = ''; + await this.userRepo.updateAvatar(userId, result); + + await this.userRepo.logActivity({ + id: createId(), + userId, + eventType: 'AVATAR_CHANGED', + metadata: { + url: result, + }, + }); + + return { + success: true, + }; + } +} diff --git a/src/user/application/user.facade.ts b/src/user/application/user.facade.ts new file mode 100644 index 0000000..b881969 --- /dev/null +++ b/src/user/application/user.facade.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { + FindProfileQuery, + GetActivityQuery, + UpdateNotificationsUseCase, + UpdateProfileUseCase, +} from './use-cases'; +import { UpdateProfileDto, UpdateNotificationsDto } from './dtos'; + +@Injectable() +export class UserFacade { + constructor( + private readonly findProfileQuery: FindProfileQuery, + private readonly getActivityQuery: GetActivityQuery, + private readonly updateNotificationsUC: UpdateNotificationsUseCase, + private readonly updateProfileUC: UpdateProfileUseCase, + ) {} + + public async getProfile(userId: string) { + return this.findProfileQuery.execute(userId); + } + + public async getActivity(userId: string, page: number, limit: number) { + return this.getActivityQuery.execute(userId, page, limit); + } + + public async updateProfile(userId: string, dto: UpdateProfileDto) { + return this.updateProfileUC.execute(userId, dto); + } + + public async updateNotifications(userId: string, dto: UpdateNotificationsDto) { + return this.updateNotificationsUC.execute(userId, dto); + } +} diff --git a/src/user/domain/entities/index.ts b/src/user/domain/entities/index.ts new file mode 100644 index 0000000..54f31af --- /dev/null +++ b/src/user/domain/entities/index.ts @@ -0,0 +1 @@ +export type * from './user.domain'; diff --git a/src/user/domain/entities/user.domain.ts b/src/user/domain/entities/user.domain.ts new file mode 100644 index 0000000..2bf467e --- /dev/null +++ b/src/user/domain/entities/user.domain.ts @@ -0,0 +1,30 @@ +import { InferSelectModel, InferInsertModel } from 'drizzle-orm'; +import { + users, + userSecurity, + userNotifications, + userActivity, +} from '../../infrastructure/persistence/models/user.entity'; + +export type User = InferSelectModel; +export type NewUser = InferInsertModel; + +export type UserSecurity = InferSelectModel; +export type NewUserSecurity = InferInsertModel; + +export type UserNotifications = InferSelectModel; +export type NotificationSettings = Pick; + +export type UserActivity = InferSelectModel; +export type NewUserActivity = InferInsertModel; + +export type UserProfile = { + user: User; + security: Pick; + notifications: NotificationSettings['settings']; +}; + +export type UserWithSecurity = { + user: User; + security: Pick; +}; diff --git a/src/user/domain/repository/index.ts b/src/user/domain/repository/index.ts new file mode 100644 index 0000000..a9419f8 --- /dev/null +++ b/src/user/domain/repository/index.ts @@ -0,0 +1 @@ +export { IUserRepository } from './user.repository.interface'; diff --git a/src/user/domain/repository/user.repository.interface.ts b/src/user/domain/repository/user.repository.interface.ts new file mode 100644 index 0000000..e2c4bbe --- /dev/null +++ b/src/user/domain/repository/user.repository.interface.ts @@ -0,0 +1,28 @@ +import type { + NewUser, + NewUserActivity, + User, + UserActivity, + UserNotifications, + UserProfile, + UserWithSecurity, +} from '../entities/user.domain'; + +export interface IUserRepository { + create(data: NewUser): Promise; + findById(id: string): Promise; + findByEmail(email: string): Promise; + findProfile(id: string): Promise; + findActivityByUser( + userId: string, + options: { limit: number; offset: number }, + ): Promise<{ + items: UserActivity[]; + total: number; + }>; + updateAvatar(id: string, url: string): Promise; + updateProfile(id: string, data: Partial): Promise; + updatePasswordHash(id: string, hash: string): Promise; + updateNotifications(id: string, settings: UserNotifications['settings']): Promise; + logActivity(data: NewUserActivity): Promise; +} diff --git a/src/user/index.ts b/src/user/index.ts new file mode 100644 index 0000000..4e472d9 --- /dev/null +++ b/src/user/index.ts @@ -0,0 +1,3 @@ +export { UserModule } from './user.module'; +export { RegisterUserUseCase, FindUserQuery, UpdatePasswordUseCase } from './application/use-cases'; +export { User } from './domain/entities/user.domain'; diff --git a/src/user/infrastructure/listeners/index.ts b/src/user/infrastructure/listeners/index.ts new file mode 100644 index 0000000..1725655 --- /dev/null +++ b/src/user/infrastructure/listeners/index.ts @@ -0,0 +1,3 @@ +import { UpdateAvatarListener } from './update-avatar.listener'; + +export const LISTENERS = [UpdateAvatarListener]; diff --git a/src/user/infrastructure/listeners/update-avatar.listener.ts b/src/user/infrastructure/listeners/update-avatar.listener.ts new file mode 100644 index 0000000..7563c22 --- /dev/null +++ b/src/user/infrastructure/listeners/update-avatar.listener.ts @@ -0,0 +1,53 @@ +import { IUserRepository } from '@core/user/domain/repository'; +import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { Inject } from '@nestjs/common'; +import { MEDIA_JOBS, MEDIA_QUEUES, type UpdateMediaUser } from '@shared/media'; +import { UnrecoverableError, type Job } from 'bullmq'; + +@Processor(MEDIA_QUEUES.SAVE_ENTITY) +export class UpdateAvatarListener extends WorkerHost { + constructor( + @Inject('IUserRepository') + private readonly repository: IUserRepository, + ) { + super(); + } + + async process(job: Job) { + if (job.name !== MEDIA_JOBS.UPDATE_USER_AVATAR) return; + + const { entity, path } = job.data; + + try { + await job.updateProgress(10); + await job.log(path); + if (!path) { + throw new UnrecoverableError( + `Media processing failed: no storage path returned for entity ${entity.id}`, + ); + } + + await job.updateProgress(40); + + const userAccount = await this.repository.findById(entity.id); + + if (!userAccount) { + await job.log(`User ${entity.id} missing in database.`); + return { status: 'aborted', reason: 'USER_NOT_FOUND' }; + } + + await job.updateProgress(70); + + await this.repository.updateAvatar(userAccount.user.id, path); + + await job.updateProgress(100); + + await job.log(`Successfully updated avatar for user ${userAccount.user.id}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + await job.log(`Critical failure: ${errorMessage}`); + + throw error; + } + } +} diff --git a/src/user/infrastructure/persistence/models/index.ts b/src/user/infrastructure/persistence/models/index.ts new file mode 100644 index 0000000..426faed --- /dev/null +++ b/src/user/infrastructure/persistence/models/index.ts @@ -0,0 +1 @@ +export { userActivity, userNotifications, userSecurity, users } from './user.entity'; diff --git a/src/user/infrastructure/persistence/models/user.entity.ts b/src/user/infrastructure/persistence/models/user.entity.ts new file mode 100644 index 0000000..4078a21 --- /dev/null +++ b/src/user/infrastructure/persistence/models/user.entity.ts @@ -0,0 +1,59 @@ +import { createId } from '@paralleldrive/cuid2'; +import { varchar, text, timestamp, boolean, jsonb } from 'drizzle-orm/pg-core'; +import { baseSchema } from '@shared/entities'; + +export const users = baseSchema.table('users', { + id: text('id') + .primaryKey() + .$defaultFn(() => createId()), + + firstName: varchar('first_name', { length: 50 }).notNull(), + lastName: varchar('last_name', { length: 50 }).notNull(), + middleName: varchar('middle_name', { length: 50 }), + email: varchar('email', { length: 255 }).notNull().unique(), + bio: text('bio'), + avatarUrl: varchar('avatar_url', { length: 512 }), + timezone: varchar('timezone', { length: 50 }).default('UTC').notNull(), + language: varchar('language', { length: 5 }).default('ru').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), +}); + +export const userSecurity = baseSchema.table('user_security', { + userId: text('user_id') + .primaryKey() + .references(() => users.id, { onDelete: 'cascade' }), + passwordHash: varchar('password_hash', { length: 255 }).notNull(), + is2faEnabled: boolean('is_2fa_enabled').default(false).notNull(), + twoFactorSecret: text('two_factor_secret'), + lastPasswordChange: timestamp('last_password_change', { withTimezone: true }) + .defaultNow() + .notNull(), +}); + +export const userNotifications = baseSchema.table('user_notifications', { + userId: text('user_id') + .primaryKey() + .references(() => users.id, { onDelete: 'cascade' }), + settings: jsonb('settings') + .$type<{ + email: { task_assigned: boolean; mentions: boolean; daily_summary: boolean }; + push: { task_assigned: boolean; reminders: boolean }; + }>() + .default({ + email: { task_assigned: true, mentions: true, daily_summary: false }, + push: { task_assigned: true, reminders: true }, + }) + .notNull(), +}); + +export const userActivity = baseSchema.table('user_activity', { + id: text('id').primaryKey(), + userId: text('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + eventType: varchar('event_type', { length: 50 }).notNull(), + entityId: varchar('entity_id'), + metadata: jsonb('metadata'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), +}); diff --git a/src/user/infrastructure/persistence/repositories/index.ts b/src/user/infrastructure/persistence/repositories/index.ts new file mode 100644 index 0000000..c9c59cf --- /dev/null +++ b/src/user/infrastructure/persistence/repositories/index.ts @@ -0,0 +1 @@ +export { UserRepository } from './user.repository'; diff --git a/src/user/infrastructure/persistence/repositories/user.repository.ts b/src/user/infrastructure/persistence/repositories/user.repository.ts new file mode 100644 index 0000000..76e4b06 --- /dev/null +++ b/src/user/infrastructure/persistence/repositories/user.repository.ts @@ -0,0 +1,142 @@ +import { IUserRepository } from '@core/user/domain/repository'; +import * as sc from '../models'; +import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; +import { Inject, Injectable } from '@nestjs/common'; +import { createId } from '@paralleldrive/cuid2'; +import { desc, eq, count } from 'drizzle-orm'; +import type { NewUser, NewUserActivity, User, UserNotifications } from '@core/user/domain/entities'; + +@Injectable() +export class UserRepository implements IUserRepository { + constructor( + @Inject(DATABASE_SERVICE) + private readonly db: DatabaseService, + ) {} + + private get fullUserQuery() { + return this.db + .select() + .from(sc.users) + .leftJoin(sc.userSecurity, eq(sc.users.id, sc.userSecurity.userId)) + .leftJoin(sc.userNotifications, eq(sc.users.id, sc.userNotifications.userId)); + } + + async findProfile(id: string) { + const [rows] = await this.fullUserQuery.where(eq(sc.users.id, id)); + if (!rows.users) return null; + const { lastPasswordChange, is2faEnabled } = rows.user_security; + const { settings } = rows.user_notifications; + + return { + user: rows.users, + security: { lastPasswordChange, is2faEnabled }, + notifications: settings, + }; + } + + async findById(id: string) { + const [row] = await this.fullUserQuery.where(eq(sc.users.id, id)); + if (!row || !row.user_security) return null; + return { + user: row.users, + security: { + passwordHash: row.user_security.passwordHash, + }, + }; + } + + async findByEmail(email: string) { + const [row] = await this.fullUserQuery.where(eq(sc.users.email, email.toLowerCase())); + if (!row || !row.user_security) return null; + return { + user: row.users, + security: { + passwordHash: row.user_security.passwordHash, + }, + }; + } + + async findSecurityByUserId(userId: string) { + const [result] = await this.db + .select() + .from(sc.userSecurity) + .where(eq(sc.userSecurity.userId, userId)); + return result || null; + } + + async create(data: NewUser) { + return await this.db.transaction(async (tx) => { + const [newUser] = await tx.insert(sc.users).values(data).returning(); + + await tx.insert(sc.userNotifications).values({ + userId: newUser.id, + }); + + return newUser; + }); + } + + async updateProfile(id: string, data: Partial) { + const result = await this.db + .update(sc.users) + .set({ ...data, updatedAt: new Date() }) + .where(eq(sc.users.id, id)); + return (result?.count ?? 0) > 0; + } + + async updateNotifications(id: string, settings: UserNotifications['settings']) { + const result = await this.db + .update(sc.userNotifications) + .set({ settings }) + .where(eq(sc.userNotifications.userId, id)); + return (result?.count ?? 0) > 0; + } + + async updateAvatar(id: string, url: string) { + const result = await this.db + .update(sc.users) + .set({ avatarUrl: url, updatedAt: new Date() }) + .where(eq(sc.users.id, id)); + return (result?.count ?? 0) > 0; + } + + async updatePasswordHash(id: string, hash: string) { + const result = await this.db + .insert(sc.userSecurity) + .values({ userId: id, passwordHash: hash }) + .onConflictDoUpdate({ + target: sc.userSecurity.userId, + set: { passwordHash: hash, lastPasswordChange: new Date() }, + }); + return (result?.count ?? 0) > 0; + } + + async logActivity(data: NewUserActivity) { + const result = await this.db.insert(sc.userActivity).values({ + ...data, + id: data.id ?? createId(), + }); + return (result?.count ?? 0) > 0; + } + + async findActivityByUser(userId: string, options: { limit: number; offset: number }) { + const [totalResult, items] = await Promise.all([ + this.db + .select({ value: count() }) + .from(sc.userActivity) + .where(eq(sc.userActivity.userId, userId)), + this.db + .select() + .from(sc.userActivity) + .where(eq(sc.userActivity.userId, userId)) + .limit(options.limit) + .offset(options.offset) + .orderBy(desc(sc.userActivity.createdAt)), + ]); + + return { + items, + total: Number(totalResult[0]?.value ?? 0), + }; + } +} diff --git a/src/user/user.module.ts b/src/user/user.module.ts new file mode 100644 index 0000000..e473132 --- /dev/null +++ b/src/user/user.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { UserRepository } from './infrastructure/persistence/repositories'; +import { UserController, UserSettingsController } from './application/controller'; +import { UserFacade } from './application/user.facade'; +import { USER_EXTERNAL_USE_CASES, UserQueries, UserUseCases } from './application/use-cases'; +import { LISTENERS } from './infrastructure/listeners'; + +const REPOSITORY = { + provide: 'IUserRepository', + useClass: UserRepository, +}; + +@Module({ + imports: [], + controllers: [UserController, UserSettingsController], + providers: [REPOSITORY, ...UserUseCases, ...UserQueries, ...LISTENERS, UserFacade], + exports: [...USER_EXTERNAL_USE_CASES], +}) +export class UserModule {} diff --git a/templates/confirmation.hbs b/templates/confirmation.hbs new file mode 100644 index 0000000..a8cf39d --- /dev/null +++ b/templates/confirmation.hbs @@ -0,0 +1,91 @@ + + + + + + +
+
+

Task Tracker

+
+
+

Проверка безопасности

+

Привет, {{name}}! Используйте этот код для подтверждения:

+ +
{{#each codeArray}}{{this}}{{/each}}
+ +

Код будет активен в течение 15 минут.

+
+ +
+ + \ No newline at end of file diff --git a/templates/reset-password.hbs b/templates/reset-password.hbs new file mode 100644 index 0000000..735b91c --- /dev/null +++ b/templates/reset-password.hbs @@ -0,0 +1,92 @@ + + + + + + +
+
+

Task Tracker

+
+
+

Сброс пароля

+

Здравствуйте!

+

Мы получили запрос на восстановление пароля для вашего аккаунта.
Ваш + одноразовый код для сброса:

+
{{#each codeArray}}
{{this}}
{{/each}}
+ +

Никому не сообщайте этот код. Если вы не + запрашивали сброс пароля, немедленно обратитесь в поддержку.

+
+ +
+ + \ No newline at end of file diff --git a/templates/team-invitation.hbs b/templates/team-invitation.hbs new file mode 100644 index 0000000..9ed932b --- /dev/null +++ b/templates/team-invitation.hbs @@ -0,0 +1,94 @@ + + + + + + +
+
+

Task Tracker

+
+
+

Приглашение в команду

+

Вас пригласили присоединиться к команде {{teamName}}!

+ Присоединиться к команде +

+ Если кнопка не работает, скопируйте и вставьте эту ссылку в браузер:
+ {{inviteUrl}} +

+
+ +
+ + \ No newline at end of file diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index 50cda62..733a1f5 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -1,24 +1,32 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { AppModule } from './../src/app.module'; +import { AppModule } from '../src/app.module'; +import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; -describe('AppController (e2e)', () => { - let app: INestApplication; +describe('App (e2e)', () => { + let app: NestFastifyApplication; - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); - app = moduleFixture.createNestApplication(); - await app.init(); - }); + app = moduleFixture.createNestApplication(new FastifyAdapter()); - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); + await app.init(); + await app.getHttpAdapter().getInstance().ready(); + }); + + afterEach(async () => { + await app.close(); + }); + + it('/health (GET)', async () => { + const res = await app.inject({ + method: 'GET', + url: '/health', + }); + + expect(res.statusCode).toBe(200); + expect(res.payload).toBe('healthy'); + }); }); diff --git a/test/jest-e2e.json b/test/jest-e2e.json deleted file mode 100644 index e9d912f..0000000 --- a/test/jest-e2e.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", - "testEnvironment": "node", - "testRegex": ".e2e-spec.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - } -} diff --git a/tsconfig.build.json b/tsconfig.build.json index 64f86c6..aed3485 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,4 @@ { - "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] } diff --git a/tsconfig.json b/tsconfig.json index 95f5641..4884f1b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,21 +1,48 @@ { - "compilerOptions": { - "module": "commonjs", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2021", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": false, - "noImplicitAny": false, - "strictBindCallApply": false, - "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false - } + "compilerOptions": { + "module": "commonjs", + "declaration": false, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "resolveJsonModule": true, + "esModuleInterop": true, + "sourceMap": true, + "outDir": "./dist", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "types": ["node", "vitest/globals"], + "paths": { + "@libs/bootstrap": ["./libs/bootstrap/src"], + "@libs/bootstrap/*": ["./libs/bootstrap/src/*"], + "@libs/config": ["./libs/config/src"], + "@libs/config/*": ["./libs/config/src/*"], + "@libs/database": ["./libs/database/src"], + "@libs/database/*": ["./libs/database/src/*"], + "@libs/health": ["./libs/health/src"], + "@libs/health/*": ["./libs/health/src/*"], + "@libs/imagor": ["./libs/imagor/src"], + "@libs/imagor/*": ["./libs/imagor/src/*"], + "@libs/s3": ["./libs/s3/src"], + "@libs/s3/*": ["./libs/s3/src/*"], + "@shared/*": ["./src/shared/*"], + "@core/*": ["./src/*"] + } + }, + "include": [ + "src/**/*", + "libs/**/*", + "test/**/*", + "drizzle.config.ts", + "vitest.config.ts", + "vitest.config.e2e.ts" + ], + "exclude": ["dist", "node_modules"] } diff --git a/vitest.config.e2e.ts b/vitest.config.e2e.ts new file mode 100644 index 0000000..494b16e --- /dev/null +++ b/vitest.config.e2e.ts @@ -0,0 +1,14 @@ +import { mergeConfig, defineConfig } from 'vitest/config'; +import baseConfig from './vitest.config'; + +export default mergeConfig( + baseConfig, + defineConfig({ + test: { + include: ['test/**/*.e2e-spec.ts'], + exclude: [], + pool: 'forks', + isolate: true, + }, + }), +); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..156845a --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,36 @@ +import path from 'path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + root: './', + globals: true, + environment: 'node', + include: ['**/*.spec.ts'], + exclude: [ + '**/node_modules/**', + '**/dist/**', + '**/cypress/**', + '**/.{idea,git,cache,output,temp}/**', + '**/infra/**', + ], + alias: { + '@core': path.resolve(__dirname, './src'), + '@shared': path.resolve(__dirname, './src/shared'), + '@libs/bootstrap': path.join(process.cwd(), 'libs/bootstrap/src'), + '@libs/config': path.join(process.cwd(), 'libs/config/src'), + '@libs/database': path.join(process.cwd(), 'libs/database/src'), + '@libs/health': path.join(process.cwd(), 'libs/health/src'), + '@libs/imagor': path.join(process.cwd(), 'libs/s3/imagor'), + '@libs/s3': path.join(process.cwd(), 'libs/s3/src'), + }, + typecheck: { + enabled: true, + }, + }, + resolve: { + alias: { + src: path.resolve(__dirname, './src'), + }, + }, +});