diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fcf9c35 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,55 @@ +# Dependencies +node_modules + +# Build output +dist + +# Git +.git +.gitignore + +# IDE +.vscode +.idea +*.swp +*.swo + +# Logs +*.log +storage/logs/* +!storage/logs/.gitkeep + +# Environment files +.env +.env.* +!env.example + +# Documentation +*.md +docs + +# Tests +**/*.test.ts +**/*.spec.ts +coverage +.nyc_output + +# OS files +.DS_Store +Thumbs.db + +# Docker +Dockerfile +docker-compose*.yml +.dockerignore + +# Husky +.husky + +# GitHub +.github + +# Misc +*.tgz +*.tar.gz + diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6646a90 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..5b59421 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,33 @@ +name: CI Pipeline + +on: + push: + branches: [main] + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + +jobs: + ci: + name: Build & Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linting + run: npm run lint + + - name: Build application + run: npm run build + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/myapp diff --git a/.gitignore b/.gitignore index 9a5aced..c9a1b32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,139 +1,42 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories +# Dependencies node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache -# Optional REPL history -.node_repl_history +# Build output +dist/ -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files +# Environment .env .env.* -!.env.example - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache +!env.example -# Sveltekit cache directory -.svelte-kit/ - -# vitepress build output -**/.vitepress/dist - -# vitepress cache directory -**/.vitepress/cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ +# Logs +*.log +storage/logs/* +!storage/logs/.gitkeep -# FuseBox cache -.fusebox/ +# IDE +.vscode/ +.idea/ -# DynamoDB Local files -.dynamodb/ +# OS +.DS_Store +Thumbs.db -# Firebase cache directory -.firebase/ +# TypeScript +*.tsbuildinfo -# TernJS port file -.tern-port +# Test coverage +coverage/ +.nyc_output/ -# Stores VSCode versions used for testing VSCode extensions -.vscode-test +# RSA keys (generated per project) +storage/keys/**/*.key +storage/keys/**/*.pub +!storage/keys/**/.gitkeep -# yarn v3 -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions +# Prisma generated client +src/database/prisma/ -# Vite logs files -vite.config.js.timestamp-* -vite.config.ts.timestamp-* +# npm +.npm +*.tgz diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..c7aad07 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +set -e + +# Run linting with auto-fix +echo "šŸ“ Fixing and checking code style..." +npm run lint:fix || exit 1 + +# Run linting +echo "šŸ“ Checking code style..." +npm run lint || exit 1 + +# Build application +echo "šŸ”Ø Building application..." +npm run build || exit 1 + +echo "āœ… All pre-commit checks passed!" \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..9a547e3 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 120, + "arrowParens": "avoid" +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a8aff31 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# Build stage +FROM node:22-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +# Production stage +FROM node:22-alpine AS production + +WORKDIR /app + +RUN apk add --no-cache curl + +COPY package*.json ./ +RUN npm ci --omit=dev + +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/storage ./storage + +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 -G nodejs && \ + chown -R nodejs:nodejs /app + +USER nodejs + +EXPOSE 8070 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD curl -f http://localhost:8070/health || exit 1 + +CMD ["node", "dist/index.js"] diff --git a/README.md b/README.md index 336e0cb..7ab5eed 100644 --- a/README.md +++ b/README.md @@ -1 +1,118 @@ -# nodejs-boilerplate \ No newline at end of file +# Node.js Boilerplate + +Modular monolith backend boilerplate built with Express 5, Prisma 7, TypeScript, Redis, and BullMQ. + +## Stack + +- **Runtime:** Node.js 22+ with TypeScript 5.9 (strict mode) +- **Framework:** Express 5 +- **ORM:** Prisma 7 with PostgreSQL (pg adapter) +- **Cache:** Redis 7 (ioredis) +- **Queue:** BullMQ (session update throttling) +- **Auth:** RS256 JWT with refresh tokens, OTP, 2FA/MFA, RBAC +- **Logging:** Winston with daily rotation +- **File Storage:** AWS S3 via presigned URLs (express-storage) +- **Security:** Helmet, CORS, rate limiting, XSS sanitization, PBKDF2 password hashing, RSA encryption + +## Architecture + +``` +src/ +ā”œā”€ā”€ index.ts # Entry point (cluster mode in production) +ā”œā”€ā”€ app.ts # Express app setup (middleware chain) +ā”œā”€ā”€ config/ # Environment, CORS, Helmet configs +ā”œā”€ā”€ database/ # Prisma schema, client, seed +ā”œā”€ā”€ routes/ # Root router +└── app/ + ā”œā”€ā”€ core/ # Framework-agnostic core layer + │ ā”œā”€ā”€ container.ts # DI container + │ ā”œā”€ā”€ constants/ # Auth, HTTP, roles, permissions + │ ā”œā”€ā”€ database/ # Base repository, Prisma client + │ ā”œā”€ā”€ errors/ # AppError hierarchy (400-500) + │ ā”œā”€ā”€ types/ # Express augmentations, common types + │ └── utils/ # Date, pagination, random, sanitize + └── http/ # Express HTTP layer + ā”œā”€ā”€ controllers/ # Base controller, app controller + ā”œā”€ā”€ middleware/ # Auth, error, logging, rate-limit, sanitize + ā”œā”€ā”€ services/ # JWT, password, encryption, logger, storage, health + └── modules/ # Feature modules + ā”œā”€ā”€ auth/ # OTP login, password login, 2FA, sessions, password reset + ā”œā”€ā”€ gallery/ # S3 file upload/confirm/view/delete + └── admin/ # Config, roles, permissions management +``` + +Each module follows: **controller → service → repository** with co-located types, validation, and routes. + +## Getting Started + +```bash +# 1. Clone the repo +git clone my-project +cd my-project + +# 2. Install dependencies +npm install + +# 3. Copy environment file +cp env.example .env + +# 4. Start infrastructure (PostgreSQL + Redis) +docker compose up -d postgres redis + +# 5. Generate RSA keys for JWT signing +mkdir -p storage/keys/api storage/keys/encryption +openssl genrsa -out storage/keys/api/private.key 2048 +openssl rsa -in storage/keys/api/private.key -pubout -out storage/keys/api/public.key +openssl genrsa -out storage/keys/encryption/private.key 2048 +openssl rsa -in storage/keys/encryption/private.key -pubout -out storage/keys/encryption/private.key.pub + +# 6. Run database migrations +npm run prisma:migrate + +# 7. Seed the database +npm run seed + +# 8. Start development server +npm run dev +``` + +## Scripts + +| Script | Description | +|---|---| +| `npm run dev` | Start dev server with hot reload | +| `npm run build` | Build for production | +| `npm start` | Run production build | +| `npm run lint` | Run ESLint | +| `npm run typecheck` | TypeScript type checking | +| `npm run prisma:migrate` | Run database migrations | +| `npm run prisma:studio` | Open Prisma Studio | +| `npm run seed` | Seed database with roles, permissions, test users | + +## Test Users (after seeding) + +| Role | Email | Auth Method | +|---|---|---| +| Customer | customer@test.com | OTP (dev code: 123456) | +| Admin | admin@test.com | Password: Admin@123 | +| Super Admin | superadmin@test.com | Password: Admin@123 | + +## API Routes + +``` +GET / # Landing +GET /health # Health check +POST /v1/auth/* # Authentication (OTP, password, 2FA, sessions) +* /v1/gallery/* # File uploads (S3 presigned URLs) +* /v1/admin/* # Admin (config, roles, permissions) +``` + +## Docker + +```bash +# Full stack (Postgres + Redis + Backend) +docker compose up -d + +# Just infrastructure +docker compose up -d postgres redis +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..62f3b10 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,71 @@ +services: + # PostgreSQL database + postgres: + image: postgres:16-alpine + container_name: app-postgres + environment: + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} + POSTGRES_DB: ${DB_NAME:-myapp} + ports: + - "${DB_PORT:-5432}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - app-network + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-myapp}" ] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + # Redis cache + redis: + image: redis:7-alpine + container_name: app-redis + ports: + - "${REDIS_PORT:-6379}:6379" + volumes: + - redis_data:/data + networks: + - app-network + healthcheck: + test: [ "CMD", "redis-cli", "ping" ] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + # Backend service + backend: + build: + context: . + dockerfile: Dockerfile + container_name: app-backend + ports: + - "${PORT:-8070}:8070" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - app-network + restart: unless-stopped + environment: + - NODE_ENV=${NODE_ENV:-development} + - PORT=8070 + - DATABASE_URL=postgresql://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@postgres:5432/${DB_NAME:-myapp} + - REDIS_URL=redis://redis:6379 + env_file: + - .env + +volumes: + postgres_data: + redis_data: + + +networks: + app-network: + driver: bridge diff --git a/env.example b/env.example new file mode 100644 index 0000000..27f4756 --- /dev/null +++ b/env.example @@ -0,0 +1,92 @@ +## NODE ENVIRONMENT CONFIGURATION + +# ============================================================================= +# BASIC CONFIGURATION +# ============================================================================= +NODE_ENV=development +PORT=8070 +TIMEZONE=UTC + +# ============================================================================= +# APP INFORMATION +# ============================================================================= +APP_NAME="My App" +APP_VERSION=1 +APP_DESCRIPTION="Backend API Service" +APP_URL=http://localhost:8070 + +# ============================================================================= +# CORS CONFIGURATION +# ============================================================================= +CORS_ORIGIN=http://127.0.0.1:3000,http://localhost:3000 +CORS_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS +CORS_MAX_AGE=86400 + +# ============================================================================= +# RATE LIMIT CONFIGURATION +# ============================================================================= +RATE_LIMIT_WINDOW_MS=60000 +RATE_LIMIT_MAX_REQUESTS=120 +RATE_LIMIT_LOGIN_WINDOW_MS=900000 +RATE_LIMIT_LOGIN_MAX_REQUESTS=20 +RATE_LIMIT_OTP_WINDOW_MS=3600000 +RATE_LIMIT_OTP_MAX_REQUESTS=5 +RATE_LIMIT_RETRY_AFTER_MS=60000 + +# ============================================================================= +# DATABASE CONFIGURATION (PostgreSQL) +# ============================================================================= +DATABASE_URL=postgresql://postgres:change-me@127.0.0.1:5432/myapp +DATABASE_POOL_MAX=10 +DATABASE_POOL_IDLE_TIMEOUT_MS=30000 +DATABASE_POOL_CONNECTION_TIMEOUT_MS=5000 + +# ============================================================================= +# REDIS CONFIGURATION +# ============================================================================= +REDIS_URL=redis://127.0.0.1:6379 +REDIS_CONNECTION_NAME=app-service +REDIS_MAX_RETRIES_PER_REQUEST=3 + +# ============================================================================= +# KEY FILES (RSA keys for JWT signing & encryption) +# ============================================================================= +KEY_FILES=storage/keys/api/private.key,storage/keys/api/public.key,storage/keys/encryption/private.key,storage/keys/encryption/public.key + +# ============================================================================= +# SYSTEM RESOURCE THRESHOLDS +# ============================================================================= +SYSTEM_RESOURCES_MIN_FREE_MEMORY_RATIO=0.03 +SYSTEM_RESOURCES_MAX_LOAD_MULTIPLIER=1.5 +SYSTEM_RESOURCES_MAX_LOAD_ABSOLUTE=6 + +# ============================================================================= +# JWT CONFIGURATION +# ============================================================================= +JWT_EXPIRES_IN=15m +JWT_REFRESH_EXPIRES_IN=7d + +# ============================================================================= +# AUTH QUEUE CONFIGURATION (lastUsed batching) +# ============================================================================= +AUTH_SESSION_LAST_USED_DEBOUNCE_MS=120000 +AUTH_SESSION_LAST_USED_MAX_WAIT_MS=600000 + +# Placeholder suffix used for phone-only accounts until a real email is provided +AUTH_PLACEHOLDER_EMAIL_SUFFIX=@temp.placeholder.local + +# ============================================================================= +# LOGGING CONFIGURATION +# ============================================================================= +LOG_LEVEL=debug +LOG_MAX_SIZE=20m +LOG_MAX_FILES=30d + +# ============================================================================= +# AWS CONFIGURATION (S3 File Storage) +# ============================================================================= +AWS_REGION=us-east-1 +AWS_ACCESS_KEY_ID=your_aws_access_key +AWS_SECRET_ACCESS_KEY=your_aws_secret_key +AWS_S3_BUCKET=my-app-documents +AWS_S3_BUCKET_PUBLIC=my-app-public diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..ee6f37b --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,77 @@ +const js = require('@eslint/js'); +const typescript = require('@typescript-eslint/eslint-plugin'); +const typescriptParser = require('@typescript-eslint/parser'); + +module.exports = [ + js.configs.recommended, + { + files: ['src/**/*.ts'], + languageOptions: { + parser: typescriptParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + }, + globals: { + process: 'readonly', + console: 'readonly', + Buffer: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + global: 'readonly', + module: 'readonly', + require: 'readonly', + exports: 'readonly', + setTimeout: 'readonly', + setInterval: 'readonly', + clearTimeout: 'readonly', + clearInterval: 'readonly', + setImmediate: 'readonly', + clearImmediate: 'readonly', + URL: 'readonly', + URLSearchParams: 'readonly' + } + }, + plugins: { + '@typescript-eslint': typescript + }, + rules: { + '@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-inferrable-types': 'off', + '@typescript-eslint/no-var-requires': 'error', + '@typescript-eslint/ban-ts-comment': 'warn', + '@typescript-eslint/no-non-null-assertion': 'warn', + + 'no-console': 'warn', + 'no-debugger': 'error', + 'no-duplicate-imports': 'error', + 'no-unused-vars': 'off', + 'prefer-const': 'error', + 'no-var': 'error', + 'no-restricted-properties': [ + 'error', + { + object: 'process', + property: 'env', + message: 'Use @config/environment.config instead of process.env directly.' + } + ] + } + }, + { + files: ['src/config/environment.config.ts'], + rules: { + 'no-restricted-properties': 'off' + } + }, + { + ignores: [ + 'dist/', + 'node_modules/', + 'src/database/prisma/', + '*.js', + '*.d.ts' + ] + } +]; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2b90398 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,9198 @@ +{ + "name": "nodejs-boilerplate", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nodejs-boilerplate", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/adapter-pg": "^7.0.0", + "@prisma/client": "^7.0.0", + "@th3hero/request-validator": "^1.1.9", + "bullmq": "^5.64.0", + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.1.0", + "express-rate-limit": "^8.1.0", + "express-storage": "^3.0.0", + "helmet": "^8.1.0", + "ioredis": "^5.4.1", + "jsonwebtoken": "^9.0.2", + "pg": "^8.13.1", + "rate-limit-redis": "^4.3.1", + "winston": "^3.18.3", + "winston-daily-rotate-file": "^5.0.0" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^24.6.1", + "@types/pg": "^8.11.6", + "@typescript-eslint/eslint-plugin": "^8.45.0", + "@typescript-eslint/parser": "^8.45.0", + "eslint": "^9.36.0", + "husky": "^9.1.7", + "prisma": "^7.0.0", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "tsc-alias": "^1.8.16", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.9.3" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1000.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1000.0.tgz", + "integrity": "sha512-7kPy33qNGq3NfwHC0412T6LDK1bp4+eiPzetX0sVd9cpTSXuQDKpoOFnB0Njj6uZjJDcLS3n2OeyarwwgkQ0Ow==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/credential-provider-node": "^3.972.14", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.6", + "@aws-sdk/middleware-expect-continue": "^3.972.6", + "@aws-sdk/middleware-flexible-checksums": "^3.973.1", + "@aws-sdk/middleware-host-header": "^3.972.6", + "@aws-sdk/middleware-location-constraint": "^3.972.6", + "@aws-sdk/middleware-logger": "^3.972.6", + "@aws-sdk/middleware-recursion-detection": "^3.972.6", + "@aws-sdk/middleware-sdk-s3": "^3.972.15", + "@aws-sdk/middleware-ssec": "^3.972.6", + "@aws-sdk/middleware-user-agent": "^3.972.15", + "@aws-sdk/region-config-resolver": "^3.972.6", + "@aws-sdk/signature-v4-multi-region": "^3.996.3", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-endpoints": "^3.996.3", + "@aws-sdk/util-user-agent-browser": "^3.972.6", + "@aws-sdk/util-user-agent-node": "^3.973.0", + "@smithy/config-resolver": "^4.4.9", + "@smithy/core": "^3.23.6", + "@smithy/eventstream-serde-browser": "^4.2.10", + "@smithy/eventstream-serde-config-resolver": "^4.3.10", + "@smithy/eventstream-serde-node": "^4.2.10", + "@smithy/fetch-http-handler": "^5.3.11", + "@smithy/hash-blob-browser": "^4.2.11", + "@smithy/hash-node": "^4.2.10", + "@smithy/hash-stream-node": "^4.2.10", + "@smithy/invalid-dependency": "^4.2.10", + "@smithy/md5-js": "^4.2.10", + "@smithy/middleware-content-length": "^4.2.10", + "@smithy/middleware-endpoint": "^4.4.20", + "@smithy/middleware-retry": "^4.4.37", + "@smithy/middleware-serde": "^4.2.11", + "@smithy/middleware-stack": "^4.2.10", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/node-http-handler": "^4.4.12", + "@smithy/protocol-http": "^5.3.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-body-length-browser": "^4.2.1", + "@smithy/util-body-length-node": "^4.2.2", + "@smithy/util-defaults-mode-browser": "^4.3.36", + "@smithy/util-defaults-mode-node": "^4.2.39", + "@smithy/util-endpoints": "^3.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-retry": "^4.2.10", + "@smithy/util-stream": "^4.5.15", + "@smithy/util-utf8": "^4.2.1", + "@smithy/util-waiter": "^4.2.10", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.973.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.15.tgz", + "integrity": "sha512-AlC0oQ1/mdJ8vCIqu524j5RB7M8i8E24bbkZmya1CuiQxkY7SdIZAyw7NDNMGaNINQFq/8oGRMX0HeOfCVsl/A==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/xml-builder": "^3.972.8", + "@smithy/core": "^3.23.6", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/signature-v4": "^5.3.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.3.tgz", + "integrity": "sha512-UExeK+EFiq5LAcbHm96CQLSia+5pvpUVSAsVApscBzayb7/6dJBJKwV4/onsk4VbWSmqxDMcfuTD+pC4RxgZHg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.13.tgz", + "integrity": "sha512-6ljXKIQ22WFKyIs1jbORIkGanySBHaPPTOI4OxACP5WXgbcR0nDYfqNJfXEGwCK7IzHdNbCSFsNKKs0qCexR8Q==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.15.tgz", + "integrity": "sha512-dJuSTreu/T8f24SHDNTjd7eQ4rabr0TzPh2UTCwYexQtzG3nTDKm1e5eIdhiroTMDkPEJeY+WPkA6F9wod/20A==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/types": "^3.973.4", + "@smithy/fetch-http-handler": "^5.3.11", + "@smithy/node-http-handler": "^4.4.12", + "@smithy/property-provider": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.15", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.13.tgz", + "integrity": "sha512-JKSoGb7XeabZLBJptpqoZIFbROUIS65NuQnEHGOpuT9GuuZwag2qciKANiDLFiYk4u8nSrJC9JIOnWKVvPVjeA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/credential-provider-env": "^3.972.13", + "@aws-sdk/credential-provider-http": "^3.972.15", + "@aws-sdk/credential-provider-login": "^3.972.13", + "@aws-sdk/credential-provider-process": "^3.972.13", + "@aws-sdk/credential-provider-sso": "^3.972.13", + "@aws-sdk/credential-provider-web-identity": "^3.972.13", + "@aws-sdk/nested-clients": "^3.996.3", + "@aws-sdk/types": "^3.973.4", + "@smithy/credential-provider-imds": "^4.2.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.13.tgz", + "integrity": "sha512-RtYcrxdnJHKY8MFQGLltCURcjuMjnaQpAxPE6+/QEdDHHItMKZgabRe/KScX737F9vJMQsmJy9EmMOkCnoC1JQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/nested-clients": "^3.996.3", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.14.tgz", + "integrity": "sha512-WqoC2aliIjQM/L3oFf6j+op/enT2i9Cc4UTxxMEKrJNECkq4/PlKE5BOjSYFcq6G9mz65EFbXJh7zOU4CvjSKQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.13", + "@aws-sdk/credential-provider-http": "^3.972.15", + "@aws-sdk/credential-provider-ini": "^3.972.13", + "@aws-sdk/credential-provider-process": "^3.972.13", + "@aws-sdk/credential-provider-sso": "^3.972.13", + "@aws-sdk/credential-provider-web-identity": "^3.972.13", + "@aws-sdk/types": "^3.973.4", + "@smithy/credential-provider-imds": "^4.2.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.13.tgz", + "integrity": "sha512-rsRG0LQA4VR+jnDyuqtXi2CePYSmfm5GNL9KxiW8DSe25YwJSr06W8TdUfONAC+rjsTI+aIH2rBGG5FjMeANrw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.13.tgz", + "integrity": "sha512-fr0UU1wx8kNHDhTQBXioc/YviSW8iXuAxHvnH7eQUtn8F8o/FU3uu6EUMvAQgyvn7Ne5QFnC0Cj0BFlwCk+RFw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/nested-clients": "^3.996.3", + "@aws-sdk/token-providers": "3.999.0", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.13.tgz", + "integrity": "sha512-a6iFMh1pgUH0TdcouBppLJUfPM7Yd3R9S1xFodPtCRoLqCz2RQFA3qjA8x4112PVYXEd4/pHX2eihapq39w0rA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/nested-clients": "^3.996.3", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/lib-storage": { + "version": "3.1000.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1000.0.tgz", + "integrity": "sha512-/5KUjz08OS6ErUAaBBBXosFWcjUQJ7R9taPDYfmeKALQF4YXirS+n4/nholInOG4/8Cg89DeufqA/Ru89jC5Kw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/abort-controller": "^4.2.10", + "@smithy/middleware-endpoint": "^4.4.20", + "@smithy/smithy-client": "^4.12.0", + "buffer": "5.6.0", + "events": "3.3.0", + "stream-browserify": "3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.1000.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.6.tgz", + "integrity": "sha512-3H2bhvb7Cb/S6WFsBy/Dy9q2aegC9JmGH1inO8Lb2sWirSqpLJlZmvQHPE29h2tIxzv6el/14X/tLCQ8BQU6ZQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-arn-parser": "^3.972.2", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.6.tgz", + "integrity": "sha512-QMdffpU+GkSGC+bz6WdqlclqIeCsOfgX8JFZ5xvwDtX+UTj4mIXm3uXu7Ko6dBseRcJz1FA6T9OmlAAY6JgJUg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.973.1.tgz", + "integrity": "sha512-QLXsxsI6VW8LuGK+/yx699wzqP/NMCGk/hSGP+qtB+Lcff+23UlbahyouLlk+nfT7Iu021SkXBhnAuVd6IZcPw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/crc64-nvme": "^3.972.3", + "@aws-sdk/types": "^3.973.4", + "@smithy/is-array-buffer": "^4.2.1", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-stream": "^4.5.15", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.6.tgz", + "integrity": "sha512-5XHwjPH1lHB+1q4bfC7T8Z5zZrZXfaLcjSMwTd1HPSPrCmPFMbg3UQ5vgNWcVj0xoX4HWqTGkSf2byrjlnRg5w==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.6.tgz", + "integrity": "sha512-XdZ2TLwyj3Am6kvUc67vquQvs6+D8npXvXgyEUJAdkUDx5oMFJKOqpK+UpJhVDsEL068WAJl2NEGzbSik7dGJQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.6.tgz", + "integrity": "sha512-iFnaMFMQdljAPrvsCVKYltPt2j40LQqukAbXvW7v0aL5I+1GO7bZ/W8m12WxW3gwyK5p5u1WlHg8TSAizC5cZw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.6.tgz", + "integrity": "sha512-dY4v3of5EEMvik6+UDwQ96KfUFDk8m1oZDdkSc5lwi4o7rFrjnv0A+yTV+gu230iybQZnKgDLg/rt2P3H+Vscw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.15.tgz", + "integrity": "sha512-WDLgssevOU5BFx1s8jA7jj6cE5HuImz28sy9jKOaVtz0AW1lYqSzotzdyiybFaBcQTs5zxXOb2pUfyMxgEKY3Q==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-arn-parser": "^3.972.2", + "@smithy/core": "^3.23.6", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/signature-v4": "^5.3.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-stream": "^4.5.15", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.6.tgz", + "integrity": "sha512-acvMUX9jF4I2Ew+Z/EA6gfaFaz9ehci5wxBmXCZeulLuv8m+iGf6pY9uKz8TPjg39bdAz3hxoE0eLP8Qz+IYlA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.15.tgz", + "integrity": "sha512-ABlFVcIMmuRAwBT+8q5abAxOr7WmaINirDJBnqGY5b5jSDo00UMlg/G4a0xoAgwm6oAECeJcwkvDlxDwKf58fQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-endpoints": "^3.996.3", + "@smithy/core": "^3.23.6", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.996.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.3.tgz", + "integrity": "sha512-AU5TY1V29xqwg/MxmA2odwysTez+ccFAhmfRJk+QZT5HNv90UTA9qKd1J9THlsQkvmH7HWTEV1lDNxkQO5PzNw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/middleware-host-header": "^3.972.6", + "@aws-sdk/middleware-logger": "^3.972.6", + "@aws-sdk/middleware-recursion-detection": "^3.972.6", + "@aws-sdk/middleware-user-agent": "^3.972.15", + "@aws-sdk/region-config-resolver": "^3.972.6", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-endpoints": "^3.996.3", + "@aws-sdk/util-user-agent-browser": "^3.972.6", + "@aws-sdk/util-user-agent-node": "^3.973.0", + "@smithy/config-resolver": "^4.4.9", + "@smithy/core": "^3.23.6", + "@smithy/fetch-http-handler": "^5.3.11", + "@smithy/hash-node": "^4.2.10", + "@smithy/invalid-dependency": "^4.2.10", + "@smithy/middleware-content-length": "^4.2.10", + "@smithy/middleware-endpoint": "^4.4.20", + "@smithy/middleware-retry": "^4.4.37", + "@smithy/middleware-serde": "^4.2.11", + "@smithy/middleware-stack": "^4.2.10", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/node-http-handler": "^4.4.12", + "@smithy/protocol-http": "^5.3.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-body-length-browser": "^4.2.1", + "@smithy/util-body-length-node": "^4.2.2", + "@smithy/util-defaults-mode-browser": "^4.3.36", + "@smithy/util-defaults-mode-node": "^4.2.39", + "@smithy/util-endpoints": "^3.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-retry": "^4.2.10", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.6.tgz", + "integrity": "sha512-Aa5PusHLXAqLTX1UKDvI3pHQJtIsF7Q+3turCHqfz/1F61/zDMWfbTC8evjhrrYVAtz9Vsv3SJ/waSUeu7B6gw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/config-resolver": "^4.4.9", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.1000.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1000.0.tgz", + "integrity": "sha512-DP6EbwCD0CKzBwBnT1X6STB5i+bY765CxjMbWCATDhCgOB343Q6AHM9c1S/300Uc5waXWtI/Wdeak9Ru56JOvg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "^3.996.3", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-format-url": "^3.972.6", + "@smithy/middleware-endpoint": "^4.4.20", + "@smithy/protocol-http": "^5.3.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.3.tgz", + "integrity": "sha512-gQYI/Buwp0CAGQxY7mR5VzkP56rkWq2Y1ROkFuXh5XY94DsSjJw62B3I0N0lysQmtwiL2ht2KHI9NylM/RP4FA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.15", + "@aws-sdk/types": "^3.973.4", + "@smithy/protocol-http": "^5.3.10", + "@smithy/signature-v4": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.999.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.999.0.tgz", + "integrity": "sha512-cx0hHUlgXULfykx4rdu/ciNAJaa3AL5xz3rieCz7NKJ68MJwlj3664Y8WR5MGgxfyYJBdamnkjNSx5Kekuc0cg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.973.15", + "@aws-sdk/nested-clients": "^3.996.3", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.4.tgz", + "integrity": "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz", + "integrity": "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.3.tgz", + "integrity": "sha512-yWIQSNiCjykLL+ezN5A+DfBb1gfXTytBxm57e64lYmwxDHNmInYHRJYYRAGWG1o77vKEiWaw4ui28e3yb1k5aQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-endpoints": "^3.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.6.tgz", + "integrity": "sha512-0YNVNgFyziCejXJx0rzxPiD2rkxTWco4c9wiMF6n37Tb9aQvIF8+t7GyEyIFCwQHZ0VMQaAl+nCZHOYz5I5EKw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/querystring-builder": "^4.2.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.4.tgz", + "integrity": "sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.6.tgz", + "integrity": "sha512-Fwr/llD6GOrFgQnKaI2glhohdGuBDfHfora6iG9qsBBBR8xv1SdCSwbtf5CWlUdCw5X7g76G/9Hf0Inh0EmoxA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.0.tgz", + "integrity": "sha512-A9J2G4Nf236e9GpaC1JnA8wRn6u6GjnOXiTwBLA6NUJhlBTIGfrTy+K1IazmF8y+4OFdW3O5TZlhyspJMqiqjA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.15", + "@aws-sdk/types": "^3.973.4", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.8.tgz", + "integrity": "sha512-Ql8elcUdYCha83Ol7NznBsgN5GVZnv3vUd86fEc6waU6oUdY0T1O9NODkEEOS/Uaogr87avDrUC6DSeM4oXjZg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "fast-xml-parser": "5.3.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", + "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.2.tgz", + "integrity": "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", + "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-xml": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.5.0.tgz", + "integrity": "sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "fast-xml-parser": "^5.0.7", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz", + "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.29.0.tgz", + "integrity": "sha512-/f3eHkSNUTl6DLQHm+bKecjBKcRQxbd/XLx8lvSYp8Nl/HRyPuIPOijt9Dt0sH50/SxOwQ62RnFCmFlGK+bR/w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@azure/msal-common": "15.15.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.15.0.tgz", + "integrity": "sha512-/n+bN0AKlVa+AOcETkJSKj38+bvFs78BaP4rNtv3MJCmPH0YrHiskMRe74OhyZ5DZjGISlFyxqvf9/4QVEi2tw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "3.8.8", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.8.tgz", + "integrity": "sha512-+f1VrJH1iI517t4zgmuhqORja0bL6LDQXfBqkjuMmfTYXTQQnh1EvwwxO3UbKLT05N0obF72SRHFrC1RBDv5Gg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@azure/msal-common": "15.15.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@azure/msal-node/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@azure/storage-blob": { + "version": "12.31.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.31.0.tgz", + "integrity": "sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.3", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/core-xml": "^1.4.5", + "@azure/logger": "^1.1.4", + "@azure/storage-common": "^12.3.0", + "events": "^3.0.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/storage-common": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/@azure/storage-common/-/storage-common-12.3.0.tgz", + "integrity": "sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.1.4", + "events": "^3.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/types": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@electric-sql/pglite": { + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", + "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.20.tgz", + "integrity": "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.3.15" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.20.tgz", + "integrity": "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.3.15" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "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/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-array/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/@eslint/config-array/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/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "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.1", + "minimatch": "^3.1.3", + "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/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/@eslint/eslintrc/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/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "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/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz", + "integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "<4.1.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^5.3.4", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "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/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, + "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.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@mrleebo/prisma-ast": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.13.1.tgz", + "integrity": "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chevrotain": "^10.5.0", + "lilconfig": "^2.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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/@prisma/adapter-pg": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.4.2.tgz", + "integrity": "sha512-oUo2Zhe9Tf6YwVL8kLPuOLTK1Z2pwi/Ua77t2PuGyBan2w7shRKqHvYK+3XXmRH9RWhPJ4SMtHZKpNo6Ax/4bQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.4.2", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, + "node_modules/@prisma/client": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.4.2.tgz", + "integrity": "sha512-ts2mu+cQHriAhSxngO3StcYubBGTWDtu/4juZhXCUKOwgh26l+s4KD3vT2kMUzFyrYnll9u/3qWrtzRv9CGWzA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.4.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.4.2.tgz", + "integrity": "sha512-cID+rzOEb38VyMsx5LwJMEY4NGIrWCNpKu/0ImbeooQ2Px7TI+kOt7cm0NelxUzF2V41UVVXAmYjANZQtCu1/Q==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.4.2.tgz", + "integrity": "sha512-CftBjWxav99lzY1Z4oDgomdb1gh9BJFAOmWF6P2v1xRfXqQb56DfBub+QKcERRdNoAzCb3HXy3Zii8Vb4AsXhg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.4.2.tgz", + "integrity": "sha512-aP7qzu+g/JnbF6U69LMwHoUkELiserKmWsE2shYuEpNUJ4GrtxBCvZwCyCBHFSH2kLTF2l1goBlBh4wuvRq62w==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.20.0.tgz", + "integrity": "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.3.15", + "@electric-sql/pglite-socket": "0.0.20", + "@electric-sql/pglite-tools": "0.2.20", + "@hono/node-server": "1.19.9", + "@mrleebo/prisma-ast": "0.13.1", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "4.11.4", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.2.0", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.4.2.tgz", + "integrity": "sha512-REdjFpT/ye9KdDs+CXAXPIbMQkVLhne9G5Pe97sNY4Ovx4r2DAbWM9hOFvvB1Oq8H8bOCdu0Ri3AoGALquQqVw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.4.2" + } + }, + "node_modules/@prisma/engines": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.4.2.tgz", + "integrity": "sha512-B+ZZhI4rXlzjVqRw/93AothEKOU5/x4oVyJFGo9RpHPnBwaPwk4Pi0Q4iGXipKxeXPs/dqljgNBjK0m8nocOJA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.4.2", + "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", + "@prisma/fetch-engine": "7.4.2", + "@prisma/get-platform": "7.4.2" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919.tgz", + "integrity": "sha512-5FIKY3KoYQlBuZC2yc16EXfVRQ8HY+fLqgxkYfWCtKhRb3ajCRzP/rPeoSx11+NueJDANdh4hjY36mdmrTcGSg==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.2.tgz", + "integrity": "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.4.2" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.4.2.tgz", + "integrity": "sha512-f/c/MwYpdJO7taLETU8rahEstLeXfYgQGlz5fycG7Fbmva3iPdzGmjiSWHeSWIgNnlXnelUdCJqyZnFocurZuA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.4.2", + "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", + "@prisma/get-platform": "7.4.2" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.2.tgz", + "integrity": "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.4.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/studio-core": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.13.1.tgz", + "integrity": "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.10.tgz", + "integrity": "sha512-qocxM/X4XGATqQtUkbE9SPUB6wekBi+FyJOMbPj0AhvyvFGYEmOlz6VB22iMePCQsFmMIvFSeViDvA7mZJG47g==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.1.tgz", + "integrity": "sha512-y5d4xRiD6TzeP5BWlb+Ig/VFqF+t9oANNhGeMqyzU7obw7FYgTgVi50i5JqBTeKp+TABeDIeeXFZdz65RipNtA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.2.tgz", + "integrity": "sha512-QzzYIlf4yg0w5TQaC9VId3B3ugSk1MI/wb7tgcHtd7CBV9gNRKZrhc2EPSxSZuDy10zUZ0lomNMgkc6/VVe8xg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/util-base64": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.9.tgz", + "integrity": "sha512-ejQvXqlcU30h7liR9fXtj7PIAau1t/sFbJpgWPfiYDs7zd16jpH0IsSXKcba2jF6ChTXvIjACs27kNMc5xxE2Q==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.1", + "@smithy/util-endpoints": "^3.3.1", + "@smithy/util-middleware": "^4.2.10", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.23.6", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.6.tgz", + "integrity": "sha512-4xE+0L2NrsFKpEVFlFELkIHQddBvMbQ41LRIP74dGCXnY1zQ9DgksrBcRBDJT+iOzGy4VEJIeU3hkUK5mn06kg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/middleware-serde": "^4.2.11", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-body-length-browser": "^4.2.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-stream": "^4.5.15", + "@smithy/util-utf8": "^4.2.1", + "@smithy/uuid": "^1.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.10.tgz", + "integrity": "sha512-3bsMLJJLTZGZqVGGeBVFfLzuRulVsGTj12BzRKODTHqUABpIr0jMN1vN3+u6r2OfyhAQ2pXaMZWX/swBK5I6PQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/node-config-provider": "^4.3.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.10.tgz", + "integrity": "sha512-A4ynrsFFfSXUHicfTcRehytppFBcY3HQxEGYiyGktPIOye3Ot7fxpiy4VR42WmtGI4Wfo6OXt/c1Ky1nUFxYYQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.13.0", + "@smithy/util-hex-encoding": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.10.tgz", + "integrity": "sha512-0xupsu9yj9oDVuQ50YCTS9nuSYhGlrwqdaKQel9y2Fz7LU9fNErVlw9N0o4pm4qqvWEGbSTI4HKc6XJfB30MVw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.10.tgz", + "integrity": "sha512-8kn6sinrduk0yaYHMJDsNuiFpXwQwibR7n/4CDUqn4UgaG+SeBHu5jHGFdU9BLFAM7Q4/gvr9RYxBHz9/jKrhA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.10.tgz", + "integrity": "sha512-uUrxPGgIffnYfvIOUmBM5i+USdEBRTdh7mLPttjphgtooxQ8CtdO1p6K5+Q4BBAZvKlvtJ9jWyrWpBJYzBKsyQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.10.tgz", + "integrity": "sha512-aArqzOEvcs2dK+xQVCgLbpJQGfZihw8SD4ymhkwNTtwKbnrzdhJsFDKuMQnam2kF69WzgJYOU5eJlCx+CA32bw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/eventstream-codec": "^4.2.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.11.tgz", + "integrity": "sha512-wbTRjOxdFuyEg0CpumjZO0hkUl+fetJFqxNROepuLIoijQh51aMBmzFLfoQdwRjxsuuS2jizzIUTjPWgd8pd7g==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/protocol-http": "^5.3.10", + "@smithy/querystring-builder": "^4.2.10", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.11.tgz", + "integrity": "sha512-DrcAx3PM6AEbWZxsKl6CWAGnVwiz28Wp1ZhNu+Hi4uI/6C1PIZBIaPM2VoqBDAsOWbM6ZVzOEQMxFLLdmb4eBQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.1", + "@smithy/chunked-blob-reader-native": "^4.2.2", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.10.tgz", + "integrity": "sha512-1VzIOI5CcsvMDvP3iv1vG/RfLJVVVc67dCRyLSB2Hn9SWCZrDO3zvcIzj3BfEtqRW5kcMg5KAeVf1K3dR6nD3w==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-buffer-from": "^4.2.1", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.10.tgz", + "integrity": "sha512-w78xsYrOlwXKwN5tv1GnKIRbHb1HygSpeZMP6xDxCPGf1U/xDHjCpJu64c5T35UKyEPwa0bPeIcvU69VY3khUA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.10.tgz", + "integrity": "sha512-vy9KPNSFUU0ajFYk0sDZIYiUlAWGEAhRfehIr5ZkdFrRFTAuXEPUd41USuqHU6vvLX4r6Q9X7MKBco5+Il0Org==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.1.tgz", + "integrity": "sha512-Yfu664Qbf1B4IYIsYgKoABt010daZjkaCRvdU/sPnZG6TtHOB0md0RjNdLGzxe5UIdn9js4ftPICzmkRa9RJ4Q==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.10.tgz", + "integrity": "sha512-Op+Dh6dPLWTjWITChFayDllIaCXRofOed8ecpggTC5fkh8yXes0vAEX7gRUfjGK+TlyxoCAA05gHbZW/zB9JwQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.10.tgz", + "integrity": "sha512-TQZ9kX5c6XbjhaEBpvhSvMEZ0klBs1CFtOdPFwATZSbC9UeQfKHPLPN9Y+I6wZGMOavlYTOlHEPDrt42PMSH9w==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.20.tgz", + "integrity": "sha512-9W6Np4ceBP3XCYAGLoMCmn8t2RRVzuD1ndWPLBbv7H9CrwM9Bprf6Up6BM9ZA/3alodg0b7Kf6ftBK9R1N04vw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/core": "^3.23.6", + "@smithy/middleware-serde": "^4.2.11", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-middleware": "^4.2.10", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.37", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.37.tgz", + "integrity": "sha512-/1psZZllBBSQ7+qo5+hhLz7AEPGLx3Z0+e3ramMBEuPK2PfvLK4SrncDB9VegX5mBn+oP/UTDrM6IHrFjvX1ZA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/node-config-provider": "^4.3.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/service-error-classification": "^4.2.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-retry": "^4.2.10", + "@smithy/uuid": "^1.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.11.tgz", + "integrity": "sha512-STQdONGPwbbC7cusL60s7vOa6He6A9w2jWhoapL0mgVjmR19pr26slV+yoSP76SIssMTX/95e5nOZ6UQv6jolg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.10.tgz", + "integrity": "sha512-pmts/WovNcE/tlyHa8z/groPeOtqtEpp61q3W0nW1nDJuMq/x+hWa/OVQBtgU0tBqupeXq0VBOLA4UZwE8I0YA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.10.tgz", + "integrity": "sha512-UALRbJtVX34AdP2VECKVlnNgidLHA2A7YgcJzwSBg1hzmnO/bZBHl/LDQQyYifzUwp1UOODnl9JJ3KNawpUJ9w==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.12.tgz", + "integrity": "sha512-zo1+WKJkR9x7ZtMeMDAAsq2PufwiLDmkhcjpWPRRkmeIuOm6nq1qjFICSZbnjBvD09ei8KMo26BWxsu2BUU+5w==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/abort-controller": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/querystring-builder": "^4.2.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.10.tgz", + "integrity": "sha512-5jm60P0CU7tom0eNrZ7YrkgBaoLFXzmqB0wVS+4uK8PPGmosSrLNf6rRd50UBvukztawZ7zyA8TxlrKpF5z9jw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.10.tgz", + "integrity": "sha512-2NzVWpYY0tRdfeCJLsgrR89KE3NTWT2wGulhNUxYlRmtRmPwLQwKzhrfVaiNlA9ZpJvbW7cjTVChYKgnkqXj1A==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.10.tgz", + "integrity": "sha512-HeN7kEvuzO2DmAzLukE9UryiUvejD3tMp9a1D1NJETerIfKobBUCLfviP6QEk500166eD2IATaXM59qgUI+YDA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-uri-escape": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.10.tgz", + "integrity": "sha512-4Mh18J26+ao1oX5wXJfWlTT+Q1OpDR8ssiC9PDOuEgVBGloqg18Fw7h5Ct8DyT9NBYwJgtJ2nLjKKFU6RP1G1Q==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.10.tgz", + "integrity": "sha512-0R/+/Il5y8nB/By90o8hy/bWVYptbIfvoTYad0igYQO5RefhNCDmNzqxaMx7K1t/QWo0d6UynqpqN5cCQt1MCg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.5.tgz", + "integrity": "sha512-pHgASxl50rrtOztgQCPmOXFjRW+mCd7ALr/3uXNzRrRoGV5G2+78GOsQ3HlQuBVHCh9o6xqMNvlIKZjWn4Euug==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.10.tgz", + "integrity": "sha512-Wab3wW8468WqTKIxI+aZe3JYO52/RYT/8sDOdzkUhjnLakLe9qoQqIcfih/qxcF4qWEFoWBszY0mj5uxffaVXA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/is-array-buffer": "^4.2.1", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-hex-encoding": "^4.2.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-uri-escape": "^4.2.1", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.0.tgz", + "integrity": "sha512-R8bQ9K3lCcXyZmBnQqUZJF4ChZmtWT5NLi6x5kgWx5D+/j0KorXcA0YcFg/X5TOgnTCy1tbKc6z2g2y4amFupQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/core": "^3.23.6", + "@smithy/middleware-endpoint": "^4.4.20", + "@smithy/middleware-stack": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.15", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz", + "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.10.tgz", + "integrity": "sha512-uypjF7fCDsRk26u3qHmFI/ePL7bxxB9vKkE+2WKEciHhz+4QtbzWiHRVNRJwU3cKhrYDYQE3b0MRFtqfLYdA4A==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/querystring-parser": "^4.2.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.1.tgz", + "integrity": "sha512-BKGuawX4Doq/bI/uEmg+Zyc36rJKWuin3py89PquXBIBqmbnJwBBsmKhdHfNEp0+A4TDgLmT/3MSKZ1SxHcR6w==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/util-buffer-from": "^4.2.1", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.1.tgz", + "integrity": "sha512-SiJeLiozrAoCrgDBUgsVbmqHmMgg/2bA15AzcbcW+zan7SuyAVHN4xTSbq0GlebAIwlcaX32xacnrG488/J/6g==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.2.tgz", + "integrity": "sha512-4rHqBvxtJEBvsZcFQSPQqXP2b/yy/YlB66KlcEgcH2WNoOKCKB03DSLzXmOsXjbl8dJ4OEYTn31knhdznwk7zw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.1.tgz", + "integrity": "sha512-/swhmt1qTiVkaejlmMPPDgZhEaWb/HWMGRBheaxwuVkusp/z+ErJyQxO6kaXumOciZSWlmq6Z5mNylCd33X7Ig==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/is-array-buffer": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.1.tgz", + "integrity": "sha512-462id/00U8JWFw6qBuTSWfN5TxOHvDu4WliI97qOIOnuC/g+NDAknTU8eoGXEPlLkRVgWEr03jJBLV4o2FL8+A==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.36", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.36.tgz", + "integrity": "sha512-R0smq7EHQXRVMxkAxtH5akJ/FvgAmNF6bUy/GwY/N20T4GrwjT633NFm0VuRpC+8Bbv8R9A0DoJ9OiZL/M3xew==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/property-provider": "^4.2.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.39", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.39.tgz", + "integrity": "sha512-otWuoDm35btJV1L8MyHrPl462B07QCdMTktKc7/yM+Psv6KbED/ziXiHnmr7yPHUjfIwE9S8Max0LO24Mo3ZVg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/config-resolver": "^4.4.9", + "@smithy/credential-provider-imds": "^4.2.10", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/smithy-client": "^4.12.0", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.1.tgz", + "integrity": "sha512-xyctc4klmjmieQiF9I1wssBWleRV0RhJ2DpO8+8yzi2LO1Z+4IWOZNGZGNj4+hq9kdo+nyfrRLmQTzc16Op2Vg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.1.tgz", + "integrity": "sha512-c1hHtkgAWmE35/50gmdKajgGAKV3ePJ7t6UtEmpfCWJmQE9BQAQPz0URUVI89eSkcDqCtzqllxzG28IQoZPvwA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.10.tgz", + "integrity": "sha512-LxaQIWLp4y0r72eA8mwPNQ9va4h5KeLM0I3M/HV9klmFaY2kN766wf5vsTzmaOpNNb7GgXAd9a25P3h8T49PSA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.10.tgz", + "integrity": "sha512-HrBzistfpyE5uqTwiyLsFHscgnwB0kgv8vySp7q5kZ0Eltn/tjosaSGGDj/jJ9ys7pWzIP/icE2d+7vMKXLv7A==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/service-error-classification": "^4.2.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.15", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.15.tgz", + "integrity": "sha512-OlOKnaqnkU9X+6wEkd7mN+WB7orPbCVDauXOj22Q7VtiTkvy7ZdSsOg4QiNAZMgI4OkvNf+/VLUC3VXkxuWJZw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.11", + "@smithy/node-http-handler": "^4.4.12", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-buffer-from": "^4.2.1", + "@smithy/util-hex-encoding": "^4.2.1", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.1.tgz", + "integrity": "sha512-YmiUDn2eo2IOiWYYvGQkgX5ZkBSiTQu4FlDo5jNPpAxng2t6Sjb6WutnZV9l6VR4eJul1ABmCrnWBC9hKHQa6Q==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.1.tgz", + "integrity": "sha512-DSIwNaWtmzrNQHv8g7DBGR9mulSit65KSj5ymGEIAknmIN8IpbZefEep10LaMG/P/xquwbmJ1h9ectz8z6mV6g==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/util-buffer-from": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.10.tgz", + "integrity": "sha512-4eTWph/Lkg1wZEDAyObwme0kmhEb7J/JjibY2znJdrYRgKbKqB7YoEhhJVJ4R1g/SYih4zuwX7LpJaM8RsnTVg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/abort-controller": "^4.2.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.1.tgz", + "integrity": "sha512-dSfDCeihDmZlV2oyr0yWPTUfh07suS+R5OB+FZGiv/hHyK3hrFBW5rR1UYjfa57vBsrP9lciFkRPzebaV1Qujw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@th3hero/request-validator": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@th3hero/request-validator/-/request-validator-1.1.9.tgz", + "integrity": "sha512-GNrAs10kmUO+J9Y+NXUhI5qhQdXp4Ii6XkmNdOo9y1R1pqqr7rYIv0GL3jvUCotJ/XP+orxyz1xHHslDQjN4JA==", + "license": "MIT", + "dependencies": { + "express": "^4.18.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/th3hero" + }, + "optionalDependencies": { + "mysql": "^2.18.1" + } + }, + "node_modules/@th3hero/request-validator/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@th3hero/request-validator/node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/@th3hero/request-validator/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@th3hero/request-validator/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/@th3hero/request-validator/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@th3hero/request-validator/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@th3hero/request-validator/node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@th3hero/request-validator/node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@th3hero/request-validator/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@th3hero/request-validator/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@th3hero/request-validator/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@th3hero/request-validator/node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@th3hero/request-validator/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@th3hero/request-validator/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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@th3hero/request-validator/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==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@th3hero/request-validator/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@th3hero/request-validator/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/@th3hero/request-validator/node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@th3hero/request-validator/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@th3hero/request-validator/node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@th3hero/request-validator/node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@th3hero/request-validator/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "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/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz", + "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/pg": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/request": { + "version": "2.48.13", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.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.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.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 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" + }, + "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.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" + }, + "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.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "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.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.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 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "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.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.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/utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.3.tgz", + "integrity": "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "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/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "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/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/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/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "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/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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", + "optional": true, + "peer": true + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bullmq": { + "version": "5.70.1", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.70.1.tgz", + "integrity": "sha512-HjfGHfICkAClrFL0Y07qNbWcmiOCv1l+nusupXUjrvTPuDEyPEJ23MP0lUwUs/QEy1a3pWt/P/sCsSZ1RjRK+w==", + "license": "MIT", + "dependencies": { + "cron-parser": "4.9.0", + "ioredis": "5.9.3", + "msgpackr": "1.11.5", + "node-abort-controller": "3.1.1", + "semver": "7.7.4", + "tslib": "2.8.1", + "uuid": "11.1.0" + } + }, + "node_modules/bullmq/node_modules/@ioredis/commands": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz", + "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==", + "license": "MIT" + }, + "node_modules/bullmq/node_modules/ioredis": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.3.tgz", + "integrity": "sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/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/chevrotain": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "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/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "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==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "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/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT", + "optional": true + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "devOptional": 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.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT", + "peer": true + }, + "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/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/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "peer": true, + "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/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "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.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "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.3", + "@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-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": "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/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/eslint/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/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/eslint/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/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/espree/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/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "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/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/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-storage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/express-storage/-/express-storage-3.0.0.tgz", + "integrity": "sha512-WT3hdiQmfg41LrWgNUbFE6dR5mMJn7BsutzM3Q8A1/otlUJdvFy3zKm+3qpH2gP0i5ucGQAc4ipeCajzLQhjfQ==", + "license": "MIT", + "dependencies": { + "dotenv": "^16.3.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.600.0", + "@aws-sdk/lib-storage": "^3.600.0", + "@aws-sdk/s3-request-presigner": "^3.600.0", + "@azure/identity": "^4.0.0", + "@azure/storage-blob": "^12.17.0", + "@google-cloud/storage": "^7.7.0", + "express": "^4.18.0 || ^5.0.0", + "multer": "^1.4.5-lts.1 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-s3": { + "optional": true + }, + "@aws-sdk/lib-storage": { + "optional": true + }, + "@aws-sdk/s3-request-presigner": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@google-cloud/storage": { + "optional": true + }, + "express": { + "optional": true + }, + "multer": { + "optional": true + } + } + }, + "node_modules/express-storage/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "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/fast-xml-parser": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz", + "integrity": "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "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/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, + "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/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/form-data/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==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/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==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "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==", + "license": "MIT", + "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-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "devOptional": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/glob/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/glob/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/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/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/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "peer": true, + "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==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/hono": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", + "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause", + "optional": true, + "peer": true + }, + "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/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/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.0.tgz", + "integrity": "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-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-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT", + "optional": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "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/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "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/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "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/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "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/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/micromatch/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/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "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/msgpackr": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", + "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/mylas": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.14.tgz", + "integrity": "sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/raouldeheer" + } + }, + "node_modules/mysql": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", + "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", + "license": "MIT", + "optional": true, + "dependencies": { + "bignumber.js": "9.0.0", + "readable-stream": "2.3.7", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql/node_modules/bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/mysql/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "license": "MIT", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/mysql/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "optional": true + }, + "node_modules/mysql/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "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/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "devOptional": 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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": 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/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "devOptional": 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, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.19.0.tgz", + "integrity": "sha512-QIcLGi508BAHkQ3pJNptsFz5WQMlpGbuBGBaIaXsWK8mel2kQ/rThYI+DbgjUvZrIr7MiuEuc9LcChJoEZK1xQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.11.0", + "pg-pool": "^3.12.0", + "pg-protocol": "^1.12.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz", + "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.12.0.tgz", + "integrity": "sha512-eIJ0DES8BLaziFHW7VgJEBPi5hg3Nyng5iKpYtj3wbcAUV9A1wLgWiY7ajf/f/oO1wfxt83phXPY8Emztg7ITg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.12.0.tgz", + "integrity": "sha512-uOANXNRACNdElMXJ0tPz6RBM0XQ61nONGAwlt8da5zs/iUOOCLBQOHSXnrC6fMsvtjxbOJrZZl5IScGv+7mpbg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "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/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/plimit-lit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", + "integrity": "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue-lit": "^1.5.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/prisma": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.4.2.tgz", + "integrity": "sha512-2bP8Ruww3Q95Z2eH4Yqh4KAENRsj/SxbdknIVBfd6DmjPwmpsC4OVFMLOeHt6tM3Amh8ebjvstrUz3V/hOe1dA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.4.2", + "@prisma/dev": "0.20.0", + "@prisma/engines": "7.4.2", + "@prisma/studio-core": "0.13.1", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT", + "optional": true + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-lit": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", + "integrity": "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "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/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rate-limit-redis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-4.3.1.tgz", + "integrity": "sha512-+a1zU8+D7L8siDK9jb14refQXz60vq427VuiplgnaLk9B2LnvGe/APLTfhwb4uNIL7eWVknh8GnRp/unCj+lMA==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "express-rate-limit": ">= 6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "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/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "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/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "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==", + "devOptional": true, + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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==", + "devOptional": 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==", + "devOptional": 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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/strnum": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT", + "optional": true, + "peer": true + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/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/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/ts-node-dev/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/ts-node-dev/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/ts-node-dev/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/tsc-alias": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", + "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "commander": "^9.0.0", + "get-tsconfig": "^4.10.0", + "globby": "^11.0.4", + "mylas": "^2.1.9", + "normalize-path": "^3.0.0", + "plimit-lit": "^1.2.6" + }, + "bin": { + "tsc-alias": "dist/bin/index.js" + }, + "engines": { + "node": ">=16.20.2" + } + }, + "node_modules/tsc-alias/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tsc-alias/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/tsc-alias/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/tsc-alias/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "0BSD" + }, + "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/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause", + "optional": true, + "peer": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..04333ee --- /dev/null +++ b/package.json @@ -0,0 +1,65 @@ +{ + "name": "nodejs-boilerplate", + "version": "1.0.0", + "description": "Node.js Backend Boilerplate - Modular monolith with Express 5, Prisma 7, TypeScript, Redis, and BullMQ.", + "license": "ISC", + "author": "", + "type": "commonjs", + "main": "dist/index.js", + "scripts": { + "build": "prisma generate --config prisma.config.ts && tsc && tsc-alias && cp -r src/database/prisma dist/database/", + "start": "node -r tsconfig-paths/register dist/index.js", + "dev": "ts-node-dev -r tsconfig-paths/register --respawn --transpile-only src/index.ts", + "dev:build": "tsc --watch", + "clean": "rm -rf dist", + "lint": "eslint src/**/*.ts", + "lint:fix": "eslint src/**/*.ts --fix", + "typecheck": "tsc --noEmit", + "test": "echo \"Error: no test specified\" && exit 1", + "prepare": "husky", + "prisma:generate": "prisma generate --config prisma.config.ts", + "prisma:migrate": "prisma migrate dev --config prisma.config.ts", + "prisma:migrate:deploy": "prisma migrate deploy --config prisma.config.ts", + "prisma:studio": "prisma studio --config prisma.config.ts", + "prisma:format": "prisma format --config prisma.config.ts", + "seed": "ts-node -r tsconfig-paths/register src/database/seed.ts" + }, + "dependencies": { + "@prisma/adapter-pg": "^7.0.0", + "@prisma/client": "^7.0.0", + "@th3hero/request-validator": "^1.1.9", + "bullmq": "^5.64.0", + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.1.0", + "express-rate-limit": "^8.1.0", + "express-storage": "^3.0.0", + "helmet": "^8.1.0", + "ioredis": "^5.4.1", + "jsonwebtoken": "^9.0.2", + "pg": "^8.13.1", + "rate-limit-redis": "^4.3.1", + "winston": "^3.18.3", + "winston-daily-rotate-file": "^5.0.0" + }, + "engines": { + "node": ">=22.0.0" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^24.6.1", + "@types/pg": "^8.11.6", + "@typescript-eslint/eslint-plugin": "^8.45.0", + "@typescript-eslint/parser": "^8.45.0", + "eslint": "^9.36.0", + "husky": "^9.1.7", + "prisma": "^7.0.0", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "tsc-alias": "^1.8.16", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.9.3" + } +} diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..6ded194 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,12 @@ +import 'dotenv/config'; +import { defineConfig, env } from 'prisma/config'; + +export default defineConfig({ + schema: 'src/database/schema.prisma', + migrations: { + path: 'src/database/migrations' + }, + datasource: { + url: env('DATABASE_URL') + } +}); diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..0545644 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,64 @@ +/** + * Express Application Setup + * Configures middleware, routes, and error handling + */ + +import express, { Express } from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import path from 'node:path'; + +// Config +import corsConfiguration from '@config/cors.config'; +import helmetConfiguration from '@config/helmet.config'; + +// HTTP Middleware +import { + requestContextMiddleware, + sanitizeMiddleware, + generalRateLimiter, + httpLoggingMiddleware, + errorLoggingMiddleware, + errorHandler, + notFoundHandler +} from '@middleware/index'; + +// Routes +import router from '@/routes'; + +// ============================================================================ +// Application Setup +// ============================================================================ + +const app: Express = express(); + +// Request context (must be first - provides request ID for logging) +app.use(requestContextMiddleware); + +// Security middleware +app.use(cors(corsConfiguration)); +app.use(helmet(helmetConfiguration)); +app.use(generalRateLimiter); + +// Logging middleware +app.use(httpLoggingMiddleware); + +// Body parsing +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); + +// Input sanitization (XSS prevention) +app.use(sanitizeMiddleware); + +// Static files +app.use(express.static(path.join(__dirname, '../storage/assets/public'))); + +// Routes +app.use(router); + +// Error handling +app.use(notFoundHandler); +app.use(errorLoggingMiddleware); +app.use(errorHandler); + +export default app; diff --git a/src/app/core/cache/index.ts b/src/app/core/cache/index.ts new file mode 100644 index 0000000..ded9015 --- /dev/null +++ b/src/app/core/cache/index.ts @@ -0,0 +1 @@ +export { getRedisClient, disconnectRedisClient, isRedisConfigured } from './redis.client'; diff --git a/src/app/core/cache/redis.client.ts b/src/app/core/cache/redis.client.ts new file mode 100644 index 0000000..196bded --- /dev/null +++ b/src/app/core/cache/redis.client.ts @@ -0,0 +1,84 @@ +import Redis, { type RedisOptions } from 'ioredis'; +import environment from '@config/environment.config'; +import { createLogger } from '@services/logger.service'; + +const log = createLogger('redis'); + +let redisClient: Redis | null = null; +let redisConnectPromise: Promise | null = null; + +export const isRedisConfigured = (): boolean => { + return Boolean(environment.redis.url); +}; + +const createRedisOptions = (): RedisOptions => ({ + lazyConnect: true, + maxRetriesPerRequest: environment.redis.maxRetriesPerRequest, + enableOfflineQueue: false, + reconnectOnError: () => true, + retryStrategy: times => Math.min(1000 * times, 10000), + connectionName: environment.redis.connectionName +}); + +const createRedisClient = (): Redis => { + if (!environment.redis.url) { + throw new Error('REDIS_URL is not configured'); + } + + const client = new Redis(environment.redis.url, createRedisOptions()); + + client.on('error', error => { + log.error('Redis client error', { + message: error.message, + stack: error.stack + }); + }); + + client.on('close', () => { + log.warn('Redis connection closed'); + }); + + return client; +}; + +export const getRedisClient = async (): Promise => { + if (!redisClient) { + redisClient = createRedisClient(); + } + + if (redisClient.status === 'ready') { + return redisClient; + } + + const needsConnect = + redisClient.status === 'wait' || + redisClient.status === 'close' || + redisClient.status === 'end' || + redisClient.status === 'connecting'; + + if (needsConnect) { + if (!redisConnectPromise) { + redisConnectPromise = redisClient.connect().finally(() => { + redisConnectPromise = null; + }); + } + await redisConnectPromise; + } + + return redisClient; +}; + +export const disconnectRedisClient = async (): Promise => { + if (!redisClient) { + return; + } + + try { + await redisClient.quit(); + } catch { + redisClient.disconnect(); + } finally { + redisConnectPromise = null; + redisClient = null; + } +}; diff --git a/src/app/core/constants/auth.constants.ts b/src/app/core/constants/auth.constants.ts new file mode 100644 index 0000000..e27c7ee --- /dev/null +++ b/src/app/core/constants/auth.constants.ts @@ -0,0 +1,62 @@ +/** + * Authentication & Session Constants + */ + +import environment from '@config/environment.config'; + +const { auth } = environment; + +export const AUTH = { + /** OTP expiry time in minutes */ + OTP_EXPIRY_MINUTES: auth.otpExpiryMinutes, + + /** 2FA/MFA verification expiry time in minutes */ + TWO_FACTOR_EXPIRY_MINUTES: auth.twoFactorExpiryMinutes, + + /** Maximum failed login attempts before lockout */ + MAX_LOGIN_ATTEMPTS: auth.maxLoginAttempts, + + /** Account lockout duration in minutes */ + LOCKOUT_MINUTES: auth.lockoutMinutes, + + /** Maximum OTP resend attempts per session */ + MAX_RESEND_ATTEMPTS: auth.maxResendAttempts, + + /** Cooldown between OTP resends in milliseconds */ + RESEND_COOLDOWN_MS: auth.resendCooldownSeconds * 1000, + + /** Password reset token expiry in milliseconds (1 hour) */ + PASSWORD_RESET_EXPIRY_MS: 60 * 60 * 1000, + + /** Fixed OTP code for development environment */ + DEV_OTP_CODE: 123456, + + /** Minimum password length */ + MIN_PASSWORD_LENGTH: 8, + + /** Maximum password length */ + MAX_PASSWORD_LENGTH: 128, + + /** Session token length in bytes */ + SESSION_TOKEN_BYTES: 11, + + /** Reference string length for OTP verification */ + OTP_REFERENCE_LENGTH: 32 +} as const; + +export const JWT = { + /** Access token expiry */ + ACCESS_TOKEN_EXPIRY: '15m', + + /** Refresh token expiry */ + REFRESH_TOKEN_EXPIRY: '7d', + + /** Token mode: access */ + MODE_ACCESS: 0, + + /** Token mode: refresh */ + MODE_REFRESH: 1, + + /** Algorithm for signing */ + ALGORITHM: 'RS256' +} as const; diff --git a/src/app/core/constants/http.constants.ts b/src/app/core/constants/http.constants.ts new file mode 100644 index 0000000..66454d0 --- /dev/null +++ b/src/app/core/constants/http.constants.ts @@ -0,0 +1,94 @@ +/** + * HTTP & API Constants + */ + +export const ERROR_CODES = { + // Authentication Errors + INVALID_CREDENTIALS: 'INVALID_CREDENTIALS', + ACCOUNT_LOCKED: 'ACCOUNT_LOCKED', + ACCOUNT_SUSPENDED: 'ACCOUNT_SUSPENDED', + ACCOUNT_NOT_VERIFIED: 'ACCOUNT_NOT_VERIFIED', + TOKEN_EXPIRED: 'TOKEN_EXPIRED', + TOKEN_INVALID: 'TOKEN_INVALID', + SESSION_EXPIRED: 'SESSION_EXPIRED', + + // 2FA/MFA Errors + TWO_FA_REQUIRED: 'TWO_FA_REQUIRED', + INVALID_2FA_CODE: 'INVALID_2FA_CODE', + TOTP_REQUIRED: 'TOTP_REQUIRED', + INVALID_TOTP_CODE: 'INVALID_TOTP_CODE', + BACKUP_CODE_USED: 'BACKUP_CODE_USED', + + // Password Errors + PASSWORD_TOO_WEAK: 'PASSWORD_TOO_WEAK', + PASSWORD_REUSE_NOT_ALLOWED: 'PASSWORD_REUSE_NOT_ALLOWED', + PASSWORD_EXPIRED: 'PASSWORD_EXPIRED', + INVALID_PASSWORD_RESET_TOKEN: 'INVALID_PASSWORD_RESET_TOKEN', + + // Permission Errors + INSUFFICIENT_PERMISSIONS: 'INSUFFICIENT_PERMISSIONS', + ROLE_NOT_FOUND: 'ROLE_NOT_FOUND', + PERMISSION_DENIED: 'PERMISSION_DENIED', + + // Validation Errors + VALIDATION_ERROR: 'VALIDATION_ERROR', + REQUIRED_FIELD_MISSING: 'REQUIRED_FIELD_MISSING', + INVALID_EMAIL_FORMAT: 'INVALID_EMAIL_FORMAT', + INVALID_PHONE_FORMAT: 'INVALID_PHONE_FORMAT', + RECAPTCHA_VERIFICATION_FAILED: 'RECAPTCHA_VERIFICATION_FAILED', + + // Rate Limiting + RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED', + TOO_MANY_ATTEMPTS: 'TOO_MANY_ATTEMPTS', + + // System Errors + INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR', + SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE', + DATABASE_ERROR: 'DATABASE_ERROR', + NOT_FOUND: 'NOT_FOUND', + UNAUTHORIZED: 'UNAUTHORIZED', + + // Session Errors + SESSION_INACTIVE: 'SESSION_INACTIVE', + SESSION_NOT_FOUND: 'SESSION_NOT_FOUND', + SESSION_NOT_ACTIVE: 'SESSION_NOT_ACTIVE', + SESSION_NOT_VALID: 'SESSION_NOT_VALID', + SESSION_NOT_VALIDATED: 'SESSION_NOT_VALIDATED', + SESSION_NOT_VERIFIED: 'SESSION_NOT_VERIFIED', + + // Resource Errors + ALREADY_EXISTS: 'ALREADY_EXISTS', + CONSTRAINT_VIOLATION: 'CONSTRAINT_VIOLATION' +} as const; + +export const RATE_LIMITS = { + /** General API rate limit */ + GENERAL: { + windowMs: 60_000, + max: 120 + }, + + /** Login rate limit */ + LOGIN: { + windowMs: 900_000, // 15 minutes + max: 20 + }, + + /** OTP rate limit */ + OTP: { + windowMs: 3_600_000, // 1 hour + max: 5 + }, + + /** Password reset rate limit */ + PASSWORD_RESET: { + windowMs: 300_000, // 5 minutes + max: 3 + } +} as const; + +export const PAGINATION = { + DEFAULT_PAGE: 1, + DEFAULT_LIMIT: 20, + MAX_LIMIT: 100 +} as const; diff --git a/src/app/core/constants/index.ts b/src/app/core/constants/index.ts new file mode 100644 index 0000000..5a9fabf --- /dev/null +++ b/src/app/core/constants/index.ts @@ -0,0 +1,9 @@ +/** + * Central constants export + * All application constants are organized by domain + */ + +export * from './auth.constants'; +export * from './roles.constants'; +export * from './permissions.constants'; +export * from './http.constants'; diff --git a/src/app/core/constants/permissions.constants.ts b/src/app/core/constants/permissions.constants.ts new file mode 100644 index 0000000..0f66745 --- /dev/null +++ b/src/app/core/constants/permissions.constants.ts @@ -0,0 +1,156 @@ +/** + * Permission Constants + * Module-based permission system for RBAC + */ + +// ============================================================================ +// Permission Actions +// ============================================================================ + +/** + * Standard permission actions available for each module + */ +export const PERMISSION_ACTIONS = { + /** Create new resource (POST) */ + CREATE: 'create', + /** View single resource (GET /:id) */ + READ: 'read', + /** View resource list (GET /) */ + LIST: 'list', + /** Modify existing resource (PUT/PATCH) */ + UPDATE: 'update', + /** Remove resource (DELETE) */ + DELETE: 'delete' +} as const; + +export type PermissionAction = (typeof PERMISSION_ACTIONS)[keyof typeof PERMISSION_ACTIONS]; + +// ============================================================================ +// Permission Scope +// ============================================================================ + +/** + * Permission scope determines resource visibility: + * - 'own': User can only access their own resources + * - 'all': User can access all resources (admin-level) + */ +export const PERMISSION_SCOPES = { + /** Access only own resources */ + OWN: 'own', + /** Access all resources (admin) */ + ALL: 'all' +} as const; + +export type PermissionScope = (typeof PERMISSION_SCOPES)[keyof typeof PERMISSION_SCOPES]; + +// ============================================================================ +// Permission Modules +// ============================================================================ + +/** + * Application modules that have permission controls + * Add new modules here when created + */ +export const PERMISSION_MODULES = { + /** User management */ + USERS: 'users', + /** File uploads & gallery */ + GALLERY: 'gallery', + /** System configuration */ + CONFIG: 'config', + /** Roles management */ + ROLES: 'roles', + /** Permissions management */ + PERMISSIONS: 'permissions' +} as const; + +export type PermissionModule = (typeof PERMISSION_MODULES)[keyof typeof PERMISSION_MODULES]; + +// ============================================================================ +// Permission Types +// ============================================================================ + +/** + * Permission flags for a single module + * - Actions: create, read, list, update, delete + * - Scope: 'own' (user's resources only) or 'all' (all resources) + */ +export interface ModulePermissions { + create: boolean; + read: boolean; + list: boolean; + update: boolean; + delete: boolean; + /** Resource access scope - 'own' for user's resources, 'all' for admin access */ + scope: PermissionScope; +} + +/** + * Full permissions object - all modules with their permission flags + */ +export type Permissions = { + [K in PermissionModule]?: ModulePermissions; +}; + +/** + * Permission check request for middleware + */ +export interface PermissionCheck { + module: PermissionModule; + action: PermissionAction | PermissionAction[]; +} + +// ============================================================================ +// Default Permissions +// ============================================================================ + +/** + * All permissions enabled for a module with 'all' scope (admin-level) + */ +export const ALL_PERMISSIONS: ModulePermissions = { + create: true, + read: true, + list: true, + update: true, + delete: true, + scope: 'all' +}; + +/** + * Read-only permissions for a module with 'all' scope + */ +export const READ_ONLY_PERMISSIONS: ModulePermissions = { + create: false, + read: true, + list: true, + update: false, + delete: false, + scope: 'all' +}; + +/** + * No permissions for a module + */ +export const NO_PERMISSIONS: ModulePermissions = { + create: false, + read: false, + list: false, + update: false, + delete: false, + scope: 'own' +}; + +// ============================================================================ +// Permission Helpers +// ============================================================================ + +/** + * Get the permission scope for a module + * Returns 'own' if module not found or scope not set (safe default) + */ +export const getPermissionScope = (permissions: Permissions | undefined, module: PermissionModule): PermissionScope => { + if (!permissions) return 'own'; + const modulePerms = permissions[module]; + if (!modulePerms) return 'own'; + return modulePerms.scope ?? 'own'; +}; diff --git a/src/app/core/constants/roles.constants.ts b/src/app/core/constants/roles.constants.ts new file mode 100644 index 0000000..08c3a01 --- /dev/null +++ b/src/app/core/constants/roles.constants.ts @@ -0,0 +1,14 @@ +/** + * User Roles Constants + */ + +export const ROLES = { + /** Customer role - default app user */ + CUSTOMER: 'customer', + + /** Admin - general admin access */ + ADMIN: 'admin', + + /** Super admin - full system access */ + SUPER_ADMIN: 'super_admin' +} as const; diff --git a/src/app/core/container.ts b/src/app/core/container.ts new file mode 100644 index 0000000..dec0c60 --- /dev/null +++ b/src/app/core/container.ts @@ -0,0 +1,41 @@ +/** + * Dependency Injection Container + */ + +import prismaClient from '@core/database/prisma.client'; +import { createLogger, Logger } from '@services/logger.service'; + +// ============================================================================ +// Container Types +// ============================================================================ + +export interface Container { + prisma: typeof prismaClient; + createLogger: (context: string) => Logger; +} + +// ============================================================================ +// Container Singleton +// ============================================================================ + +let containerInstance: Container | null = null; + +export const initContainer = (): Container => { + if (containerInstance) { + return containerInstance; + } + + containerInstance = { + prisma: prismaClient, + createLogger + }; + + return containerInstance; +}; + +export const getPrisma = (): typeof prismaClient => { + if (!containerInstance) { + initContainer(); + } + return containerInstance!.prisma; +}; diff --git a/src/app/core/database/base.repository.ts b/src/app/core/database/base.repository.ts new file mode 100644 index 0000000..a83e338 --- /dev/null +++ b/src/app/core/database/base.repository.ts @@ -0,0 +1,44 @@ +/** + * Base Repository + * Abstract base class for all repositories with common operations + */ + +import prismaClient from './prisma.client'; + +// ============================================================================ +// Types - Using any for PrismaClient since it's generated +// ============================================================================ + + +type PrismaClientType = typeof prismaClient; + +type TransactionClient = Parameters[0]>[0]; + +// ============================================================================ +// Base Repository Class +// ============================================================================ + +export abstract class BaseRepository { + protected readonly prisma: PrismaClientType; + + constructor(prisma?: PrismaClientType) { + this.prisma = prisma ?? prismaClient; + } + + /** + * Execute operations in a transaction + */ + protected async transaction( + fn: (tx: TransactionClient) => Promise, + options?: { maxWait?: number; timeout?: number } + ): Promise { + return this.prisma.$transaction(fn, options); + } + + /** + * Execute raw SQL query + */ + protected async rawQuery(query: string, ...params: unknown[]): Promise { + return this.prisma.$queryRawUnsafe(query, ...params) as Promise; + } +} diff --git a/src/app/core/database/index.ts b/src/app/core/database/index.ts new file mode 100644 index 0000000..ff2cbf4 --- /dev/null +++ b/src/app/core/database/index.ts @@ -0,0 +1,6 @@ +/** + * Database Layer Export + */ + +export { default as prismaClient, default, connectDatabase, disconnectDatabase } from './prisma.client'; +export { BaseRepository } from './base.repository'; diff --git a/src/app/core/database/prisma.client.ts b/src/app/core/database/prisma.client.ts new file mode 100644 index 0000000..5a80ef0 --- /dev/null +++ b/src/app/core/database/prisma.client.ts @@ -0,0 +1,48 @@ +/** + * Prisma Client Re-export + * Re-exports the existing Prisma client from database folder + */ + +import prismaClient from '@database/prisma.client'; +import { createLogger } from '@services/logger.service'; + +const log = createLogger('database'); + +// ============================================================================ +// Connection Management +// ============================================================================ + +/** + * Connect to the database + */ +export const connectDatabase = async (): Promise => { + try { + await prismaClient.$connect(); + log.info('Database connected'); + } catch (error) { + log.error('Database connection failed', { + error: error instanceof Error ? error.message : 'Unknown error' + }); + throw error; + } +}; + +/** + * Disconnect from the database + */ +export const disconnectDatabase = async (): Promise => { + try { + await prismaClient.$disconnect(); + log.info('Database disconnected'); + } catch (error) { + log.error('Database disconnection failed', { + error: error instanceof Error ? error.message : 'Unknown error' + }); + } +}; + +// ============================================================================ +// Export +// ============================================================================ + +export default prismaClient; diff --git a/src/app/core/errors/index.ts b/src/app/core/errors/index.ts new file mode 100644 index 0000000..ec7b5a3 --- /dev/null +++ b/src/app/core/errors/index.ts @@ -0,0 +1,154 @@ +/** + * Custom Error Classes + * Provides typed, consistent error handling across the application + */ + +import { ERROR_CODES } from '@core/constants/http.constants'; + +// ============================================================================ +// Abstract Base +// ============================================================================ + +export abstract class AppError extends Error { + abstract readonly statusCode: number; + abstract readonly errorCode: string; + readonly isOperational = true; + readonly timestamp: string; + + constructor(message: string, public readonly details?: Record) { + super(message); + this.timestamp = new Date().toISOString(); + Object.setPrototypeOf(this, new.target.prototype); + Error.captureStackTrace(this, this.constructor); + } + + toJSON(): Record { + return { + success: false, + message: this.message, + error_code: this.errorCode, + error: this.details ?? { message: this.message }, + statusCode: this.statusCode, + timestamp: this.timestamp + }; + } +} + +// ============================================================================ +// 400 Bad Request +// ============================================================================ + +export class BadRequestError extends AppError { + readonly statusCode = 400; + readonly errorCode: string; + + constructor(message = 'Bad request', errorCode = ERROR_CODES.VALIDATION_ERROR, details?: Record) { + super(message, details); + this.errorCode = errorCode; + } +} + +// ============================================================================ +// 401 Unauthorized +// ============================================================================ + +export class UnauthorizedError extends AppError { + readonly statusCode = 401; + readonly errorCode: string; + + constructor(message = 'Unauthorized', errorCode = ERROR_CODES.UNAUTHORIZED, details?: Record) { + super(message, details); + this.errorCode = errorCode; + } +} + +// ============================================================================ +// 403 Forbidden +// ============================================================================ + +export class ForbiddenError extends AppError { + readonly statusCode = 403; + readonly errorCode: string; + + constructor( + message = 'Permission denied', + errorCode = ERROR_CODES.PERMISSION_DENIED, + details?: Record + ) { + super(message, details); + this.errorCode = errorCode; + } +} + +// ============================================================================ +// 404 Not Found +// ============================================================================ + +export class NotFoundError extends AppError { + readonly statusCode = 404; + readonly errorCode = ERROR_CODES.NOT_FOUND; + + constructor(message = 'Resource not found', details?: Record) { + super(message, details); + } +} + +// ============================================================================ +// 409 Conflict +// ============================================================================ + +export class ConflictError extends AppError { + readonly statusCode = 409; + readonly errorCode = ERROR_CODES.ALREADY_EXISTS; + + constructor(message = 'Resource already exists', details?: Record) { + super(message, details); + } +} + +// ============================================================================ +// 422 Validation Error +// ============================================================================ + +export class ValidationError extends AppError { + readonly statusCode = 422; + readonly errorCode = ERROR_CODES.VALIDATION_ERROR; + + constructor(message = 'Validation failed', details?: Record) { + super(message, details); + } +} + +// ============================================================================ +// 429 Rate Limit +// ============================================================================ + +export class RateLimitError extends AppError { + readonly statusCode = 429; + readonly errorCode = ERROR_CODES.RATE_LIMIT_EXCEEDED; + + constructor(message = 'Too many requests', details?: Record) { + super(message, details); + } +} + +// ============================================================================ +// 500 Internal Server Error +// ============================================================================ + +export class InternalError extends AppError { + readonly statusCode = 500; + readonly errorCode = ERROR_CODES.INTERNAL_SERVER_ERROR; + + constructor(message = 'Internal server error', details?: Record) { + super(message, details); + } +} + +// ============================================================================ +// Type guard for AppError +// ============================================================================ + +export const isAppError = (error: unknown): error is AppError => { + return error instanceof AppError; +}; diff --git a/src/app/core/index.ts b/src/app/core/index.ts new file mode 100644 index 0000000..c55c7f7 --- /dev/null +++ b/src/app/core/index.ts @@ -0,0 +1,18 @@ +/** + * Core Module Export + */ + +export * from './constants'; +export * from './types'; +export * from './errors'; + +export { default as prismaClient } from './database/prisma.client'; +export { BaseRepository } from './database/base.repository'; +export { connectDatabase, disconnectDatabase } from './database/prisma.client'; + +export * from './utils'; +export * from './queue'; +export * from './cache'; + +export { initContainer, getPrisma } from './container'; +export type { Container } from './container'; diff --git a/src/app/core/queue/bullmq.connection.ts b/src/app/core/queue/bullmq.connection.ts new file mode 100644 index 0000000..8af11f0 --- /dev/null +++ b/src/app/core/queue/bullmq.connection.ts @@ -0,0 +1,19 @@ +import Redis from 'ioredis'; +import type { ConnectionOptions } from 'bullmq'; +import environment from '@config/environment.config'; + +/** + * Create a BullMQ-compatible Redis connection. + * Keep this separate from app Redis client to avoid blocking-mode conflicts. + */ +export const createBullMqConnection = (): ConnectionOptions => { + const redisUrl = environment.redis.url; + if (!redisUrl) { + throw new Error('REDIS_URL is not configured'); + } + + return new Redis(redisUrl, { + maxRetriesPerRequest: null, + enableReadyCheck: false + }) as unknown as ConnectionOptions; +}; diff --git a/src/app/core/queue/bullmq.defaults.ts b/src/app/core/queue/bullmq.defaults.ts new file mode 100644 index 0000000..8f4c5e5 --- /dev/null +++ b/src/app/core/queue/bullmq.defaults.ts @@ -0,0 +1,15 @@ +import type { DefaultJobOptions } from 'bullmq'; + +/** + * Shared default BullMQ job options. + * Modules can override per job when needed. + */ +export const DEFAULT_BULLMQ_JOB_OPTIONS: DefaultJobOptions = { + removeOnComplete: true, + removeOnFail: 100, + attempts: 3, + backoff: { + type: 'exponential', + delay: 1000 + } +}; diff --git a/src/app/core/queue/index.ts b/src/app/core/queue/index.ts new file mode 100644 index 0000000..62fb4fa --- /dev/null +++ b/src/app/core/queue/index.ts @@ -0,0 +1,2 @@ +export { createBullMqConnection } from './bullmq.connection'; +export { DEFAULT_BULLMQ_JOB_OPTIONS } from './bullmq.defaults'; diff --git a/src/app/core/types/api.types.ts b/src/app/core/types/api.types.ts new file mode 100644 index 0000000..b22ef5e --- /dev/null +++ b/src/app/core/types/api.types.ts @@ -0,0 +1,26 @@ +/** + * API Request/Response Types + */ + +export interface BaseDto { + [key: string]: unknown; +} + +export interface ApiResponse { + success: boolean; + message: string; + data: T; + statusCode: number; + timestamp: string; + requestId?: string; +} + +export interface ApiErrorResponse { + success: false; + message: string; + error_code: string; + error: Record; + statusCode: number; + timestamp: string; + requestId?: string; +} diff --git a/src/app/core/types/common.types.ts b/src/app/core/types/common.types.ts new file mode 100644 index 0000000..2a88b3a --- /dev/null +++ b/src/app/core/types/common.types.ts @@ -0,0 +1,10 @@ +/** + * Common Types used across the application + */ + +export interface RequestContext { + requestId: string; + userId?: bigint; + sessionId?: bigint; + startTime: number; +} diff --git a/src/app/core/types/express.types.ts b/src/app/core/types/express.types.ts new file mode 100644 index 0000000..467c63e --- /dev/null +++ b/src/app/core/types/express.types.ts @@ -0,0 +1,55 @@ +/** + * Express Type Augmentation + */ + +import { Request } from 'express'; +import { RequestContext } from './common.types'; +import type { Permissions } from '@core/constants'; + +// ============================================================================ +// Extended User Type with Relations +// ============================================================================ + +export interface UserWithRole { + id: bigint; + firstName: string; + lastName: string; + email: string; + phone: string; + countryCode: string; + status: string; + verified: boolean; + roleId: bigint; + role: { + id: bigint; + name: string; + slug: string; + forApp: boolean; + passwordRequired: boolean; + }; + [key: string]: unknown; +} + +// ============================================================================ +// Login Session Type +// ============================================================================ + +export interface LoginSessionType { + id: bigint; + userId: bigint; + token: string; + status: boolean; + expiresAt: Date; + [key: string]: unknown; +} + +// ============================================================================ +// Authenticated Request +// ============================================================================ + +export interface AuthenticatedRequest extends Request { + user: UserWithRole; + session: LoginSessionType; + permissions: Permissions; + context?: RequestContext; +} diff --git a/src/app/core/types/index.ts b/src/app/core/types/index.ts new file mode 100644 index 0000000..53a53ff --- /dev/null +++ b/src/app/core/types/index.ts @@ -0,0 +1,7 @@ +/** + * Central types export + */ + +export * from './api.types'; +export * from './common.types'; +export * from './express.types'; diff --git a/src/app/core/utils/index.ts b/src/app/core/utils/index.ts new file mode 100644 index 0000000..cd360a1 --- /dev/null +++ b/src/app/core/utils/index.ts @@ -0,0 +1,7 @@ +/** + * Core Utilities Export + */ + +export * from './path.utils'; +export * from './random.utils'; +export * from './sanitize.utils'; diff --git a/src/app/core/utils/path.utils.ts b/src/app/core/utils/path.utils.ts new file mode 100644 index 0000000..4ff4e08 --- /dev/null +++ b/src/app/core/utils/path.utils.ts @@ -0,0 +1,12 @@ +/** + * Path Utilities + */ + +let projectRoot: string | null = null; + +export const getProjectRoot = (): string => { + if (!projectRoot) { + projectRoot = process.cwd(); + } + return projectRoot; +}; diff --git a/src/app/core/utils/random.utils.ts b/src/app/core/utils/random.utils.ts new file mode 100644 index 0000000..4cd1cf9 --- /dev/null +++ b/src/app/core/utils/random.utils.ts @@ -0,0 +1,29 @@ +/** + * Random Generation Utilities + * Cryptographically secure random value generation + */ + +import crypto from 'node:crypto'; + +/** + * Generate a numeric OTP of specified length + * Note: In development, use a fixed OTP from constants instead + * + * @param length - Number of digits (default: 6) + * @returns Random numeric OTP + */ +export const generateOTP = (length: number = 6): number => { + const min = Math.pow(10, length - 1); + const max = Math.pow(10, length) - 1; + return crypto.randomInt(min, max + 1); +}; + +/** + * Generate a random hexadecimal string + * + * @param length - Number of bytes (output will be 2x this in hex chars) + * @returns Random hex string + */ +export const generateRandomString = (length: number = 16): string => { + return crypto.randomBytes(length).toString('hex'); +}; diff --git a/src/app/core/utils/sanitize.utils.ts b/src/app/core/utils/sanitize.utils.ts new file mode 100644 index 0000000..737927a --- /dev/null +++ b/src/app/core/utils/sanitize.utils.ts @@ -0,0 +1,140 @@ +/** + * Sanitization Utilities + * Input sanitization and data masking + */ + +// ============================================================================ +// Sensitive Fields for Logging +// ============================================================================ + +const SENSITIVE_FIELDS = [ + 'password', + 'passwd', + 'pwd', + 'secret', + 'token', + 'key', + 'api_key', + 'apikey', + 'access_token', + 'refresh_token', + 'auth_token', + 'session_id', + 'jwt', + 'authorization', + 'credit_card', + 'card_number', + 'cvv', + 'ssn', + 'sin', // Canadian Social Insurance Number + 'account_number', + 'transit_number', + 'routing_number' +]; + +const isSensitiveField = (fieldName: string): boolean => { + const lower = fieldName.toLowerCase(); + return SENSITIVE_FIELDS.some(pattern => lower.includes(pattern)); +}; + +// ============================================================================ +// Log Sanitization (Mask Sensitive Data) +// ============================================================================ + +type SanitizedValue = string | number | boolean | null | undefined | SanitizedObject | SanitizedValue[]; + +interface SanitizedObject { + [key: string]: SanitizedValue; +} + +/** + * Sanitize object for logging (masks sensitive fields) + */ +export const sanitizeForLog = (obj: unknown): SanitizedValue => { + if (obj === null || obj === undefined) { + return obj; + } + + if (typeof obj !== 'object') { + return obj as string | number | boolean; + } + + if (Array.isArray(obj)) { + return obj.map(sanitizeForLog); + } + + const sanitized: SanitizedObject = {}; + for (const [key, value] of Object.entries(obj)) { + sanitized[key] = isSensitiveField(key) ? '[REDACTED]' : sanitizeForLog(value); + } + + return sanitized; +}; + +// ============================================================================ +// Input Sanitization (XSS Prevention) +// ============================================================================ + +const HTML_ENTITIES: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + // Note: '/' is NOT escaped - it's not dangerous for XSS and breaks MIME types +}; + +/** + * Escape HTML special characters + * Only escapes characters that can be used in XSS attacks + */ +export const escapeHtml = (str: string): string => { + return str.replace(/[&<>"'`]/g, char => HTML_ENTITIES[char] ?? char); +}; + +/** + * Sanitize string input (escape HTML, trim whitespace) + */ +export const sanitizeString = (str: string): string => { + return escapeHtml(str.trim()); +}; + +/** + * Sanitize object recursively (for request body) + */ +export const sanitizeInput = (input: T): T => { + if (input === null || input === undefined) { + return input; + } + + if (typeof input === 'string') { + return sanitizeString(input) as T; + } + + if (typeof input !== 'object') { + return input; + } + + if (Array.isArray(input)) { + return input.map(sanitizeInput) as T; + } + + const sanitized: Record = {}; + for (const [key, value] of Object.entries(input)) { + sanitized[key] = sanitizeInput(value); + } + + return sanitized as T; +}; + +// ============================================================================ +// Phone Number Sanitization +// ============================================================================ + +/** + * Remove non-digit characters from phone number + */ +export const cleanPhoneNumber = (phone: string): string => { + return phone.replace(/\D/g, ''); +}; diff --git a/src/app/http/controllers/app/app.controller.ts b/src/app/http/controllers/app/app.controller.ts new file mode 100644 index 0000000..d448ce2 --- /dev/null +++ b/src/app/http/controllers/app/app.controller.ts @@ -0,0 +1,36 @@ +/** + * App Controller + * System endpoints (landing, health check) + */ + +import type { Request, Response } from 'express'; +import { getHealthSnapshot } from '@services/helpers/health/health.service'; +import environment from '@config/environment.config'; + +export class AppController { + public appLanding = (_req: Request, res: Response): Response => { + return res.status(200).json({ + success: true, + message: 'API Service', + version: environment.app.version, + environment: environment.basic.environment, + timestamp: new Date().toISOString() + }); + }; + + public appHealth = async (_req: Request, res: Response): Promise => { + const health = await getHealthSnapshot(); + + const overallStatus = health.overallStatus; + const status = overallStatus === 'healthy' ? 200 : overallStatus === 'degraded' ? 503 : 500; + + return res.status(status).json({ + success: overallStatus === 'healthy', + message: `System ${overallStatus}`, + data: health, + timestamp: new Date().toISOString() + }); + }; +} + +export default new AppController(); diff --git a/src/app/http/controllers/controller.ts b/src/app/http/controllers/controller.ts new file mode 100644 index 0000000..efe947b --- /dev/null +++ b/src/app/http/controllers/controller.ts @@ -0,0 +1,239 @@ +/** + * Base Controller + * Abstract controller with common response methods and validation + */ + +import { validateInput } from '@th3hero/request-validator'; +import type { Request, Response } from 'express'; +import { ERROR_CODES, ROLES, getPermissionScope, PERMISSION_SCOPES } from '@core/constants'; +import type { PermissionModule } from '@core/constants'; +import { AuthenticatedRequest } from '@core/types'; +import { getRequestId } from '@middleware/request-context.middleware'; + +export abstract class Controller { + /** + * Get authenticated user ID or send 401 response + * Returns null if user is not authenticated (response already sent) + */ + protected requireAuth(req: Request, res: Response): bigint | null { + const authReq = req as AuthenticatedRequest; + const userId = authReq.user?.id; + if (!userId) { + this.sendErrorResponse( + res, + { auth: 'User not authenticated' }, + ERROR_CODES.UNAUTHORIZED, + 'User not authenticated', + 401 + ); + return null; + } + return userId; + } + + /** + * Get optional authenticated user ID (no error if missing) + */ + protected getAuthUserId(req: Request): bigint | undefined { + return (req as AuthenticatedRequest).user?.id; + } + + /** + * Check if user has 'all' scope for a module (can access all resources) + * Super admin always has 'all' scope + */ + protected hasAllScope(req: Request, module: PermissionModule): boolean { + const authReq = req as AuthenticatedRequest; + + // Super admin bypasses scope checks + if (authReq.user?.role?.slug === ROLES.SUPER_ADMIN) { + return true; + } + + const scope = getPermissionScope(authReq.permissions, module); + return scope === PERMISSION_SCOPES.ALL; + } + + /** + * Get the owner filter for queries based on user's scope + * Returns undefined if user has 'all' scope (no filter needed) + * Returns userId if user has 'own' scope (must filter to own resources) + */ + protected getOwnerFilter(req: Request, module: PermissionModule): bigint | undefined { + if (this.hasAllScope(req, module)) { + return undefined; // No filter - user can see all + } + return this.getAuthUserId(req); // Filter to user's own resources + } + + /** + * Check if user can access a specific resource based on scope + * @param req - Request object + * @param module - Permission module + * @param resourceOwnerId - The owner ID of the resource being accessed + * @returns true if user can access, false otherwise + */ + protected canAccessResource(req: Request, module: PermissionModule, resourceOwnerId: bigint): boolean { + // User has 'all' scope - can access any resource + if (this.hasAllScope(req, module)) { + return true; + } + + // User has 'own' scope - can only access their own resources + const userId = this.getAuthUserId(req); + return userId !== undefined && userId === resourceOwnerId; + } + + /** + * Verify user can access a resource, send 403 if not + * Returns true if access granted, false if denied (response already sent) + */ + protected verifyResourceAccess( + req: Request, + res: Response, + module: PermissionModule, + resourceOwnerId: bigint + ): boolean { + if (this.canAccessResource(req, module, resourceOwnerId)) { + return true; + } + + this.sendErrorResponse( + res, + { access: 'You do not have permission to access this resource' }, + ERROR_CODES.PERMISSION_DENIED, + 'Access denied', + 403 + ); + return false; + } + + /** + * Parse and validate BigInt ID from route params + * Returns null if invalid (response already sent) + */ + protected parseBigIntParam(req: Request, res: Response, paramName: string, entityName: string): bigint | null { + const rawParam = req.params[paramName]; + const param = Array.isArray(rawParam) ? rawParam[0] : rawParam; + + if (!param) { + this.sendErrorResponse( + res, + { [paramName]: `${entityName} ID is required` }, + ERROR_CODES.VALIDATION_ERROR, + `${entityName} ID is required`, + 400 + ); + return null; + } + + try { + return BigInt(param); + } catch { + this.sendErrorResponse( + res, + { [paramName]: `Invalid ${entityName} ID` }, + ERROR_CODES.VALIDATION_ERROR, + `Invalid ${entityName} ID`, + 400 + ); + return null; + } + } + + /** + * Parse and validate string param from route params + * Returns null if missing (response already sent) + */ + protected parseStringParam(req: Request, res: Response, paramName: string, label: string): string | null { + const rawParam = req.params[paramName]; + const param = Array.isArray(rawParam) ? rawParam[0] : rawParam; + + if (!param) { + this.sendErrorResponse( + res, + { [paramName]: `${label} is required` }, + ERROR_CODES.VALIDATION_ERROR, + `${label} is required`, + 400 + ); + return null; + } + + return param; + } + + /** + * Send a success response + */ + public sendSuccessResponse = ( + res: Response, + data: object | null | string | number | [], + message: string = 'Successfully Executed', + statusCode: number = 200 + ): Response => { + return res.status(statusCode).json({ + success: true, + message, + data, + statusCode, + timestamp: new Date().toISOString(), + requestId: getRequestId() + }); + }; + + /** + * Send an error response + */ + public sendErrorResponse = ( + res: Response, + error: object | string | number | string[] | [], + errorCode: string, + message: string = 'Something Went Wrong', + statusCode: number = 500 + ): Response => { + return res.status(statusCode).json({ + success: false, + message, + error_code: errorCode, + error, + timestamp: new Date().toISOString(), + requestId: getRequestId() + }); + }; + + /** + * Validate request body against rules + */ + public async validate>( + req: Request, + res: Response, + rules: Record + ): Promise { + try { + if (!req.body) req.body = {}; + const validation = await validateInput(req, rules); + if (validation.failed) { + this.sendErrorResponse( + res, + validation.errors ?? {}, + ERROR_CODES.VALIDATION_ERROR, + 'Input validation failed', + 422 + ); + return null; + } + return req.body as T; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Validation error occurred'; + this.sendErrorResponse( + res, + { message: errorMessage }, + ERROR_CODES.VALIDATION_ERROR, + 'Input validation failed', + 422 + ); + return null; + } + } +} diff --git a/src/app/http/middleware/auth.middleware.ts b/src/app/http/middleware/auth.middleware.ts new file mode 100644 index 0000000..890ff79 --- /dev/null +++ b/src/app/http/middleware/auth.middleware.ts @@ -0,0 +1,337 @@ +/** + * Authentication Middleware + * Validates JWT tokens and attaches user to request + * Uses Redis cache to avoid DB reads on every request + */ + +import { Request, Response, NextFunction } from 'express'; +import { verifyToken } from '@services/index'; +import { ERROR_CODES, ROLES, PermissionCheck, PermissionAction } from '@core/constants'; +import { AuthenticatedRequest, UserWithRole } from '@core/types'; +import { setContextUserId, setContextSessionId, getRequestId } from './request-context.middleware'; +import { prismaClient } from '@core/database'; +import { getCachedSession, setCachedSession, CachedSession } from '@http/modules/auth/cache'; +import { queueSessionLastUsedUpdate } from '@http/modules/auth/queue'; + +// ============================================================================ +// Types +// ============================================================================ + +interface AuthMiddlewareOptions { + forRefresh?: boolean; + optional?: boolean; +} + +// ============================================================================ +// Response Helpers +// ============================================================================ + +const sendError = (res: Response, message: string, errorCode: string, statusCode: number): Response => { + return res.status(statusCode).json({ + success: false, + message, + error_code: errorCode, + error: { message }, + timestamp: new Date().toISOString(), + requestId: getRequestId() + }); +}; + +// ============================================================================ +// Cache Helpers +// ============================================================================ + +const cachedToUserWithRole = (cached: CachedSession['user']): UserWithRole => ({ + id: BigInt(cached.id), + firstName: cached.firstName, + lastName: cached.lastName, + email: cached.email, + phone: cached.phone, + countryCode: cached.countryCode, + status: cached.status, + verified: cached.verified, + roleId: BigInt(cached.roleId), + role: { + id: BigInt(cached.role.id), + name: cached.role.name, + slug: cached.role.slug, + forApp: cached.role.forApp, + passwordRequired: cached.role.passwordRequired + } +}); + +interface DbSession { + id: bigint; + userId: bigint; + token: string; + status: boolean; + verified: boolean; + expiresAt: Date; + user: { + id: bigint; + firstName: string; + lastName: string; + email: string; + phone: string; + countryCode: string; + status: string; + verified: boolean; + roleId: bigint; + role: { + id: bigint; + name: string; + slug: string; + forApp: boolean; + passwordRequired: boolean; + permission: { permissions: unknown } | null; + }; + }; +} + +const sessionToCacheable = (session: DbSession): CachedSession => { + const roleSlug = session.user.role.slug; + const permRecord = session.user.role.permission; + const permissions: CachedSession['permissions'] = + permRecord?.permissions && typeof permRecord.permissions === 'object' + ? (permRecord.permissions as CachedSession['permissions']) + : {}; + + return { + sessionId: session.id.toString(), + userId: session.userId.toString(), + token: session.token, + roleSlug, + status: session.status, + verified: session.verified, + expiresAt: session.expiresAt.toISOString(), + permissions, + user: { + id: session.user.id.toString(), + firstName: session.user.firstName, + lastName: session.user.lastName, + email: session.user.email, + phone: session.user.phone, + countryCode: session.user.countryCode, + status: session.user.status, + verified: session.user.verified, + roleId: session.user.roleId.toString(), + role: { + id: session.user.role.id.toString(), + name: session.user.role.name, + slug: session.user.role.slug, + forApp: session.user.role.forApp, + passwordRequired: session.user.role.passwordRequired + } + } + }; +}; + +// ============================================================================ +// Core Authentication Logic +// ============================================================================ + +const authenticateRequest = async ( + req: Request, + res: Response, + next: NextFunction, + options: AuthMiddlewareOptions = {} +): Promise => { + const { forRefresh = false, optional = false } = options; + const tokenType = forRefresh ? 'Refresh' : 'Access'; + + const authHeader = req.headers.authorization || (req.headers['Authorization'] as string); + + if (!authHeader?.startsWith('Bearer ')) { + if (optional) { + return next(); + } + return sendError(res, `${tokenType} token required`, ERROR_CODES.SESSION_NOT_VALID, 401); + } + + const token = authHeader.substring(7); + + const decoded = verifyToken(token); + + if (!decoded) { + if (optional) { + return next(); + } + return sendError(res, `${tokenType} token invalid or expired`, ERROR_CODES.SESSION_EXPIRED, 401); + } + + const expectedMode = forRefresh ? 1 : 0; + if (decoded.m !== expectedMode) { + return sendError(res, `Invalid token type. ${tokenType} token required.`, ERROR_CODES.TOKEN_INVALID, 401); + } + + const roleSlug = decoded.r; + if (!roleSlug) { + return sendError(res, 'Invalid token format. Please re-login.', ERROR_CODES.TOKEN_INVALID, 401); + } + + let cachedSession = await getCachedSession(roleSlug, decoded.t); + let sessionId: bigint; + let user: UserWithRole; + let permissions: CachedSession['permissions'] = {}; + + if (cachedSession) { + if (!cachedSession.status) { + return sendError(res, 'Session is inactive/logged out', ERROR_CODES.SESSION_INACTIVE, 401); + } + + if (!cachedSession.verified) { + return sendError(res, '2FA verification required', ERROR_CODES.SESSION_NOT_VERIFIED, 401); + } + + if (new Date(cachedSession.expiresAt) < new Date()) { + return sendError(res, 'Session has expired', ERROR_CODES.SESSION_EXPIRED, 401); + } + + const userStatus = cachedSession.user.status; + if (userStatus === 'banned' || userStatus === 'suspended') { + return sendError(res, 'Account is suspended or banned', ERROR_CODES.ACCOUNT_SUSPENDED, 403); + } + + sessionId = BigInt(cachedSession.sessionId); + user = cachedToUserWithRole(cachedSession.user); + permissions = cachedSession.permissions; + } else { + const session = await prismaClient.loginSession.findFirst({ + where: { token: decoded.t }, + include: { + user: { + include: { + role: { + include: { + permission: { select: { permissions: true } } + } + } + } + } + } + }); + + if (!session) { + if (optional) { + return next(); + } + return sendError(res, `${tokenType} token invalid or expired`, ERROR_CODES.SESSION_NOT_VALID, 401); + } + + if (!session.status) { + return sendError(res, 'Session is inactive/logged out', ERROR_CODES.SESSION_INACTIVE, 401); + } + + if (!session.verified) { + return sendError(res, '2FA verification required', ERROR_CODES.SESSION_NOT_VERIFIED, 401); + } + + if (session.expiresAt < new Date()) { + return sendError(res, 'Session has expired', ERROR_CODES.SESSION_EXPIRED, 401); + } + + if (!session.user) { + return sendError(res, 'User not found', ERROR_CODES.NOT_FOUND, 401); + } + + const userStatus = session.user.status; + if (userStatus === 'banned' || userStatus === 'suspended') { + return sendError(res, 'Account is suspended or banned', ERROR_CODES.ACCOUNT_SUSPENDED, 403); + } + + const cacheable = sessionToCacheable(session as DbSession); + setCachedSession(roleSlug, decoded.t, cacheable).catch(() => {}); + + sessionId = session.id; + user = session.user as UserWithRole; + permissions = cacheable.permissions; + } + + queueSessionLastUsedUpdate(sessionId).catch(() => {}); + + (req as AuthenticatedRequest).user = user; + (req as AuthenticatedRequest).session = { + id: sessionId, + userId: user.id, + token: decoded.t, + status: true, + expiresAt: cachedSession ? new Date(cachedSession.expiresAt) : new Date() + }; + (req as AuthenticatedRequest).permissions = permissions; + + setContextUserId(user.id); + setContextSessionId(sessionId); + + next(); +}; + +// ============================================================================ +// Middleware Factory +// ============================================================================ + +export const createAuthMiddleware = (options: AuthMiddlewareOptions = {}) => { + return (req: Request, res: Response, next: NextFunction): Promise => { + return authenticateRequest(req, res, next, options); + }; +}; + +// ============================================================================ +// Pre-configured Middleware +// ============================================================================ + +export const authCheck = createAuthMiddleware({ forRefresh: false }); + +export const authRefresh = createAuthMiddleware({ forRefresh: true }); + +// ============================================================================ +// Permission-based Authorization +// ============================================================================ + +const hasPermission = ( + permissions: CachedSession['permissions'], + module: string, + actions: PermissionAction | PermissionAction[] +): boolean => { + const modulePerms = permissions[module as keyof typeof permissions]; + if (!modulePerms) return false; + + const actionsArray = Array.isArray(actions) ? actions : [actions]; + return actionsArray.some(action => modulePerms[action as keyof typeof modulePerms] === true); +}; + +export const requirePermission = (checks: PermissionCheck | PermissionCheck[]) => { + return (req: Request, res: Response, next: NextFunction): Response | void => { + const authReq = req as AuthenticatedRequest; + const user = authReq.user; + const permissions = authReq.permissions; + + if (!user) { + return sendError(res, 'Authentication required', ERROR_CODES.SESSION_NOT_VALID, 401); + } + + if (user.role?.slug === ROLES.SUPER_ADMIN) { + return next(); + } + + if (!permissions) { + return sendError(res, 'Permissions not loaded', ERROR_CODES.PERMISSION_DENIED, 403); + } + + const checksArray = Array.isArray(checks) ? checks : [checks]; + + for (const check of checksArray) { + if (!hasPermission(permissions, check.module, check.action)) { + return sendError( + res, + `Permission denied: ${check.module}:${ + Array.isArray(check.action) ? check.action.join('|') : check.action + }`, + ERROR_CODES.PERMISSION_DENIED, + 403 + ); + } + } + + next(); + }; +}; diff --git a/src/app/http/middleware/error.middleware.ts b/src/app/http/middleware/error.middleware.ts new file mode 100644 index 0000000..59321a6 --- /dev/null +++ b/src/app/http/middleware/error.middleware.ts @@ -0,0 +1,164 @@ +/** + * Error Handling Middleware + * Global error handler for the application + */ + +import { Request, Response, NextFunction } from 'express'; +import { isAppError } from '@core/errors'; +import { ERROR_CODES } from '@core/constants'; +import { createLogger } from '@services/index'; +import { getRequestId } from './request-context.middleware'; +import environment from '@config/environment.config'; + +const log = createLogger('error'); + +// ============================================================================ +// Environment Check +// ============================================================================ + +const { isDev } = environment.basic; + +// ============================================================================ +// Error Response Format +// ============================================================================ + +interface ErrorResponse { + success: false; + message: string; + error_code: string; + error: Record; + timestamp: string; + requestId: string | undefined; + stack?: string; +} + +// ============================================================================ +// Global Error Handler +// ============================================================================ + +/** + * Global error handling middleware + * Catches all errors and returns consistent error response + */ +export const errorHandler = (error: Error, req: Request, res: Response, _next: NextFunction): Response => { + const requestId = getRequestId(); + + // Log the error + log.exception('Unhandled error', error, { + path: req.originalUrl, + method: req.method, + requestId + }); + + // Handle AppError (our custom errors) + if (isAppError(error)) { + const response: ErrorResponse = { + success: false, + message: error.message, + error_code: error.errorCode, + error: error.details ?? { message: error.message }, + timestamp: error.timestamp, + requestId + }; + + if (isDev && error.stack) { + response.stack = error.stack; + } + + return res.status(error.statusCode).json(response); + } + + // Handle Prisma errors + if (error.name === 'PrismaClientKnownRequestError') { + const prismaError = error as Error & { code?: string; meta?: { target?: string[] } }; + + // Unique constraint violation + if (prismaError.code === 'P2002') { + const target = prismaError.meta?.target?.join(', ') ?? 'field'; + return res.status(409).json({ + success: false, + message: `A record with this ${target} already exists`, + error_code: 'DUPLICATE', + error: { field: target }, + timestamp: new Date().toISOString(), + requestId + }); + } + + // Record not found + if (prismaError.code === 'P2025') { + return res.status(404).json({ + success: false, + message: 'Record not found', + error_code: ERROR_CODES.NOT_FOUND, + error: { message: 'The requested resource was not found' }, + timestamp: new Date().toISOString(), + requestId + }); + } + } + + // Handle validation errors from express-validator or similar + if (error.name === 'ValidationError') { + return res.status(422).json({ + success: false, + message: 'Validation failed', + error_code: ERROR_CODES.VALIDATION_ERROR, + error: { message: error.message }, + timestamp: new Date().toISOString(), + requestId + }); + } + + // Handle JWT errors + if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') { + return res.status(401).json({ + success: false, + message: 'Invalid or expired token', + error_code: ERROR_CODES.TOKEN_INVALID, + error: { message: error.message }, + timestamp: new Date().toISOString(), + requestId + }); + } + + // Default: Internal Server Error + const response: ErrorResponse = { + success: false, + message: isDev ? error.message : 'Internal Server Error', + error_code: ERROR_CODES.INTERNAL_SERVER_ERROR, + error: { + message: isDev ? error.message : 'An unexpected error occurred', + ...(isDev && { name: error.name }) + }, + timestamp: new Date().toISOString(), + requestId + }; + + if (isDev && error.stack) { + response.stack = error.stack; + } + + return res.status(500).json(response); +}; + +// ============================================================================ +// Not Found Handler +// ============================================================================ + +/** + * Handler for 404 - Route not found + */ +export const notFoundHandler = (req: Request, res: Response): Response => { + return res.status(404).json({ + success: false, + message: 'Route not found', + error_code: ERROR_CODES.NOT_FOUND, + error: { + path: req.originalUrl, + method: req.method + }, + timestamp: new Date().toISOString(), + requestId: getRequestId() + }); +}; diff --git a/src/app/http/middleware/index.ts b/src/app/http/middleware/index.ts new file mode 100644 index 0000000..c20e02d --- /dev/null +++ b/src/app/http/middleware/index.ts @@ -0,0 +1,10 @@ +/** + * HTTP Middleware Export + */ + +export * from './auth.middleware'; +export * from './error.middleware'; +export * from './rate-limit.middleware'; +export * from './logging.middleware'; +export * from './request-context.middleware'; +export * from './sanitize.middleware'; diff --git a/src/app/http/middleware/logging.middleware.ts b/src/app/http/middleware/logging.middleware.ts new file mode 100644 index 0000000..ee783c5 --- /dev/null +++ b/src/app/http/middleware/logging.middleware.ts @@ -0,0 +1,80 @@ +/** + * Logging Middleware + * HTTP request/response logging and error logging + */ + +import { Request, Response, NextFunction } from 'express'; +import { createLogger } from '@services/index'; +import { getRequestId, getRequestElapsedMs } from './request-context.middleware'; +import { sanitizeForLog } from '@core/utils'; + +const httpLog = createLogger('http'); +const errorLog = createLogger('error'); + +// ============================================================================ +// HTTP Request Logging +// ============================================================================ + +/** + * Log incoming HTTP requests and responses + */ +export const httpLoggingMiddleware = (req: Request, res: Response, next: NextFunction): void => { + // Log request start + const startLog = { + method: req.method, + path: req.originalUrl, + ip: req.ip, + userAgent: req.get('user-agent') + }; + + httpLog.info(`→ ${req.method} ${req.originalUrl}`, startLog); + + // Capture response finish + res.on('finish', () => { + const duration = getRequestElapsedMs() ?? 0; + const requestId = getRequestId(); + + const finishLog = { + method: req.method, + path: req.originalUrl, + statusCode: res.statusCode, + duration: `${duration}ms`, + requestId + }; + + // Log level based on status code + if (res.statusCode >= 500) { + httpLog.error(`← ${req.method} ${req.originalUrl} ${res.statusCode} (${duration}ms)`, finishLog); + } else if (res.statusCode >= 400) { + httpLog.warn(`← ${req.method} ${req.originalUrl} ${res.statusCode} (${duration}ms)`, finishLog); + } else { + httpLog.info(`← ${req.method} ${req.originalUrl} ${res.statusCode} (${duration}ms)`, finishLog); + } + }); + + next(); +}; + +// ============================================================================ +// Error Logging (for error middleware chain) +// ============================================================================ + +/** + * Log errors before they reach the error handler + * This runs before errorHandler to capture all errors + */ +export const errorLoggingMiddleware = (error: Error, req: Request, _res: Response, next: NextFunction): void => { + const requestId = getRequestId(); + const duration = getRequestElapsedMs() ?? 0; + + errorLog.exception(`Error in ${req.method} ${req.originalUrl} (${duration}ms)`, error, { + method: req.method, + path: req.originalUrl, + requestId, + body: sanitizeForLog(req.body), + query: sanitizeForLog(req.query), + params: sanitizeForLog(req.params) + }); + + next(error); +}; diff --git a/src/app/http/middleware/rate-limit.middleware.ts b/src/app/http/middleware/rate-limit.middleware.ts new file mode 100644 index 0000000..3d1602b --- /dev/null +++ b/src/app/http/middleware/rate-limit.middleware.ts @@ -0,0 +1,128 @@ +/** + * Rate Limiting Middleware + * Protects API endpoints from abuse + */ + +import rateLimit, { RateLimitRequestHandler } from 'express-rate-limit'; +import { RedisStore, type SendCommandFn } from 'rate-limit-redis'; +import { Request, Response } from 'express'; +import { ERROR_CODES } from '@core/constants'; +import { getRedisClient } from '@core/cache'; +import environment from '@config/environment.config'; +import { createLogger } from '@services/logger.service'; +import { getRequestId } from './request-context.middleware'; + +// ============================================================================ +// Types +// ============================================================================ + +interface RateLimitConfig { + key: string; + windowMs: number; + max: number; + message?: string; + skipSuccessfulRequests?: boolean; + skipFailedRequests?: boolean; +} + +const log = createLogger('rate-limit'); +let redisRateLimitEnabledLogged = false; + +const getRateLimitStore = (key: string): RedisStore | undefined => { + if (!environment.redis.url) { + return undefined; + } + + const sendCommand: SendCommandFn = async (...args: string[]) => { + const [command, ...params] = args; + if (!command) { + throw new Error('Redis command is required for rate limit store'); + } + const redis = await getRedisClient(); + return redis.call(command, ...params) as never; + }; + + const store = new RedisStore({ + prefix: `app:rate-limit:${key}:`, + sendCommand + }); + + if (!redisRateLimitEnabledLogged) { + log.info('Using Redis-backed rate limit store'); + redisRateLimitEnabledLogged = true; + } + + return store; +}; + +// ============================================================================ +// Rate Limit Factory +// ============================================================================ + +export const createRateLimit = (config: RateLimitConfig): RateLimitRequestHandler => { + const { + key, + windowMs, + max, + message = 'Too many requests, please try again later', + skipSuccessfulRequests = false, + skipFailedRequests = false + } = config; + + const store = getRateLimitStore(key); + + return rateLimit({ + windowMs, + max, + ...(store && { store, passOnStoreError: true }), + standardHeaders: true, + legacyHeaders: false, + skipSuccessfulRequests, + skipFailedRequests, + handler: (_req: Request, res: Response): void => { + res.status(429).json({ + success: false, + message, + error_code: ERROR_CODES.RATE_LIMIT_EXCEEDED, + error: { message }, + timestamp: new Date().toISOString(), + requestId: getRequestId() + }); + } + }); +}; + +// ============================================================================ +// Pre-configured Rate Limiters (from environment config) +// ============================================================================ + +const { rateLimit: rl } = environment; + +export const generalRateLimiter = createRateLimit({ + key: 'general', + windowMs: rl.windowMs, + max: rl.max, + message: 'API rate limit exceeded. Please try again later.' +}); + +export const loginRateLimiter = createRateLimit({ + key: 'login', + windowMs: rl.login.windowMs, + max: rl.login.max, + message: 'Too many login attempts. Please try again in 15 minutes.', + skipSuccessfulRequests: true +}); + +export const otpRateLimiter = createRateLimit({ + key: 'otp', + windowMs: rl.otp.windowMs, + max: rl.otp.max, + message: 'Too many OTP requests. Please try again in 1 hour.' +}); + +export const passwordResetRateLimiter = createRateLimit({ + key: 'password-reset', + windowMs: 300_000, + max: 3, + message: 'Too many password reset requests. Please try again in 5 minutes.' +}); diff --git a/src/app/http/middleware/request-context.middleware.ts b/src/app/http/middleware/request-context.middleware.ts new file mode 100644 index 0000000..d7bdd38 --- /dev/null +++ b/src/app/http/middleware/request-context.middleware.ts @@ -0,0 +1,92 @@ +/** + * Request Context Middleware + * Provides request correlation ID and timing for logging/tracing + */ + +import { Request, Response, NextFunction } from 'express'; +import { AsyncLocalStorage } from 'async_hooks'; +import { randomUUID } from 'crypto'; +import { RequestContext } from '@core/types'; +import { setRequestContextGetter } from '@services/logger.service'; + +// ============================================================================ +// Async Local Storage for Request Context +// ============================================================================ + +export const requestContextStorage = new AsyncLocalStorage(); + +// Register with core logger so it can access request context +setRequestContextGetter(() => requestContextStorage.getStore()); + +// ============================================================================ +// Middleware +// ============================================================================ + +/** + * Initialize request context with correlation ID and timing + */ +export const requestContextMiddleware = (req: Request, res: Response, next: NextFunction): void => { + // Use existing request ID from header or generate new one + const requestId = (req.headers['x-request-id'] as string) || randomUUID(); + + // Set correlation ID in response header + res.setHeader('x-request-id', requestId); + + // Create context + const context: RequestContext = { + requestId, + startTime: Date.now() + }; + + // Run the rest of the request in this context + requestContextStorage.run(context, () => { + next(); + }); +}; + +// ============================================================================ +// Context Accessors +// ============================================================================ + +/** + * Get current request context (null if not in request) + */ +export const getRequestContext = (): RequestContext | undefined => { + return requestContextStorage.getStore(); +}; + +/** + * Get current request ID (undefined if not in request) + */ +export const getRequestId = (): string | undefined => { + return requestContextStorage.getStore()?.requestId; +}; + +/** + * Get request elapsed time in milliseconds + */ +export const getRequestElapsedMs = (): number | undefined => { + const ctx = requestContextStorage.getStore(); + if (!ctx) return undefined; + return Date.now() - ctx.startTime; +}; + +/** + * Set user ID in current context (after authentication) + */ +export const setContextUserId = (userId: bigint): void => { + const ctx = requestContextStorage.getStore(); + if (ctx) { + ctx.userId = userId; + } +}; + +/** + * Set session ID in current context (after authentication) + */ +export const setContextSessionId = (sessionId: bigint): void => { + const ctx = requestContextStorage.getStore(); + if (ctx) { + ctx.sessionId = sessionId; + } +}; diff --git a/src/app/http/middleware/sanitize.middleware.ts b/src/app/http/middleware/sanitize.middleware.ts new file mode 100644 index 0000000..55bfe0f --- /dev/null +++ b/src/app/http/middleware/sanitize.middleware.ts @@ -0,0 +1,45 @@ +/** + * Input Sanitization Middleware + * Prevents XSS attacks by sanitizing request input + */ + +import { Request, Response, NextFunction } from 'express'; +import { sanitizeInput } from '@core/utils'; + +/** + * Sanitize request body and params + * Escapes HTML special characters to prevent XSS + * Note: req.query is read-only in Express 5, so we sanitize values in-place + */ +export const sanitizeMiddleware = (req: Request, _res: Response, next: NextFunction): void => { + // Sanitize request body (mutable) + if (req.body && typeof req.body === 'object') { + req.body = sanitizeInput(req.body); + } + + // Sanitize query parameters in-place (req.query is read-only in Express 5) + if (req.query && typeof req.query === 'object') { + for (const key of Object.keys(req.query)) { + const value = req.query[key]; + if (typeof value === 'string') { + (req.query as Record)[key] = sanitizeInput(value); + } else if (Array.isArray(value)) { + (req.query as Record)[key] = value.map(v => + typeof v === 'string' ? sanitizeInput(v) : v + ); + } + } + } + + // Sanitize URL parameters in-place + if (req.params && typeof req.params === 'object') { + for (const key of Object.keys(req.params)) { + const value = req.params[key]; + if (typeof value === 'string') { + req.params[key] = sanitizeInput(value) as string; + } + } + } + + next(); +}; diff --git a/src/app/http/modules/admin/config/cache.ts b/src/app/http/modules/admin/config/cache.ts new file mode 100644 index 0000000..e5f7ca9 --- /dev/null +++ b/src/app/http/modules/admin/config/cache.ts @@ -0,0 +1,101 @@ +/** + * Config Cache + * Redis-based caching for system configurations + */ + +import { getRedisClient } from '@core/cache'; +import { createLogger } from '@services/logger.service'; + +const logger = createLogger('config:cache'); + +const CACHE_PREFIX = 'app:config:'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface CachedConfig { + key: string; + value: string; + type: string; + description: string | null; +} + +// ============================================================================ +// Cache Operations +// ============================================================================ + +/** + * Get a single config value from cache + */ +export const getCachedConfig = async (key: string): Promise => { + try { + const redis = await getRedisClient(); + const cached = await redis.get(`${CACHE_PREFIX}${key}`); + + if (!cached) { + return null; + } + + return JSON.parse(cached) as CachedConfig; + } catch (error) { + logger.warn('Cache get failed', { + key, + error: error instanceof Error ? error.message : String(error) + }); + return null; + } +}; + +/** + * Set a single config in cache + */ +export const setCachedConfig = async (config: CachedConfig): Promise => { + try { + const redis = await getRedisClient(); + await redis.set(`${CACHE_PREFIX}${config.key}`, JSON.stringify(config)); + logger.debug('Config cached', { key: config.key }); + } catch (error) { + logger.warn('Cache set failed', { + key: config.key, + error: error instanceof Error ? error.message : String(error) + }); + } +}; + +/** + * Set all configs in cache (batch operation) + */ +export const setAllCachedConfigs = async (configs: CachedConfig[]): Promise => { + try { + const redis = await getRedisClient(); + const pipeline = redis.pipeline(); + + for (const config of configs) { + pipeline.set(`${CACHE_PREFIX}${config.key}`, JSON.stringify(config)); + } + + await pipeline.exec(); + logger.debug('All configs cached', { count: configs.length }); + } catch (error) { + logger.warn('Cache set all failed', { + error: error instanceof Error ? error.message : String(error) + }); + } +}; + +/** + * Invalidate a single config from cache + */ +export const invalidateCachedConfig = async (key: string): Promise => { + try { + const redis = await getRedisClient(); + await redis.del(`${CACHE_PREFIX}${key}`); + logger.debug('Config cache invalidated', { key }); + } catch (error) { + logger.warn('Cache invalidate failed', { + key, + error: error instanceof Error ? error.message : String(error) + }); + } +}; diff --git a/src/app/http/modules/admin/config/controller.ts b/src/app/http/modules/admin/config/controller.ts new file mode 100644 index 0000000..a25ae88 --- /dev/null +++ b/src/app/http/modules/admin/config/controller.ts @@ -0,0 +1,148 @@ +/** + * Config Controller + * Handles system configuration endpoints + */ + +import type { Request, Response } from 'express'; + +import { ERROR_CODES } from '@core/constants'; +import { createLogger } from '@services/index'; +import { Controller } from '@http/controllers/controller'; + +import service from './service'; +import { CreateConfigDto, UpdateConfigDto } from './types'; +import { createConfigRules, updateConfigRules } from './validation'; + +const log = createLogger('admin:config'); + +// ============================================================================ +// Controller +// ============================================================================ + +export class ConfigController extends Controller { + constructor() { + super(); + } + + /** + * GET /admin/config - List all configurations + */ + public list = async (_req: Request, res: Response): Promise => { + const configs = await service.getAll(); + + return this.sendSuccessResponse(res, { configs, total: configs.length }, 'Configurations fetched', 200); + }; + + /** + * GET /admin/config/:key - Get single configuration + */ + public get = async (req: Request, res: Response): Promise => { + const key = this.parseStringParam(req, res, 'key', 'Key'); + if (!key) return; + + const config = await service.getByKey(key); + + if (!config) { + return this.sendErrorResponse( + res, + { key: 'Configuration not found' }, + ERROR_CODES.NOT_FOUND, + 'Configuration not found', + 404 + ); + } + + return this.sendSuccessResponse(res, { config }, 'Configuration fetched', 200); + }; + + /** + * POST /admin/config - Create new configuration + * Requires super_admin role + */ + public create = async (req: Request, res: Response): Promise => { + const data = await this.validate(req, res, createConfigRules); + if (!data) return; + + // Check if key already exists + const exists = await service.exists(data.key); + if (exists) { + return this.sendErrorResponse( + res, + { key: 'Configuration key already exists' }, + ERROR_CODES.ALREADY_EXISTS, + 'Configuration key already exists', + 409 + ); + } + + // Validate value matches type + const typeValidation = service.validateValueType(data.value, data.type); + if (!typeValidation.valid) { + return this.sendErrorResponse( + res, + { value: typeValidation.error }, + ERROR_CODES.VALIDATION_ERROR, + typeValidation.error ?? 'Invalid value for type', + 422 + ); + } + + const config = await service.create(data); + const adminId = this.getAuthUserId(req); + log.info('Config created by admin', { + key: data.key, + adminId: adminId?.toString() + }); + + return this.sendSuccessResponse(res, { config }, 'Configuration created', 201); + }; + + /** + * PUT /admin/config/:key - Update configuration + * Requires super_admin role + */ + public update = async (req: Request, res: Response): Promise => { + const key = this.parseStringParam(req, res, 'key', 'Key'); + if (!key) return; + + const data = await this.validate(req, res, updateConfigRules); + if (!data) return; + + // Check if config exists + const existing = await service.getByKey(key); + if (!existing) { + return this.sendErrorResponse( + res, + { key: 'Configuration not found' }, + ERROR_CODES.NOT_FOUND, + 'Configuration not found', + 404 + ); + } + + // Validate value matches existing type + const typeValidation = service.validateValueType(data.value, existing.type); + if (!typeValidation.valid) { + return this.sendErrorResponse( + res, + { value: typeValidation.error }, + ERROR_CODES.VALIDATION_ERROR, + typeValidation.error ?? 'Invalid value for type', + 422 + ); + } + + const config = await service.update(key, data); + const adminId = this.getAuthUserId(req); + log.info('Config updated by admin', { + key, + adminId: adminId?.toString(), + oldValue: existing.value, + newValue: data.value + }); + + return this.sendSuccessResponse(res, { config }, 'Configuration updated', 200); + }; +} + +export default new ConfigController(); diff --git a/src/app/http/modules/admin/config/index.ts b/src/app/http/modules/admin/config/index.ts new file mode 100644 index 0000000..9dae558 --- /dev/null +++ b/src/app/http/modules/admin/config/index.ts @@ -0,0 +1,6 @@ +/** + * Config Module Export + */ + +export { default as routes } from './routes'; +export { default as service } from './service'; diff --git a/src/app/http/modules/admin/config/repository.ts b/src/app/http/modules/admin/config/repository.ts new file mode 100644 index 0000000..a4487a3 --- /dev/null +++ b/src/app/http/modules/admin/config/repository.ts @@ -0,0 +1,84 @@ +/** + * Config Repository + * Database operations for system configurations + */ + +import { ConfigValueType } from '@database/prisma'; +import { getPrisma } from '@core/container'; +import { ConfigEntity } from './types'; + +// ============================================================================ +// Repository +// ============================================================================ + +export class ConfigRepository { + /** + * Find all configs + */ + async findAll(): Promise { + const prisma = getPrisma(); + return prisma.systemConfig.findMany({ + orderBy: { key: 'asc' } + }); + } + + /** + * Find config by key + */ + async findByKey(key: string): Promise { + const prisma = getPrisma(); + return prisma.systemConfig.findUnique({ + where: { key } + }); + } + + /** + * Create new config + */ + async create(data: { + key: string; + value: string; + type: ConfigValueType; + description?: string; + }): Promise { + const prisma = getPrisma(); + return prisma.systemConfig.create({ + data: { + key: data.key, + value: data.value, + type: data.type, + description: data.description ?? null + } + }); + } + + /** + * Update config by key + */ + async updateByKey( + key: string, + data: { value: string; description?: string } + ): Promise { + const prisma = getPrisma(); + return prisma.systemConfig.update({ + where: { key }, + data: { + value: data.value, + ...(data.description !== undefined && { description: data.description }) + } + }); + } + + /** + * Check if key exists + */ + async exists(key: string): Promise { + const prisma = getPrisma(); + const count = await prisma.systemConfig.count({ + where: { key } + }); + return count > 0; + } +} + +export default new ConfigRepository(); diff --git a/src/app/http/modules/admin/config/routes.ts b/src/app/http/modules/admin/config/routes.ts new file mode 100644 index 0000000..e037143 --- /dev/null +++ b/src/app/http/modules/admin/config/routes.ts @@ -0,0 +1,30 @@ +/** + * Config Routes + */ + +import { Router } from 'express'; +import { authCheck, requirePermission } from '@middleware/index'; +import { PERMISSION_MODULES } from '@core/constants'; + +import controller from './controller'; + +const router = Router(); + +// All routes require authentication +router.use(authCheck); + +const { CONFIG } = PERMISSION_MODULES; + +// GET /admin/config - List all (requires config:list) +router.get('/', requirePermission({ module: CONFIG, action: 'list' }), controller.list); + +// GET /admin/config/:key - Get single (requires config:read) +router.get('/:key', requirePermission({ module: CONFIG, action: 'read' }), controller.get); + +// POST /admin/config - Create (requires config:create) +router.post('/', requirePermission({ module: CONFIG, action: 'create' }), controller.create); + +// PUT /admin/config/:key - Update (requires config:update) +router.put('/:key', requirePermission({ module: CONFIG, action: 'update' }), controller.update); + +export default router; diff --git a/src/app/http/modules/admin/config/service.ts b/src/app/http/modules/admin/config/service.ts new file mode 100644 index 0000000..d30252c --- /dev/null +++ b/src/app/http/modules/admin/config/service.ts @@ -0,0 +1,197 @@ +/** + * Config Service + * Business logic for system configurations with caching + */ + +import { ConfigValueType } from '@database/prisma'; +import { createLogger } from '@services/logger.service'; + +import repository from './repository'; +import { getCachedConfig, setCachedConfig, setAllCachedConfigs, invalidateCachedConfig, CachedConfig } from './cache'; +import { ConfigEntity, ConfigResponse, CreateConfigDto, UpdateConfigDto } from './types'; + +// ============================================================================ +// Constants +// ============================================================================ + +const CACHE_HIT_LOG = 'Configs served from cache'; + +const log = createLogger('config'); + +// ============================================================================ +// Helpers +// ============================================================================ + +const parseValue = (value: string, type: ConfigValueType): string | number | boolean | object => { + switch (type) { + case 'number': + return parseFloat(value); + case 'boolean': + return value === 'true' || value === '1'; + case 'json': + try { + return JSON.parse(value) as object; + } catch { + return value; + } + default: + return value; + } +}; + +const entityToResponse = (entity: ConfigEntity): ConfigResponse => ({ + id: entity.id.toString(), + key: entity.key, + value: entity.value, + parsed_value: parseValue(entity.value, entity.type), + type: entity.type, + description: entity.description, + created_at: entity.createdAt.toISOString(), + updated_at: entity.updatedAt.toISOString() +}); + +const entityToCached = (entity: ConfigEntity): CachedConfig => ({ + key: entity.key, + value: entity.value, + type: entity.type, + description: entity.description +}); + +// ============================================================================ +// Service +// ============================================================================ + +export class ConfigService { + /** + * Get all configs + * Note: Always fetches from DB for full response (ID, timestamps) + * Cache is updated on each fetch for individual key lookups + */ + async getAll(): Promise { + const configs = await repository.findAll(); + + // Update cache for individual key lookups + await setAllCachedConfigs(configs.map(entityToCached)); + + return configs.map(entityToResponse); + } + + /** + * Get config by key (with caching for value lookups) + */ + async getByKey(key: string): Promise { + const config = await repository.findByKey(key); + + if (!config) { + return null; + } + + // Update cache for this key + await setCachedConfig(entityToCached(config)); + + return entityToResponse(config); + } + + /** + * Get config value by key (for internal use) + * Returns parsed value, uses cache-first strategy + */ + async getValue(key: string, defaultValue: T): Promise { + // Try cache first + const cached = await getCachedConfig(key); + + if (cached) { + log.debug(CACHE_HIT_LOG, { key }); + return parseValue(cached.value, cached.type as ConfigValueType) as T; + } + + // Fetch from database + const config = await repository.findByKey(key); + + if (!config) { + return defaultValue; + } + + // Update cache + await setCachedConfig(entityToCached(config)); + + return parseValue(config.value, config.type) as T; + } + + /** + * Create new config + */ + async create(data: CreateConfigDto): Promise { + const config = await repository.create({ + key: data.key, + value: data.value, + type: data.type, + ...(data.description !== undefined && { description: data.description }) + }); + + // Update cache + await setCachedConfig(entityToCached(config)); + + log.info('Config created', { key: data.key }); + + return entityToResponse(config); + } + + /** + * Update config by key + */ + async update(key: string, data: UpdateConfigDto): Promise { + const config = await repository.updateByKey(key, { + value: data.value, + ...(data.description !== undefined && { description: data.description }) + }); + + // Invalidate and update cache + await invalidateCachedConfig(key); + await setCachedConfig(entityToCached(config)); + + log.info('Config updated', { key }); + + return entityToResponse(config); + } + + /** + * Check if key exists + */ + async exists(key: string): Promise { + return repository.exists(key); + } + + /** + * Validate value matches type + */ + validateValueType(value: string, type: ConfigValueType): { valid: boolean; error?: string } { + switch (type) { + case 'number': { + const num = parseFloat(value); + if (isNaN(num)) { + return { valid: false, error: 'Value must be a valid number' }; + } + return { valid: true }; + } + case 'boolean': { + if (!['true', 'false', '1', '0'].includes(value.toLowerCase())) { + return { valid: false, error: 'Value must be true, false, 1, or 0' }; + } + return { valid: true }; + } + case 'json': { + try { + JSON.parse(value); + return { valid: true }; + } catch { + return { valid: false, error: 'Value must be valid JSON' }; + } + } + default: + return { valid: true }; + } + } +} + +export default new ConfigService(); diff --git a/src/app/http/modules/admin/config/types.ts b/src/app/http/modules/admin/config/types.ts new file mode 100644 index 0000000..17d68f4 --- /dev/null +++ b/src/app/http/modules/admin/config/types.ts @@ -0,0 +1,50 @@ +/** + * Config Module Types + */ + +import { ConfigValueType } from '@database/prisma'; + +// ============================================================================ +// DTOs +// ============================================================================ + +export interface CreateConfigDto extends Record { + key: string; + value: string; + type: ConfigValueType; + description?: string; +} + +export interface UpdateConfigDto extends Record { + value: string; + description?: string; +} + +// ============================================================================ +// Response Types +// ============================================================================ + +export interface ConfigResponse { + id: string; + key: string; + value: string; + parsed_value: string | number | boolean | object; + type: ConfigValueType; + description: string | null; + created_at: string; + updated_at: string; +} + +// ============================================================================ +// Internal Types +// ============================================================================ + +export interface ConfigEntity { + id: bigint; + key: string; + value: string; + type: ConfigValueType; + description: string | null; + createdAt: Date; + updatedAt: Date; +} diff --git a/src/app/http/modules/admin/config/validation.ts b/src/app/http/modules/admin/config/validation.ts new file mode 100644 index 0000000..5da45a8 --- /dev/null +++ b/src/app/http/modules/admin/config/validation.ts @@ -0,0 +1,15 @@ +/** + * Config Module Validation Rules + */ + +export const createConfigRules = { + key: 'required|string|min:3|max:255', + value: 'required|string', + type: 'required|string|in:string,number,boolean,json', + description: 'string|max:500' +}; + +export const updateConfigRules = { + value: 'required|string', + description: 'string|max:500' +}; diff --git a/src/app/http/modules/admin/index.ts b/src/app/http/modules/admin/index.ts new file mode 100644 index 0000000..f8651ed --- /dev/null +++ b/src/app/http/modules/admin/index.ts @@ -0,0 +1,16 @@ +/** + * Admin Module Export + */ + +import { Router } from 'express'; +import { routes as configRoutes } from './config'; +import { routes as rolesRoutes } from './roles'; +import { routes as permissionsRoutes } from './permissions'; + +const router = Router(); + +router.use('/config', configRoutes); +router.use('/roles', rolesRoutes); +router.use('/permissions', permissionsRoutes); + +export default router; diff --git a/src/app/http/modules/admin/permissions/controller.ts b/src/app/http/modules/admin/permissions/controller.ts new file mode 100644 index 0000000..46c9aa7 --- /dev/null +++ b/src/app/http/modules/admin/permissions/controller.ts @@ -0,0 +1,45 @@ +/** + * Permissions Controller + * Handles permission viewing endpoints + */ + +import type { Request, Response } from 'express'; + +import { Controller } from '@http/controllers/controller'; + +import service from './service'; + +// ============================================================================ +// Controller +// ============================================================================ + +export class PermissionsController extends Controller { + constructor() { + super(); + } + + /** + * GET /admin/permissions/modules - Get available permission modules + */ + public getModules = async (_req: Request, res: Response): Promise => { + const modules = service.getAvailableModules(); + + return this.sendSuccessResponse(res, { modules }, 'Permission modules fetched', 200); + }; + + /** + * GET /admin/permissions - Get all role permissions summary + */ + public list = async (_req: Request, res: Response): Promise => { + const permissions = await service.getAllRolePermissions(); + + return this.sendSuccessResponse( + res, + { permissions, total: permissions.length }, + 'Role permissions fetched', + 200 + ); + }; +} + +export default new PermissionsController(); diff --git a/src/app/http/modules/admin/permissions/index.ts b/src/app/http/modules/admin/permissions/index.ts new file mode 100644 index 0000000..c8c79b5 --- /dev/null +++ b/src/app/http/modules/admin/permissions/index.ts @@ -0,0 +1,6 @@ +/** + * Permissions Module Export + */ + +export { default as routes } from './routes'; +export { default as service } from './service'; diff --git a/src/app/http/modules/admin/permissions/repository.ts b/src/app/http/modules/admin/permissions/repository.ts new file mode 100644 index 0000000..4911eed --- /dev/null +++ b/src/app/http/modules/admin/permissions/repository.ts @@ -0,0 +1,34 @@ +/** + * Permissions Repository + * Database operations for permissions + */ + +import { getPrisma } from '@core/container'; + +// ============================================================================ +// Repository +// ============================================================================ + +export class PermissionsRepository { + /** + * Get all permissions with role info + */ + async findAllWithRoles(): Promise< + Array<{ + id: bigint; + roleId: bigint; + permissions: unknown; + role: { id: bigint; name: string; slug: string }; + }> + > { + const prisma = getPrisma(); + return prisma.permission.findMany({ + include: { + role: { select: { id: true, name: true, slug: true } } + }, + orderBy: { roleId: 'asc' } + }); + } +} + +export default new PermissionsRepository(); diff --git a/src/app/http/modules/admin/permissions/routes.ts b/src/app/http/modules/admin/permissions/routes.ts new file mode 100644 index 0000000..bf6b9bf --- /dev/null +++ b/src/app/http/modules/admin/permissions/routes.ts @@ -0,0 +1,24 @@ +/** + * Permissions Routes + */ + +import { Router } from 'express'; +import { authCheck, requirePermission } from '@middleware/index'; +import { PERMISSION_MODULES } from '@core/constants'; + +import controller from './controller'; + +const router = Router(); + +// All routes require authentication +router.use(authCheck); + +const { PERMISSIONS } = PERMISSION_MODULES; + +// GET /admin/permissions/modules - Get available permission modules (requires permissions:read) +router.get('/modules', requirePermission({ module: PERMISSIONS, action: 'read' }), controller.getModules); + +// GET /admin/permissions - List all role permissions (requires permissions:list) +router.get('/', requirePermission({ module: PERMISSIONS, action: 'list' }), controller.list); + +export default router; diff --git a/src/app/http/modules/admin/permissions/service.ts b/src/app/http/modules/admin/permissions/service.ts new file mode 100644 index 0000000..c0cfde7 --- /dev/null +++ b/src/app/http/modules/admin/permissions/service.ts @@ -0,0 +1,43 @@ +/** + * Permissions Service + * Business logic for permissions viewing + */ + +import { PERMISSION_MODULES, PERMISSION_ACTIONS, PERMISSION_SCOPES, type Permissions } from '@core/constants'; + +import repository from './repository'; +import type { PermissionModuleInfo, RolePermissionSummary } from './types'; + +// ============================================================================ +// Service +// ============================================================================ + +export class PermissionsService { + /** + * Get all available permission modules, actions, and scopes + */ + getAvailableModules(): PermissionModuleInfo[] { + return Object.entries(PERMISSION_MODULES).map(([key, module]) => ({ + key, + module, + actions: Object.values(PERMISSION_ACTIONS), + scopes: Object.values(PERMISSION_SCOPES) + })); + } + + /** + * Get permissions summary for all roles + */ + async getAllRolePermissions(): Promise { + const permissions = await repository.findAllWithRoles(); + + return permissions.map(perm => ({ + role_id: perm.role.id.toString(), + role_name: perm.role.name, + role_slug: perm.role.slug, + permissions: (perm.permissions ?? {}) as Permissions + })); + } +} + +export default new PermissionsService(); diff --git a/src/app/http/modules/admin/permissions/types.ts b/src/app/http/modules/admin/permissions/types.ts new file mode 100644 index 0000000..1e5da0b --- /dev/null +++ b/src/app/http/modules/admin/permissions/types.ts @@ -0,0 +1,23 @@ +/** + * Permissions Module Types + */ + +import type { ModulePermissions } from '@core/constants'; + +// ============================================================================ +// Response Types +// ============================================================================ + +export interface PermissionModuleInfo { + key: string; + module: string; + actions: string[]; + scopes: string[]; +} + +export interface RolePermissionSummary { + role_id: string; + role_name: string; + role_slug: string; + permissions: Record; +} diff --git a/src/app/http/modules/admin/roles/controller.ts b/src/app/http/modules/admin/roles/controller.ts new file mode 100644 index 0000000..754c30a --- /dev/null +++ b/src/app/http/modules/admin/roles/controller.ts @@ -0,0 +1,249 @@ +/** + * Roles Controller + * Handles role management endpoints + */ + +import type { Request, Response } from 'express'; + +import { ERROR_CODES, PERMISSION_MODULES } from '@core/constants'; +import { createLogger } from '@services/index'; +import { Controller } from '@http/controllers/controller'; + +import service from './service'; +import type { CreateRoleDto, UpdateRoleDto, UpdateRolePermissionsDto } from './types'; +import { createRoleRules, updateRoleRules, updatePermissionsRules } from './validation'; + +const log = createLogger('admin:roles'); + +// ============================================================================ +// Controller +// ============================================================================ + +export class RolesController extends Controller { + constructor() { + super(); + } + + /** + * GET /admin/roles - List all roles + */ + public list = async (_req: Request, res: Response): Promise => { + const roles = await service.getAll(); + + return this.sendSuccessResponse(res, { roles, total: roles.length }, 'Roles fetched', 200); + }; + + /** + * GET /admin/roles/:id - Get single role with permissions + */ + public get = async (req: Request, res: Response): Promise => { + const id = this.parseBigIntParam(req, res, 'id', 'Role'); + if (!id) return; + + const role = await service.getById(id); + + if (!role) { + return this.sendErrorResponse(res, { id: 'Role not found' }, ERROR_CODES.NOT_FOUND, 'Role not found', 404); + } + + return this.sendSuccessResponse(res, { role }, 'Role fetched', 200); + }; + + /** + * POST /admin/roles - Create new role + */ + public create = async (req: Request, res: Response): Promise => { + const data = await this.validate(req, res, createRoleRules); + if (!data) return; + + // Check if slug already exists + const slugExists = await service.slugExists(data.slug); + if (slugExists) { + return this.sendErrorResponse( + res, + { slug: 'Role slug already exists' }, + ERROR_CODES.ALREADY_EXISTS, + 'Role slug already exists', + 409 + ); + } + + const role = await service.create(data); + const adminId = this.getAuthUserId(req); + + log.info('Role created by admin', { + roleId: role.id, + slug: role.slug, + adminId: adminId?.toString() + }); + + return this.sendSuccessResponse(res, { role }, 'Role created', 201); + }; + + /** + * PUT /admin/roles/:id - Update role + */ + public update = async (req: Request, res: Response): Promise => { + const id = this.parseBigIntParam(req, res, 'id', 'Role'); + if (!id) return; + + const data = await this.validate(req, res, updateRoleRules); + if (!data) return; + + // Check if role exists + const exists = await service.exists(id); + if (!exists) { + return this.sendErrorResponse(res, { id: 'Role not found' }, ERROR_CODES.NOT_FOUND, 'Role not found', 404); + } + + const role = await service.update(id, data); + const adminId = this.getAuthUserId(req); + + log.info('Role updated by admin', { + roleId: role.id, + adminId: adminId?.toString() + }); + + return this.sendSuccessResponse(res, { role }, 'Role updated', 200); + }; + + /** + * PUT /admin/roles/:id/permissions - Update role permissions + */ + public updatePermissions = async (req: Request, res: Response): Promise => { + const id = this.parseBigIntParam(req, res, 'id', 'Role'); + if (!id) return; + + const data = await this.validate(req, res, updatePermissionsRules); + if (!data) return; + + // Check if role exists + const exists = await service.exists(id); + if (!exists) { + return this.sendErrorResponse(res, { id: 'Role not found' }, ERROR_CODES.NOT_FOUND, 'Role not found', 404); + } + + // Validate permission structure + const validationError = this.validatePermissionsStructure(data.permissions); + if (validationError) { + return this.sendErrorResponse( + res, + { permissions: validationError }, + ERROR_CODES.VALIDATION_ERROR, + validationError, + 422 + ); + } + + const role = await service.updatePermissions(id, data.permissions); + const adminId = this.getAuthUserId(req); + + log.info('Role permissions updated by admin', { + roleId: role.id, + adminId: adminId?.toString() + }); + + return this.sendSuccessResponse(res, { role }, 'Role permissions updated', 200); + }; + + /** + * DELETE /admin/roles/:id - Delete role + */ + public delete = async (req: Request, res: Response): Promise => { + const id = this.parseBigIntParam(req, res, 'id', 'Role'); + if (!id) return; + + // Check if role exists + const exists = await service.exists(id); + if (!exists) { + return this.sendErrorResponse(res, { id: 'Role not found' }, ERROR_CODES.NOT_FOUND, 'Role not found', 404); + } + + // Check if role has users + const hasUsers = await service.hasUsers(id); + if (hasUsers) { + return this.sendErrorResponse( + res, + { id: 'Cannot delete role with assigned users' }, + ERROR_CODES.CONSTRAINT_VIOLATION, + 'Cannot delete role with assigned users', + 409 + ); + } + + await service.delete(id); + const adminId = this.getAuthUserId(req); + + log.info('Role deleted by admin', { + roleId: id.toString(), + adminId: adminId?.toString() + }); + + return this.sendSuccessResponse(res, null, 'Role deleted', 200); + }; + + /** + * GET /admin/roles/modules - Get available permission modules + */ + public getModules = async (_req: Request, res: Response): Promise => { + const modules = Object.entries(PERMISSION_MODULES).map(([key, value]) => ({ + key, + module: value, + actions: ['create', 'read', 'list', 'update', 'delete'], + scopes: ['own', 'all'] + })); + + return this.sendSuccessResponse(res, { modules }, 'Permission modules fetched', 200); + }; + + // ============================================================================ + // Helpers + // ============================================================================ + + /** + * Validate permissions structure including scope + */ + private validatePermissionsStructure(permissions: Record): string | null { + const validModules = Object.values(PERMISSION_MODULES); + const validActions = ['create', 'read', 'list', 'update', 'delete']; + const validScopes = ['own', 'all']; + + for (const [module, perms] of Object.entries(permissions)) { + // Check module name + if (!validModules.includes(module as (typeof validModules)[number])) { + return `Invalid module: ${module}. Valid modules: ${validModules.join(', ')}`; + } + + // Check permission structure + if (typeof perms !== 'object' || perms === null) { + return `Permissions for module ${module} must be an object`; + } + + const permObj = perms as Record; + + // Check each action + for (const action of validActions) { + if (!(action in permObj)) { + return `Missing action '${action}' in module ${module}`; + } + if (typeof permObj[action] !== 'boolean') { + return `Action '${action}' in module ${module} must be a boolean`; + } + } + + // Check scope field + if (!('scope' in permObj)) { + return `Missing 'scope' in module ${module}. Valid scopes: ${validScopes.join(', ')}`; + } + if (!validScopes.includes(permObj['scope'] as string)) { + return `Invalid scope '${permObj['scope']}' in module ${module}. Valid scopes: ${validScopes.join( + ', ' + )}`; + } + } + + return null; + } +} + +export default new RolesController(); diff --git a/src/app/http/modules/admin/roles/index.ts b/src/app/http/modules/admin/roles/index.ts new file mode 100644 index 0000000..e3db642 --- /dev/null +++ b/src/app/http/modules/admin/roles/index.ts @@ -0,0 +1,6 @@ +/** + * Roles Module Export + */ + +export { default as routes } from './routes'; +export { default as service } from './service'; diff --git a/src/app/http/modules/admin/roles/repository.ts b/src/app/http/modules/admin/roles/repository.ts new file mode 100644 index 0000000..bc7c527 --- /dev/null +++ b/src/app/http/modules/admin/roles/repository.ts @@ -0,0 +1,102 @@ +/** + * Roles Repository + * Database operations for roles management + */ + +import { getPrisma } from '@core/container'; +import type { RoleEntity, RoleWithPermissionsEntity } from './types'; + +// ============================================================================ +// Repository +// ============================================================================ + +export class RolesRepository { + async findAll(): Promise { + const prisma = getPrisma(); + return prisma.role.findMany({ + orderBy: { id: 'asc' } + }) as Promise; + } + + async findById(id: bigint): Promise { + const prisma = getPrisma(); + return prisma.role.findUnique({ + where: { id }, + include: { + permission: { select: { id: true, permissions: true } } + } + }) as Promise; + } + + async create(data: { + name: string; + slug: string; + forApp?: boolean; + passwordRequired?: boolean; + }): Promise { + const prisma = getPrisma(); + return prisma.role.create({ + data: { + name: data.name, + slug: data.slug, + forApp: data.forApp ?? true, + passwordRequired: data.passwordRequired ?? false + } + }) as Promise; + } + + async update( + id: bigint, + data: { + name?: string; + forApp?: boolean; + passwordRequired?: boolean; + } + ): Promise { + const prisma = getPrisma(); + return prisma.role.update({ + where: { id }, + data: { + ...(data.name !== undefined && { name: data.name }), + ...(data.forApp !== undefined && { forApp: data.forApp }), + ...(data.passwordRequired !== undefined && { passwordRequired: data.passwordRequired }) + } + }) as Promise; + } + + async delete(id: bigint): Promise { + const prisma = getPrisma(); + await prisma.permission.deleteMany({ where: { roleId: id } }); + await prisma.role.delete({ where: { id } }); + } + + async slugExists(slug: string, excludeId?: bigint): Promise { + const prisma = getPrisma(); + const count = await prisma.role.count({ + where: { + slug, + ...(excludeId && { id: { not: excludeId } }) + } + }); + return count > 0; + } + + async hasUsers(id: bigint): Promise { + const prisma = getPrisma(); + const count = await prisma.user.count({ + where: { roleId: id } + }); + return count > 0; + } + + async upsertPermissions(roleId: bigint, permissions: Record): Promise { + const prisma = getPrisma(); + await prisma.permission.upsert({ + where: { roleId }, + update: { permissions: permissions as object }, + create: { roleId, permissions: permissions as object } + }); + } +} + +export default new RolesRepository(); diff --git a/src/app/http/modules/admin/roles/routes.ts b/src/app/http/modules/admin/roles/routes.ts new file mode 100644 index 0000000..ac2783a --- /dev/null +++ b/src/app/http/modules/admin/roles/routes.ts @@ -0,0 +1,43 @@ +/** + * Roles Routes + */ + +import { Router } from 'express'; +import { authCheck, requirePermission } from '@middleware/index'; +import { PERMISSION_MODULES } from '@core/constants'; + +import controller from './controller'; + +const router = Router(); + +// All routes require authentication +router.use(authCheck); + +const { ROLES } = PERMISSION_MODULES; + +// GET /admin/roles/modules - Get available permission modules (requires roles:read) +router.get('/modules', requirePermission({ module: ROLES, action: 'read' }), controller.getModules); + +// GET /admin/roles - List all (requires roles:list) +router.get('/', requirePermission({ module: ROLES, action: 'list' }), controller.list); + +// GET /admin/roles/:id - Get single with permissions (requires roles:read) +router.get('/:id', requirePermission({ module: ROLES, action: 'read' }), controller.get); + +// POST /admin/roles - Create (requires roles:create) +router.post('/', requirePermission({ module: ROLES, action: 'create' }), controller.create); + +// PUT /admin/roles/:id - Update (requires roles:update) +router.put('/:id', requirePermission({ module: ROLES, action: 'update' }), controller.update); + +// PUT /admin/roles/:id/permissions - Update permissions (requires permissions:update) +router.put( + '/:id/permissions', + requirePermission({ module: 'permissions', action: 'update' }), + controller.updatePermissions +); + +// DELETE /admin/roles/:id - Delete (requires roles:delete) +router.delete('/:id', requirePermission({ module: ROLES, action: 'delete' }), controller.delete); + +export default router; diff --git a/src/app/http/modules/admin/roles/service.ts b/src/app/http/modules/admin/roles/service.ts new file mode 100644 index 0000000..d0e9c1c --- /dev/null +++ b/src/app/http/modules/admin/roles/service.ts @@ -0,0 +1,147 @@ +/** + * Roles Service + * Business logic for roles management + */ + +import { createLogger } from '@services/logger.service'; +import type { ModulePermissions, Permissions } from '@core/constants'; +import { invalidateRoleSessions } from '@http/modules/auth/cache'; + +import repository from './repository'; +import type { + RoleEntity, + RoleWithPermissionsEntity, + RoleResponse, + RoleWithPermissionsResponse, + CreateRoleDto, + UpdateRoleDto +} from './types'; + +// ============================================================================ +// Constants +// ============================================================================ + +const log = createLogger('admin:roles'); + +// ============================================================================ +// Helpers +// ============================================================================ + +const entityToResponse = (entity: RoleEntity): RoleResponse => ({ + id: entity.id.toString(), + name: entity.name, + slug: entity.slug, + for_app: entity.forApp, + password_required: entity.passwordRequired, + created_at: entity.createdAt.toISOString(), + updated_at: entity.updatedAt.toISOString() +}); + +const entityWithPermissionsToResponse = (entity: RoleWithPermissionsEntity): RoleWithPermissionsResponse => { + const permRecord = entity.permission; + const permissions: Permissions = + permRecord?.permissions && typeof permRecord.permissions === 'object' + ? (permRecord.permissions as Permissions) + : {}; + + return { + ...entityToResponse(entity), + permissions + }; +}; + +// ============================================================================ +// Service +// ============================================================================ + +export class RolesService { + async getAll(): Promise { + const roles = await repository.findAll(); + return roles.map(entityToResponse); + } + + async getById(id: bigint): Promise { + const role = await repository.findById(id); + if (!role) return null; + return entityWithPermissionsToResponse(role); + } + + async create(data: CreateRoleDto): Promise { + const role = await repository.create({ + name: data.name, + slug: data.slug, + ...(data.for_app !== undefined && { forApp: data.for_app }), + ...(data.password_required !== undefined && { passwordRequired: data.password_required }) + }); + + log.info('Role created', { id: role.id.toString(), slug: role.slug }); + + return entityToResponse(role); + } + + async update(id: bigint, data: UpdateRoleDto): Promise { + const updateData: { + name?: string; + forApp?: boolean; + passwordRequired?: boolean; + } = {}; + + if (data.name !== undefined) updateData.name = data.name; + if (data.for_app !== undefined) updateData.forApp = data.for_app; + if (data.password_required !== undefined) updateData.passwordRequired = data.password_required; + + const role = await repository.update(id, updateData); + + log.info('Role updated', { id: role.id.toString(), slug: role.slug }); + + return entityToResponse(role); + } + + async updatePermissions( + id: bigint, + permissions: Record + ): Promise { + await repository.upsertPermissions(id, permissions); + + const role = await repository.findById(id); + if (!role) { + throw new Error('Role not found after permission update'); + } + + const invalidatedCount = await invalidateRoleSessions(role.slug); + + log.info('Role permissions updated', { + roleId: id.toString(), + roleSlug: role.slug, + sessionsInvalidated: invalidatedCount + }); + + return entityWithPermissionsToResponse(role); + } + + async delete(id: bigint): Promise { + const role = await repository.findById(id); + if (role) { + await invalidateRoleSessions(role.slug); + } + + await repository.delete(id); + + log.info('Role deleted', { id: id.toString() }); + } + + async slugExists(slug: string, excludeId?: bigint): Promise { + return repository.slugExists(slug, excludeId); + } + + async hasUsers(id: bigint): Promise { + return repository.hasUsers(id); + } + + async exists(id: bigint): Promise { + const role = await repository.findById(id); + return role !== null; + } +} + +export default new RolesService(); diff --git a/src/app/http/modules/admin/roles/types.ts b/src/app/http/modules/admin/roles/types.ts new file mode 100644 index 0000000..188c584 --- /dev/null +++ b/src/app/http/modules/admin/roles/types.ts @@ -0,0 +1,66 @@ +/** + * Roles Module Types + */ + +import type { ModulePermissions } from '@core/constants'; + +// ============================================================================ +// DTOs +// ============================================================================ + +export interface CreateRoleDto extends Record { + name: string; + slug: string; + for_app?: boolean; + password_required?: boolean; +} + +export interface UpdateRoleDto extends Record { + name?: string; + for_app?: boolean; + password_required?: boolean; +} + +export interface UpdateRolePermissionsDto extends Record { + permissions: Record; +} + +// ============================================================================ +// Response Types +// ============================================================================ + +export interface RoleResponse { + id: string; + name: string; + slug: string; + for_app: boolean; + password_required: boolean; + created_at: string; + updated_at: string; +} + +export interface RoleWithPermissionsResponse extends RoleResponse { + permissions: Record; +} + +// ============================================================================ +// Internal Types +// ============================================================================ + +export interface RoleEntity { + id: bigint; + name: string; + slug: string; + forApp: boolean; + passwordRequired: boolean; + createdAt: Date; + updatedAt: Date; +} + +export interface RoleWithPermissionsEntity extends RoleEntity { + /** Permission is 1:1 with Role (roleId has @unique constraint) */ + permission: { + id: bigint; + permissions: unknown; // JSONB + } | null; +} diff --git a/src/app/http/modules/admin/roles/validation.ts b/src/app/http/modules/admin/roles/validation.ts new file mode 100644 index 0000000..f1761e2 --- /dev/null +++ b/src/app/http/modules/admin/roles/validation.ts @@ -0,0 +1,20 @@ +/** + * Roles Module Validation Rules + */ + +export const createRoleRules = { + name: 'required|string|min:2|max:100', + slug: 'required|string|min:2|max:100|regex:/^[a-z0-9_]+$/', + for_app: 'boolean', + password_required: 'boolean' +}; + +export const updateRoleRules = { + name: 'string|min:2|max:100', + for_app: 'boolean', + password_required: 'boolean' +}; + +export const updatePermissionsRules = { + permissions: 'required|object' +}; diff --git a/src/app/http/modules/auth/cache.ts b/src/app/http/modules/auth/cache.ts new file mode 100644 index 0000000..31e2a67 --- /dev/null +++ b/src/app/http/modules/auth/cache.ts @@ -0,0 +1,228 @@ +/** + * Session Cache + * Redis-based caching for login sessions to avoid DB reads on every authenticated request + * + * Cache key patterns: + * - Session: auth:session:: + * - User sessions set: auth:user-sessions: + * - Role sessions set: auth:role-sessions: + */ + +import { getRedisClient } from '@core/cache'; +import { createLogger } from '@services/logger.service'; +import type { Permissions } from '@core/constants'; + +const logger = createLogger('auth:session-cache'); + +const SESSION_PREFIX = 'auth:session:'; +const USER_SESSIONS_PREFIX = 'auth:user-sessions:'; +const ROLE_SESSIONS_PREFIX = 'auth:role-sessions:'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface CachedSession { + sessionId: string; + userId: string; + token: string; + roleSlug: string; + status: boolean; + verified: boolean; + expiresAt: string; + permissions: Permissions; + user: { + id: string; + firstName: string; + lastName: string; + email: string; + phone: string; + countryCode: string; + status: string; + verified: boolean; + roleId: string; + role: { + id: string; + name: string; + slug: string; + forApp: boolean; + passwordRequired: boolean; + }; + }; +} + +const buildSessionKey = (roleSlug: string, token: string): string => { + return `${SESSION_PREFIX}${roleSlug}:${token}`; +}; + +// ============================================================================ +// Helper Functions +// ============================================================================ + +const calculateTtl = (expiresAt: Date | string): number => { + const expiry = typeof expiresAt === 'string' ? new Date(expiresAt) : expiresAt; + const ttlMs = expiry.getTime() - Date.now(); + return Math.max(1, Math.floor(ttlMs / 1000)); +}; + +// ============================================================================ +// Cache Operations +// ============================================================================ + +export const getCachedSession = async (roleSlug: string, token: string): Promise => { + try { + const redis = await getRedisClient(); + const key = buildSessionKey(roleSlug, token); + const cached = await redis.get(key); + + if (!cached) { + logger.debug('Session cache miss', { roleSlug, token: token.substring(0, 8) + '...' }); + return null; + } + + logger.debug('Session cache hit', { roleSlug, token: token.substring(0, 8) + '...' }); + return JSON.parse(cached) as CachedSession; + } catch (error) { + logger.warn('Session cache get failed', { + error: error instanceof Error ? error.message : String(error) + }); + return null; + } +}; + +export const setCachedSession = async (roleSlug: string, token: string, session: CachedSession): Promise => { + try { + const redis = await getRedisClient(); + const ttl = calculateTtl(session.expiresAt); + const sessionKey = buildSessionKey(roleSlug, token); + + const pipeline = redis.pipeline(); + + pipeline.setex(sessionKey, ttl, JSON.stringify(session)); + + pipeline.sadd(`${USER_SESSIONS_PREFIX}${session.userId}`, `${roleSlug}:${token}`); + pipeline.expire(`${USER_SESSIONS_PREFIX}${session.userId}`, ttl); + + pipeline.sadd(`${ROLE_SESSIONS_PREFIX}${roleSlug}`, `${session.userId}:${token}`); + pipeline.expire(`${ROLE_SESSIONS_PREFIX}${roleSlug}`, ttl); + + await pipeline.exec(); + + logger.debug('Session cached', { + roleSlug, + token: token.substring(0, 8) + '...', + userId: session.userId, + ttl + }); + } catch (error) { + logger.warn('Session cache set failed', { + error: error instanceof Error ? error.message : String(error) + }); + } +}; + +export const invalidateCachedSession = async (roleSlug: string, token: string, userId?: string): Promise => { + try { + const redis = await getRedisClient(); + const sessionKey = buildSessionKey(roleSlug, token); + const pipeline = redis.pipeline(); + + pipeline.del(sessionKey); + + if (userId) { + pipeline.srem(`${USER_SESSIONS_PREFIX}${userId}`, `${roleSlug}:${token}`); + pipeline.srem(`${ROLE_SESSIONS_PREFIX}${roleSlug}`, `${userId}:${token}`); + } + + await pipeline.exec(); + + logger.debug('Session cache invalidated', { roleSlug, token: token.substring(0, 8) + '...' }); + } catch (error) { + logger.warn('Session cache invalidate failed', { + error: error instanceof Error ? error.message : String(error) + }); + } +}; + +export const invalidateAllUserSessions = async (userId: string | bigint): Promise => { + try { + const redis = await getRedisClient(); + const userIdStr = userId.toString(); + + const entries = await redis.smembers(`${USER_SESSIONS_PREFIX}${userIdStr}`); + + if (entries.length === 0) { + logger.debug('No sessions to invalidate for user', { userId: userIdStr }); + return 0; + } + + const pipeline = redis.pipeline(); + for (const entry of entries) { + const [roleSlug, token] = entry.split(':'); + if (roleSlug && token) { + pipeline.del(buildSessionKey(roleSlug, token)); + pipeline.srem(`${ROLE_SESSIONS_PREFIX}${roleSlug}`, `${userIdStr}:${token}`); + } + } + pipeline.del(`${USER_SESSIONS_PREFIX}${userIdStr}`); + + await pipeline.exec(); + + logger.info('All user sessions invalidated', { + userId: userIdStr, + count: entries.length + }); + + return entries.length; + } catch (error) { + logger.warn('User sessions invalidation failed', { + userId: userId.toString(), + error: error instanceof Error ? error.message : String(error) + }); + return 0; + } +}; + +export const invalidateRoleSessions = async (roleSlug: string): Promise => { + try { + const redis = await getRedisClient(); + + const entries = await redis.smembers(`${ROLE_SESSIONS_PREFIX}${roleSlug}`); + + if (entries.length === 0) { + logger.debug('No sessions to invalidate for role', { roleSlug }); + return 0; + } + + const pipeline = redis.pipeline(); + const processedUsers = new Set(); + + for (const entry of entries) { + const [userId, token] = entry.split(':'); + if (!userId || !token) continue; + + pipeline.del(buildSessionKey(roleSlug, token)); + pipeline.srem(`${USER_SESSIONS_PREFIX}${userId}`, `${roleSlug}:${token}`); + + processedUsers.add(userId); + } + + pipeline.del(`${ROLE_SESSIONS_PREFIX}${roleSlug}`); + + await pipeline.exec(); + + logger.info('All role sessions invalidated', { + roleSlug, + sessionCount: entries.length, + affectedUsers: processedUsers.size + }); + + return entries.length; + } catch (error) { + logger.warn('Role sessions invalidation failed', { + roleSlug, + error: error instanceof Error ? error.message : String(error) + }); + return 0; + } +}; diff --git a/src/app/http/modules/auth/controller.ts b/src/app/http/modules/auth/controller.ts new file mode 100644 index 0000000..4098510 --- /dev/null +++ b/src/app/http/modules/auth/controller.ts @@ -0,0 +1,556 @@ +/** + * Auth Controller + * Handles authentication endpoints + */ + +import type { Request, Response } from 'express'; +import { CodeVerificationPurpose } from '@database/prisma'; + +// Core imports +import { ERROR_CODES, AUTH } from '@core/constants'; +import { cleanPhoneNumber } from '@core/utils'; +import { createLogger } from '@services/index'; +import { AuthenticatedRequest } from '@core/types'; +import { generateViewUrl, STORAGE_BUCKETS } from '@services/storage'; + +// Module imports +import { Controller } from '@http/controllers/controller'; +import service from './service'; +import type { + SendOtpDto, + VerifyOtpDto, + ResendOtpDto, + PasswordLoginDto, + ForgotPasswordDto, + ResetPasswordDto, + UserWithRole +} from './types'; +import { + sendOtpRules, + verifyOtpRules, + resendOtpRules, + passwordLoginRules, + forgotPasswordRules, + resetPasswordRules +} from './validation'; +import { errors, checkAccountStatus, buildLoginResponse2FARequired, buildLoginResponseAuthenticated } from './helpers'; +import environment from '@config/environment.config'; + +const { isDev } = environment.basic; + +const log = createLogger('auth'); + +const VERIFICATION_PURPOSE_MAP: Record = { + otp: CodeVerificationPurpose.phone_auth, + '2fa': CodeVerificationPurpose.two_factor_auth, + mfa: CodeVerificationPurpose.mfa_auth, + email: CodeVerificationPurpose.email_verify +}; + +/** Types that complete 2FA/MFA login flow (don't create new sessions) */ +const TWO_FACTOR_TYPES = ['2fa', 'mfa']; + +export class AuthController extends Controller { + constructor() { + super(); + } + + // POST /auth/phone - Send OTP to phone, creates user if not exists + public sendOTP = async (req: Request, res: Response): Promise => { + const data = await this.validate(req, res, sendOtpRules); + if (!data) return; + + const phone = cleanPhoneNumber(data.phone); + const countryCode = cleanPhoneNumber(data.country_code); + + let user = await service.findUserByPhone(countryCode, phone); + + if (!user) { + const customerRole = await service.getCustomerRole(); + if (!customerRole) { + log.error('Customer role not found in database'); + return errors.serverError(res, 'System configuration error'); + } + + user = await service.createUser(countryCode, phone, customerRole.id); + log.info('New user created', { userId: user.id.toString(), phone }); + } + + if (checkAccountStatus(user, res)) return; + + const { reference, expiresIn } = await service.createVerification(user.id, CodeVerificationPurpose.phone_auth); + log.info('OTP generated', { userId: user.id.toString(), reference }); + + return this.sendSuccessResponse( + res, + { + reference, + expires_in: expiresIn, + ...(isDev && { otp: AUTH.DEV_OTP_CODE }) + }, + 'OTP sent successfully', + 200 + ); + }; + + // POST /auth/verify/:type - Verify OTP/2FA/MFA and create/complete session + // Types: otp (phone login), 2fa (two-factor auth), mfa (authenticator), email (email verify) + public verifyOTP = async (req: Request, res: Response): Promise => { + const type = String(req.params['type'] || ''); + const purpose = VERIFICATION_PURPOSE_MAP[type]; + + if (!purpose) { + return errors.validation(res, { type: 'Invalid verification type' }, 'Invalid verification type'); + } + + const data = await this.validate(req, res, verifyOtpRules); + if (!data) return; + + const verification = await service.findVerification(data.reference); + if (!verification) { + return errors.invalidCredentials(res, { reference: 'Invalid reference' }); + } + + if (verification.expiredAt < new Date()) { + await service.deleteVerification(verification.id); + return errors.tokenExpired(res, 'code'); + } + + if (verification.purpose !== purpose) { + return errors.validation(res, { type: 'Verification type mismatch' }, 'Invalid type for reference'); + } + + const { user } = verification; + if (!user) { + return errors.notFound(res, 'message', 'User not found'); + } + + // Verification currently supports OTP code validation. + // Pending integration: validate MFA flow using TOTP and user.mfaSecret. + const isValidCode = service.verifyCode(verification, data.code); + + if (!isValidCode) { + return this.sendErrorResponse( + res, + { code: 'Invalid verification code' }, + ERROR_CODES.INVALID_2FA_CODE, + 'Invalid verification code', + 400 + ); + } + + // Build additional update data based on verification type + const additionalUpdateData: Record = {}; + if (purpose === CodeVerificationPurpose.phone_auth) { + additionalUpdateData['phoneVerifiedAt'] = new Date(); + } else if (purpose === CodeVerificationPurpose.email_verify) { + additionalUpdateData['emailVerifiedAt'] = new Date(); + } + + // Complete login and cleanup verification + await service.deleteVerification(verification.id); + const { session, userResponse, twoFactorStatus, permissions } = await service.completeLogin( + user as UserWithRole, + req, + data, + additionalUpdateData + ); + + const is2FAFlow = TWO_FACTOR_TYPES.includes(type); + log.info(is2FAFlow ? 'User completed 2FA verification' : 'User logged in via OTP', { + userId: user.id.toString(), + type + }); + + return this.sendSuccessResponse( + res, + buildLoginResponseAuthenticated(userResponse, user.verified, session, twoFactorStatus, permissions), + is2FAFlow ? 'Verification successful' : 'Login successful', + 200 + ); + }; + + // POST /auth/otp/resend - Resend OTP with rate limiting + public resendOTP = async (req: Request, res: Response): Promise => { + const data = await this.validate(req, res, resendOtpRules); + if (!data) return; + + const verification = await service.findVerification(data.reference); + if (!verification) { + return errors.notFound(res, 'reference', 'Invalid reference'); + } + + const result = await service.resendOtp(verification); + + if (!result.success) { + const msg = result.error === 'max_attempts' ? 'Maximum resend attempts exceeded' : 'Please wait 30 seconds'; + return errors.tooMany(res, msg); + } + + log.info('OTP resent', { reference: data.reference }); + + return this.sendSuccessResponse( + res, + { + reference: data.reference, + expires_in: AUTH.OTP_EXPIRY_MINUTES * 60, + attempts_remaining: result.attemptsRemaining, + ...(isDev && { otp: AUTH.DEV_OTP_CODE }) + }, + 'OTP resent', + 200 + ); + }; + + // POST /auth/login - Email + password authentication + public passwordLogin = async (req: Request, res: Response): Promise => { + const data = await this.validate(req, res, passwordLoginRules); + if (!data) return; + + const user = await service.findUserByEmail(data.email); + if (!user) { + return errors.invalidCredentials(res); + } + + if (!user.role?.passwordRequired) { + return errors.permissionDenied(res, 'Password login not available. Use OTP.'); + } + + if (checkAccountStatus(user, res)) return; + + const activePassword = user.passwords[0]; + if (!activePassword) { + return errors.invalidCredentials(res, { message: 'Password not set. Contact support.' }); + } + + if (!service.verifyPassword(data.password, activePassword.password, activePassword.salt)) { + const { newAttempts, isLocked: locked } = await service.incrementLoginAttempts( + user.id, + user.loginAttempts, + AUTH.MAX_LOGIN_ATTEMPTS, + AUTH.LOCKOUT_MINUTES + ); + + if (locked) { + log.warn('Account locked due to failed attempts', { userId: user.id.toString() }); + } + + return errors.invalidCredentials(res, { attempts_remaining: AUTH.MAX_LOGIN_ATTEMPTS - newAttempts }); + } + + // Check if 2FA/MFA is required + const { twoFactorEnabled, mfaEnabled, requiresVerification } = service.requires2FA( + user as unknown as UserWithRole + ); + + if (requiresVerification) { + // Create verification record for 2FA/MFA + // Priority: MFA (authenticator) > 2FA (OTP via email/phone) + const purpose = mfaEnabled ? CodeVerificationPurpose.mfa_auth : CodeVerificationPurpose.two_factor_auth; + const { reference, code, expiresIn } = await service.createVerification(user.id, purpose); + + // If 2FA (not MFA), send OTP via email/phone + if (!mfaEnabled && twoFactorEnabled) { + log.info('2FA OTP generated for login', { userId: user.id.toString(), reference }); + // Pending integration: dispatch 2FA OTP via notification provider (email/SMS). + if (isDev) { + log.debug('2FA OTP code', { code }); + } + } else { + log.info('MFA verification required', { userId: user.id.toString(), reference }); + } + + return this.sendSuccessResponse( + res, + buildLoginResponse2FARequired({ twoFactorEnabled, mfaEnabled }, reference, expiresIn), + mfaEnabled ? 'MFA verification required' : '2FA verification required', + 200 + ); + } + + // No 2FA/MFA required - complete login directly + const { session, userResponse, twoFactorStatus, permissions } = await service.completeLogin( + user as unknown as UserWithRole, + req, + data + ); + + log.info('User logged in via password', { userId: user.id.toString() }); + + return this.sendSuccessResponse( + res, + buildLoginResponseAuthenticated(userResponse, user.verified, session, twoFactorStatus, permissions), + 'Login successful', + 200 + ); + }; + + // POST /auth/password/forgot - Request password reset email + public forgotPassword = async (req: Request, res: Response): Promise => { + const data = await this.validate(req, res, forgotPasswordRules); + if (!data) return; + + const user = await service.findUserByEmail(data.email); + const successMsg = { message: 'If the email exists, a reset link has been sent.' }; + + // Always return success to prevent email enumeration + if (!user || !user.role?.passwordRequired) { + return this.sendSuccessResponse(res, successMsg, 'Password reset requested', 200); + } + + const token = await service.createPasswordResetToken(user.id, user.email); + log.info('Password reset requested', { userId: user.id.toString() }); + + // Pending integration: dispatch password reset email via notification provider. + + return this.sendSuccessResponse( + res, + { + ...successMsg, + ...(isDev && { reset_token: token }) + }, + 'Password reset requested', + 200 + ); + }; + + // POST /auth/password/reset - Reset password with token + public resetPassword = async (req: Request, res: Response): Promise => { + const data = await this.validate(req, res, resetPasswordRules); + if (!data) return; + + if (data.password !== data.password_confirmation) { + return errors.validation( + res, + { password_confirmation: 'Passwords do not match' }, + 'Passwords do not match' + ); + } + + const magicToken = await service.findPasswordResetToken(data.token); + + if (!magicToken || magicToken.purpose !== 'password_reset') { + return this.sendErrorResponse( + res, + { token: 'Invalid token' }, + ERROR_CODES.INVALID_PASSWORD_RESET_TOKEN, + 'Invalid token', + 400 + ); + } + + if (magicToken.used || magicToken.expiresAt < new Date()) { + return errors.tokenExpired(res); + } + + if (!magicToken.userId) { + return errors.notFound(res, 'token', 'User not found'); + } + + await service.resetPassword(magicToken.userId, data.password, magicToken.id); + log.info('Password reset completed', { userId: magicToken.userId.toString() }); + + return this.sendSuccessResponse( + res, + { message: 'Password reset successful. Please login.' }, + 'Password reset successful', + 200 + ); + }; + + // GET /auth/me - Get current authenticated user (returns all user attributes) + public me = async (req: Request, res: Response): Promise => { + const { user } = req as AuthenticatedRequest; + + if (!user) { + return errors.notFound(res, 'message', 'User not found'); + } + + const fullUser = await service.findUserFullProfile(user.id); + if (!fullUser) { + return errors.notFound(res, 'message', 'User not found'); + } + + // Generate avatar URL if avatar exists + let avatarUrl: string | null = null; + if (fullUser.avatar && fullUser.avatar.status === 'uploaded') { + try { + const bucket = fullUser.avatar.isPublic ? STORAGE_BUCKETS.PUBLIC : STORAGE_BUCKETS.DOCUMENTS; + const viewUrlResult = await generateViewUrl(fullUser.avatar.reference, bucket); + avatarUrl = viewUrlResult.viewUrl; + } catch { + // If URL generation fails, leave as null + } + } + + // Return all user attributes from the database + return this.sendSuccessResponse( + res, + { + user: { + id: fullUser.id.toString(), + first_name: fullUser.firstName, + last_name: fullUser.lastName, + email: fullUser.email, + phone: fullUser.phone, + country_code: fullUser.countryCode, + avatar_id: fullUser.avatarId?.toString() ?? null, + avatar: fullUser.avatar + ? { + id: fullUser.avatar.id.toString(), + reference: fullUser.avatar.reference, + path: fullUser.avatar.path, + thumbnail: fullUser.avatar.thumbnail, + bucket: fullUser.avatar.bucket, + is_public: fullUser.avatar.isPublic, + url: avatarUrl + } + : null, + latitude: fullUser.latitude ? Number(fullUser.latitude) : null, + longitude: fullUser.longitude ? Number(fullUser.longitude) : null, + mfa_enabled: fullUser.mfaEnabled, + otp_auth: fullUser.otpAuth, + status: fullUser.status, + remarks: fullUser.remarks, + last_login_at: fullUser.lastLoginAt?.toISOString() ?? null, + verified: fullUser.verified, + email_verified: Boolean(fullUser.emailVerifiedAt), + email_verified_at: fullUser.emailVerifiedAt?.toISOString() ?? null, + phone_verified: Boolean(fullUser.phoneVerifiedAt), + phone_verified_at: fullUser.phoneVerifiedAt?.toISOString() ?? null, + created_at: fullUser.createdAt.toISOString(), + updated_at: fullUser.updatedAt.toISOString(), + role: fullUser.role + ? { + id: fullUser.role.id.toString(), + name: fullUser.role.name, + slug: fullUser.role.slug, + for_app: fullUser.role.forApp, + password_required: fullUser.role.passwordRequired + } + : null + } + }, + 'User fetched', + 200 + ); + }; + + // POST /auth/logout - Invalidate current session + public logout = async (req: Request, res: Response): Promise => { + const { session, user } = req as AuthenticatedRequest; + + if (!session || !user) { + return errors.notFound(res, 'message', 'Session not found'); + } + + const roleSlug = user.role?.slug ?? 'customer'; + await service.logoutSession(session.id, session.token, user.id, roleSlug); + log.info('User logged out', { userId: user.id.toString() }); + + return this.sendSuccessResponse(res, { message: 'Logged out successfully' }, 'Logout successful', 200); + }; + + // POST /auth/refresh - Refresh access token using refresh token + public refreshToken = async (req: Request, res: Response): Promise => { + const { session, user } = req as AuthenticatedRequest; + + if (!session || !user) { + return errors.sessionInvalid(res); + } + + // Fetch user with full profile including avatar + const fullUser = await service.findUserFullProfile(user.id); + if (!fullUser) { + return errors.notFound(res, 'message', 'User not found'); + } + + const newSession = await service.refreshSession(session.id, session.token, fullUser as UserWithRole); + + // Generate avatar URL if avatar exists + const avatarData = fullUser.avatar ? await service.generateAvatarUrl(fullUser.avatar) : null; + + const userResponse = service.formatUserResponse(fullUser as UserWithRole, false, avatarData); + + return this.sendSuccessResponse( + res, + { + user: userResponse, + tokens: newSession.keys, + token_expiry: newSession.token_expiry, + permissions: newSession.permissions + }, + 'Token refreshed', + 200 + ); + }; + + // PUT /auth/me/avatar - Update user avatar + public updateAvatar = async (req: Request, res: Response): Promise => { + const { user } = req as AuthenticatedRequest; + + if (!user) { + return errors.notFound(res, 'message', 'User not found'); + } + + const { avatar_id } = req.body as { avatar_id: string | null }; + + // Validate avatar_id if provided + let avatarIdBigInt: bigint | null = null; + if (avatar_id !== null && avatar_id !== undefined) { + try { + avatarIdBigInt = BigInt(avatar_id); + } catch { + return errors.validation(res, { avatar_id: 'Invalid avatar ID' }); + } + + // Verify the gallery exists and belongs to the user (or user has permission) + const gallery = await service.verifyGalleryOwnership(avatarIdBigInt, user.id); + if (!gallery) { + return errors.notFound(res, 'avatar_id', 'Gallery not found or not accessible'); + } + } + + // Update user avatar + const updatedUser = await service.updateUserAvatar(user.id, avatarIdBigInt); + if (!updatedUser) { + return errors.notFound(res, 'message', 'User not found'); + } + + log.info('User avatar updated', { userId: user.id.toString(), avatarId: avatar_id }); + + // Generate avatar URL if avatar exists + let avatarUrl: string | null = null; + if (updatedUser.avatar && updatedUser.avatar.status === 'uploaded') { + try { + const bucket = updatedUser.avatar.isPublic ? STORAGE_BUCKETS.PUBLIC : STORAGE_BUCKETS.DOCUMENTS; + const viewUrlResult = await generateViewUrl(updatedUser.avatar.reference, bucket); + avatarUrl = viewUrlResult.viewUrl; + } catch { + // If URL generation fails, leave as null + } + } + + return this.sendSuccessResponse( + res, + { + avatar_id: updatedUser.avatarId?.toString() ?? null, + avatar: updatedUser.avatar + ? { + id: updatedUser.avatar.id.toString(), + reference: updatedUser.avatar.reference, + path: updatedUser.avatar.path, + thumbnail: updatedUser.avatar.thumbnail, + bucket: updatedUser.avatar.bucket, + is_public: updatedUser.avatar.isPublic, + url: avatarUrl + } + : null + }, + 'Avatar updated', + 200 + ); + }; +} + +export default new AuthController(); diff --git a/src/app/http/modules/auth/helpers.ts b/src/app/http/modules/auth/helpers.ts new file mode 100644 index 0000000..ecdd29f --- /dev/null +++ b/src/app/http/modules/auth/helpers.ts @@ -0,0 +1,148 @@ +/** + * Auth Module Helpers + * Utility functions for authentication + */ + +import type { Response } from 'express'; +import { ERROR_CODES } from '@core/constants'; +import { getRequestId } from '@middleware/request-context.middleware'; +import type { + UserResponse, + SessionTokens, + LoginResponseAuthenticated, + LoginResponse2FARequired, + TwoFactorStatus +} from './types'; + +// ============================================================================ +// Types +// ============================================================================ + +type ErrorData = Record; + +interface AccountCheckable { + inactiveTill: Date | null; + status: string; +} + +// ============================================================================ +// Error Response Helpers +// ============================================================================ + +const send = (res: Response, data: ErrorData, code: string, message: string, status: number): Response => + res.status(status).json({ + success: false, + message, + error_code: code, + error: data, + timestamp: new Date().toISOString(), + requestId: getRequestId() + }); + +/** + * Pre-configured error responses for auth module + */ +export const errors = { + notFound: (res: Response, field = 'resource', msg = 'Not found'): Response => + send(res, { [field]: msg }, ERROR_CODES.NOT_FOUND, msg, 404), + + locked: (res: Response, minutes: number): Response => + send( + res, + { message: `Account locked. Try again in ${minutes} minutes.` }, + ERROR_CODES.ACCOUNT_LOCKED, + 'Account locked', + 423 + ), + + suspended: (res: Response): Response => + send(res, { message: 'Account suspended' }, ERROR_CODES.ACCOUNT_SUSPENDED, 'Account suspended', 403), + + invalidCredentials: (res: Response, extra?: ErrorData): Response => + send( + res, + { email: 'Invalid credentials', ...extra }, + ERROR_CODES.INVALID_CREDENTIALS, + 'Invalid credentials', + 401 + ), + + permissionDenied: (res: Response, msg = 'Permission denied'): Response => + send(res, { message: msg }, ERROR_CODES.PERMISSION_DENIED, msg, 403), + + validation: (res: Response, data: ErrorData, msg = 'Validation failed'): Response => + send(res, data, ERROR_CODES.VALIDATION_ERROR, msg, 422), + + serverError: (res: Response, msg = 'Internal server error'): Response => + send(res, { message: msg }, ERROR_CODES.INTERNAL_SERVER_ERROR, msg, 500), + + tokenExpired: (res: Response, field = 'token'): Response => + send(res, { [field]: 'Token expired' }, ERROR_CODES.TOKEN_EXPIRED, 'Token expired', 400), + + tooMany: (res: Response, msg = 'Too many requests'): Response => + send(res, { message: msg }, ERROR_CODES.TOO_MANY_ATTEMPTS, msg, 429), + + sessionInvalid: (res: Response): Response => + send(res, { message: 'Invalid session' }, ERROR_CODES.SESSION_NOT_VALID, 'Invalid session', 401) +}; + +// ============================================================================ +// Account Status Helpers +// ============================================================================ + +const isLocked = (inactiveTill: Date | null): boolean => Boolean(inactiveTill && inactiveTill > new Date()); + +const isSuspended = (status: string): boolean => status === 'banned' || status === 'suspended'; + +const lockoutMinutes = (inactiveTill: Date): number => Math.ceil((inactiveTill.getTime() - Date.now()) / 60_000); + +/** + * Check if account has issues - returns true if blocked, false if OK + */ +export const checkAccountStatus = (user: AccountCheckable, res: Response): boolean => { + if (isLocked(user.inactiveTill)) { + errors.locked(res, lockoutMinutes(user.inactiveTill!)); + return true; + } + if (isSuspended(user.status)) { + errors.suspended(res); + return true; + } + return false; +}; + +// ============================================================================ +// Login Response Builders +// ============================================================================ + +/** + * Build login response when 2FA/MFA verification is required + */ +export const buildLoginResponse2FARequired = ( + twoFactorStatus: TwoFactorStatus, + reference: string, + expiresInSeconds: number +): LoginResponse2FARequired => ({ + two_factor_enabled: twoFactorStatus.twoFactorEnabled, + mfa_enabled: twoFactorStatus.mfaEnabled, + reference, + expires_in: expiresInSeconds +}); + +/** + * Build login response when fully authenticated (no 2FA/MFA or after verification) + */ +export const buildLoginResponseAuthenticated = ( + userResponse: UserResponse, + verified: boolean, + session: SessionTokens, + twoFactorStatus: TwoFactorStatus = { twoFactorEnabled: false, mfaEnabled: false }, + permissions: Record = {} +): LoginResponseAuthenticated => ({ + two_factor_enabled: twoFactorStatus.twoFactorEnabled, + mfa_enabled: twoFactorStatus.mfaEnabled, + user: { ...userResponse, is_verified: verified }, + tokens: session.keys, + token_expiry: session.token_expiry, + permissions +}); diff --git a/src/app/http/modules/auth/index.ts b/src/app/http/modules/auth/index.ts new file mode 100644 index 0000000..0dc677d --- /dev/null +++ b/src/app/http/modules/auth/index.ts @@ -0,0 +1,16 @@ +/** + * Auth Module Export + */ + +export { default as routes } from './routes'; +export { default as controller } from './controller'; +export { default as service } from './service'; +export { default as repository } from './repository'; +export * from './types'; +export * from './validation'; + +// Cache exports for external use (e.g., admin ban/suspend user, permission updates) +export { invalidateAllUserSessions, invalidateRoleSessions } from './cache'; + +// Queue exports for initialization +export { initSessionUpdateWorker, shutdownSessionQueue } from './queue'; diff --git a/src/app/http/modules/auth/queue.ts b/src/app/http/modules/auth/queue.ts new file mode 100644 index 0000000..ac05201 --- /dev/null +++ b/src/app/http/modules/auth/queue.ts @@ -0,0 +1,222 @@ +/** + * Auth Queue + * BullMQ-based job queue for session lastUsed updates + * + * Uses a debounce + throttle ceiling pattern: + * - Every authenticated request buffers the latest activity time in Redis + * - A delayed BullMQ job fires after a quiet period (debounce) + * - If activity continues, the job re-schedules itself up to a max wait ceiling + * - On flush, the most recent timestamp is written to DB in a single write + */ + +import { Queue, Worker, Job } from 'bullmq'; +import environment from '@config/environment.config'; +import { getRedisClient } from '@core/cache'; +import { createBullMqConnection, DEFAULT_BULLMQ_JOB_OPTIONS } from '@core/queue'; +import { createLogger } from '@services/logger.service'; +import { prismaClient } from '@core/database'; + +const logger = createLogger('auth:queue'); + +// ============================================================================ +// Configuration +// ============================================================================ + +const QUEUE_NAME = 'auth-session-updates'; + +/** Quiet period before flushing — if a new request arrives within this window, the flush is deferred */ +const DEBOUNCE_MS = Number.isFinite(environment.auth.sessionLastUsedDebounceMs) + ? Math.max(1_000, environment.auth.sessionLastUsedDebounceMs) + : 120_000; + +/** Maximum time a batch can stay buffered before being force-flushed regardless of activity */ +const MAX_WAIT_MS = Number.isFinite(environment.auth.sessionLastUsedMaxWaitMs) + ? Math.max(DEBOUNCE_MS, environment.auth.sessionLastUsedMaxWaitMs) + : 600_000; + +/** Redis hash key prefix for buffered session updates */ +const BUFFER_PREFIX = 'app:session-buf:'; + +/** Redis key TTL — safety net to auto-clean orphaned keys */ +const BUFFER_TTL_S = Math.ceil((MAX_WAIT_MS + DEBOUNCE_MS) / 1000) + 60; + +// ============================================================================ +// Types +// ============================================================================ + +interface SessionFlushJob { + sessionId: string; +} + +// ============================================================================ +// Queue Instance +// ============================================================================ + +let queue: Queue | null = null; +let worker: Worker | null = null; + +const getSessionUpdateQueue = async (): Promise> => { + if (queue) return queue; + + queue = new Queue(QUEUE_NAME, { + connection: createBullMqConnection(), + defaultJobOptions: DEFAULT_BULLMQ_JOB_OPTIONS + }); + + logger.info('Session update queue initialized'); + return queue; +}; + +// ============================================================================ +// Worker +// ============================================================================ + +/** + * Flush handler — runs when a delayed job fires. + * Decides whether to flush to DB or re-schedule if there's been recent activity. + */ +const processFlushJob = async (job: Job): Promise => { + const { sessionId } = job.data; + const redis = await getRedisClient(); + const key = `${BUFFER_PREFIX}${sessionId}`; + + const data = await redis.hgetall(key); + const latestRaw = data['latestMs'] ?? data['latest']; + const batchStartRaw = data['batchStartMs'] ?? data['batchStart']; + + if (!latestRaw || !batchStartRaw) { + return; + } + + const now = Date.now(); + const latestMs = Number(latestRaw); + const batchStartMs = Number(batchStartRaw); + if (!Number.isFinite(latestMs) || !Number.isFinite(batchStartMs)) { + await redis.del(key); + return; + } + const timeSinceLatest = now - latestMs; + const batchAge = now - batchStartMs; + + if (timeSinceLatest < DEBOUNCE_MS && batchAge + DEBOUNCE_MS <= MAX_WAIT_MS) { + const nextDelay = Math.min(DEBOUNCE_MS, MAX_WAIT_MS - batchAge); + const q = await getSessionUpdateQueue(); + await q.add('flush-session', { sessionId }, { delay: nextDelay }); + + logger.debug('Session flush re-debounced', { + sessionId, + nextDelayMs: nextDelay, + batchAgeMs: batchAge + }); + return; + } + + try { + await prismaClient.loginSession.update({ + where: { id: BigInt(sessionId) }, + data: { lastUsed: new Date(latestMs) } + }); + + logger.debug('Session lastUsed flushed to DB', { + sessionId, + batchAgeMs: batchAge, + timestampMs: latestMs + }); + } catch (error) { + if (error instanceof Error && error.message.includes('Record to update not found')) { + logger.debug('Session not found for lastUsed flush (likely revoked)', { sessionId }); + } else { + throw error; + } + } + + await redis.del(key); +}; + +export const initSessionUpdateWorker = async (): Promise> => { + if (worker) return worker; + + worker = new Worker(QUEUE_NAME, processFlushJob, { + connection: createBullMqConnection(), + concurrency: 5 + }); + + worker.on('failed', (job, error) => { + logger.error('Session flush job failed', { + jobId: job?.id, + sessionId: job?.data.sessionId, + error: error.message + }); + }); + + logger.info('Session update worker initialized'); + return worker; +}; + +// ============================================================================ +// Public API +// ============================================================================ + +/** + * Buffer a session lastUsed update. + * + * Each call stores the current timestamp in Redis. Only the first call in a batch + * schedules a delayed flush job. The job will either flush or re-debounce when it fires. + */ +export const queueSessionLastUsedUpdate = async (sessionId: bigint): Promise => { + try { + const redis = await getRedisClient(); + const key = `${BUFFER_PREFIX}${sessionId.toString()}`; + const nowMs = Date.now(); + + const txResults = await redis + .multi() + .hset(key, 'latestMs', String(nowMs)) + .hsetnx(key, 'batchStartMs', String(nowMs)) + .expire(key, BUFFER_TTL_S) + .exec(); + + if (!txResults) { + throw new Error('Failed to buffer session update'); + } + + const hsetnxResult = txResults[1]?.[1]; + const isNewBatch = hsetnxResult === 1; + + if (isNewBatch) { + const q = await getSessionUpdateQueue(); + await q.add('flush-session', { sessionId: sessionId.toString() }, { delay: DEBOUNCE_MS }); + + logger.debug('Session update batch started', { + sessionId: sessionId.toString(), + debounceMs: DEBOUNCE_MS + }); + } + + return true; + } catch (error) { + logger.warn('Failed to buffer session lastUsed update', { + sessionId: sessionId.toString(), + error: error instanceof Error ? error.message : String(error) + }); + return false; + } +}; + +// ============================================================================ +// Shutdown +// ============================================================================ + +export const shutdownSessionQueue = async (): Promise => { + if (worker) { + await worker.close(); + worker = null; + logger.info('Session update worker closed'); + } + + if (queue) { + await queue.close(); + queue = null; + logger.info('Session update queue closed'); + } +}; diff --git a/src/app/http/modules/auth/repository.ts b/src/app/http/modules/auth/repository.ts new file mode 100644 index 0000000..2174556 --- /dev/null +++ b/src/app/http/modules/auth/repository.ts @@ -0,0 +1,348 @@ +/** + * Auth Module Repository + * Database operations for authentication + */ + +import { CodeVerificationPurpose, GalleryStatus, MagicTokenPurpose, Platform } from '@database/prisma'; +import { BaseRepository } from '@core/database/base.repository'; +import environment from '@config/environment.config'; +import { UserWithRole, UserWithPassword, VerificationWithUser, UserFullProfile } from './types'; + +// ============================================================================ +// Include Configurations +// ============================================================================ + +const ROLE_WITH_PERMISSIONS_INCLUDE = { + permission: { select: { permissions: true } } +} as const; + +const USER_WITH_ROLE_INCLUDE = { + role: { include: ROLE_WITH_PERMISSIONS_INCLUDE } +} as const; + +const USER_FULL_PROFILE_INCLUDE = { + role: { include: ROLE_WITH_PERMISSIONS_INCLUDE }, + avatar: true +} as const; + +const USER_WITH_PASSWORD_INCLUDE = { + role: { include: ROLE_WITH_PERMISSIONS_INCLUDE }, + passwords: { + where: { status: true, expired: false }, + orderBy: { createdAt: 'desc' as const }, + take: 1 + } +} as const; + +const VERIFICATION_WITH_USER_INCLUDE = { + user: { include: USER_WITH_ROLE_INCLUDE } +} as const; + +const PLACEHOLDER_EMAIL_SUFFIX = environment.auth.placeholderEmailSuffix; + +// ============================================================================ +// Repository Class +// ============================================================================ + +export class AuthRepository extends BaseRepository { + // ======================================================================== + // User Operations + // ======================================================================== + + async findUserByPhone(countryCode: string, phone: string): Promise { + return this.prisma.user.findFirst({ + where: { countryCode, phone }, + include: USER_WITH_ROLE_INCLUDE + }) as Promise; + } + + async findUserByEmail(email: string): Promise { + return this.prisma.user.findFirst({ + where: { + email: email.toLowerCase(), + NOT: { + email: { + endsWith: PLACEHOLDER_EMAIL_SUFFIX + } + } + }, + include: USER_WITH_PASSWORD_INCLUDE + }) as Promise; + } + + async findUserById(id: bigint): Promise { + return this.prisma.user.findUnique({ + where: { id }, + include: USER_WITH_ROLE_INCLUDE + }) as Promise; + } + + async findUserFullProfile(id: bigint): Promise { + return this.prisma.user.findUnique({ + where: { id }, + include: USER_FULL_PROFILE_INCLUDE + }) as Promise; + } + + async updateUserAvatar(userId: bigint, avatarId: bigint | null): Promise { + return this.prisma.user.update({ + where: { id: userId }, + data: { avatarId }, + include: USER_FULL_PROFILE_INCLUDE + }) as Promise; + } + + async findGalleryForUser(galleryId: bigint, userId: bigint) { + return this.prisma.gallery.findFirst({ + where: { + id: galleryId, + uploadedById: userId, + status: GalleryStatus.uploaded + } + }); + } + + async createUser(countryCode: string, phone: string, roleId: bigint): Promise { + return this.prisma.user.create({ + data: { + firstName: '', + lastName: '', + email: `${countryCode}${phone}${PLACEHOLDER_EMAIL_SUFFIX}`, + countryCode, + phone, + roleId + }, + include: USER_WITH_ROLE_INCLUDE + }) as Promise; + } + + async updateUser(userId: bigint, data: Record): Promise { + return this.prisma.user.update({ + where: { id: userId }, + data, + include: USER_WITH_ROLE_INCLUDE + }) as Promise; + } + + async incrementLoginAttempts( + userId: bigint, + currentAttempts: number, + maxAttempts: number, + lockoutMinutes: number + ): Promise<{ newAttempts: number; isLocked: boolean }> { + const newAttempts = currentAttempts + 1; + const isLocked = newAttempts >= maxAttempts; + + await this.prisma.user.update({ + where: { id: userId }, + data: { + loginAttempts: newAttempts, + ...(isLocked && { + inactiveTill: new Date(Date.now() + lockoutMinutes * 60 * 1000) + }) + } + }); + + return { newAttempts, isLocked }; + } + + // ======================================================================== + // Role Operations + // ======================================================================== + + async findRoleBySlug(slug: string) { + return this.prisma.role.findUnique({ + where: { slug } + }); + } + + // ======================================================================== + // Verification (OTP) Operations + // ======================================================================== + + async createVerification( + userId: bigint, + reference: string, + purpose: CodeVerificationPurpose, + code: number, + expiresAt: Date + ) { + return this.prisma.codeVerification.create({ + data: { + userId, + reference, + purpose, + phoneCode: code, + expiredAt: expiresAt + } + }); + } + + async findVerificationByReference(reference: string): Promise { + return this.prisma.codeVerification.findUnique({ + where: { reference }, + include: VERIFICATION_WITH_USER_INCLUDE + }) as Promise; + } + + async updateVerificationForResend(id: bigint, code: number, expiresAt: Date, resendAttempts: number) { + return this.prisma.codeVerification.update({ + where: { id }, + data: { + phoneCode: code, + expiredAt: expiresAt, + resendAttempts, + lastResendAt: new Date() + } + }); + } + + async deleteVerification(id: bigint) { + return this.prisma.codeVerification.delete({ + where: { id } + }); + } + + // ======================================================================== + // Session Operations + // ======================================================================== + + async createSession( + userId: bigint, + token: string, + userAgent: string, + ipAddress: string, + expiresAt: Date, + verified = true + ) { + return this.prisma.loginSession.create({ + data: { + userId, + token, + userAgent: { raw: userAgent }, + ipAddress, + expiresAt, + verified + } + }); + } + + async revokeSession(sessionId: bigint) { + return this.prisma.loginSession.update({ + where: { id: sessionId }, + data: { + status: false, + revokedAt: new Date() + } + }); + } + + async refreshSession(sessionId: bigint, newToken: string, expiresAt: Date) { + return this.prisma.loginSession.update({ + where: { id: sessionId }, + data: { + token: newToken, + expiresAt, + lastUsed: new Date() + } + }); + } + + async revokeAllUserSessions(userId: bigint) { + return this.prisma.loginSession.updateMany({ + where: { userId, status: true }, + data: { + status: false, + revokedAt: new Date() + } + }); + } + + // ======================================================================== + // FCM Token Operations + // ======================================================================== + + async upsertFcmToken( + userId: bigint, + fcmToken: string, + identifier: string, + platform: Platform, + device: Record + ) { + const deviceJson = device as Parameters[0]['data']['device']; + + return this.prisma.fcmToken.upsert({ + where: { identifier }, + update: { + token: fcmToken, + platform, + device: deviceJson, + lastSeenAt: new Date(), + status: true + }, + create: { + userId, + token: fcmToken, + identifier, + platform, + device: deviceJson, + lastSeenAt: new Date() + } + }); + } + + // ======================================================================== + // Magic Token (Password Reset) Operations + // ======================================================================== + + async createPasswordResetToken(userId: bigint, token: string, email: string, expiresAt: Date) { + return this.prisma.magicToken.create({ + data: { + userId, + token, + email, + purpose: MagicTokenPurpose.password_reset, + expiresAt + } + }); + } + + async findMagicTokenByToken(token: string) { + return this.prisma.magicToken.findUnique({ + where: { token } + }); + } + + // ======================================================================== + // Transaction: Reset Password + // ======================================================================== + + async resetPassword(userId: bigint, hashedPassword: string, salt: string, tokenId: bigint) { + return this.transaction(async tx => { + await tx.password.updateMany({ + where: { userId, status: true }, + data: { status: false, expired: true, expiredAt: new Date() } + }); + + await tx.password.create({ + data: { userId, password: hashedPassword, salt } + }); + + await tx.magicToken.update({ + where: { id: tokenId }, + data: { used: true, usedAt: new Date() } + }); + + await tx.loginSession.updateMany({ + where: { userId, status: true }, + data: { status: false, revokedAt: new Date() } + }); + }); + } +} + +// ============================================================================ +// Export Singleton +// ============================================================================ + +export default new AuthRepository(); diff --git a/src/app/http/modules/auth/routes.ts b/src/app/http/modules/auth/routes.ts new file mode 100644 index 0000000..bd94244 --- /dev/null +++ b/src/app/http/modules/auth/routes.ts @@ -0,0 +1,31 @@ +/** + * Auth Routes + */ + +import { Router } from 'express'; + +// Middleware imports +import { authCheck, authRefresh, loginRateLimiter, otpRateLimiter, passwordResetRateLimiter } from '@middleware/index'; + +// Module imports +import controller from './controller'; + +const router = Router(); + +// OTP-based authentication +router.post('/phone', otpRateLimiter, controller.sendOTP); +router.post('/verify/:type', controller.verifyOTP); +router.post('/otp/resend', otpRateLimiter, controller.resendOTP); + +// Password-based authentication +router.post('/login', loginRateLimiter, controller.passwordLogin); +router.post('/password/forgot', passwordResetRateLimiter, controller.forgotPassword); +router.post('/password/reset', controller.resetPassword); + +// Session management +router.get('/me', authCheck, controller.me); +router.put('/me/avatar', authCheck, controller.updateAvatar); +router.post('/logout', authCheck, controller.logout); +router.post('/refresh', authRefresh, controller.refreshToken); + +export default router; diff --git a/src/app/http/modules/auth/service.ts b/src/app/http/modules/auth/service.ts new file mode 100644 index 0000000..cc7ea41 --- /dev/null +++ b/src/app/http/modules/auth/service.ts @@ -0,0 +1,425 @@ +/** + * Auth Service + * Business logic for authentication + */ + +import { CodeVerificationPurpose, Platform } from '@database/prisma'; + +import { AUTH, ROLES } from '@core/constants'; +import { generateOTP, generateRandomString } from '@core/utils'; +import { generateTokenPair, generateSalt, hashPassword, verifyPassword as verifyPwd } from '@services/index'; +import { generateViewUrl, STORAGE_BUCKETS } from '@services/storage'; +import environment from '@config/environment.config'; + +import repository from './repository'; +import type { + FcmData, + UserWithRole, + UserResponse, + AvatarData, + RoleData, + RequestMeta, + SessionTokens, + ResendResult, + TwoFactorStatus +} from './types'; +import { setCachedSession, invalidateCachedSession, invalidateAllUserSessions, CachedSession } from './cache'; + +// ============================================================================ +// Helper Functions +// ============================================================================ + +const { isDev } = environment.basic; + +const PLATFORM_MAP: Record = { + ios: Platform.ios, + web: Platform.web, + android: Platform.android +}; +const PLACEHOLDER_EMAIL_SUFFIX = environment.auth.placeholderEmailSuffix; + +const getPlatformValue = (platform?: string): Platform => PLATFORM_MAP[platform ?? ''] ?? Platform.android; + +const calculateSessionExpiry = (): Date => { + const refreshExpiresIn = environment.jwt?.refreshExpiresIn || '7d'; + const days = parseInt(refreshExpiresIn.replace(/\D/g, ''), 10) || 7; + return new Date(Date.now() + days * 24 * 60 * 60 * 1000); +}; + +const formatRoleResponse = (role: UserWithRole['role']): RoleData | undefined => { + if (!role) return undefined; + return { + id: role.id.toString(), + name: role.name, + slug: role.slug, + for_app: role.forApp, + password_required: role.passwordRequired + }; +}; + +const generateOtpCode = (): number => (isDev ? AUTH.DEV_OTP_CODE : generateOTP(6)); + +const createExpiry = (minutes: number): Date => new Date(Date.now() + minutes * 60 * 1000); + +/** + * Extract permissions from user's role (1:1 relation) + */ +const extractPermissions = (user: UserWithRole): CachedSession['permissions'] => { + const permRecord = user.role?.permission; + if (!permRecord?.permissions || typeof permRecord.permissions !== 'object') return {}; + return permRecord.permissions as CachedSession['permissions']; +}; + +/** + * Convert user and session to cacheable format + */ +const toCacheableSession = ( + sessionId: bigint, + token: string, + expiresAt: Date, + user: UserWithRole, + permissions: CachedSession['permissions'] = {}, + sessionVerified = true +): CachedSession => ({ + sessionId: sessionId.toString(), + userId: user.id.toString(), + token, + roleSlug: user.role?.slug ?? ROLES.CUSTOMER, + status: true, + verified: sessionVerified, + expiresAt: expiresAt.toISOString(), + permissions, + user: { + id: user.id.toString(), + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + phone: user.phone, + countryCode: user.countryCode, + status: user.status, + verified: user.verified, + roleId: user.roleId.toString(), + role: { + id: user.role!.id.toString(), + name: user.role!.name, + slug: user.role!.slug, + forApp: user.role!.forApp, + passwordRequired: user.role!.passwordRequired + } + } +}); + +// ============================================================================ +// Auth Service +// ============================================================================ + +export class AuthService { + async generateAvatarUrl( + avatar: { id: bigint; reference: string; isPublic: boolean; status: string } | null + ): Promise { + if (!avatar || avatar.status !== 'uploaded') return null; + + try { + const bucket = avatar.isPublic ? STORAGE_BUCKETS.PUBLIC : STORAGE_BUCKETS.DOCUMENTS; + const viewUrlResult = await generateViewUrl(avatar.reference, bucket); + return { + id: avatar.id.toString(), + url: viewUrlResult.viewUrl + }; + } catch { + return { + id: avatar.id.toString(), + url: null + }; + } + } + + formatUserResponse(user: UserWithRole, includeContactInfo = true, avatarData?: AvatarData | null): UserResponse { + const isProfileComplete = Boolean(user.firstName && user.lastName && !user.email.endsWith(PLACEHOLDER_EMAIL_SUFFIX)); + + const response: UserResponse = { + id: user.id.toString(), + first_name: user.firstName, + last_name: user.lastName, + email: user.email, + role: formatRoleResponse(user.role), + avatar: avatarData ?? null + }; + + if (includeContactInfo) { + response.phone = user.phone; + response.country_code = user.countryCode; + response.is_profile_complete = isProfileComplete; + response.is_verified = user.verified; + } + + return response; + } + + getRequestMeta(req: { + get: (name: string) => string | undefined; + ip?: string | undefined; + socket?: { remoteAddress?: string | undefined } | undefined; + }): RequestMeta { + return { + userAgent: req.get('user-agent') || '', + ipAddress: req.ip ?? req.socket?.remoteAddress ?? '0.0.0.0' + }; + } + + async findUserByPhone(countryCode: string, phone: string) { + return repository.findUserByPhone(countryCode, phone); + } + + async findUserByEmail(email: string) { + return repository.findUserByEmail(email); + } + + async findUserById(id: bigint) { + return repository.findUserById(id); + } + + async findUserFullProfile(id: bigint) { + return repository.findUserFullProfile(id); + } + + async updateUserAvatar(userId: bigint, avatarId: bigint | null) { + return repository.updateUserAvatar(userId, avatarId); + } + + async verifyGalleryOwnership(galleryId: bigint, userId: bigint) { + return repository.findGalleryForUser(galleryId, userId); + } + + async getCustomerRole() { + return repository.findRoleBySlug(ROLES.CUSTOMER); + } + + async createUser(countryCode: string, phone: string, roleId: bigint) { + return repository.createUser(countryCode, phone, roleId); + } + + async updateUserLogin(userId: bigint, updateData: Record) { + return repository.updateUser(userId, updateData); + } + + async incrementLoginAttempts(userId: bigint, currentAttempts: number, maxAttempts: number, lockoutMinutes: number) { + return repository.incrementLoginAttempts(userId, currentAttempts, maxAttempts, lockoutMinutes); + } + + /** + * Create verification record (OTP, 2FA, MFA, email verification) + */ + async createVerification( + userId: bigint, + purpose: CodeVerificationPurpose, + expiryMinutes?: number + ): Promise<{ code: number; reference: string; expiresIn: number }> { + const minutes = expiryMinutes ?? this.getDefaultExpiryMinutes(purpose); + const code = generateOtpCode(); + const reference = generateRandomString(32); + + await repository.createVerification(userId, reference, purpose, code, createExpiry(minutes)); + + return { code, reference, expiresIn: minutes * 60 }; + } + + private getDefaultExpiryMinutes(purpose: CodeVerificationPurpose): number { + switch (purpose) { + case CodeVerificationPurpose.two_factor_auth: + case CodeVerificationPurpose.mfa_auth: + return AUTH.TWO_FACTOR_EXPIRY_MINUTES; + default: + return AUTH.OTP_EXPIRY_MINUTES; + } + } + + async findVerification(reference: string) { + return repository.findVerificationByReference(reference); + } + + async deleteVerification(id: bigint) { + return repository.deleteVerification(id); + } + + async resendOtp(verification: { + id: bigint; + resendAttempts: number; + lastResendAt: Date | null; + }): Promise { + if (verification.resendAttempts >= AUTH.MAX_RESEND_ATTEMPTS) { + return { success: false, error: 'max_attempts' }; + } + + const timeSinceLastResend = verification.lastResendAt + ? Date.now() - verification.lastResendAt.getTime() + : Infinity; + + if (timeSinceLastResend < AUTH.RESEND_COOLDOWN_MS) { + return { success: false, error: 'rate_limit' }; + } + + const newCode = generateOtpCode(); + + await repository.updateVerificationForResend( + verification.id, + newCode, + createExpiry(AUTH.OTP_EXPIRY_MINUTES), + verification.resendAttempts + 1 + ); + + return { + success: true, + code: newCode, + attemptsRemaining: AUTH.MAX_RESEND_ATTEMPTS - verification.resendAttempts - 1 + }; + } + + async createLoginSession( + userId: bigint, + userAgent: string, + ipAddress: string, + user: UserWithRole, + verified = true + ): Promise { + const roleSlug = user.role?.slug ?? ROLES.CUSTOMER; + const { token, tokenExpiry, keys } = generateTokenPair({ roleSlug }); + const expiresAt = calculateSessionExpiry(); + const permissions = extractPermissions(user); + + const session = await repository.createSession(userId, token, userAgent, ipAddress, expiresAt, verified); + + if (verified) { + const cacheable = toCacheableSession(session.id, token, expiresAt, user, permissions); + setCachedSession(roleSlug, token, cacheable).catch(() => {}); + } + + return { token, token_expiry: tokenExpiry, keys }; + } + + async completeLogin( + user: UserWithRole, + req: { + get: (name: string) => string | undefined; + ip?: string | undefined; + socket?: { remoteAddress?: string | undefined } | undefined; + }, + fcmData: FcmData, + additionalUpdateData?: Record + ): Promise<{ + session: SessionTokens; + userResponse: UserResponse; + twoFactorStatus: TwoFactorStatus; + permissions: Record; + }> { + await this.upsertFcmToken(user.id, fcmData); + + const { userAgent, ipAddress } = this.getRequestMeta(req); + const session = await this.createLoginSession(user.id, userAgent, ipAddress, user, true); + + const updateData: Record = { + lastLoginAt: new Date(), + loginAttempts: 0, + inactiveTill: null, + ...additionalUpdateData + }; + await this.updateUserLogin(user.id, updateData); + + const userWithAvatar = await repository.findUserFullProfile(user.id); + const avatarData = userWithAvatar?.avatar ? await this.generateAvatarUrl(userWithAvatar.avatar) : null; + + const userResponse = this.formatUserResponse(user, true, avatarData); + const { twoFactorEnabled, mfaEnabled } = this.requires2FA(user); + + const permissions = extractPermissions(user); + + return { session, userResponse, twoFactorStatus: { twoFactorEnabled, mfaEnabled }, permissions }; + } + + async logoutSession(sessionId: bigint, token: string, userId: bigint, roleSlug: string): Promise { + await repository.revokeSession(sessionId); + + invalidateCachedSession(roleSlug, token, userId.toString()).catch(() => {}); + } + + async refreshSession( + sessionId: bigint, + oldToken: string, + user: UserWithRole + ): Promise }> { + const roleSlug = user.role?.slug ?? ROLES.CUSTOMER; + const { token, tokenExpiry, keys } = generateTokenPair({ roleSlug }); + const expiresAt = calculateSessionExpiry(); + const permissions = extractPermissions(user); + + await repository.refreshSession(sessionId, token, expiresAt); + + invalidateCachedSession(roleSlug, oldToken, user.id.toString()).catch(() => {}); + + const cacheable = toCacheableSession(sessionId, token, expiresAt, user, permissions); + setCachedSession(roleSlug, token, cacheable).catch(() => {}); + + return { token, token_expiry: tokenExpiry, keys, permissions }; + } + + async revokeAllUserSessions(userId: bigint): Promise { + await repository.revokeAllUserSessions(userId); + + invalidateAllUserSessions(userId).catch(() => {}); + } + + async upsertFcmToken(userId: bigint, data: FcmData): Promise { + if (!data.fcm_token || !data.identifier) return; + + await repository.upsertFcmToken( + userId, + data.fcm_token, + data.identifier, + getPlatformValue(data.platform), + data.device || {} + ); + } + + verifyPassword(password: string, hash: string, salt: string): boolean { + return verifyPwd(password, hash, salt); + } + + async createPasswordResetToken(userId: bigint, email: string): Promise { + const token = generateRandomString(32); + const expiresAt = new Date(Date.now() + AUTH.PASSWORD_RESET_EXPIRY_MS); + + await repository.createPasswordResetToken(userId, token, email, expiresAt); + + return token; + } + + async findPasswordResetToken(token: string) { + return repository.findMagicTokenByToken(token); + } + + async resetPassword(userId: bigint, password: string, tokenId: bigint): Promise { + const salt = generateSalt(); + const hashedPassword = hashPassword(password, salt); + + await repository.resetPassword(userId, hashedPassword, salt, tokenId); + + invalidateAllUserSessions(userId).catch(() => {}); + } + + requires2FA(user: UserWithRole): TwoFactorStatus & { requiresVerification: boolean } { + const twoFactorEnabled = user.otpAuth ?? false; + const mfaEnabled = user.mfaEnabled ?? false; + return { + twoFactorEnabled, + mfaEnabled, + requiresVerification: twoFactorEnabled || mfaEnabled + }; + } + + verifyCode(verification: { phoneCode: number | null; emailCode: number | null }, providedCode: string): boolean { + const code = parseInt(providedCode, 10); + return verification.phoneCode === code || verification.emailCode === code; + } +} + +export default new AuthService(); diff --git a/src/app/http/modules/auth/types.ts b/src/app/http/modules/auth/types.ts new file mode 100644 index 0000000..b06cc10 --- /dev/null +++ b/src/app/http/modules/auth/types.ts @@ -0,0 +1,197 @@ +/** + * Auth Module Types + * DTOs and interfaces for authentication + */ + +import { User, Role, CodeVerification, Gallery } from '@database/prisma'; +import { BaseDto } from '@core/types'; + +type Platform = 'android' | 'ios' | 'web'; + +// ============================================================================ +// Request DTOs +// ============================================================================ + +export interface SendOtpDto extends BaseDto { + /** Country code without + (e.g., "1") */ + country_code: string; + /** Phone number */ + phone: string; +} + +export interface VerifyOtpDto extends BaseDto { + /** Reference from sendOTP response */ + reference: string; + /** 6-digit OTP code */ + code: string; + /** FCM push notification token */ + fcm_token?: string; + /** Device information */ + device?: Record; + /** Unique device identifier */ + identifier?: string; + /** Platform type */ + platform?: Platform; +} + +export interface ResendOtpDto extends BaseDto { + /** Reference from sendOTP response */ + reference: string; +} + +export interface PasswordLoginDto extends BaseDto { + /** Email address */ + email: string; + /** Password (min 8 chars) */ + password: string; + /** FCM push notification token */ + fcm_token?: string; + /** Device information */ + device?: Record; + /** Unique device identifier */ + identifier?: string; + /** Platform type */ + platform?: Platform; +} + +export interface ForgotPasswordDto extends BaseDto { + /** Email address */ + email: string; +} + +export interface ResetPasswordDto extends BaseDto { + /** Reset token from email */ + token: string; + /** New password */ + password: string; + /** Password confirmation */ + password_confirmation: string; +} + +// ============================================================================ +// Response DTOs +// ============================================================================ + +export interface RoleData { + id: string; + name: string; + slug: string; + for_app: boolean; + password_required: boolean; +} + +export interface AvatarData { + id: string; + url: string | null; +} + +export interface UserResponse { + id: string; + first_name: string; + last_name: string; + email: string; + phone?: string | undefined; + country_code?: string | undefined; + role?: RoleData | undefined; + avatar?: AvatarData | null | undefined; + is_profile_complete?: boolean | undefined; + is_verified?: boolean | undefined; + phone_verified?: boolean | undefined; + email_verified?: boolean | undefined; +} + +// ============================================================================ +// Internal Types +// ============================================================================ + +export interface FcmData { + fcm_token?: string; + device?: Record; + identifier?: string; + platform?: Platform; +} + +export interface RequestMeta { + userAgent: string; + ipAddress: string; +} + +export interface SessionTokens { + token: string; + token_expiry: Date; + keys: { auth: string; refresh: string }; +} + +export interface ResendResult { + success: boolean; + error?: 'max_attempts' | 'rate_limit'; + code?: number; + attemptsRemaining?: number; +} + +// ============================================================================ +// Login Response Types +// ============================================================================ + +/** Two-factor authentication status */ +export interface TwoFactorStatus { + twoFactorEnabled: boolean; + mfaEnabled: boolean; +} + +/** Base login response - always included */ +export interface LoginResponseBase { + two_factor_enabled: boolean; + mfa_enabled: boolean; +} + +/** Login response when 2FA/MFA required */ +export interface LoginResponse2FARequired extends LoginResponseBase { + reference: string; + expires_in: number; +} + +/** Login response when no verification needed */ +export interface LoginResponseAuthenticated extends LoginResponseBase { + user: UserResponse & { is_verified: boolean }; + tokens: { auth: string; refresh: string }; + token_expiry: Date; + permissions: Record; +} + +// ============================================================================ +// Database Types with Relations +// ============================================================================ + +/** Permission record from database */ +export interface PermissionRecord { + permissions: unknown; // JSONB +} + +/** Role with permission (1:1 relation) */ +export interface RoleWithPermissions extends Role { + permission: PermissionRecord | null; +} + +export type UserWithRole = User & { + role: RoleWithPermissions | null; +}; + +export type UserWithPassword = User & { + role: RoleWithPermissions | null; + passwords: Array<{ + id: bigint; + password: string; + salt: string; + status: boolean; + }>; +}; + +export type VerificationWithUser = CodeVerification & { + user: UserWithRole; +}; + +export type UserFullProfile = User & { + role: RoleWithPermissions | null; + avatar: Gallery | null; +}; diff --git a/src/app/http/modules/auth/validation.ts b/src/app/http/modules/auth/validation.ts new file mode 100644 index 0000000..11251eb --- /dev/null +++ b/src/app/http/modules/auth/validation.ts @@ -0,0 +1,38 @@ +// Auth module validation rules + +export const sendOtpRules = { + country_code: 'required|string|min:1|max:5', + phone: 'required|string|min:10|max:15' +}; + +export const verifyOtpRules = { + reference: 'required|string', + code: 'required|string|digits:6', + fcm_token: 'nullable|string', + device: 'nullable|object', + identifier: 'nullable|string', + platform: 'nullable|string|in:android,ios,web' +}; + +export const resendOtpRules = { + reference: 'required|string' +}; + +export const passwordLoginRules = { + email: 'required|email', + password: 'required|string|min:8', + fcm_token: 'nullable|string', + device: 'nullable|object', + identifier: 'nullable|string', + platform: 'nullable|string|in:android,ios,web' +}; + +export const forgotPasswordRules = { + email: 'required|email' +}; + +export const resetPasswordRules = { + token: 'required|string', + password: 'required|string|min:8', + password_confirmation: 'required|string|min:8' +}; diff --git a/src/app/http/modules/gallery/controller.ts b/src/app/http/modules/gallery/controller.ts new file mode 100644 index 0000000..3957a71 --- /dev/null +++ b/src/app/http/modules/gallery/controller.ts @@ -0,0 +1,188 @@ +/** + * Gallery Controller + * Handles file upload endpoints using presigned URLs + */ + +import type { Request, Response } from 'express'; + +import { ERROR_CODES, PERMISSION_MODULES } from '@core/constants'; +import { createLogger } from '@services/index'; +import { Controller } from '@http/controllers/controller'; + +import service from './service'; +import type { UploadInitDto, UploadConfirmDto } from './types'; +import { uploadInitRules, uploadConfirmRules } from './validation'; + +const logger = createLogger('gallery'); +const GALLERY_MODULE = PERMISSION_MODULES.GALLERY; + +// ============================================================================ +// Controller +// ============================================================================ + +export class GalleryController extends Controller { + constructor() { + super(); + } + + /** + * POST /gallery/init - Initialize presigned upload + */ + public initUpload = async (req: Request, res: Response): Promise => { + const data = await this.validate(req, res, uploadInitRules); + if (!data) return; + + const userId = this.requireAuth(req, res); + if (!userId) return; + + try { + const result = await service.initUpload(userId, data); + + logger.debug('Upload initialized', { + userId: userId.toString(), + reference: result.reference, + bucket: result.bucket + }); + + return this.sendSuccessResponse(res, result, 'Upload initialized', 200); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to initialize upload'; + logger.error('Upload initialization failed', { + userId: userId.toString(), + error: message + }); + + return this.sendErrorResponse(res, { upload: message }, ERROR_CODES.VALIDATION_ERROR, message, 400); + } + }; + + /** + * POST /gallery/confirm - Confirm upload completion + */ + public confirmUpload = async (req: Request, res: Response): Promise => { + const data = await this.validate(req, res, uploadConfirmRules); + if (!data) return; + + const userId = this.requireAuth(req, res); + if (!userId) return; + + const result = await service.confirmUpload(userId, data.reference); + + if (!result.success) { + return this.sendErrorResponse( + res, + { upload: result.error }, + ERROR_CODES.VALIDATION_ERROR, + result.error ?? 'Upload confirmation failed', + 400 + ); + } + + return this.sendSuccessResponse(res, { gallery: result.gallery }, 'Upload confirmed', 200); + }; + + /** + * GET /gallery/:id - Get gallery details with view URL + */ + public get = async (req: Request, res: Response): Promise => { + const galleryId = this.parseBigIntParam(req, res, 'id', 'Gallery'); + if (!galleryId) return; + + const userId = this.getAuthUserId(req); + const gallery = await service.getById(galleryId, userId); + + if (!gallery) { + return this.sendErrorResponse( + res, + { id: 'Gallery not found' }, + ERROR_CODES.NOT_FOUND, + 'Gallery not found', + 404 + ); + } + + return this.sendSuccessResponse(res, { gallery }, 'Gallery fetched', 200); + }; + + /** + * GET /gallery/:id/url - Get presigned view URL only + */ + public getViewUrl = async (req: Request, res: Response): Promise => { + const galleryId = this.parseBigIntParam(req, res, 'id', 'Gallery'); + if (!galleryId) return; + + const userId = this.getAuthUserId(req); + const result = await service.getViewUrl(galleryId, userId); + + if (!result) { + return this.sendErrorResponse( + res, + { id: 'Gallery not found or not ready' }, + ERROR_CODES.NOT_FOUND, + 'Gallery not found or not ready', + 404 + ); + } + + return this.sendSuccessResponse(res, result, 'View URL generated', 200); + }; + + /** + * DELETE /gallery/:id - Delete gallery and file + */ + public delete = async (req: Request, res: Response): Promise => { + const galleryId = this.parseBigIntParam(req, res, 'id', 'Gallery'); + if (!galleryId) return; + + const userId = this.requireAuth(req, res); + if (!userId) return; + + const result = await service.deleteGallery(galleryId, userId); + + if (!result.success) { + const statusCode = result.error === 'Gallery not found' ? 404 : 403; + const errorCode = + result.error === 'Gallery not found' ? ERROR_CODES.NOT_FOUND : ERROR_CODES.PERMISSION_DENIED; + + return this.sendErrorResponse( + res, + { delete: result.error }, + errorCode, + result.error ?? 'Delete failed', + statusCode + ); + } + + return this.sendSuccessResponse(res, { deleted: true }, 'Gallery deleted', 200); + }; + + /** + * GET /gallery - List uploads (own or all based on scope) + */ + public list = async (req: Request, res: Response): Promise => { + const userId = this.requireAuth(req, res); + if (!userId) return; + + const limit = Math.min(parseInt(req.query['limit'] as string) || 50, 100); + const offset = parseInt(req.query['offset'] as string) || 0; + + // Get owner filter based on user's scope for this module + const ownerFilter = this.getOwnerFilter(req, GALLERY_MODULE); + + const result = await service.listUploads(ownerFilter, { limit, offset }); + + return this.sendSuccessResponse( + res, + { + galleries: result.galleries, + total: result.total, + limit, + offset + }, + 'Galleries fetched', + 200 + ); + }; +} + +export default new GalleryController(); diff --git a/src/app/http/modules/gallery/index.ts b/src/app/http/modules/gallery/index.ts new file mode 100644 index 0000000..ea2bfe7 --- /dev/null +++ b/src/app/http/modules/gallery/index.ts @@ -0,0 +1,8 @@ +/** + * Gallery Module Exports + */ + +export { default as routes } from './routes'; +export { default as service } from './service'; +export { default as repository } from './repository'; +export * from './types'; diff --git a/src/app/http/modules/gallery/repository.ts b/src/app/http/modules/gallery/repository.ts new file mode 100644 index 0000000..a7a19da --- /dev/null +++ b/src/app/http/modules/gallery/repository.ts @@ -0,0 +1,149 @@ +/** + * Gallery Repository + * Database operations for gallery/file records + */ + +import { prismaClient } from '@core/database'; +import type { Gallery, GalleryStatus } from '@database/prisma'; +import type { CreateGalleryInput } from './types'; + +/** + * Create a new gallery record + */ +export const create = async (data: CreateGalleryInput): Promise => { + return prismaClient.gallery.create({ + data: { + uploadedById: data.uploadedById, + reference: data.reference, + filetype: data.filetype, + extension: data.extension, + size: data.size, + bucket: data.bucket, + isPublic: data.isPublic, + type: data.type, + status: 'pending', + path: data.path, + ...(data.title != null && { title: data.title }), + ...(data.description != null && { description: data.description }) + } + }); +}; + +/** + * Find gallery by ID + */ +export const findById = async (id: bigint): Promise => { + return prismaClient.gallery.findUnique({ + where: { id } + }); +}; + +/** + * Find gallery by reference + */ +export const findByReference = async (reference: string): Promise => { + return prismaClient.gallery.findUnique({ + where: { reference } + }); +}; + +/** + * Update gallery status + */ +export const updateStatus = async (id: bigint, status: GalleryStatus): Promise => { + return prismaClient.gallery.update({ + where: { id }, + data: { status } + }); +}; + +/** + * Update gallery with confirmed upload data + */ +export const confirmUpload = async ( + id: bigint, + data: { size?: bigint | null; status: GalleryStatus } +): Promise => { + return prismaClient.gallery.update({ + where: { id }, + data: { + status: data.status, + ...(data.size != null && { size: data.size }) + } + }); +}; + +/** + * Delete gallery record + */ +export const deleteById = async (id: bigint): Promise => { + return prismaClient.gallery.delete({ + where: { id } + }); +}; + +/** + * Find galleries by user ID with pagination + */ +export const findByUserId = async ( + userId: bigint, + options?: { + limit?: number; + offset?: number; + status?: GalleryStatus; + } +): Promise<{ galleries: Gallery[]; total: number }> => { + const where = { + uploadedById: userId, + ...(options?.status && { status: options.status }) + }; + + const [galleries, total] = await Promise.all([ + prismaClient.gallery.findMany({ + where, + take: options?.limit ?? 50, + skip: options?.offset ?? 0, + orderBy: { createdAt: 'desc' } + }), + prismaClient.gallery.count({ where }) + ]); + + return { galleries, total }; +}; + +/** + * Find all galleries with pagination (admin scope) + */ +export const findAll = async (options?: { + limit?: number; + offset?: number; + status?: GalleryStatus; +}): Promise<{ galleries: Gallery[]; total: number }> => { + const where = { + ...(options?.status && { status: options.status }) + }; + + const [galleries, total] = await Promise.all([ + prismaClient.gallery.findMany({ + where, + take: options?.limit ?? 50, + skip: options?.offset ?? 0, + orderBy: { createdAt: 'desc' }, + include: { uploadedBy: { select: { id: true, firstName: true, lastName: true, email: true } } } + }), + prismaClient.gallery.count({ where }) + ]); + + return { galleries, total }; +}; + +export default { + create, + findById, + findByReference, + updateStatus, + confirmUpload, + deleteById, + findByUserId, + findAll +}; diff --git a/src/app/http/modules/gallery/routes.ts b/src/app/http/modules/gallery/routes.ts new file mode 100644 index 0000000..4a93ae8 --- /dev/null +++ b/src/app/http/modules/gallery/routes.ts @@ -0,0 +1,60 @@ +/** + * Gallery Routes + * File upload endpoints using presigned URLs + */ + +import { Router } from 'express'; +import { authCheck, requirePermission } from '@http/middleware/auth.middleware'; +import { PERMISSION_MODULES } from '@core/constants'; +import controller from './controller'; + +const router = Router(); + +// All routes require authentication +router.use(authCheck); + +const { GALLERY } = PERMISSION_MODULES; + +/** + * @route POST /v1/gallery/init + * @desc Initialize presigned upload URL + * @access Authenticated - requires gallery:create + */ +router.post('/init', requirePermission({ module: GALLERY, action: 'create' }), controller.initUpload); + +/** + * @route POST /v1/gallery/confirm + * @desc Confirm upload completion + * @access Authenticated - requires gallery:create + */ +router.post('/confirm', requirePermission({ module: GALLERY, action: 'create' }), controller.confirmUpload); + +/** + * @route GET /v1/gallery + * @desc List user's uploads + * @access Authenticated - requires gallery:list + */ +router.get('/', requirePermission({ module: GALLERY, action: 'list' }), controller.list); + +/** + * @route GET /v1/gallery/:id + * @desc Get gallery details with view URL + * @access Authenticated - requires gallery:read + */ +router.get('/:id', requirePermission({ module: GALLERY, action: 'read' }), controller.get); + +/** + * @route GET /v1/gallery/:id/url + * @desc Get presigned view URL only + * @access Authenticated - requires gallery:read + */ +router.get('/:id/url', requirePermission({ module: GALLERY, action: 'read' }), controller.getViewUrl); + +/** + * @route DELETE /v1/gallery/:id + * @desc Delete gallery and file + * @access Authenticated - requires gallery:delete + */ +router.delete('/:id', requirePermission({ module: GALLERY, action: 'delete' }), controller.delete); + +export default router; diff --git a/src/app/http/modules/gallery/service.ts b/src/app/http/modules/gallery/service.ts new file mode 100644 index 0000000..737483e --- /dev/null +++ b/src/app/http/modules/gallery/service.ts @@ -0,0 +1,323 @@ +/** + * Gallery Service + * Business logic for file uploads using presigned URLs + */ + +import { GalleryType } from '@database/prisma'; +import { createLogger } from '@services/logger.service'; +import { + generateUploadUrl as storageGenerateUploadUrl, + confirmUpload as storageConfirmUpload, + generateViewUrl as storageGenerateViewUrl, + deleteFile as storageDeleteFile, + getFileCategory, + STORAGE_BUCKETS, + StorageBucket +} from '@services/storage'; + +import repository from './repository'; +import type { ViewUrlResponse } from '@services/storage/types'; +import type { + UploadInitDto, + UploadInitResponse, + UploadConfirmResponse, + GalleryResponse +} from './types'; +import { toGalleryResponse } from './types'; + +const logger = createLogger('gallery'); + +/** + * Determine GalleryType from MIME type + */ +const getGalleryType = (mimeType: string): GalleryType => { + const mime = mimeType.toLowerCase(); + if (mime.startsWith('image/')) return 'image'; + if (mime.startsWith('video/')) return 'video'; + if (mime === 'application/pdf') return 'pdf'; + return 'image'; // Default to image +}; + +/** + * Extract file extension from filename + */ +const getExtension = (fileName: string): string => { + const lastDot = fileName.lastIndexOf('.'); + if (lastDot === -1) return ''; + return fileName.substring(lastDot).toLowerCase(); +}; + +/** + * Initialize upload and get presigned URL + */ +export const initUpload = async (userId: bigint, dto: UploadInitDto): Promise => { + const bucket = (dto.bucket ?? STORAGE_BUCKETS.DOCUMENTS) as StorageBucket; + const isPublic = dto.isPublic ?? bucket === STORAGE_BUCKETS.PUBLIC; + + // Validate file category + const category = getFileCategory(dto.contentType); + if (!category) { + throw new Error(`Unsupported file type: ${dto.contentType}`); + } + + // Generate presigned upload URL + const uploadResult = await storageGenerateUploadUrl({ + fileName: dto.fileName, + contentType: dto.contentType, + fileSize: dto.fileSize, + bucket, + folder: dto.folder ?? 'uploads', + isPublic + }); + + // Create gallery record in pending state + const gallery = await repository.create({ + uploadedById: userId, + reference: uploadResult.reference, + title: dto.title ?? null, + description: dto.description ?? null, + filetype: dto.contentType, + extension: getExtension(dto.fileName), + size: BigInt(dto.fileSize), + bucket, + isPublic, + type: getGalleryType(dto.contentType), + path: uploadResult.path + }); + + logger.info('Upload initialized', { + userId: userId.toString(), + galleryId: gallery.id.toString(), + reference: uploadResult.reference, + bucket, + contentType: dto.contentType + }); + + return { + reference: uploadResult.reference, + uploadUrl: uploadResult.uploadUrl, + bucket: uploadResult.bucket, + path: uploadResult.path, + expiresIn: uploadResult.expiresIn, + galleryId: gallery.id.toString() + }; +}; + +/** + * Confirm upload completion + */ +export const confirmUpload = async (userId: bigint, reference: string): Promise => { + // Find gallery record + const gallery = await repository.findByReference(reference); + if (!gallery) { + return { + success: false, + error: 'Upload reference not found' + }; + } + + // Verify ownership + if (gallery.uploadedById !== userId) { + return { + success: false, + error: 'Not authorized to confirm this upload' + }; + } + + // Check if already confirmed + if (gallery.status === 'uploaded') { + const viewUrlResult = await storageGenerateViewUrl(gallery.reference, gallery.bucket as StorageBucket); + return { + success: true, + gallery: toGalleryResponse(gallery, viewUrlResult.viewUrl) + }; + } + + // Check if failed + if (gallery.status === 'failed') { + return { + success: false, + error: 'Upload has failed or expired' + }; + } + + // Confirm with storage service + const confirmResult = await storageConfirmUpload( + { reference, expectedContentType: gallery.filetype }, + gallery.bucket as StorageBucket + ); + + if (!confirmResult.success) { + // Mark as failed in database + await repository.updateStatus(gallery.id, 'failed'); + logger.warn('Upload confirmation failed', { + galleryId: gallery.id.toString(), + reference, + error: confirmResult.error + }); + return { + success: false, + error: confirmResult.error ?? 'Upload confirmation failed' + }; + } + + // Update gallery record + const updatedGallery = await repository.confirmUpload(gallery.id, { + size: confirmResult.actualFileSize != null ? BigInt(confirmResult.actualFileSize) : null, + status: 'uploaded' + }); + + logger.info('Upload confirmed', { + galleryId: gallery.id.toString(), + reference, + size: confirmResult.actualFileSize + }); + + return { + success: true, + gallery: toGalleryResponse(updatedGallery, confirmResult.viewUrl) + }; +}; + +/** + * Get gallery by ID with view URL + */ +export const getById = async (id: bigint, userId?: bigint): Promise => { + const gallery = await repository.findById(id); + if (!gallery) return null; + + // Check ownership if userId provided (non-public file) + if (!gallery.isPublic && userId && gallery.uploadedById !== userId) { + return null; + } + + // Only generate view URL for uploaded files + let viewUrl: string | undefined; + if (gallery.status === 'uploaded') { + try { + const viewUrlResult = await storageGenerateViewUrl(gallery.reference, gallery.bucket as StorageBucket); + viewUrl = viewUrlResult.viewUrl; + } catch (error) { + logger.warn('Failed to generate view URL', { + galleryId: gallery.id.toString(), + error: error instanceof Error ? error.message : 'Unknown error' + }); + } + } + + return toGalleryResponse(gallery, viewUrl); +}; + +/** + * Generate view URL for existing gallery + */ +export const getViewUrl = async (id: bigint, userId?: bigint): Promise => { + const gallery = await repository.findById(id); + if (!gallery) return null; + + // Check ownership if not public + if (!gallery.isPublic && userId && gallery.uploadedById !== userId) { + return null; + } + + // Only for uploaded files + if (gallery.status !== 'uploaded') { + return null; + } + + return storageGenerateViewUrl(gallery.reference, gallery.bucket as StorageBucket); +}; + +/** + * Delete gallery and file from storage + */ +export const deleteGallery = async (id: bigint, userId: bigint): Promise<{ success: boolean; error?: string }> => { + const gallery = await repository.findById(id); + if (!gallery) { + return { success: false, error: 'Gallery not found' }; + } + + // Verify ownership + if (gallery.uploadedById !== userId) { + return { success: false, error: 'Not authorized to delete this file' }; + } + + // Delete from storage (only if uploaded) + if (gallery.status === 'uploaded') { + try { + await storageDeleteFile(gallery.reference, gallery.bucket as StorageBucket); + } catch (error) { + logger.warn('Failed to delete file from storage', { + galleryId: gallery.id.toString(), + error: error instanceof Error ? error.message : 'Unknown error' + }); + // Continue with database deletion even if storage deletion fails + } + } + + // Delete from database (may fail if gallery is referenced by other records) + try { + await repository.deleteById(id); + } catch (error: unknown) { + // Handle foreign key constraint violation (P2003) — gallery is still in use + const prismaError = error as Error & { code?: string }; + if (prismaError.code === 'P2003') { + logger.warn('Cannot delete gallery — still referenced by other records', { + galleryId: gallery.id.toString() + }); + return { + success: false, + error: 'Cannot delete file — it is still in use by another resource (e.g., avatar, document)' + }; + } + throw error; + } + + logger.info('Gallery deleted', { + galleryId: gallery.id.toString(), + userId: userId.toString() + }); + + return { success: true }; +}; + +/** + * List uploads with optional owner filter + * @param ownerId - If provided, filter to this user's uploads. If undefined, return all (admin scope) + */ +export const listUploads = async ( + ownerId: bigint | undefined, + options?: { limit?: number; offset?: number } +): Promise<{ galleries: GalleryResponse[]; total: number }> => { + const result = ownerId + ? await repository.findByUserId(ownerId, { ...options, status: 'uploaded' }) + : await repository.findAll({ ...options, status: 'uploaded' }); + + // Generate view URLs for all galleries + const galleriesWithUrls = await Promise.all( + result.galleries.map(async gallery => { + let viewUrl: string | undefined; + try { + const viewUrlResult = await storageGenerateViewUrl(gallery.reference, gallery.bucket as StorageBucket); + viewUrl = viewUrlResult.viewUrl; + } catch { + // Ignore errors for individual files + } + return toGalleryResponse(gallery, viewUrl); + }) + ); + + return { + galleries: galleriesWithUrls, + total: result.total + }; +}; + +export default { + initUpload, + confirmUpload, + getById, + getViewUrl, + deleteGallery, + listUploads +}; diff --git a/src/app/http/modules/gallery/types.ts b/src/app/http/modules/gallery/types.ts new file mode 100644 index 0000000..1d441ed --- /dev/null +++ b/src/app/http/modules/gallery/types.ts @@ -0,0 +1,145 @@ +/** + * Gallery Module Types + * Type definitions for file upload operations + */ + +import type { Gallery, GalleryStatus, GalleryType } from '@database/prisma'; +import type { StorageBucket } from '@services/storage'; + +// ============================================================================ +// DTOs +// ============================================================================ + +/** + * Request to initialize a presigned upload + */ +export interface UploadInitDto extends Record { + /** Original filename */ + fileName: string; + /** MIME type of the file */ + contentType: string; + /** File size in bytes */ + fileSize: number; + /** Target bucket (documents or public) */ + bucket?: StorageBucket; + /** Folder path within the bucket */ + folder?: string; + /** Whether the file should be publicly accessible */ + isPublic?: boolean; + /** Optional title for the file */ + title?: string; + /** Optional description */ + description?: string; +} + +/** + * Request to confirm upload completion + */ +export interface UploadConfirmDto extends Record { + /** Upload reference from init response */ + reference: string; +} + +// ============================================================================ +// API Responses +// ============================================================================ + +/** + * Upload initialization response + */ +export interface UploadInitResponse { + /** Unique reference for the upload */ + reference: string; + /** Presigned URL for uploading */ + uploadUrl: string; + /** Target bucket */ + bucket: StorageBucket; + /** File path in storage */ + path: string; + /** URL expiry in seconds */ + expiresIn: number; + /** Gallery record ID */ + galleryId: string; +} + +/** + * Upload confirmation response + */ +export interface UploadConfirmResponse { + /** Whether confirmation was successful */ + success: boolean; + /** Gallery record with view URL */ + gallery?: GalleryResponse; + /** Error message if failed */ + error?: string; +} + +/** + * Gallery record response + */ +export interface GalleryResponse { + id: string; + reference: string; + title: string | null; + description: string | null; + filetype: string; + extension: string; + size: string; + height: number | null; + width: number | null; + bucket: string; + is_public: boolean; + type: GalleryType; + status: GalleryStatus; + view_url?: string; + created_at: string; + updated_at: string; +} + +// ============================================================================ +// Internal Types +// ============================================================================ + +/** + * Gallery creation input + */ +export interface CreateGalleryInput { + uploadedById: bigint; + reference: string; + title?: string | null; + description?: string | null; + filetype: string; + extension: string; + size: bigint; + bucket: string; + isPublic: boolean; + type: GalleryType; + path: string; +} + +/** + * Transform gallery to response format + */ +export const toGalleryResponse = (gallery: Gallery, viewUrl?: string): GalleryResponse => { + const response: GalleryResponse = { + id: gallery.id.toString(), + reference: gallery.reference, + title: gallery.title, + description: gallery.description, + filetype: gallery.filetype, + extension: gallery.extension, + size: gallery.size.toString(), + height: gallery.height, + width: gallery.width, + bucket: gallery.bucket, + is_public: gallery.isPublic, + type: gallery.type, + status: gallery.status, + created_at: gallery.createdAt.toISOString(), + updated_at: gallery.updatedAt.toISOString() + }; + if (viewUrl !== undefined) { + response.view_url = viewUrl; + } + return response; +}; diff --git a/src/app/http/modules/gallery/validation.ts b/src/app/http/modules/gallery/validation.ts new file mode 100644 index 0000000..00b0d5d --- /dev/null +++ b/src/app/http/modules/gallery/validation.ts @@ -0,0 +1,18 @@ +/** + * Gallery Module Validation Rules + */ + +export const uploadInitRules = { + fileName: 'required|string|min:1|max:255', + contentType: 'required|string|min:3|max:100', + fileSize: 'required|integer|min:1', + bucket: 'string|in:documents,public', + folder: 'string|max:255', + isPublic: 'boolean', + title: 'string|max:255', + description: 'string|max:255' +}; + +export const uploadConfirmRules = { + reference: 'required|string|min:1|max:255' +}; diff --git a/src/app/http/modules/index.ts b/src/app/http/modules/index.ts new file mode 100644 index 0000000..3bf972f --- /dev/null +++ b/src/app/http/modules/index.ts @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import { routes as authRoutes } from './auth'; +import { routes as galleryRoutes } from './gallery'; +import adminModule from './admin'; + +const router = Router(); + +router.use('/auth', authRoutes); +router.use('/gallery', galleryRoutes); +router.use('/admin', adminModule); + +export default router; diff --git a/src/app/http/services/encryption.service.ts b/src/app/http/services/encryption.service.ts new file mode 100644 index 0000000..90c501f --- /dev/null +++ b/src/app/http/services/encryption.service.ts @@ -0,0 +1,69 @@ +/** + * Encryption Service + * RSA encryption for sensitive data (bank accounts, etc.) + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import { getProjectRoot } from '@core/utils/path.utils'; + +const projectRoot = getProjectRoot(); + +// Load RSA keys for data encryption (separate from JWT keys) +const publicKey = fs.readFileSync(path.resolve(projectRoot, 'storage/keys/encryption/public.key'), 'utf8'); +const privateKey = fs.readFileSync(path.resolve(projectRoot, 'storage/keys/encryption/private.key'), 'utf8'); + +// ============================================================================ +// RSA Encryption (for small data like account numbers) +// ============================================================================ + +/** + * Encrypt content using RSA public key + * Suitable for small pieces of sensitive data + * + * @param content - Plain text content to encrypt (max ~190 bytes for 2048-bit key) + * @returns Encrypted content (base64 encoded) + */ +export const encrypt = (content: string): string => { + const buffer = Buffer.from(content, 'utf8'); + const encrypted = crypto.publicEncrypt( + { + key: publicKey, + padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, + oaepHash: 'sha256' + }, + buffer + ); + return encrypted.toString('base64'); +}; + +/** + * Decrypt content using RSA private key + * + * @param encryptedContent - Encrypted content (base64 encoded) + * @returns Decrypted content + */ +export const decrypt = (encryptedContent: string): string => { + const buffer = Buffer.from(encryptedContent, 'base64'); + const decrypted = crypto.privateDecrypt( + { + key: privateKey, + padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, + oaepHash: 'sha256' + }, + buffer + ); + return decrypted.toString('utf8'); +}; + +// ============================================================================ +// Service Class (for DI compatibility) +// ============================================================================ + +export class EncryptionService { + encrypt = encrypt; + decrypt = decrypt; +} + +export default new EncryptionService(); diff --git a/src/app/http/services/helpers/health/database.service.ts b/src/app/http/services/helpers/health/database.service.ts new file mode 100644 index 0000000..fcf8876 --- /dev/null +++ b/src/app/http/services/helpers/health/database.service.ts @@ -0,0 +1,37 @@ +/** + * Database Health Service + */ + +import { createLogger } from '@services/logger.service'; +import { HealthCheckResult } from './types'; +import { getPrismaClient } from '@database/prisma.client'; + +const logger = createLogger('health:database'); + +export const verifyDatabaseConnection = async (): Promise => { + const startTime = process.hrtime.bigint(); + try { + const prisma = getPrismaClient(); + await prisma.$queryRaw`SELECT 1`; + const endTime = process.hrtime.bigint(); + const latency = Number(endTime - startTime) / 1_000_000; // Convert nanoseconds to milliseconds + + return { + status: 'healthy', + details: `PostgreSQL reachable (latency=${latency.toFixed(0)}ms)` + }; + } catch (error) { + if (error instanceof Error) { + logger.error('PostgreSQL health check failed', { error: error.message, stack: error.stack }); + return { + status: 'unhealthy', + details: `PostgreSQL connection failed: ${error.message}` + }; + } + + return { + status: 'unhealthy', + details: 'PostgreSQL connection failed: unknown error' + }; + } +}; diff --git a/src/app/http/services/helpers/health/health.service.ts b/src/app/http/services/helpers/health/health.service.ts new file mode 100644 index 0000000..4c6a0d1 --- /dev/null +++ b/src/app/http/services/helpers/health/health.service.ts @@ -0,0 +1,98 @@ +import os from 'node:os'; +import environment from '@config/environment.config'; +import { verifyDatabaseConnection } from './database.service'; +import { verifyRedisConnection } from './redis.service'; +import { verifyKeyFiles } from './key-file.service'; +import { HealthCheckResult, ServiceHealthStatus } from './types'; + +export interface HealthSnapshot { + overallStatus: ServiceHealthStatus; + checks: { + database: HealthCheckResult; + redis: HealthCheckResult; + keyFiles: HealthCheckResult; + systemResources: HealthCheckResult; + }; +} + +const evaluateSystemResources = (): HealthCheckResult => { + const freeMemoryBytes = os.freemem(); + const totalMemoryBytes = os.totalmem(); + const freeMemoryRatio = totalMemoryBytes > 0 ? freeMemoryBytes / totalMemoryBytes : 0; + const [oneMinuteLoad] = os.loadavg(); + let loadAverage = 0; + if (typeof oneMinuteLoad === 'number' && Number.isFinite(oneMinuteLoad)) { + loadAverage = oneMinuteLoad; + } + const cpuCount = Math.max(os.cpus().length, 1); + const { + minFreeMemoryRatio = 0.03, + maxLoadAverageMultiplier = 1.5, + maxLoadAverageAbsolute = 6 + } = environment.systemResources ?? {}; + + const memoryThresholdBreached = freeMemoryRatio < minFreeMemoryRatio; + const loadThreshold = Math.min(cpuCount * maxLoadAverageMultiplier, maxLoadAverageAbsolute); + const loadThresholdBreached = loadAverage > loadThreshold; + + if (memoryThresholdBreached || loadThresholdBreached) { + return { + status: 'degraded', + details: `High resource usage detected (freeMemoryRatio=${freeMemoryRatio.toFixed( + 2 + )}, loadAverage=${loadAverage.toFixed(2)}, thresholds: minFreeMemoryRatio=${minFreeMemoryRatio.toFixed( + 2 + )}, maxLoad=${loadThreshold.toFixed(2)})` + }; + } + + return { + status: 'healthy', + details: `System resources nominal (freeMemoryRatio=${freeMemoryRatio.toFixed( + 2 + )}, loadAverage=${loadAverage.toFixed(2)})` + }; +}; + +const deriveOverallStatus = (checks: HealthSnapshot['checks']): ServiceHealthStatus => { + const statuses = Object.values(checks).map(check => check.status); + + if (statuses.includes('unhealthy')) { + return 'unhealthy'; + } + + if (statuses.includes('degraded')) { + return 'degraded'; + } + + if (statuses.every(status => status === 'unknown')) { + return 'unknown'; + } + + return 'healthy'; +}; + +export const getHealthSnapshot = async (): Promise => { + const [database, redis] = await Promise.all([ + verifyDatabaseConnection().catch((error: Error) => ({ + status: 'unhealthy' as ServiceHealthStatus, + details: `Database check failed: ${error.message}` + })), + verifyRedisConnection().catch((error: Error) => ({ + status: 'unhealthy' as ServiceHealthStatus, + details: `Redis check failed: ${error.message}` + })) + ]); + + const checks: HealthSnapshot['checks'] = { + database, + redis, + keyFiles: await verifyKeyFiles(), + systemResources: evaluateSystemResources() + }; + + return { + overallStatus: deriveOverallStatus(checks), + checks + }; +}; diff --git a/src/app/http/services/helpers/health/key-file.service.ts b/src/app/http/services/helpers/health/key-file.service.ts new file mode 100644 index 0000000..3dad9e1 --- /dev/null +++ b/src/app/http/services/helpers/health/key-file.service.ts @@ -0,0 +1,87 @@ +import { promises as fs, constants as fsConstants } from 'node:fs'; +import path from 'node:path'; +import environment from '@config/environment.config'; +import { HealthCheckResult } from './types'; + +const resolvePath = (filePath: string): string => { + if (path.isAbsolute(filePath)) { + return filePath; + } + return path.resolve(process.cwd(), filePath); +}; + +const checkFile = async (filePath: string): Promise => { + try { + const resolvedPath = resolvePath(filePath); + const stats = await fs.stat(resolvedPath); + + if (!stats.isFile()) { + return { + status: 'unhealthy', + details: `Key path is not a file: ${resolvedPath}` + }; + } + + if (stats.size <= 0) { + return { + status: 'degraded', + details: `Key file is empty: ${resolvedPath}` + }; + } + + await fs.access(resolvedPath, fsConstants.R_OK); + + return { + status: 'healthy', + details: `Key file accessible: ${resolvedPath}` + }; + } catch (error) { + if (error instanceof Error) { + return { + status: 'unhealthy', + details: `Key file check failed for ${filePath}: ${error.message}` + }; + } + + return { + status: 'unhealthy', + details: `Key file check failed for ${filePath}: unknown error` + }; + } +}; + +export const verifyKeyFiles = async (): Promise => { + const configuredFiles = environment.keyFiles?.required ?? []; + + if (configuredFiles.length === 0) { + return { + status: 'unknown', + details: 'No key files configured' + }; + } + + const results = await Promise.all(configuredFiles.map(checkFile)); + + const statuses = results.map((result: HealthCheckResult) => result.status); + + if (statuses.includes('unhealthy')) { + const errors = results.filter((result: HealthCheckResult) => result.status === 'unhealthy').map((result: HealthCheckResult) => result.details); + return { + status: 'unhealthy', + details: errors.join('; ') + }; + } + + if (statuses.includes('degraded')) { + const warnings = results.filter((result: HealthCheckResult) => result.status === 'degraded').map((result: HealthCheckResult) => result.details); + return { + status: 'degraded', + details: warnings.join('; ') + }; + } + + return { + status: 'healthy', + details: results.map((result: HealthCheckResult) => result.details).join('; ') + }; +}; diff --git a/src/app/http/services/helpers/health/redis.service.ts b/src/app/http/services/helpers/health/redis.service.ts new file mode 100644 index 0000000..219a6f8 --- /dev/null +++ b/src/app/http/services/helpers/health/redis.service.ts @@ -0,0 +1,58 @@ +/** + * Redis Health Service + */ + +import { getRedisClient, isRedisConfigured } from '@core/cache'; +import { HealthCheckResult } from './types'; + +export const verifyRedisConnection = async (): Promise => { + if (!isRedisConfigured()) { + return { + status: 'degraded', + details: 'Redis not configured — session caching and job queues are disabled' + }; + } + + try { + const client = await getRedisClient(); + const needsConnect = client.status === 'wait' || client.status === 'close' || client.status === 'end'; + if (needsConnect) { + await client.connect(); + } + + if (client.status !== 'ready') { + return { + status: 'degraded', + details: `Redis status ${client.status}` + }; + } + + const startedAt = Date.now(); + const response = await client.ping(); + const latency = Date.now() - startedAt; + + if (response !== 'PONG') { + return { + status: 'degraded', + details: `Unexpected Redis response: ${response}` + }; + } + + return { + status: 'healthy', + details: `Redis reachable (latency=${latency}ms)` + }; + } catch (error) { + if (error instanceof Error) { + return { + status: 'unhealthy', + details: `Redis connection failed: ${error.message}` + }; + } + + return { + status: 'unhealthy', + details: 'Redis connection failed: unknown error' + }; + } +}; diff --git a/src/app/http/services/helpers/health/types.ts b/src/app/http/services/helpers/health/types.ts new file mode 100644 index 0000000..edc520c --- /dev/null +++ b/src/app/http/services/helpers/health/types.ts @@ -0,0 +1,6 @@ +export type ServiceHealthStatus = 'healthy' | 'degraded' | 'unhealthy' | 'unknown'; + +export interface HealthCheckResult { + status: ServiceHealthStatus; + details: string; +} diff --git a/src/app/http/services/index.ts b/src/app/http/services/index.ts new file mode 100644 index 0000000..f8cf4b8 --- /dev/null +++ b/src/app/http/services/index.ts @@ -0,0 +1,10 @@ +/** + * Core Services Export + * Shared utility services used across modules + */ + +export * from './jwt.service'; +export * from './password.service'; +export * from './encryption.service'; +export * from './logger.service'; +export * from './storage'; \ No newline at end of file diff --git a/src/app/http/services/jwt.service.ts b/src/app/http/services/jwt.service.ts new file mode 100644 index 0000000..4663fa6 --- /dev/null +++ b/src/app/http/services/jwt.service.ts @@ -0,0 +1,111 @@ +/** + * JWT Service + * Handles token generation and verification using RS256 + */ + +import path from 'node:path'; +import fs from 'node:fs'; +import crypto from 'node:crypto'; +import jwt, { SignOptions } from 'jsonwebtoken'; +import { JWT, AUTH } from '@core/constants'; +import { getProjectRoot } from '@core/utils/path.utils'; + +const projectRoot = getProjectRoot(); + +// Load RSA keys +const privateKey = fs.readFileSync(path.resolve(projectRoot, 'storage/keys/api/private.key'), 'utf8'); +const publicKey = fs.readFileSync(path.resolve(projectRoot, 'storage/keys/api/public.key'), 'utf8'); + +// ============================================================================ +// Types +// ============================================================================ + +export interface TokenPayload { + /** Session token reference */ + t: string; + /** Token mode: 0 = access, 1 = refresh */ + m: number; + /** Role slug for cache key partitioning */ + r: string; +} + +export interface TokenPair { + auth: string; + refresh: string; +} + +export interface TokenResponse { + /** Internal session token (stored in DB) */ + token: string; + /** Access token expiry timestamp */ + tokenExpiry: Date; + /** JWT token pair */ + keys: TokenPair; +} + +// ============================================================================ +// Token Generation +// ============================================================================ + +export interface GenerateTokenOptions { + /** Role slug for cache key partitioning */ + roleSlug: string; + /** Access token expiry (default: 15m) */ + expiresIn?: string; + /** Refresh token expiry (default: 7d) */ + refreshExpiresIn?: string; +} + +/** + * Generate session token and JWT pair (access + refresh) + */ +export const generateTokenPair = (options: GenerateTokenOptions): TokenResponse => { + const { roleSlug, expiresIn, refreshExpiresIn } = options; + const token = crypto.randomBytes(AUTH.SESSION_TOKEN_BYTES).toString('hex'); + + const accessExpiry = expiresIn ?? JWT.ACCESS_TOKEN_EXPIRY; + const refreshExpiry = refreshExpiresIn ?? JWT.REFRESH_TOKEN_EXPIRY; + + // Parse expiry for response + const minutes = parseInt(accessExpiry.replace(/[^0-9]/g, ''), 10) || 15; + const tokenExpiry = new Date(Date.now() + minutes * 60 * 1000); + + const auth = jwt.sign({ t: token, m: JWT.MODE_ACCESS, r: roleSlug }, privateKey, { + algorithm: JWT.ALGORITHM, + expiresIn: accessExpiry + } as SignOptions); + + const refresh = jwt.sign({ t: token, m: JWT.MODE_REFRESH, r: roleSlug }, privateKey, { + algorithm: JWT.ALGORITHM, + expiresIn: refreshExpiry + } as SignOptions); + + return { token, tokenExpiry, keys: { auth, refresh } }; +}; + +// ============================================================================ +// Token Verification +// ============================================================================ + +/** + * Verify and decode a JWT token + * @returns Decoded payload or null if invalid + */ +export const verifyToken = (token: string): TokenPayload | null => { + try { + return jwt.verify(token, publicKey, { algorithms: [JWT.ALGORITHM] }) as TokenPayload; + } catch { + return null; + } +}; + +// ============================================================================ +// Service Class (for DI compatibility) +// ============================================================================ + +export class JwtService { + generateTokenPair = generateTokenPair; + verifyToken = verifyToken; +} + +export default new JwtService(); diff --git a/src/app/http/services/logger.service.ts b/src/app/http/services/logger.service.ts new file mode 100644 index 0000000..1c9f28b --- /dev/null +++ b/src/app/http/services/logger.service.ts @@ -0,0 +1,169 @@ +/** + * Logger Service + * Contextual logging with request correlation + */ + +import winston from 'winston'; +import DailyRotateFile from 'winston-daily-rotate-file'; +import path from 'node:path'; +import { getProjectRoot } from '@core/utils/path.utils'; +import environment from '@config/environment.config'; + +const projectRoot = getProjectRoot(); +const logsDir = path.join(projectRoot, 'storage', 'logs'); + +// ============================================================================ +// Request Context Integration (Lazy loaded to avoid circular deps) +// ============================================================================ + +let getRequestContext: (() => { requestId?: string; userId?: bigint } | undefined) | null = null; + +/** + * Set the request context getter (called from app/http/middleware) + * This allows core logger to access HTTP request context without direct dependency + */ +export const setRequestContextGetter = (getter: typeof getRequestContext): void => { + getRequestContext = getter; +}; + +// ============================================================================ +// Configuration +// ============================================================================ + +const { isDev } = environment.basic; +const logLevel = environment.logging.level; +const { maxSize, maxFiles } = environment.logging; + +// ============================================================================ +// Formatters +// ============================================================================ + +const formatMeta = (meta: Record): string => { + const filtered = Object.entries(meta) + .filter(([key]) => !['level', 'message', 'timestamp', 'context', 'requestId'].includes(key)) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); + + return Object.keys(filtered).length > 0 ? ` ${JSON.stringify(filtered)}` : ''; +}; + +const consoleFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.colorize(), + winston.format.printf(({ level, message, timestamp, context, requestId, ...meta }) => { + const ctx = context ? `[${context}]` : ''; + const reqId = requestId ? `[${requestId}]` : ''; + const metaStr = formatMeta(meta); + return `${timestamp} ${level} ${ctx}${reqId} ${message}${metaStr}`; + }) +); + +const fileFormat = winston.format.combine(winston.format.timestamp(), winston.format.json()); + +// ============================================================================ +// Transports +// ============================================================================ + +const transports: winston.transport[] = [ + new winston.transports.Console({ + format: consoleFormat + }) +]; + +// Add file transports in production +if (!isDev) { + transports.push( + new DailyRotateFile({ + filename: path.join(logsDir, 'application-%DATE%.log'), + datePattern: 'YYYY-MM-DD', + maxSize, + maxFiles, + format: fileFormat + }), + new DailyRotateFile({ + filename: path.join(logsDir, 'error-%DATE%.log'), + datePattern: 'YYYY-MM-DD', + level: 'error', + maxSize, + maxFiles, + format: fileFormat + }) + ); +} + +// ============================================================================ +// Base Logger +// ============================================================================ + +const baseLogger = winston.createLogger({ + level: logLevel, + transports +}); + +// ============================================================================ +// Logger Service Class +// ============================================================================ + +type LogMeta = Record; + +export class Logger { + private context: string; + + constructor(context: string) { + this.context = context; + } + + private getMeta(meta?: LogMeta): LogMeta { + const requestContext = getRequestContext?.(); + return { + context: this.context, + requestId: requestContext?.requestId, + ...meta + }; + } + + info(message: string, meta?: LogMeta): void { + baseLogger.info(message, this.getMeta(meta)); + } + + error(message: string, meta?: LogMeta): void { + baseLogger.error(message, this.getMeta(meta)); + } + + warn(message: string, meta?: LogMeta): void { + baseLogger.warn(message, this.getMeta(meta)); + } + + debug(message: string, meta?: LogMeta): void { + baseLogger.debug(message, this.getMeta(meta)); + } + + /** + * Log error with stack trace + */ + exception(message: string, error: Error, meta?: LogMeta): void { + baseLogger.error(message, { + ...this.getMeta(meta), + error: { + name: error.name, + message: error.message, + stack: error.stack + } + }); + } +} + +// ============================================================================ +// Factory Function +// ============================================================================ + +/** + * Create a logger with module context + * @param context - Module or component name (e.g., 'auth', 'ride:onboarding') + */ +export const createLogger = (context: string): Logger => new Logger(context); + +// ============================================================================ +// Default Export +// ============================================================================ + +export default createLogger; diff --git a/src/app/http/services/password.service.ts b/src/app/http/services/password.service.ts new file mode 100644 index 0000000..c81d35d --- /dev/null +++ b/src/app/http/services/password.service.ts @@ -0,0 +1,90 @@ +/** + * Password Service + * Secure password hashing using PBKDF2 with pepper + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import { getProjectRoot } from '@core/utils/path.utils'; + +const projectRoot = getProjectRoot(); + +// Read pepper/secret from key file (not stored in DB, adds extra layer of security) +const pepper = fs.readFileSync(path.resolve(projectRoot, 'storage/keys/encryption/private.key'), 'utf8'); + +// ============================================================================ +// Configuration +// ============================================================================ + +const SALT_LENGTH = 32; // 32 bytes = 256 bits +const HASH_ITERATIONS = 100000; // PBKDF2 iterations (OWASP recommended minimum) +const HASH_KEY_LENGTH = 64; // 64 bytes = 512 bits +const HASH_ALGORITHM = 'sha512'; + +// ============================================================================ +// Core Functions +// ============================================================================ + +/** + * Generate a cryptographically secure random salt + * @returns Base64 encoded salt + */ +export const generateSalt = (): string => { + return crypto.randomBytes(SALT_LENGTH).toString('base64'); +}; + +/** + * Hash a password with salt and pepper (one-way process) + * Uses PBKDF2 which is resistant to brute-force attacks + * + * @param password - Plain text password + * @param salt - Base64 encoded salt (unique per password) + * @returns Base64 encoded hash + */ +export const hashPassword = (password: string, salt: string): string => { + const saltBuffer = Buffer.from(salt, 'base64'); + + // PBKDF2 with the user-specific salt + const hash = crypto.pbkdf2Sync(password, saltBuffer, HASH_ITERATIONS, HASH_KEY_LENGTH, HASH_ALGORITHM); + + // Combine with pepper (application-level secret) + const combined = Buffer.concat([hash, Buffer.from(pepper, 'utf8')]); + + // Final hash + const finalHash = crypto.createHash(HASH_ALGORITHM).update(combined).digest(); + + return finalHash.toString('base64'); +}; + +/** + * Verify if a plain text password matches a hashed password + * Uses timing-safe comparison to prevent timing attacks + * + * @param plainPassword - Plain text password to verify + * @param hashedPassword - Base64 encoded hash from database + * @param salt - Base64 encoded salt from database + * @returns True if passwords match, false otherwise + */ +export const verifyPassword = (plainPassword: string, hashedPassword: string, salt: string): boolean => { + try { + const computedHash = hashPassword(plainPassword, salt); + + // Timing-safe comparison prevents timing attacks + return crypto.timingSafeEqual(Buffer.from(computedHash, 'base64'), Buffer.from(hashedPassword, 'base64')); + } catch { + return false; + } +}; + +// ============================================================================ +// Service Class (for DI compatibility) +// ============================================================================ + +export class PasswordService { + generateSalt = generateSalt; + hashPassword = hashPassword; + verifyPassword = verifyPassword; +} + +export default new PasswordService(); diff --git a/src/app/http/services/storage/index.ts b/src/app/http/services/storage/index.ts new file mode 100644 index 0000000..4b27711 --- /dev/null +++ b/src/app/http/services/storage/index.ts @@ -0,0 +1,6 @@ +/** + * Storage Service Exports + */ + +export * from './storage.service'; +export * from './types'; diff --git a/src/app/http/services/storage/storage.service.ts b/src/app/http/services/storage/storage.service.ts new file mode 100644 index 0000000..90361b2 --- /dev/null +++ b/src/app/http/services/storage/storage.service.ts @@ -0,0 +1,269 @@ +/** + * Storage Service + * Unified file storage using express-storage with support for multiple S3 buckets + */ + +import { StorageManager } from 'express-storage'; +import environment from '@config/environment.config'; +import { createLogger } from '@services/logger.service'; +import { + STORAGE_BUCKETS, + StorageBucket, + FILE_CATEGORIES, + FileCategory, + UploadInitRequest, + UploadInitResponse, + UploadConfirmRequest, + StorageConfirmResponse, + ViewUrlResponse +} from './types'; + +const logger = createLogger('storage'); + +// Storage manager instances cache (keyed by bucket type) +const storageInstances = new Map(); + +// Bucket name mapping +const BUCKET_NAMES: Record = { + [STORAGE_BUCKETS.DOCUMENTS]: environment.aws.s3Bucket, + [STORAGE_BUCKETS.PUBLIC]: environment.aws.s3BucketPublic +}; + +/** + * Get or create storage manager for the specified bucket (lazy singleton per bucket) + */ +const getStorageManager = (bucket: StorageBucket): StorageManager => { + const existing = storageInstances.get(bucket); + if (existing) return existing; + + const bucketName = BUCKET_NAMES[bucket]; + const storage = new StorageManager({ + driver: 's3-presigned', + credentials: { + bucketName, + awsRegion: environment.aws.region, + awsAccessKey: environment.aws.accessKeyId, + awsSecretKey: environment.aws.secretAccessKey + }, + logger: { + debug: (msg: string) => logger.debug(msg), + info: (msg: string) => logger.info(msg), + warn: (msg: string) => logger.warn(msg), + error: (msg: string) => logger.error(msg) + } + }); + + storageInstances.set(bucket, storage); + logger.info('Storage manager initialized', { bucket: bucketName, type: bucket }); + + return storage; +}; + +/** + * Determine file category from MIME type + */ +export const getFileCategory = (mimeType: string): FileCategory | null => { + const normalizedMime = mimeType.toLowerCase(); + for (const [category, config] of Object.entries(FILE_CATEGORIES)) { + if (config.mimeTypes.includes(normalizedMime)) { + return category as FileCategory; + } + } + return null; +}; + +/** + * Validate file against category constraints + */ +export const validateFile = ( + mimeType: string, + fileSize: number, + category?: FileCategory +): { valid: boolean; error?: string } => { + const detectedCategory = category ?? getFileCategory(mimeType); + + if (!detectedCategory) { + return { + valid: false, + error: `Unsupported file type: ${mimeType}` + }; + } + + const config = FILE_CATEGORIES[detectedCategory]; + if (!config) { + return { + valid: false, + error: `Unknown file category: ${detectedCategory}` + }; + } + + if (!config.mimeTypes.includes(mimeType.toLowerCase())) { + return { + valid: false, + error: `Invalid MIME type for ${detectedCategory}: ${mimeType}` + }; + } + + if (fileSize > config.maxSize) { + const maxSizeMB = Math.round(config.maxSize / 1024 / 1024); + return { + valid: false, + error: `File size exceeds ${maxSizeMB}MB limit for ${detectedCategory}` + }; + } + + return { valid: true }; +}; + +/** + * Generate presigned upload URL + */ +export const generateUploadUrl = async (request: UploadInitRequest): Promise => { + const bucket = request.bucket ?? STORAGE_BUCKETS.DOCUMENTS; + const storage = getStorageManager(bucket); + + // Validate file + const validation = validateFile(request.contentType, request.fileSize); + if (!validation.valid) { + throw new Error(validation.error); + } + + // Build folder path + const folder = request.folder ?? 'uploads'; + + logger.debug('Generating upload URL', { + fileName: request.fileName, + contentType: request.contentType, + fileSize: request.fileSize, + bucket, + folder + }); + + const result = await storage.generateUploadUrl(request.fileName, request.contentType, request.fileSize, folder); + + if (!result.success) { + logger.error('Failed to generate upload URL', { error: result.error }); + throw new Error(result.error ?? 'Failed to generate upload URL'); + } + if (!result.uploadUrl || !result.reference) { + throw new Error('Storage provider returned incomplete upload URL response'); + } + + return { + reference: result.reference, + uploadUrl: result.uploadUrl, + bucket, + path: result.reference, // express-storage uses reference as the path + expiresIn: 600 // 10 minutes (default presigned URL expiry) + }; +}; + +/** + * Validate and confirm upload completion + */ +export const confirmUpload = async ( + request: UploadConfirmRequest, + bucket: StorageBucket +): Promise => { + const storage = getStorageManager(bucket); + + logger.debug('Confirming upload', { + reference: request.reference, + bucket + }); + + const options: Record = {}; + if (request.expectedContentType != null) { + options['expectedContentType'] = request.expectedContentType; + } + if (request.expectedFileSize != null) { + options['expectedFileSize'] = request.expectedFileSize; + } + + const result = await storage.validateAndConfirmUpload(request.reference, options); + + if (!result.success) { + logger.warn('Upload confirmation failed', { + reference: request.reference, + error: result.error + }); + return { + success: false, + error: result.error ?? 'Upload confirmation failed' + }; + } + + const response: StorageConfirmResponse = { + success: true, + viewUrl: result.viewUrl ?? '' + }; + if (result.actualFileSize != null) { + response.actualFileSize = result.actualFileSize; + } + return response; +}; + +/** + * Generate public URL for public bucket files (no presigning needed) + */ +export const getPublicUrl = (reference: string): string => { + const bucketName = BUCKET_NAMES[STORAGE_BUCKETS.PUBLIC]; + return `https://${bucketName}.s3.${environment.aws.region}.amazonaws.com/${reference}`; +}; + +/** + * Generate view URL for existing file + * - Public bucket: returns direct public URL (no expiry) + * - Private bucket: returns presigned URL (expires in 10 minutes) + */ +export const generateViewUrl = async (reference: string, bucket: StorageBucket): Promise => { + // For public bucket, return direct public URL (no presigning needed) + if (bucket === STORAGE_BUCKETS.PUBLIC) { + return { + viewUrl: getPublicUrl(reference), + expiresIn: 0 // No expiry for public URLs + }; + } + + // For private bucket, generate presigned URL + const storage = getStorageManager(bucket); + + const result = await storage.generateViewUrl(reference); + + if (!result.success) { + logger.error('Failed to generate view URL', { + reference, + error: result.error + }); + throw new Error(result.error ?? 'Failed to generate view URL'); + } + if (!result.viewUrl) { + throw new Error('Storage provider returned incomplete view URL response'); + } + + return { + viewUrl: result.viewUrl, + expiresIn: 600 // 10 minutes + }; +}; + +/** + * Delete file from storage + */ +export const deleteFile = async (reference: string, bucket: StorageBucket): Promise => { + const storage = getStorageManager(bucket); + + logger.debug('Deleting file', { reference, bucket }); + + const result = await storage.deleteFile(reference); + + if (!result.success) { + logger.warn('Failed to delete file', { reference, bucket, error: result.error }); + } + + return result.success; +}; + +// Re-export types and constants +export { STORAGE_BUCKETS, FILE_CATEGORIES }; +export type { StorageBucket, FileCategory }; diff --git a/src/app/http/services/storage/types.ts b/src/app/http/services/storage/types.ts new file mode 100644 index 0000000..58ec3ff --- /dev/null +++ b/src/app/http/services/storage/types.ts @@ -0,0 +1,104 @@ +/** + * Storage Service Types + * Type definitions for file storage operations + */ + +/** + * Available storage buckets + */ +export const STORAGE_BUCKETS = { + /** Private bucket for sensitive documents (license, insurance, etc.) */ + DOCUMENTS: 'documents', + /** Public bucket for publicly accessible files (avatars, thumbnails) */ + PUBLIC: 'public' +} as const; + +export type StorageBucket = (typeof STORAGE_BUCKETS)[keyof typeof STORAGE_BUCKETS]; + +/** + * File category configuration + */ +interface FileCategoryConfig { + mimeTypes: readonly string[]; + extensions: readonly string[]; + maxSize: number; +} + +/** + * File type categories with validation rules + */ +export const FILE_CATEGORIES: Record = { + IMAGE: { + mimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'], + extensions: ['.jpg', '.jpeg', '.png', '.webp', '.gif'], + maxSize: 10 * 1024 * 1024 // 10MB + }, + DOCUMENT: { + mimeTypes: ['application/pdf'], + extensions: ['.pdf'], + maxSize: 25 * 1024 * 1024 // 25MB + }, + VIDEO: { + mimeTypes: ['video/mp4', 'video/webm', 'video/quicktime'], + extensions: ['.mp4', '.webm', '.mov'], + maxSize: 100 * 1024 * 1024 // 100MB + } +}; + +export type FileCategory = 'IMAGE' | 'DOCUMENT' | 'VIDEO'; + +/** + * Upload initialization request + */ +export interface UploadInitRequest { + fileName: string; + contentType: string; + fileSize: number; + bucket?: StorageBucket; + folder: string; + isPublic?: boolean; +} + +/** + * Upload initialization response + */ +export interface UploadInitResponse { + reference: string; + uploadUrl: string; + bucket: StorageBucket; + path: string; + expiresIn: number; +} + +/** + * Upload confirmation request + */ +export interface UploadConfirmRequest { + reference: string; + expectedContentType?: string | null; + expectedFileSize?: number | null; +} + +/** + * Storage upload confirmation response + */ +export interface StorageConfirmResponseSuccess { + success: true; + viewUrl: string; + actualFileSize?: number; +} + +export interface StorageConfirmResponseFailure { + success: false; + error: string; +} + +export type StorageConfirmResponse = StorageConfirmResponseSuccess | StorageConfirmResponseFailure; + +/** + * View URL response + */ +export interface ViewUrlResponse { + viewUrl: string; + expiresIn: number; +} diff --git a/src/config/cors.config.ts b/src/config/cors.config.ts new file mode 100644 index 0000000..edca4fd --- /dev/null +++ b/src/config/cors.config.ts @@ -0,0 +1,73 @@ +import { CorsOptions } from 'cors'; +import environment from './environment.config'; + +/** + * CORS (Cross-Origin Resource Sharing) configuration. + * This configuration controls how the server responds to cross-origin requests. + * Each option is documented below for clarity and maintainability. + */ +const corsConfiguration: CorsOptions = { + /** + * Specifies the origins allowed to access the server. + * - Sourced from environment configuration (array of allowed origins). + * - Example: ["https://example.com", "http://localhost:3000"] + */ + origin: environment.cors?.origin, + + /** + * Specifies the HTTP methods allowed for CORS requests. + * - Sourced from environment configuration (array of allowed methods). + * - Example: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] + */ + methods: environment.cors?.methods, + + /** + * Specifies the headers allowed in the actual request. + * - Allows common browser headers and custom headers. + */ + allowedHeaders: [ + 'Content-Type', + 'Authorization', + 'X-Requested-With', + 'Accept', + 'Origin', + 'Referer', + 'User-Agent', + 'sec-ch-ua', + 'sec-ch-ua-mobile', + 'sec-ch-ua-platform', + 'DNT' + ], + + /** + * Indicates whether the response to the request can be exposed when credentials are present. + * - true: Allows cookies and credentials to be sent in cross-origin requests. + */ + credentials: true, + + /** + * Indicates how long (in seconds) the results of a preflight request can be cached. + * - Sourced from environment configuration. + * - Example: 86400 (24 hours) + */ + maxAge: environment.cors?.maxAge, + + /** + * Pass the CORS preflight response to the next handler. + * - false: The response will be sent immediately. + */ + preflightContinue: false, + + /** + * The status code sent for successful OPTIONS requests. + * - 204: No Content + */ + optionsSuccessStatus: 204, + + /** + * Specifies the headers that are exposed to the browser. + */ + exposedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'] +}; + +export default corsConfiguration; diff --git a/src/config/environment.config.ts b/src/config/environment.config.ts new file mode 100644 index 0000000..1ce006e --- /dev/null +++ b/src/config/environment.config.ts @@ -0,0 +1,190 @@ +import { config } from 'dotenv'; +config({ quiet: true }); + +export interface Environment { + basic: { + environment: string; + isDev: boolean; + isProduction: boolean; + port: number; + timezone: string; + }; + app: { + name: string; + version: string; + description: string; + url: string; + }; + cors: { + origin: string[]; + methods: string[]; + maxAge: number; + }; + rateLimit: { + windowMs: number; + max: number; + login: { + windowMs: number; + max: number; + }; + otp: { + windowMs: number; + max: number; + }; + defaultRetryAfterMs: number; + }; + database: { + url: string; + pool: { + max: number; + idleTimeoutMillis: number; + connectionTimeoutMillis: number; + }; + }; + redis: { + url: string; + connectionName: string; + maxRetriesPerRequest: number; + }; + keyFiles: { + required: string[]; + }; + systemResources: { + minFreeMemoryRatio: number; + maxLoadAverageMultiplier: number; + maxLoadAverageAbsolute: number; + }; + jwt: { + expiresIn: string; + refreshExpiresIn: string; + }; + auth: { + otpExpiryMinutes: number; + twoFactorExpiryMinutes: number; + maxLoginAttempts: number; + lockoutMinutes: number; + maxResendAttempts: number; + resendCooldownSeconds: number; + sessionLastUsedDebounceMs: number; + sessionLastUsedMaxWaitMs: number; + placeholderEmailSuffix: string; + }; + logging: { + level: string; + maxSize: string; + maxFiles: string; + }; + aws: { + region: string; + accessKeyId: string; + secretAccessKey: string; + s3Bucket: string; + s3BucketPublic: string; + }; +} + +const env = process.env['NODE_ENV'] || 'development'; +const placeholderEmailSuffixRaw = process.env['AUTH_PLACEHOLDER_EMAIL_SUFFIX'] || '@temp.placeholder.local'; +const placeholderEmailSuffix = placeholderEmailSuffixRaw.startsWith('@') + ? placeholderEmailSuffixRaw.toLowerCase() + : `@${placeholderEmailSuffixRaw.toLowerCase()}`; + +const PORT = parseInt(process.env['PORT'] || '8070'); +const environment: Environment = { + basic: { + environment: env, + isDev: env === 'development', + isProduction: env === 'production', + port: PORT, + timezone: process.env['TIMEZONE'] || 'UTC' + }, + app: { + name: process.env['APP_NAME'] || 'My App', + version: process.env['APP_VERSION'] || '1', + description: process.env['APP_DESCRIPTION'] || 'Backend API Service', + url: process.env['APP_URL'] || `http://127.0.0.1:${PORT}` + }, + cors: { + origin: process.env['CORS_ORIGIN']?.split(',').map(origin => origin.trim()) || ['*'], + methods: process.env['CORS_METHODS']?.split(',').map(method => method.trim()) || [ + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'DELETE', + 'OPTIONS' + ], + maxAge: parseInt(process.env['CORS_MAX_AGE'] || '86400') + }, + rateLimit: { + windowMs: parseInt(process.env['RATE_LIMIT_WINDOW_MS'] || '60000'), + max: parseInt(process.env['RATE_LIMIT_MAX_REQUESTS'] || '120'), + login: { + windowMs: parseInt(process.env['RATE_LIMIT_LOGIN_WINDOW_MS'] || '900000'), + max: parseInt(process.env['RATE_LIMIT_LOGIN_MAX_REQUESTS'] || '20') + }, + otp: { + windowMs: parseInt(process.env['RATE_LIMIT_OTP_WINDOW_MS'] || '3600000'), + max: parseInt(process.env['RATE_LIMIT_OTP_MAX_REQUESTS'] || '5') + }, + defaultRetryAfterMs: parseInt(process.env['RATE_LIMIT_RETRY_AFTER_MS'] || '60000') + }, + database: { + url: process.env['DATABASE_URL'] || 'postgresql://postgres:change-me@127.0.0.1:5432/myapp', + pool: { + max: parseInt(process.env['DATABASE_POOL_MAX'] || '10'), + idleTimeoutMillis: parseInt(process.env['DATABASE_POOL_IDLE_TIMEOUT_MS'] || '30000'), + connectionTimeoutMillis: parseInt(process.env['DATABASE_POOL_CONNECTION_TIMEOUT_MS'] || '5000') + } + }, + redis: { + url: process.env['REDIS_URL'] || 'redis://127.0.0.1:6379', + connectionName: process.env['REDIS_CONNECTION_NAME'] || 'app-service', + maxRetriesPerRequest: parseInt(process.env['REDIS_MAX_RETRIES_PER_REQUEST'] || '3') + }, + keyFiles: { + required: process.env['KEY_FILES'] + ?.split(',') + .map(entry => entry.trim()) + .filter(Boolean) || [ + 'storage/keys/api/private.key', + 'storage/keys/api/public.key', + 'storage/keys/encryption/private.key', + 'storage/keys/encryption/public.key' + ] + }, + systemResources: { + minFreeMemoryRatio: parseFloat(process.env['SYSTEM_RESOURCES_MIN_FREE_MEMORY_RATIO'] || '0.03'), + maxLoadAverageMultiplier: parseFloat(process.env['SYSTEM_RESOURCES_MAX_LOAD_MULTIPLIER'] || '1.5'), + maxLoadAverageAbsolute: parseFloat(process.env['SYSTEM_RESOURCES_MAX_LOAD_ABSOLUTE'] || '6') + }, + jwt: { + expiresIn: process.env['JWT_EXPIRES_IN'] || '15m', + refreshExpiresIn: process.env['JWT_REFRESH_EXPIRES_IN'] || '7d' + }, + auth: { + otpExpiryMinutes: parseInt(process.env['AUTH_OTP_EXPIRY_MINUTES'] || '10'), + twoFactorExpiryMinutes: parseInt(process.env['AUTH_2FA_EXPIRY_MINUTES'] || '5'), + maxLoginAttempts: parseInt(process.env['AUTH_MAX_LOGIN_ATTEMPTS'] || '5'), + lockoutMinutes: parseInt(process.env['AUTH_LOCKOUT_MINUTES'] || '30'), + maxResendAttempts: parseInt(process.env['AUTH_MAX_RESEND_ATTEMPTS'] || '3'), + resendCooldownSeconds: parseInt(process.env['AUTH_RESEND_COOLDOWN_SECONDS'] || '30'), + sessionLastUsedDebounceMs: parseInt(process.env['AUTH_SESSION_LAST_USED_DEBOUNCE_MS'] || '120000'), + sessionLastUsedMaxWaitMs: parseInt(process.env['AUTH_SESSION_LAST_USED_MAX_WAIT_MS'] || '600000'), + placeholderEmailSuffix + }, + logging: { + level: process.env['LOG_LEVEL'] || (env === 'development' ? 'debug' : 'info'), + maxSize: process.env['LOG_MAX_SIZE'] || '20m', + maxFiles: process.env['LOG_MAX_FILES'] || '30d' + }, + aws: { + region: process.env['AWS_REGION'] || 'us-east-1', + accessKeyId: process.env['AWS_ACCESS_KEY_ID'] || '', + secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'] || '', + s3Bucket: process.env['AWS_S3_BUCKET'] || 'my-app-documents', + s3BucketPublic: process.env['AWS_S3_BUCKET_PUBLIC'] || 'my-app-public' + } +}; + +export default environment; diff --git a/src/config/helmet.config.ts b/src/config/helmet.config.ts new file mode 100644 index 0000000..19fa1b8 --- /dev/null +++ b/src/config/helmet.config.ts @@ -0,0 +1,59 @@ +import { HelmetOptions } from 'helmet'; + +/** + * Helmet configuration for securing HTTP headers. + * Each setting is documented below for clarity and maintainability. + */ +const helmetConfiguration: HelmetOptions = { + /** + * Content Security Policy (CSP) helps prevent XSS attacks by restricting the sources of content. + * - defaultSrc: Only allow content from the same origin. + * - scriptSrc: Only allow scripts from the same origin. + * - styleSrc: Allow styles from the same origin and inline styles (unsafe-inline). + * - imgSrc: Allow images from the same origin, data URIs, and HTTPS sources. + * - connectSrc: Only allow connections (e.g., XHR, WebSocket) to the same origin. + * - objectSrc: Disallow all , , and elements. + * - frameSrc: Disallow embedding the site in frames/iframes. + */ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], // Only allow resources from the same origin + scriptSrc: ["'self'"], // Only allow scripts from the same origin + styleSrc: ["'self'", "'unsafe-inline'"], // Allow styles from same origin and inline styles + imgSrc: ["'self'", 'data:', 'https:'], // Allow images from same origin, data URIs, and HTTPS + connectSrc: ["'self'"], // Only allow connections to the same origin + objectSrc: ["'none'"], // Disallow all object, embed, and applet elements + frameSrc: ["'none'"] // Disallow all framing of the site + } + }, + + /** + * Frameguard helps prevent clickjacking attacks by controlling whether the site can be framed. + * - action: "deny" completely disallows framing. + */ + frameguard: { action: 'deny' }, + + /** + * HidePoweredBy removes the X-Powered-By header to make it harder for attackers to see what technology is used. + */ + hidePoweredBy: true, + + /** + * noSniff sets the X-Content-Type-Options header to "nosniff" to prevent browsers from MIME-sniffing a response away from the declared content-type. + */ + noSniff: true, + + /** + * HSTS (HTTP Strict Transport Security) enforces secure (HTTPS) connections to the server. + * - maxAge: Time in seconds that the browser should remember to only access the site via HTTPS (1 year here). + * - includeSubDomains: Apply HSTS to all subdomains. + * - preload: Allow the site to be included in browsers' HSTS preload list. + */ + hsts: { + maxAge: 31536000, // 1 year in seconds + includeSubDomains: true, + preload: true + } +}; + +export default helmetConfiguration; diff --git a/src/database/.gitignore b/src/database/.gitignore new file mode 100644 index 0000000..3ae378c --- /dev/null +++ b/src/database/.gitignore @@ -0,0 +1,2 @@ +migrations/* +prisma/ \ No newline at end of file diff --git a/src/database/README.md b/src/database/README.md new file mode 100644 index 0000000..0dc8dd7 --- /dev/null +++ b/src/database/README.md @@ -0,0 +1,61 @@ +# Database Layer + +Prisma ORM setup for Auth & Onboarding Service. + +## Structure + +``` +src/database/ +ā”œā”€ā”€ schema.prisma # Prisma schema definition +ā”œā”€ā”€ prisma.client.ts # Prisma client singleton +ā”œā”€ā”€ migrations/ # Database migration files +└── README.md # This file +``` + +## Commands + +```bash +# Generate Prisma Client +npm run prisma:generate + +# Create and apply migration +npm run prisma:migrate + +# Apply migrations in production +npm run prisma:migrate:deploy + +# Open Prisma Studio (database GUI) +npm run prisma:studio + +# Format schema file +npm run prisma:format +``` + +## First Migration + +After setting up your database: + +```bash +npm run prisma:migrate +``` + +This will: + +1. Create the initial migration +2. Apply it to your database +3. Generate the Prisma Client + +## Schema Location + +Schema is in `src/database/schema.prisma`. All migrations are stored in `src/database/migrations/`. + +## Usage + +Import the Prisma client: + +```typescript +import prisma from '../database/prisma.client'; + +// Use in your code +const user = await prisma.user.findUnique({ where: { id: '...' } }); +``` diff --git a/src/database/prisma.client.ts b/src/database/prisma.client.ts new file mode 100644 index 0000000..f953813 --- /dev/null +++ b/src/database/prisma.client.ts @@ -0,0 +1,41 @@ +import { PrismaClient } from './prisma'; +import { PrismaPg } from '@prisma/adapter-pg'; +import { Pool } from 'pg'; +import environment from '@config/environment.config'; + +// Prisma Client singleton +let prisma: PrismaClient | null = null; +let pool: Pool | null = null; + +export const getPrismaClient = (): PrismaClient => { + if (prisma) { + return prisma; + } + + const logLevel = environment.basic.isDev ? ['error', 'warn'] : ['error']; + const connectionString = environment.database.url; + + if (!connectionString) { + throw new Error('DATABASE_URL environment variable is required'); + } + + pool = new Pool({ + connectionString, + max: environment.database.pool.max, + idleTimeoutMillis: environment.database.pool.idleTimeoutMillis, + connectionTimeoutMillis: environment.database.pool.connectionTimeoutMillis + }); + const adapter = new PrismaPg(pool); + + prisma = new PrismaClient({ + adapter, + log: logLevel.map(level => ({ + level: level as 'error' | 'warn', + emit: 'stdout' as const + })) + }); + + return prisma; +}; + +export default getPrismaClient(); diff --git a/src/database/schema.prisma b/src/database/schema.prisma new file mode 100644 index 0000000..0354349 --- /dev/null +++ b/src/database/schema.prisma @@ -0,0 +1,314 @@ +// Database Schema +// Node.js Boilerplate - Modular Monolith + +generator client { + provider = "prisma-client-js" + output = "./prisma" +} + +datasource db { + provider = "postgresql" +} + +// ============================================================================ +// ENUMS +// ============================================================================ + +enum UserStatus { + active + banned + deleted + suspended +} + +enum CodeVerificationPurpose { + phone_auth + phone_verify + email_auth + email_verify + registration + two_factor_auth + mfa_auth +} + +enum Platform { + android + ios + web +} + +enum GalleryType { + image + video + pdf +} + +enum GalleryStatus { + pending + uploaded + failed +} + +enum MagicTokenPurpose { + password_reset + email_verification +} + +enum ConfigValueType { + string + number + boolean + json +} + +// ============================================================================ +// CORE TABLES +// ============================================================================ + +model SystemConfig { + id BigInt @id @default(autoincrement()) + key String @unique @db.VarChar(255) + value String @db.Text + type ConfigValueType @default(string) + description String? @db.VarChar(500) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([key]) + @@map("system_configs") +} + +model Role { + id BigInt @id @default(autoincrement()) + name String @db.VarChar(255) + slug String @unique @db.VarChar(255) + forApp Boolean @default(true) @map("for_app") + passwordRequired Boolean @default(false) @map("password_required") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + users User[] + permission Permission? + + @@map("roles") +} + +model Permission { + id BigInt @id @default(autoincrement()) + roleId BigInt @unique @map("role") + permissions Json? @db.JsonB + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + role Role @relation(fields: [roleId], references: [id]) + + @@map("permissions") +} + +// ============================================================================ +// USER & AUTHENTICATION +// ============================================================================ + +model User { + id BigInt @id @default(autoincrement()) + firstName String @map("first_name") @db.VarChar(255) + lastName String @map("last_name") @db.VarChar(255) + avatarId BigInt? @map("avatar") + email String @unique @db.VarChar(255) + emailVerifiedAt DateTime? @map("email_verified_at") + countryCode String @default("1") @map("country_code") @db.VarChar(10) + phone String @db.VarChar(20) + phoneVerifiedAt DateTime? @map("phone_verified_at") + roleId BigInt @default(1) @map("role") + latitude Decimal? @db.Decimal(10, 8) + longitude Decimal? @db.Decimal(11, 8) + mfaEnabled Boolean @default(false) @map("mfa_enabled") + mfaSecret Json? @map("mfa_secret") @db.JsonB + otpAuth Boolean @default(false) @map("otp_auth") + loginAttempts Int @default(0) @map("login_attempts") @db.SmallInt + inactiveTill DateTime? @map("inactive_till") + status UserStatus @default(active) + remarks String? @db.VarChar(255) + lastLoginAt DateTime? @map("last_login_at") + verified Boolean @default(false) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + role Role @relation(fields: [roleId], references: [id]) + avatar Gallery? @relation("UserAvatar", fields: [avatarId], references: [id]) + addresses Address[] + passwords Password[] + codeVerifications CodeVerification[] + fcmTokens FcmToken[] + loginSessions LoginSession[] + uploadedGallery Gallery[] @relation("UploadedBy") + magicTokens MagicToken[] + + @@unique([countryCode, phone]) + @@index([email]) + @@index([phone]) + @@map("users") +} + +model Password { + id BigInt @id @default(autoincrement()) + userId BigInt @map("user") + password String @db.VarChar(255) + salt String @db.VarChar(255) + status Boolean @default(true) + expired Boolean @default(false) + expiredAt DateTime? @map("expired_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id]) + + @@index([userId]) + @@map("passwords") +} + +model CodeVerification { + id BigInt @id @default(autoincrement()) + userId BigInt @map("user") + reference String @unique @db.VarChar(255) + purpose CodeVerificationPurpose @default(phone_auth) + phoneCode Int? @map("phone_code") + emailCode Int? @map("email_code") + verified Boolean @default(false) + expired Boolean @default(false) + expiredAt DateTime @map("expired_at") + verifiedAt DateTime? @map("verified_at") + resendAttempts Int @default(0) @map("resend_attempts") + lastResendAt DateTime? @map("last_resend_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id]) + + @@index([userId]) + @@index([reference]) + @@map("code_verification") +} + +model LoginSession { + id BigInt @id @default(autoincrement()) + userId BigInt @map("user") + token String @db.Text + userAgent Json? @map("user_agent") @db.JsonB + ipAddress String? @map("ip_address") @db.Text + deviceName String? @map("device_name") @db.VarChar(255) + identifier BigInt? + status Boolean @default(true) + verified Boolean @default(true) + lastUsed DateTime? @map("last_used") + revokedAt DateTime? @map("revoked_at") + expiresAt DateTime @map("expires_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id]) + fcmToken FcmToken? @relation(fields: [identifier], references: [id]) + + @@index([userId]) + @@index([token]) + @@map("login_sessions") +} + +model FcmToken { + id BigInt @id @default(autoincrement()) + userId BigInt @map("user") + token String @db.VarChar(255) + device Json @db.JsonB + identifier String @unique @db.VarChar(255) + platform Platform @default(android) + lastSeenAt DateTime @map("last_seen_at") + status Boolean @default(true) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id]) + loginSessions LoginSession[] + + @@index([userId]) + @@map("fcm_tokens") +} + +model MagicToken { + id BigInt @id @default(autoincrement()) + userId BigInt? @map("user") + token String @unique @db.VarChar(255) + purpose MagicTokenPurpose + email String @db.VarChar(255) + roleId BigInt? @map("role") + metadata Json? @db.JsonB + used Boolean @default(false) + usedAt DateTime? @map("used_at") + expiresAt DateTime @map("expires_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User? @relation(fields: [userId], references: [id]) + + @@index([token]) + @@index([email]) + @@map("magic_tokens") +} + +// ============================================================================ +// ADDRESS & LOCATION +// ============================================================================ + +model Address { + id BigInt @id @default(autoincrement()) + userId BigInt @map("user") + isPrimary Boolean @default(false) @map("is_primary") + latitude Decimal @db.Decimal(10, 8) + longitude Decimal @db.Decimal(11, 8) + address String @db.VarChar(255) + street String? @db.VarChar(255) + city String @db.VarChar(255) + province String @db.VarChar(255) + country String @default("") @db.VarChar(10) + zipCode String @map("zip_code") @db.VarChar(20) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id]) + + @@index([userId]) + @@map("addresses") +} + +// ============================================================================ +// GALLERY & MEDIA +// ============================================================================ + +model Gallery { + id BigInt @id @default(autoincrement()) + uploadedById BigInt @map("uploaded_by") + reference String @unique @db.VarChar(255) + title String? @db.VarChar(255) + description String? @db.VarChar(255) + filetype String @db.VarChar(100) + extension String @db.VarChar(20) + size BigInt @default(0) + height Int? + width Int? + bucket String @db.VarChar(100) + isPublic Boolean @default(false) @map("is_public") + type GalleryType @default(image) + status GalleryStatus @default(pending) + path String @db.VarChar(500) + thumbnail String? @db.VarChar(500) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + uploadedBy User @relation("UploadedBy", fields: [uploadedById], references: [id]) + + userAvatars User[] @relation("UserAvatar") + + @@index([uploadedById]) + @@index([reference]) + @@index([status]) + @@map("gallery") +} diff --git a/src/database/seed.ts b/src/database/seed.ts new file mode 100644 index 0000000..da7b361 --- /dev/null +++ b/src/database/seed.ts @@ -0,0 +1,361 @@ +/** + * Database Seeder + * + * Seeds foundational data required for the application to function: + * roles, permissions, test users, and system configs. + * + * Safe to run multiple times — all operations use upsert or existence checks. + */ + +/* eslint-disable no-console */ + +import 'dotenv/config'; +import { getPrismaClient } from './prisma.client'; +import type { Prisma, User, ConfigValueType } from './prisma'; +import { + ALL_PERMISSIONS, + READ_ONLY_PERMISSIONS, + NO_PERMISSIONS, + type ModulePermissions +} from '../app/core/constants/permissions.constants'; +import { generateSalt, hashPassword } from '../app/http/services/password.service'; + +const prisma = getPrismaClient(); + +// ============================================================================ +// Types +// ============================================================================ + +interface RoleSeed { + id: bigint; + name: string; + slug: string; + forApp: boolean; + passwordRequired: boolean; +} + +interface TestUserSeed { + firstName: string; + lastName: string; + email: string; + phone: string; + countryCode: string; + roleId: bigint; + verified: boolean; + phoneVerifiedAt: Date; + emailVerifiedAt: Date; +} + +interface SystemConfigSeed { + key: string; + value: string; + type: ConfigValueType; + description: string; +} + +type PermissionsMap = Record; + +interface RolePermissionSeed { + roleId: bigint; + permissions: PermissionsMap; +} + +// ============================================================================ +// Data: Roles +// ============================================================================ + +const roles: RoleSeed[] = [ + { id: 1n, name: 'Customer', slug: 'customer', forApp: true, passwordRequired: false }, + { id: 2n, name: 'Admin', slug: 'admin', forApp: false, passwordRequired: true }, + { id: 3n, name: 'Super Admin', slug: 'super_admin', forApp: false, passwordRequired: true } +]; + +// ============================================================================ +// Data: Test Users +// ============================================================================ + +const testUsers: TestUserSeed[] = [ + { + firstName: 'Test', + lastName: 'Customer', + email: 'customer@test.com', + phone: '5551234567', + countryCode: '1', + roleId: 1n, + verified: true, + phoneVerifiedAt: new Date(), + emailVerifiedAt: new Date() + }, + { + firstName: 'Admin', + lastName: 'User', + email: 'admin@test.com', + phone: '5551234569', + countryCode: '1', + roleId: 2n, + verified: true, + phoneVerifiedAt: new Date(), + emailVerifiedAt: new Date() + }, + { + firstName: 'Super', + lastName: 'Admin', + email: 'superadmin@test.com', + phone: '5551234570', + countryCode: '1', + roleId: 3n, + verified: true, + phoneVerifiedAt: new Date(), + emailVerifiedAt: new Date() + } +]; + +// ============================================================================ +// Data: System Configs +// ============================================================================ + +const systemConfigs: SystemConfigSeed[] = [ + { key: 'general.timezone', value: 'UTC', type: 'string', description: 'Default timezone for the platform' }, + { key: 'general.date_format', value: 'YYYY-MM-DD', type: 'string', description: 'Date display format' }, + { key: 'general.time_format', value: '24h', type: 'string', description: 'Time display format (12h or 24h)' }, + + { + key: 'security.session_timeout_minutes', + value: '30', + type: 'number', + description: 'Inactive session timeout in minutes' + }, + { + key: 'security.require_2fa_admin', + value: 'true', + type: 'boolean', + description: 'Require 2FA for admin accounts' + }, + { key: 'security.password_min_length', value: '8', type: 'number', description: 'Minimum password length' }, + { + key: 'security.max_login_attempts', + value: '5', + type: 'number', + description: 'Maximum failed login attempts before lockout' + }, + { + key: 'security.lockout_duration_minutes', + value: '15', + type: 'number', + description: 'Account lockout duration in minutes' + }, + + { key: 'backup.enabled', value: 'true', type: 'boolean', description: 'Enable automatic backups' }, + { + key: 'backup.frequency', + value: 'daily', + type: 'string', + description: 'Backup frequency (daily, weekly, monthly)' + }, + { key: 'backup.retention_days', value: '30', type: 'number', description: 'Number of days to retain backups' }, + + { key: 'integration.s3.enabled', value: 'false', type: 'boolean', description: 'Enable AWS S3 file storage' }, + { key: 'integration.s3.region', value: 'us-east-1', type: 'string', description: 'AWS region for S3' }, + { + key: 'integration.s3.max_file_size_mb', + value: '10', + type: 'number', + description: 'Maximum file upload size in MB' + } +]; + +// ============================================================================ +// Data: Role Permissions +// ============================================================================ + +const SUPER_ADMIN_PERMISSIONS: PermissionsMap = { + users: ALL_PERMISSIONS, + gallery: ALL_PERMISSIONS, + config: ALL_PERMISSIONS, + roles: ALL_PERMISSIONS, + permissions: ALL_PERMISSIONS +}; + +const ADMIN_PERMISSIONS: PermissionsMap = { + users: { create: false, read: true, list: true, update: true, delete: false, scope: 'all' }, + gallery: ALL_PERMISSIONS, + config: ALL_PERMISSIONS, + roles: READ_ONLY_PERMISSIONS, + permissions: READ_ONLY_PERMISSIONS +}; + +const CUSTOMER_PERMISSIONS: PermissionsMap = { + users: NO_PERMISSIONS, + gallery: { create: true, read: true, list: true, update: false, delete: true, scope: 'own' }, + config: NO_PERMISSIONS, + roles: NO_PERMISSIONS, + permissions: NO_PERMISSIONS +}; + +const rolePermissions: RolePermissionSeed[] = [ + { roleId: 1n, permissions: CUSTOMER_PERMISSIONS }, + { roleId: 2n, permissions: ADMIN_PERMISSIONS }, + { roleId: 3n, permissions: SUPER_ADMIN_PERMISSIONS } +]; + +// ============================================================================ +// Seed Functions +// ============================================================================ + +async function seedRoles(): Promise { + console.log('Seeding roles...'); + + for (const role of roles) { + await prisma.role.upsert({ + where: { id: role.id }, + update: { + name: role.name, + slug: role.slug, + forApp: role.forApp, + passwordRequired: role.passwordRequired + }, + create: { + id: role.id, + name: role.name, + slug: role.slug, + forApp: role.forApp, + passwordRequired: role.passwordRequired + } + }); + } + + console.log(` āœ“ ${roles.length} roles`); +} + +async function seedPermissions(): Promise { + console.log('Seeding permissions...'); + + for (const rp of rolePermissions) { + const permData = rp.permissions as unknown as Prisma.InputJsonValue; + + await prisma.permission.upsert({ + where: { roleId: rp.roleId }, + update: { permissions: permData }, + create: { roleId: rp.roleId, permissions: permData } + }); + } + + console.log(` āœ“ ${rolePermissions.length} role permissions`); +} + +async function seedTestUsers(): Promise { + console.log('Seeding test users...'); + + for (const userData of testUsers) { + let user: User | null = await prisma.user.findFirst({ + where: { email: userData.email } + }); + + if (!user) { + user = await prisma.user.create({ + data: { + firstName: userData.firstName, + lastName: userData.lastName, + email: userData.email, + phone: userData.phone, + countryCode: userData.countryCode, + role: { connect: { id: userData.roleId } }, + verified: userData.verified, + phoneVerifiedAt: userData.phoneVerifiedAt, + emailVerifiedAt: userData.emailVerifiedAt + } + }); + console.log(` āœ“ Created user: ${userData.email}`); + } else { + console.log(` - Exists: ${userData.email}`); + } + + const role = roles.find(r => r.id === userData.roleId); + if (role?.passwordRequired) { + const hasPassword = await prisma.password.findFirst({ where: { userId: user.id } }); + + if (!hasPassword) { + const salt = generateSalt(); + await prisma.password.create({ + data: { + user: { connect: { id: user.id } }, + password: hashPassword('Admin@123', salt), + salt + } + }); + console.log(` āœ“ Created password for: ${userData.email}`); + } + } + } +} + +async function seedSystemConfigs(): Promise { + console.log('Seeding system configs...'); + + for (const config of systemConfigs) { + const data = { value: config.value, type: config.type, description: config.description }; + + await prisma.systemConfig.upsert({ + where: { key: config.key }, + update: data, + create: { key: config.key, ...data } + }); + } + + console.log(` āœ“ ${systemConfigs.length} system configs`); +} + +async function resetSequences(): Promise { + console.log('Resetting sequences...'); + + const tables = ['roles', 'permissions']; + + for (const table of tables) { + try { + await prisma.$executeRawUnsafe( + `SELECT setval('${table}_id_seq', COALESCE((SELECT MAX(id) FROM "${table}"), 1));` + ); + } catch { + console.log(` - Skipped: ${table} (no sequence)`); + } + } + + console.log(' āœ“ Sequences reset'); +} + +// ============================================================================ +// Main +// ============================================================================ + +async function main(): Promise { + console.log('\n🌱 Starting database seed...\n'); + + try { + await seedRoles(); + await seedPermissions(); + await seedTestUsers(); + await seedSystemConfigs(); + await resetSequences(); + + console.log('\nāœ… Database seeded successfully!\n'); + console.log('Test Users (OTP login — dev OTP: 123456):'); + console.log(' Customer: customer@test.com'); + console.log(''); + console.log('Test Users (Password login — Admin@123):'); + console.log(' Admin: admin@test.com'); + console.log(' Super Admin: superadmin@test.com\n'); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + console.error('\nāŒ Seed failed:', message); + throw error; + } +} + +main() + .catch((error: unknown) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..776e786 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,111 @@ +/** + * Application Entry Point + * Starts the server with optional clustering in production + */ + +import cluster from 'cluster'; +import os from 'os'; +import http from 'http'; +import app from './app'; +import environment from '@config/environment.config'; +import { connectDatabase, disconnectDatabase, disconnectRedisClient, initContainer } from '@core/index'; +import { createLogger } from '@services/index'; +import { initSessionUpdateWorker, shutdownSessionQueue } from '@http/modules/auth'; + +const log = createLogger('server'); +const { environment: env, port } = environment.basic; +const isProduction = env === 'production'; +const cpuCount = os.cpus().length; + +// ============================================================================ +// Cluster Mode (Production) +// ============================================================================ + +if (isProduction && cluster.isPrimary) { + log.info('Starting cluster mode', { cpuCount, environment: env }); + + for (let i = 0; i < cpuCount; i++) { + cluster.fork(); + } + + cluster.on('exit', worker => { + log.warn('Worker died, restarting...', { + workerId: worker.id, + pid: worker.process.pid + }); + cluster.fork(); + }); +} else { + // ======================================================================== + // Single Process / Worker Mode + // ======================================================================== + + const startServer = async (): Promise => { + try { + // Initialize DI container + initContainer(); + + // Connect to database + await connectDatabase(); + + // Initialize session update worker (BullMQ) + await initSessionUpdateWorker(); + + // Create HTTP server + const server = http.createServer(app); + + server.listen(port, () => { + log.info('Server started successfully', { + environment: env, + port, + pid: process.pid, + url: `http://127.0.0.1:${port}` + }); + }); + + // Graceful shutdown + const gracefulShutdown = async (signal: string): Promise => { + log.info(`${signal} received, shutting down gracefully...`); + + server.close(async () => { + log.info('HTTP server closed'); + await shutdownSessionQueue(); + log.info('Session queue closed'); + await disconnectRedisClient(); + log.info('Redis disconnected'); + await disconnectDatabase(); + log.info('Database disconnected'); + process.exit(0); + }); + + // Force shutdown after 30 seconds + setTimeout(() => { + log.error('Forced shutdown after timeout'); + process.exit(1); + }, 30000); + }; + + process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); + process.on('SIGINT', () => gracefulShutdown('SIGINT')); + + // Handle uncaught exceptions + process.on('uncaughtException', error => { + log.error('Uncaught exception', { error: error.message, stack: error.stack }); + gracefulShutdown('uncaughtException'); + }); + + process.on('unhandledRejection', (reason: unknown) => { + log.error('Unhandled rejection', { + reason: reason instanceof Error ? reason.message : String(reason) + }); + }); + } catch (error) { + log.error('Failed to start server', { + error: error instanceof Error ? error.message : 'Unknown error' + }); + process.exit(1); + } + }; + + startServer(); +} diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..89a19b2 --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import environment from '@config/environment.config'; +import AppController from '@http/controllers/app/app.controller'; +import modulesRouter from '@http/modules'; + +const router = Router(); + +router.get('/', AppController.appLanding); +router.get('/health', AppController.appHealth); +router.use(`/v${environment.app?.version}`, modulesRouter); + +export default router; diff --git a/storage/assets/public/favicon.ico b/storage/assets/public/favicon.ico new file mode 100644 index 0000000..068d535 Binary files /dev/null and b/storage/assets/public/favicon.ico differ diff --git a/storage/keys/.gitignore b/storage/keys/.gitignore new file mode 100644 index 0000000..cc8e9a4 --- /dev/null +++ b/storage/keys/.gitignore @@ -0,0 +1,2 @@ +api/* +encryption/* \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b1149e0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,77 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "lib": [ + "ES2020" + ], + "outDir": "./dist", + "rootDir": "./src", + "baseUrl": "./src", + "paths": { + "@/*": [ + "./*" + ], + "@core/*": [ + "./app/core/*" + ], + "@app/*": [ + "./app/*" + ], + "@http/*": [ + "./app/http/*" + ], + "@modules/*": [ + "./app/http/modules/*" + ], + "@services/*": [ + "./app/http/services/*" + ], + "@middleware/*": [ + "./app/http/middleware/*" + ], + "@config/*": [ + "./config/*" + ], + "@database/*": [ + "./database/*" + ] + }, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "sourceMap": true, + "removeComments": true, + "types": [ + "node", + "express" + ], + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.spec.ts" + ], + "ts-node": { + "transpileOnly": true, + "files": true + } +}