From 5bd91cbb5fb9935951e0bb46fefb5f05d96b3a03 Mon Sep 17 00:00:00 2001 From: Mirsolih Mirzaahmad Date: Sat, 6 Jun 2026 21:51:35 +0500 Subject: [PATCH 1/2] Create production-minded Express starter kit --- .dockerignore | 7 + .env.example | 10 +- .github/workflows/ci.yml | 23 + .gitignore | 11 +- Dockerfile | 24 + README.md | 139 +- docker-compose.yml | 23 + package-lock.json | 2320 --------------------- package.json | 69 +- pnpm-lock.yaml | 487 ++++- src/app.ts | 62 +- src/config/config.ts | 5 - src/config/env.ts | 66 + src/config/logger.ts | 54 + src/controllers/UserController.ts | 78 - src/index.ts | 47 +- src/infrastructure/database/mongoose.ts | 15 + src/middlewares/errorHandler.ts | 11 - src/middlewares/logger.ts | 38 - src/modules/health/health.routes.ts | 23 + src/modules/openapi/openapi.routes.ts | 52 + src/modules/users/password.ts | 24 + src/modules/users/user.controller.ts | 32 + src/modules/users/user.model.ts | 47 + src/modules/users/user.repository.ts | 112 + src/modules/users/user.routes.ts | 22 + src/modules/users/user.schemas.ts | 110 + src/modules/users/user.service.ts | 72 + src/modules/users/user.types.ts | 29 + src/routes/user.ts | 12 - src/schemas/user.schema.ts | 24 - src/shared/errors/app-error.ts | 29 + src/shared/http/request-id.ts | 10 + src/shared/middleware/error-handler.ts | 25 + src/shared/middleware/not-found.ts | 10 + src/shared/middleware/rate-limit.ts | 35 + src/shared/middleware/request-logger.ts | 23 + src/shared/middleware/security.ts | 37 + src/shared/middleware/validate-request.ts | 29 + src/utils/types.ts | 5 - src/utils/util.ts | 5 - test/app.test.ts | 83 + tsconfig.json | 35 +- 43 files changed, 1668 insertions(+), 2706 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/ci.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yml delete mode 100644 package-lock.json delete mode 100644 src/config/config.ts create mode 100644 src/config/env.ts create mode 100644 src/config/logger.ts delete mode 100644 src/controllers/UserController.ts create mode 100644 src/infrastructure/database/mongoose.ts delete mode 100644 src/middlewares/errorHandler.ts delete mode 100644 src/middlewares/logger.ts create mode 100644 src/modules/health/health.routes.ts create mode 100644 src/modules/openapi/openapi.routes.ts create mode 100644 src/modules/users/password.ts create mode 100644 src/modules/users/user.controller.ts create mode 100644 src/modules/users/user.model.ts create mode 100644 src/modules/users/user.repository.ts create mode 100644 src/modules/users/user.routes.ts create mode 100644 src/modules/users/user.schemas.ts create mode 100644 src/modules/users/user.service.ts create mode 100644 src/modules/users/user.types.ts delete mode 100644 src/routes/user.ts delete mode 100644 src/schemas/user.schema.ts create mode 100644 src/shared/errors/app-error.ts create mode 100644 src/shared/http/request-id.ts create mode 100644 src/shared/middleware/error-handler.ts create mode 100644 src/shared/middleware/not-found.ts create mode 100644 src/shared/middleware/rate-limit.ts create mode 100644 src/shared/middleware/request-logger.ts create mode 100644 src/shared/middleware/security.ts create mode 100644 src/shared/middleware/validate-request.ts delete mode 100644 src/utils/types.ts delete mode 100644 src/utils/util.ts create mode 100644 test/app.test.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a8a1727 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +coverage +.env +.git +.gitignore +npm-debug.log* diff --git a/.env.example b/.env.example index 6cfe119..df98947 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,8 @@ -DB_URI=mongodb://localhost:27017 -PORT=3000 +NODE_ENV=development HOST=localhost - +PORT=3000 +DB_URI=mongodb://localhost:27017/express_starter +CORS_ORIGINS=http://localhost:3000,http://localhost:5173 +LOG_LEVEL=info +RATE_LIMIT_WINDOW_MS=900000 +RATE_LIMIT_MAX=100 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fee230e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: [master, main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10.10.0 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm lint + - run: pnpm test + - run: pnpm build diff --git a/.gitignore b/.gitignore index 3b4250c..3ad9643 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ node_modules/ .env +.env.* +!.env.example dist/ - -# pnpm -pnpm-lock.yaml -pnpm-workspace.yaml +coverage/ +.DS_Store +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a8c47f2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM node:22-alpine AS deps +WORKDIR /app +RUN corepack enable +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +FROM node:22-alpine AS build +WORKDIR /app +RUN corepack enable +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN pnpm build +RUN pnpm prune --prod + +FROM node:22-alpine AS runtime +WORKDIR /app +ENV NODE_ENV=production +RUN addgroup -S app && adduser -S app -G app +COPY --from=build /app/package.json ./package.json +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist +USER app +EXPOSE 3000 +CMD ["node", "dist/index.js"] diff --git a/README.md b/README.md index acd80e4..5b66c7f 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,115 @@ # Express Starter 🚀 -A minimal and scalable Express + TypeScript starter project. This template helps you quickly spin up a REST API or microservice with clean architecture and modern tooling. - -## ✨ Features - -- Express 5 — modern middleware and routing - -- TypeScript — strong typing out of the box - -- Mongoose — ODM for MongoDB - -- Modular structure — `controllers`, `routes`, `middlewares`, `schemas`, `utils` - -- .env support — via `tsx` and `node --env-file` - -- Tsc Alias — clean, short import paths - -## 📂 Project Structure +A simple but strong Express 5 + TypeScript starter kit for production-minded REST APIs. + +## What is included + +- Express 5 application factory with dependency injection-friendly composition. +- TypeScript strict mode with path aliases and additional safety flags. +- Built-in request validation for bodies and route params. +- Centralized error handling with safe response envelopes. +- Security headers, CORS, JSON body limits, request IDs, and in-memory rate limiting. +- Structured JSON request logging. +- Feature-based users and health modules. +- Mongoose repository implementation plus an in-memory repository for tests. +- Password hashing with Node.js `crypto.scrypt` and public DTO responses that never expose password hashes. +- Liveness/readiness health checks. +- OpenAPI JSON starter endpoint. +- Node test runner API tests. +- Docker, Docker Compose, and GitHub Actions CI. +- Graceful shutdown for HTTP server and database connections. + +## Project structure ```bash src/ -├── app.ts # Express app configuration -├── index.ts # Entry point (server starts here) -├── config/ # Configurations -├── controllers/ # Controllers -├── middlewares/ # Middlewares -├── routes/ # Routes -├── schemas/ # Mongoose schemas -└── utils/ # Helpers and types +├── app.ts # Express app factory +├── index.ts # Infrastructure bootstrap and graceful shutdown +├── config/ # Environment validation and logger +├── infrastructure/database/ # Mongoose connection lifecycle +├── modules/ +│ ├── health/ # Liveness/readiness routes +│ ├── openapi/ # OpenAPI JSON endpoint +│ └── users/ # User feature module +└── shared/ + ├── errors/ # Application error types + ├── http/ # Request lifecycle utilities + └── middleware/ # Shared middleware ``` -## 🛠️ Installation -```bash -# Clone the repository -git clone https://github.com/xteam-uz/express-starter.git -cd express-starter +## Requirements -# Install dependencies (recommended PNPM) -pnpm install -``` +- Node.js 22+ +- PNPM 10+ +- MongoDB for database-backed routes -## ⚙️ Configuration +## Setup -Create a .env file in the project root: ```bash -SERVER_HOST=localhost -SERVER_PORT=3000 -DB_URI=mongodb://localhost:27017/ +pnpm install +cp .env.example .env +pnpm dev ``` -## 🚀 Run +## Configuration + +Environment variables are validated at startup. -Development: ```bash -pnpm dev +NODE_ENV=development +HOST=localhost +PORT=3000 +DB_URI=mongodb://localhost:27017/express_starter +CORS_ORIGINS=http://localhost:3000,http://localhost:5173 +LOG_LEVEL=info +RATE_LIMIT_WINDOW_MS=900000 +RATE_LIMIT_MAX=100 ``` -Runs with hot-reload using tsx. -## Build: +## Scripts + ```bash -pnpm build +pnpm dev # Run with hot reload +pnpm build # Compile TypeScript and rewrite path aliases +pnpm start # Run compiled application +pnpm typecheck # Type-check without emitting files +pnpm lint # Run TypeScript lint-style checks +pnpm test # Run API tests ``` -## Production: +## API endpoints + ```bash -pnpm start +GET /health/live +GET /health/ready +GET /docs/openapi.json +GET /api/users +GET /api/users/:id +POST /api/users +PUT /api/users/:id +DELETE /api/users/:id ``` -## 📡 Example API +## Docker -src/routes/user.ts defines routes handled by UserController: ```bash -GET /users -POST /users +docker compose up --build ``` -The Mongoose schema is defined in src/schemas/user.schema.ts. -## 🧑‍💻 Contributing +The API listens on `http://localhost:3000` and MongoDB on `localhost:27017`. + +## Security baseline + +This starter intentionally includes a minimal security baseline instead of leaving it as an exercise: + +- Built-in middleware disables or hardens common HTTP headers. +- CORS is environment-driven instead of wide open by default. +- JSON payloads are capped at `100kb`. +- Rate limiting is enabled globally. +- Passwords are hashed before storage. +- Password hashes are excluded from public responses. +- Error responses avoid leaking raw exception objects. -Feel free to open an issue or submit a pull request to improve this project. +## Architecture notes +The application uses feature modules and constructor-injected services/repositories so that controllers stay thin and tests can run without MongoDB. This keeps the starter simple while leaving room for Clean Architecture or Hexagonal Architecture evolution. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a83d05a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +services: + api: + build: . + environment: + NODE_ENV: production + HOST: 0.0.0.0 + PORT: 3000 + DB_URI: mongodb://mongo:27017/express_starter + CORS_ORIGINS: http://localhost:3000 + ports: + - "3000:3000" + depends_on: + - mongo + + mongo: + image: mongo:7 + ports: + - "27017:27017" + volumes: + - mongo_data:/data/db + +volumes: + mongo_data: diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 5de60ed..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2320 +0,0 @@ -{ - "name": "express-starter", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "express-starter", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "chalk": "^5.6.2", - "express": "^5.1.0", - "mongoose": "^8.18.1" - }, - "devDependencies": { - "@types/chalk": "^0.4.31", - "@types/express": "^5.0.1", - "@types/mongoose": "^5.11.96", - "@types/morgan": "^1.9.10", - "@types/node": "^22.15.16", - "morgan": "^1.10.1", - "tsc-alias": "^1.8.16", - "tsx": "^4.19.4", - "typescript": "^5.8.3" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz", - "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", - "license": "MIT", - "dependencies": { - "sparse-bitfield": "^3.0.3" - } - }, - "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/@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/chalk": { - "version": "0.4.31", - "resolved": "https://registry.npmjs.org/@types/chalk/-/chalk-0.4.31.tgz", - "integrity": "sha512-nF0fisEPYMIyfrFgabFimsz9Lnuu9MwkNrrlATm2E4E46afKDyeelT+8bXfw1VSc7sLBxMxRgT7PxTC2JcqN4Q==", - "dev": true, - "license": "MIT" - }, - "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/express": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", - "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", - "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", - "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/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mongoose": { - "version": "5.11.96", - "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.11.96.tgz", - "integrity": "sha512-keiY22ljJtXyM7osgScmZOHV6eL5VFUD5tQumlu+hjS++HND5nM8jNEdj5CSWfKIJpVwQfPuwQ2SfBqUnCAVRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mongoose": "*" - } - }, - "node_modules/@types/morgan": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", - "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "22.18.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.1.tgz", - "integrity": "sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.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/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", - "license": "MIT" - }, - "node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", - "license": "MIT", - "dependencies": { - "@types/webidl-conversions": "*" - } - }, - "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/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/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/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/basic-auth/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==", - "dev": true, - "license": "MIT" - }, - "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.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "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/bson": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", - "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.20.1" - } - }, - "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/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/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "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/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/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "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/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "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/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/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/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/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/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/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" - } - }, - "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/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/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/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.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "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": ">= 0.8" - } - }, - "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/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/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-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.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "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/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/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/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/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/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "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/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/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-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-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/kareem": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", - "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "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/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "license": "MIT" - }, - "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/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.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.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mongodb": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", - "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==", - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.0" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", - "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", - "license": "Apache-2.0", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^14.1.0 || ^13.0.0" - } - }, - "node_modules/mongoose": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.18.1.tgz", - "integrity": "sha512-K0RfrUXXufqNRZZjvAGdyjydB91SnbWxlwFYi5t7zN2DxVWFD3c6puia0/7xfBwZm6RCpYOVdYFlRFpoDWiC+w==", - "license": "MIT", - "dependencies": { - "bson": "^6.10.4", - "kareem": "2.6.3", - "mongodb": "~6.18.0", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "17.1.3" - }, - "engines": { - "node": ">=16.20.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mongoose" - } - }, - "node_modules/morgan": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", - "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.1.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/morgan/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/morgan/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/mpath": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", - "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "license": "MIT", - "dependencies": { - "debug": "4.x" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "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/mylas": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz", - "integrity": "sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/raouldeheer" - } - }, - "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/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/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/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/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "dev": true, - "license": "MIT", - "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/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-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/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/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/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==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "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/raw-body": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", - "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.7.0", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "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/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/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/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/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-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/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/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "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/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/sift": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", - "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", - "license": "MIT" - }, - "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/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "license": "MIT", - "dependencies": { - "memory-pager": "^1.0.2" - } - }, - "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/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": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "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/tsx": { - "version": "4.20.5", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", - "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "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.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": 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/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": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "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" - } - } -} diff --git a/package.json b/package.json index 758a91f..2dffe4a 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,37 @@ { - "name": "express-starter", - "version": "1.0.0", - "description": "", - "main": "index.js", - "type": "module", - "scripts": { - "dev": "tsx --watch --env-file=.env src/index.ts", - "build": "tsc && tsc-alias", - "start": "node --env-file=.env dist/index.js" - }, - "keywords": [ - "express", - "node.js", - "typescript" - ], - "author": "Mirsoli Mirsultonov", - "license": "ISC", - "packageManager": "pnpm@10.10.0", - "dependencies": { - "chalk": "^5.6.2", - "express": "^5.1.0", - "mongoose": "^8.18.1" - }, - "devDependencies": { - "@types/chalk": "^0.4.31", - "@types/express": "^5.0.1", - "@types/mongoose": "^5.11.96", - "@types/morgan": "^1.9.10", - "@types/node": "^22.15.16", - "morgan": "^1.10.1", - "tsc-alias": "^1.8.16", - "tsx": "^4.19.4", - "typescript": "^5.8.3" - } + "name": "express-starter", + "version": "2.0.0", + "description": "A simple, production-minded Express 5 + TypeScript starter kit.", + "main": "dist/index.js", + "type": "module", + "scripts": { + "dev": "tsx --watch --env-file=.env src/index.ts", + "build": "tsc && tsc-alias", + "start": "node --env-file=.env dist/index.js", + "test": "tsx --test test/*.test.ts", + "test:watch": "tsx --test --watch test/*.test.ts", + "lint": "tsc --noEmit", + "typecheck": "tsc --noEmit" + }, + "keywords": [ + "express", + "node.js", + "typescript", + "starter-kit", + "api" + ], + "author": "Mirsoli Mirsultonov", + "license": "ISC", + "packageManager": "pnpm@10.10.0", + "dependencies": { + "express": "^5.1.0", + "mongoose": "^8.18.1" + }, + "devDependencies": { + "@types/express": "^5.0.1", + "@types/node": "^22.15.16", + "tsc-alias": "^1.8.16", + "tsx": "^4.19.4", + "typescript": "^5.8.3" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b3cac3..2c6a4dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,15 +11,9 @@ importers: express: specifier: ^5.1.0 version: 5.1.0 - nanoid: - specifier: ^5.1.5 - version: 5.1.5 - redis: - specifier: ^5.0.1 - version: 5.0.1 - zod: - specifier: ^3.24.4 - version: 3.24.4 + mongoose: + specifier: ^8.18.1 + version: 8.24.0 devDependencies: '@types/express': specifier: ^5.0.1 @@ -27,6 +21,9 @@ importers: '@types/node': specifier: ^22.15.16 version: 22.15.16 + tsc-alias: + specifier: ^1.8.16 + version: 1.8.17 tsx: specifier: ^4.19.4 version: 4.19.4 @@ -186,33 +183,20 @@ packages: cpu: [x64] os: [win32] - '@redis/bloom@5.0.1': - resolution: {integrity: sha512-F7L+rnuJvq/upKaVoEgsf8VT7g5pLQYWRqSUOV3uO4vpVtARzSKJ7CLyJjVsQS+wZVCGxsLMh8DwAIDcny1B+g==} - engines: {node: '>= 18'} - peerDependencies: - '@redis/client': ^5.0.1 - - '@redis/client@5.0.1': - resolution: {integrity: sha512-k0EJvlMGEyBqUD3orKe0UMZ66fPtfwqPIr+ZSd853sXj2EyhNtPXSx+J6sENXJNgAlEBhvD+57Dwt0qTisKB0A==} - engines: {node: '>= 18'} + '@mongodb-js/saslprep@1.4.11': + resolution: {integrity: sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==} - '@redis/json@5.0.1': - resolution: {integrity: sha512-t94HOTk5myfhvaHZzlUzk2hoUvH2jsjftcnMgJWuHL/pzjAJQoZDCUJzjkoXIUjWXuyJixTguaaDyOZWwqH2Kg==} - engines: {node: '>= 18'} - peerDependencies: - '@redis/client': ^5.0.1 + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} - '@redis/search@5.0.1': - resolution: {integrity: sha512-wipK6ZptY7K68B7YLVhP5I/wYCDUU+mDJMyJiUcQLuOs7/eKOBc8lTXKUSssor8QnzZSPy4A5ulcC5PZY22Zgw==} - engines: {node: '>= 18'} - peerDependencies: - '@redis/client': ^5.0.1 + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} - '@redis/time-series@5.0.1': - resolution: {integrity: sha512-k6PgbrakhnohsEWEAdQZYt3e5vSKoIzpKvgQt8//lnWLrTZx+c3ed2sj0+pKIF4FvnSeuXLo4bBWcH0Z7Urg1A==} - engines: {node: '>= 18'} - peerDependencies: - '@redis/client': ^5.0.1 + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} @@ -247,14 +231,40 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/webidl-conversions@7.0.3': + resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + + '@types/whatwg-url@11.0.5': + resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + body-parser@2.2.0: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + bson@6.10.4: + resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} + engines: {node: '>=16.20.1'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -267,9 +277,13 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - cluster-key-slot@1.1.2: - resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} - engines: {node: '>=0.10.0'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} content-disposition@1.0.0: resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} @@ -300,6 +314,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -339,6 +357,17 @@ packages: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + finalhandler@2.1.0: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} @@ -370,6 +399,14 @@ packages: get-tsconfig@4.10.0: resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -390,6 +427,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -397,9 +438,29 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + kareem@2.6.3: + resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==} + engines: {node: '>=12.0.0'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -408,10 +469,21 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} + memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} @@ -420,18 +492,63 @@ packages: resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} engines: {node: '>= 0.6'} + mongodb-connection-string-url@3.0.2: + resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} + + mongodb@6.20.0: + resolution: {integrity: sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==} + engines: {node: '>=16.20.1'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.188.0 + '@mongodb-js/zstd': ^1.1.0 || ^2.0.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' + snappy: ^7.3.2 + socks: ^2.7.1 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + + mongoose@8.24.0: + resolution: {integrity: sha512-EEZwOibDPZ5uZN3bFapfnRskEbdljAf6sP9ln6u+P4e5IfkOAh6Tqw2g8/Tag++KHOAJ095WXT/c0uqRq4Vckg==} + engines: {node: '>=16.20.1'} + + mpath@0.9.0: + resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==} + engines: {node: '>=4.0.0'} + + mquery@5.0.0: + resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} + engines: {node: '>=14.0.0'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@5.1.5: - resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} - engines: {node: ^18 || >=20} - hasBin: true + mylas@2.1.14: + resolution: {integrity: sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==} + engines: {node: '>=16.0.0'} negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -451,14 +568,37 @@ packages: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -467,17 +607,24 @@ packages: resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} engines: {node: '>= 0.8'} - redis@5.0.1: - resolution: {integrity: sha512-J8nqUjrfSq0E8NQkcHDZ4HdEQk5RMYjP3jZq02PE+ERiRxolbDNxPaTT4xh6tdrme+lJ86Goje9yMt9uzh23hQ==} - engines: {node: '>= 18'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -511,14 +658,37 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + sift@17.1.3: + resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + sparse-bitfield@3.0.3: + resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tsc-alias@1.8.17: + resolution: {integrity: sha512-EIduCZHqbNwPm8BZYfq1aD7BQ697A4h6uSGMOFQfYGoQwfrYFTKwYfy9Bv42YxHkduVBcn9Zx0DkX111DKskyg==} + engines: {node: '>=16.20.2'} + hasBin: true + tsx@4.19.4: resolution: {integrity: sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==} engines: {node: '>=18.0.0'} @@ -544,12 +714,17 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - zod@3.24.4: - resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==} - snapshots: '@esbuild/aix-ppc64@0.25.4': @@ -627,25 +802,21 @@ snapshots: '@esbuild/win32-x64@0.25.4': optional: true - '@redis/bloom@5.0.1(@redis/client@5.0.1)': - dependencies: - '@redis/client': 5.0.1 - - '@redis/client@5.0.1': + '@mongodb-js/saslprep@1.4.11': dependencies: - cluster-key-slot: 1.1.2 + sparse-bitfield: 3.0.3 - '@redis/json@5.0.1(@redis/client@5.0.1)': + '@nodelib/fs.scandir@2.1.5': dependencies: - '@redis/client': 5.0.1 + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 - '@redis/search@5.0.1(@redis/client@5.0.1)': - dependencies: - '@redis/client': 5.0.1 + '@nodelib/fs.stat@2.0.5': {} - '@redis/time-series@5.0.1(@redis/client@5.0.1)': + '@nodelib/fs.walk@1.2.8': dependencies: - '@redis/client': 5.0.1 + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 '@types/body-parser@1.19.5': dependencies: @@ -692,11 +863,26 @@ snapshots: '@types/node': 22.15.16 '@types/send': 0.17.4 + '@types/webidl-conversions@7.0.3': {} + + '@types/whatwg-url@11.0.5': + dependencies: + '@types/webidl-conversions': 7.0.3 + accepts@2.0.0: dependencies: mime-types: 3.0.1 negotiator: 1.0.0 + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + array-union@2.1.0: {} + + binary-extensions@2.3.0: {} + body-parser@2.2.0: dependencies: bytes: 3.1.2 @@ -711,6 +897,12 @@ snapshots: transitivePeerDependencies: - supports-color + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + bson@6.10.4: {} + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -723,7 +915,19 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - cluster-key-slot@1.1.2: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + commander@9.5.0: {} content-disposition@1.0.0: dependencies: @@ -741,6 +945,10 @@ snapshots: depd@2.0.0: {} + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -823,6 +1031,22 @@ snapshots: transitivePeerDependencies: - supports-color + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + finalhandler@2.1.0: dependencies: debug: 4.4.0 @@ -865,6 +1089,19 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + gopd@1.2.0: {} has-symbols@1.1.0: {} @@ -885,30 +1122,95 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ignore@5.3.2: {} + inherits@2.0.4: {} ipaddr.js@1.9.1: {} + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + is-promise@4.0.0: {} + kareem@2.6.3: {} + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} + memory-pager@1.5.0: {} + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + mime-db@1.54.0: {} mime-types@3.0.1: dependencies: mime-db: 1.54.0 + mongodb-connection-string-url@3.0.2: + dependencies: + '@types/whatwg-url': 11.0.5 + whatwg-url: 14.2.0 + + mongodb@6.20.0: + dependencies: + '@mongodb-js/saslprep': 1.4.11 + bson: 6.10.4 + mongodb-connection-string-url: 3.0.2 + + mongoose@8.24.0: + dependencies: + bson: 6.10.4 + kareem: 2.6.3 + mongodb: 6.20.0 + mpath: 0.9.0 + mquery: 5.0.0 + ms: 2.1.3 + sift: 17.1.3 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - gcp-metadata + - kerberos + - mongodb-client-encryption + - snappy + - socks + - supports-color + + mpath@0.9.0: {} + + mquery@5.0.0: + dependencies: + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + ms@2.1.3: {} - nanoid@5.1.5: {} + mylas@2.1.14: {} negotiator@1.0.0: {} + normalize-path@3.0.0: {} + object-inspect@1.13.4: {} on-finished@2.4.1: @@ -923,15 +1225,29 @@ snapshots: path-to-regexp@8.2.0: {} + path-type@4.0.0: {} + + picomatch@2.3.2: {} + + plimit-lit@1.6.1: + dependencies: + queue-lit: 1.5.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.14.0: dependencies: side-channel: 1.1.0 + queue-lit@1.5.2: {} + + queue-microtask@1.2.3: {} + range-parser@1.2.1: {} raw-body@3.0.0: @@ -941,16 +1257,14 @@ snapshots: iconv-lite: 0.6.3 unpipe: 1.0.0 - redis@5.0.1: + readdirp@3.6.0: dependencies: - '@redis/bloom': 5.0.1(@redis/client@5.0.1) - '@redis/client': 5.0.1 - '@redis/json': 5.0.1(@redis/client@5.0.1) - '@redis/search': 5.0.1(@redis/client@5.0.1) - '@redis/time-series': 5.0.1(@redis/client@5.0.1) + picomatch: 2.3.2 resolve-pkg-maps@1.0.0: {} + reusify@1.1.0: {} + router@2.2.0: dependencies: debug: 4.4.0 @@ -961,6 +1275,10 @@ snapshots: transitivePeerDependencies: - supports-color + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} @@ -1020,10 +1338,36 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + sift@17.1.3: {} + + slash@3.0.0: {} + + sparse-bitfield@3.0.3: + dependencies: + memory-pager: 1.5.0 + statuses@2.0.1: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + toidentifier@1.0.1: {} + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tsc-alias@1.8.17: + dependencies: + chokidar: 3.6.0 + commander: 9.5.0 + get-tsconfig: 4.10.0 + globby: 11.1.0 + mylas: 2.1.14 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 + tsx@4.19.4: dependencies: esbuild: 0.25.4 @@ -1045,6 +1389,11 @@ snapshots: vary@1.1.2: {} - wrappy@1.0.2: {} + webidl-conversions@7.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 - zod@3.24.4: {} + wrappy@1.0.2: {} diff --git a/src/app.ts b/src/app.ts index b367646..bc5fda6 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,32 +1,42 @@ -import { json, type Application } from "express"; -import morgan from "morgan"; -import mongoose from "mongoose"; -import UserRouter from "@routes/user.js"; -import { logger } from "@middlewares/logger.js"; -import { errorHandler } from "@middlewares/errorHandler.js"; +import express, { type Application } from "express"; +import { createHealthRouter } from "@modules/health/health.routes.js"; +import { createOpenApiRouter } from "@modules/openapi/openapi.routes.js"; +import { MongooseUserRepository } from "@modules/users/user.repository.js"; +import { createUserRouter } from "@modules/users/user.routes.js"; +import { UserService } from "@modules/users/user.service.js"; +import type { UserRepository } from "@modules/users/user.types.js"; +import { requestId } from "@shared/http/request-id.js"; +import { errorHandler } from "@shared/middleware/error-handler.js"; +import { notFoundHandler } from "@shared/middleware/not-found.js"; +import { rateLimit } from "@shared/middleware/rate-limit.js"; +import { requestLogger } from "@shared/middleware/request-logger.js"; +import { cors, securityHeaders } from "@shared/middleware/security.js"; -const App = (app: Application): void => { - // Middlewares - app.use(json()); - // app.use(morgan("dev")); +export type AppDependencies = { + userRepository?: UserRepository; +}; + +export const createApp = (dependencies: AppDependencies = {}): Application => { + const app = express(); + const userRepository = dependencies.userRepository ?? new MongooseUserRepository(); + const userService = new UserService(userRepository); + + app.disable("x-powered-by"); + app.use(requestId); + app.use(requestLogger); + app.use(securityHeaders); + app.use(cors); + app.use(express.json({ limit: "100kb" })); + app.use(rateLimit); - // Database connection - mongoose - .connect(process.env.DB_URI || "mongodb://localhost:27017", { - dbName: "express_crud", - }) - .then(() => { - console.log("Database connected"); - }) - .catch((error) => { - console.log("Database connection error: ", error); - }); + app.use("/health", createHealthRouter()); + app.use("/docs", createOpenApiRouter()); + app.use("/api/users", createUserRouter(userService)); - // Routes - app.use("/api/users", logger, UserRouter); + app.use(notFoundHandler); + app.use(errorHandler); - // Error handler - app.use(errorHandler); + return app; }; -export default App; +export default createApp; diff --git a/src/config/config.ts b/src/config/config.ts deleted file mode 100644 index 8bd5c51..0000000 --- a/src/config/config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const DEVELOPMENT = process.env.NODE_ENV === "development"; -export const PRODUCTION = process.env.NODE_ENV === "production"; - -export const SERVER_HOST = process.env.HOST || "localhost"; -export const SERVER_PORT = process.env.PORT || "8000"; diff --git a/src/config/env.ts b/src/config/env.ts new file mode 100644 index 0000000..435f124 --- /dev/null +++ b/src/config/env.ts @@ -0,0 +1,66 @@ +type NodeEnvironment = "development" | "test" | "production"; +type LogLevel = "fatal" | "error" | "warn" | "info" | "debug" | "trace" | "silent"; + +export type Env = { + NODE_ENV: NodeEnvironment; + HOST: string; + PORT: number; + DB_URI: string | undefined; + CORS_ORIGINS: string[]; + LOG_LEVEL: LogLevel; + RATE_LIMIT_WINDOW_MS: number; + RATE_LIMIT_MAX: number; +}; + +const allowedNodeEnvironments = new Set(["development", "test", "production"]); +const allowedLogLevels = new Set(["fatal", "error", "warn", "info", "debug", "trace", "silent"]); + +const readNumber = (name: string, fallback: number): number => { + const rawValue = process.env[name]; + + if (!rawValue) { + return fallback; + } + + const value = Number(rawValue); + + if (!Number.isInteger(value) || value <= 0) { + throw new Error(`${name} must be a positive integer`); + } + + return value; +}; + +const readNodeEnv = (): NodeEnvironment => { + const value = process.env.NODE_ENV ?? "development"; + + if (!allowedNodeEnvironments.has(value as NodeEnvironment)) { + throw new Error("NODE_ENV must be one of development, test, production"); + } + + return value as NodeEnvironment; +}; + +const readLogLevel = (): LogLevel => { + const value = process.env.LOG_LEVEL ?? "info"; + + if (!allowedLogLevels.has(value as LogLevel)) { + throw new Error("LOG_LEVEL is invalid"); + } + + return value as LogLevel; +}; + +export const env: Env = { + NODE_ENV: readNodeEnv(), + HOST: process.env.HOST ?? "localhost", + PORT: readNumber("PORT", 3000), + DB_URI: process.env.DB_URI, + CORS_ORIGINS: (process.env.CORS_ORIGINS ?? "http://localhost:3000") + .split(",") + .map((origin) => origin.trim()) + .filter(Boolean), + LOG_LEVEL: readLogLevel(), + RATE_LIMIT_WINDOW_MS: readNumber("RATE_LIMIT_WINDOW_MS", 15 * 60 * 1000), + RATE_LIMIT_MAX: readNumber("RATE_LIMIT_MAX", 100), +}; diff --git a/src/config/logger.ts b/src/config/logger.ts new file mode 100644 index 0000000..d8e7c60 --- /dev/null +++ b/src/config/logger.ts @@ -0,0 +1,54 @@ +import { env } from "./env.js"; + +type LogPayload = Record; +type LogLevel = "fatal" | "error" | "warn" | "info" | "debug" | "trace"; + +const levelOrder: Record = { + fatal: 0, + error: 1, + warn: 2, + info: 3, + debug: 4, + trace: 5, + silent: 6, +}; + +const serializeError = (value: unknown): unknown => { + if (value instanceof Error) { + return { + name: value.name, + message: value.message, + stack: env.NODE_ENV === "production" ? undefined : value.stack, + }; + } + + return value; +}; + +const writeLog = (level: LogLevel, payload: LogPayload | string, message?: string): void => { + if (levelOrder[level] > levelOrder[env.LOG_LEVEL]) { + return; + } + + const body = typeof payload === "string" ? { message: payload } : payload; + + console.log( + JSON.stringify({ + level, + time: new Date().toISOString(), + service: "express-starter", + environment: env.NODE_ENV, + message, + ...Object.fromEntries(Object.entries(body).map(([key, value]) => [key, serializeError(value)])), + }), + ); +}; + +export const logger = { + fatal: (payload: LogPayload | string, message?: string) => writeLog("fatal", payload, message), + error: (payload: LogPayload | string, message?: string) => writeLog("error", payload, message), + warn: (payload: LogPayload | string, message?: string) => writeLog("warn", payload, message), + info: (payload: LogPayload | string, message?: string) => writeLog("info", payload, message), + debug: (payload: LogPayload | string, message?: string) => writeLog("debug", payload, message), + trace: (payload: LogPayload | string, message?: string) => writeLog("trace", payload, message), +}; diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts deleted file mode 100644 index ecfd819..0000000 --- a/src/controllers/UserController.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { type Request, type Response } from "express"; -import { User } from "@utils/types.js"; -import UserModel from "@schemas/user.schema.js"; - -class UserController { - getAllUsers = async (req: Request, res: Response) => { - try { - const users: User[] = await UserModel.find(); - return res.status(200).json({ data: users }); - } catch (error) { - return res.status(500).json({ message: "Server Error", error }); - } - }; - - getUser = async (req: Request, res: Response) => { - try { - const { id } = req.params; - const user: User | null = await UserModel.findById(id); - return res.status(200).json({ data: user }); - } catch (error) { - return res.status(500).json({ message: "Server Error", error }); - } - }; - - createUser = async (req: Request, res: Response) => { - try { - const { name, email, password } = req.body; - const newUser = new UserModel({ name, email, password }); - await newUser.save(); - return res - .status(201) - .json({ message: "User successfuly saved", data: newUser }); - } catch (error) { - return res.status(500).json({ message: "Server Error", error }); - } - }; - - updateUser = async (req: Request, res: Response) => { - try { - const { id } = req.params; - const { name, email, password } = req.body; - - const user = await UserModel.findById(id); - if (!user) { - return res.status(404).json({ message: "User not found" }); - } - - user.name = name || user.name; - user.email = email || user.email; - user.password = password || user.password; - - const resUser = await user.save(); - - return res - .status(201) - .json({ message: "User successfuly updated", data: resUser }); - } catch (error) { - return res.status(500).json({ message: "Server Error", error }); - } - }; - - deleteUser = async (req: Request, res: Response) => { - try { - const { id } = req.params; - const user = await UserModel.findByIdAndDelete(id); - if (!user) { - return res.status(404).json({ message: "User not found" }); - } - return res - .status(200) - .json({ message: "User successfuly deleted", data: user }); - } catch (error) { - return res.status(500).json({ message: "Server Error", error }); - } - }; -} - -export default new UserController(); diff --git a/src/index.ts b/src/index.ts index fff2783..c4e4738 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,42 @@ -import express from "express"; -import { SERVER_HOST, SERVER_PORT } from "@config/config.js"; -import App from "./app.js"; +import type { Server } from "node:http"; +import { env } from "@config/env.js"; +import { logger } from "@config/logger.js"; +import { connectDatabase, disconnectDatabase } from "@infrastructure/database/mongoose.js"; +import { createApp } from "./app.js"; -const app = express(); +const start = async (): Promise => { + if (env.DB_URI) { + await connectDatabase(env.DB_URI); + } else { + logger.warn("DB_URI is not configured. Database-backed routes will fail until it is provided."); + } -App(app); + const app = createApp(); + const server = app.listen(env.PORT, env.HOST, () => { + logger.info({ host: env.HOST, port: env.PORT }, "Server started"); + }); -app.listen(SERVER_PORT || 8000, () => - console.log(`Server running on http://${SERVER_HOST}:${SERVER_PORT}`), -).on("error", (error) => { - throw new Error(error.message); + const shutdown = async (signal: NodeJS.Signals): Promise => { + logger.info({ signal }, "Shutdown signal received"); + server.close(async () => { + await disconnectDatabase(); + logger.info("Shutdown complete"); + process.exit(0); + }); + }; + + process.on("SIGTERM", (signal) => { + void shutdown(signal); + }); + + process.on("SIGINT", (signal) => { + void shutdown(signal); + }); + + return server; +}; + +void start().catch((error: unknown) => { + logger.fatal({ err: error }, "Failed to start server"); + process.exit(1); }); diff --git a/src/infrastructure/database/mongoose.ts b/src/infrastructure/database/mongoose.ts new file mode 100644 index 0000000..950e02b --- /dev/null +++ b/src/infrastructure/database/mongoose.ts @@ -0,0 +1,15 @@ +import mongoose from "mongoose"; +import { logger } from "@config/logger.js"; + +export const connectDatabase = async (uri: string): Promise => { + mongoose.set("strictQuery", true); + await mongoose.connect(uri); + logger.info("Database connected"); +}; + +export const disconnectDatabase = async (): Promise => { + await mongoose.connection.close(false); + logger.info("Database disconnected"); +}; + +export const isDatabaseReady = (): boolean => mongoose.connection.readyState === 1; diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts deleted file mode 100644 index aeaeaf2..0000000 --- a/src/middlewares/errorHandler.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type Request, type Response, type NextFunction } from "express"; - -export function errorHandler( - err: any, - req: Request, - res: Response, - next: NextFunction, -) { - console.error(err); - res.status(500).json({ error: "Internal Server Error" }); -} diff --git a/src/middlewares/logger.ts b/src/middlewares/logger.ts deleted file mode 100644 index b7baef2..0000000 --- a/src/middlewares/logger.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { type NextFunction, type Request, type Response } from "express"; -import chalk from "chalk"; - -export const logger = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - const start = process.hrtime.bigint(); - - res.on("finish", () => { - const end = process.hrtime.bigint(); - const duration = Number(end - start) / 1_000_000; // ms - const length = res.getHeader("content-length") || 0; - - let coloredMethod; - switch (req.method) { - case "GET": - coloredMethod = chalk.green(req.method); - break; - case "POST": - coloredMethod = chalk.blue(req.method); - break; - case "DELETE": - coloredMethod = chalk.red(req.method); - break; - default: - coloredMethod = chalk.white(req.method); - } - - console.log( - `${coloredMethod} ${req.originalUrl} ${res.statusCode} ${duration.toFixed( - 3, - )} ms - ${length}`, - ); - }); - next(); -}; diff --git a/src/modules/health/health.routes.ts b/src/modules/health/health.routes.ts new file mode 100644 index 0000000..74d938c --- /dev/null +++ b/src/modules/health/health.routes.ts @@ -0,0 +1,23 @@ +import { Router } from "express"; +import { isDatabaseReady } from "@infrastructure/database/mongoose.js"; + +export const createHealthRouter = (): Router => { + const router = Router(); + + router.get("/live", (_req, res) => { + res.status(200).json({ status: "ok" }); + }); + + router.get("/ready", (_req, res) => { + const databaseReady = isDatabaseReady(); + + res.status(databaseReady ? 200 : 503).json({ + status: databaseReady ? "ready" : "not_ready", + checks: { + database: databaseReady ? "up" : "down", + }, + }); + }); + + return router; +}; diff --git a/src/modules/openapi/openapi.routes.ts b/src/modules/openapi/openapi.routes.ts new file mode 100644 index 0000000..6fa1667 --- /dev/null +++ b/src/modules/openapi/openapi.routes.ts @@ -0,0 +1,52 @@ +import { Router } from "express"; + +const openApiDocument = { + openapi: "3.1.0", + info: { + title: "Express Starter API", + version: "2.0.0", + }, + paths: { + "/health/live": { + get: { + summary: "Liveness check", + responses: { + "200": { description: "Application is alive" }, + }, + }, + }, + "/health/ready": { + get: { + summary: "Readiness check", + responses: { + "200": { description: "Application is ready" }, + "503": { description: "Application is not ready" }, + }, + }, + }, + "/api/users": { + get: { + summary: "List users", + responses: { "200": { description: "Users returned" } }, + }, + post: { + summary: "Create user", + responses: { + "201": { description: "User created" }, + "400": { description: "Validation failed" }, + "409": { description: "Email already registered" }, + }, + }, + }, + }, +}; + +export const createOpenApiRouter = (): Router => { + const router = Router(); + + router.get("/openapi.json", (_req, res) => { + res.status(200).json(openApiDocument); + }); + + return router; +}; diff --git a/src/modules/users/password.ts b/src/modules/users/password.ts new file mode 100644 index 0000000..1178600 --- /dev/null +++ b/src/modules/users/password.ts @@ -0,0 +1,24 @@ +import { randomBytes, scrypt as scryptCallback, timingSafeEqual } from "node:crypto"; +import { promisify } from "node:util"; + +const scrypt = promisify(scryptCallback); +const keyLength = 64; + +export const hashPassword = async (password: string): Promise => { + const salt = randomBytes(16).toString("hex"); + const derivedKey = (await scrypt(password, salt, keyLength)) as Buffer; + return `scrypt:${salt}:${derivedKey.toString("hex")}`; +}; + +export const verifyPassword = async (password: string, passwordHash: string): Promise => { + const [algorithm, salt, hash] = passwordHash.split(":"); + + if (algorithm !== "scrypt" || !salt || !hash) { + return false; + } + + const derivedKey = (await scrypt(password, salt, keyLength)) as Buffer; + const storedKey = Buffer.from(hash, "hex"); + + return storedKey.length === derivedKey.length && timingSafeEqual(storedKey, derivedKey); +}; diff --git a/src/modules/users/user.controller.ts b/src/modules/users/user.controller.ts new file mode 100644 index 0000000..37d757d --- /dev/null +++ b/src/modules/users/user.controller.ts @@ -0,0 +1,32 @@ +import type { RequestHandler } from "express"; +import type { UserService } from "./user.service.js"; +import type { CreateUserBody, UpdateUserBody } from "./user.schemas.js"; + +export class UserController { + constructor(private readonly userService: UserService) {} + + findAll: RequestHandler = async (_req, res) => { + const users = await this.userService.findAll(); + res.status(200).json({ data: users }); + }; + + findById: RequestHandler<{ id: string }> = async (req, res) => { + const user = await this.userService.findById(req.params.id); + res.status(200).json({ data: user }); + }; + + create: RequestHandler, unknown, CreateUserBody> = async (req, res) => { + const user = await this.userService.create(req.body); + res.status(201).json({ data: user }); + }; + + update: RequestHandler<{ id: string }, unknown, UpdateUserBody> = async (req, res) => { + const user = await this.userService.update(req.params.id, req.body); + res.status(200).json({ data: user }); + }; + + delete: RequestHandler<{ id: string }> = async (req, res) => { + const user = await this.userService.delete(req.params.id); + res.status(200).json({ data: user }); + }; +} diff --git a/src/modules/users/user.model.ts b/src/modules/users/user.model.ts new file mode 100644 index 0000000..1f45ffe --- /dev/null +++ b/src/modules/users/user.model.ts @@ -0,0 +1,47 @@ +import { Schema, model } from "mongoose"; + +export type UserDocument = { + name: string; + email: string; + passwordHash: string; + createdAt: Date; + updatedAt: Date; +}; + +const userSchema = new Schema( + { + name: { + type: String, + required: true, + trim: true, + maxlength: 100, + }, + email: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true, + index: true, + }, + passwordHash: { + type: String, + required: true, + select: false, + }, + }, + { + timestamps: true, + toJSON: { + virtuals: true, + versionKey: false, + transform: (_doc, ret: Record) => { + delete ret._id; + delete ret.passwordHash; + return ret; + }, + }, + }, +); + +export const UserModel = model("User", userSchema); diff --git a/src/modules/users/user.repository.ts b/src/modules/users/user.repository.ts new file mode 100644 index 0000000..1f1cea3 --- /dev/null +++ b/src/modules/users/user.repository.ts @@ -0,0 +1,112 @@ +import { randomUUID } from "node:crypto"; +import type { FilterQuery } from "mongoose"; +import { UserModel, type UserDocument } from "./user.model.js"; +import type { UserRecord, UserRepository } from "./user.types.js"; + +const toUserRecord = (document: UserDocument & { _id: unknown; id?: string }): UserRecord => ({ + id: String(document._id), + name: document.name, + email: document.email, + passwordHash: document.passwordHash, + createdAt: document.createdAt, + updatedAt: document.updatedAt, +}); + +export class MongooseUserRepository implements UserRepository { + async findAll(): Promise { + const users = await UserModel.find().select("+passwordHash").sort({ createdAt: -1 }).limit(100).exec(); + return users.map(toUserRecord); + } + + async findById(id: string): Promise { + const user = await UserModel.findById(id).select("+passwordHash").exec(); + return user ? toUserRecord(user) : null; + } + + async findByEmail(email: string): Promise { + const user = await UserModel.findOne({ email } satisfies FilterQuery).select("+passwordHash").exec(); + return user ? toUserRecord(user) : null; + } + + async create(input: Omit): Promise { + const user = await UserModel.create(input); + const hydratedUser = await UserModel.findById(user._id).select("+passwordHash").orFail().exec(); + return toUserRecord(hydratedUser); + } + + async update( + id: string, + input: Partial>, + ): Promise { + const user = await UserModel.findByIdAndUpdate(id, input, { new: true }).select("+passwordHash").exec(); + return user ? toUserRecord(user) : null; + } + + async delete(id: string): Promise { + const user = await UserModel.findByIdAndDelete(id).select("+passwordHash").exec(); + return user ? toUserRecord(user) : null; + } +} + +export class InMemoryUserRepository implements UserRepository { + private readonly users = new Map(); + + async findAll(): Promise { + return [...this.users.values()].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + } + + async findById(id: string): Promise { + return this.users.get(id) ?? null; + } + + async findByEmail(email: string): Promise { + const normalizedEmail = email.toLowerCase(); + return [...this.users.values()].find((user) => user.email === normalizedEmail) ?? null; + } + + async create(input: Omit): Promise { + const now = new Date(); + const user: UserRecord = { + ...input, + email: input.email.toLowerCase(), + id: randomUUID(), + createdAt: now, + updatedAt: now, + }; + + this.users.set(user.id, user); + return user; + } + + async update( + id: string, + input: Partial>, + ): Promise { + const user = this.users.get(id); + + if (!user) { + return null; + } + + const updatedUser: UserRecord = { + ...user, + ...input, + email: input.email?.toLowerCase() ?? user.email, + updatedAt: new Date(), + }; + + this.users.set(id, updatedUser); + return updatedUser; + } + + async delete(id: string): Promise { + const user = this.users.get(id); + + if (!user) { + return null; + } + + this.users.delete(id); + return user; + } +} diff --git a/src/modules/users/user.routes.ts b/src/modules/users/user.routes.ts new file mode 100644 index 0000000..fbc2457 --- /dev/null +++ b/src/modules/users/user.routes.ts @@ -0,0 +1,22 @@ +import { Router } from "express"; +import { validateRequest } from "@shared/middleware/validate-request.js"; +import { UserController } from "./user.controller.js"; +import { createUserBodySchema, updateUserBodySchema, userIdParamsSchema } from "./user.schemas.js"; +import type { UserService } from "./user.service.js"; + +export const createUserRouter = (userService: UserService): Router => { + const router = Router(); + const controller = new UserController(userService); + + router.get("/", controller.findAll); + router.get("/:id", validateRequest({ params: userIdParamsSchema }), controller.findById); + router.post("/", validateRequest({ body: createUserBodySchema }), controller.create); + router.put( + "/:id", + validateRequest({ params: userIdParamsSchema, body: updateUserBodySchema }), + controller.update, + ); + router.delete("/:id", validateRequest({ params: userIdParamsSchema }), controller.delete); + + return router; +}; diff --git a/src/modules/users/user.schemas.ts b/src/modules/users/user.schemas.ts new file mode 100644 index 0000000..f458685 --- /dev/null +++ b/src/modules/users/user.schemas.ts @@ -0,0 +1,110 @@ +import { ValidationError } from "@shared/errors/app-error.js"; + +export type CreateUserBody = { + name: string; + email: string; + password: string; +}; + +export type UpdateUserBody = Partial; + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +const readString = ( + source: Record, + key: string, + options: { min?: number; max?: number; required?: boolean } = {}, +): string | undefined => { + const value = source[key]; + + if (value === undefined || value === null) { + if (options.required) { + throw new ValidationError([{ path: key, message: `${key} is required` }]); + } + + return undefined; + } + + if (typeof value !== "string") { + throw new ValidationError([{ path: key, message: `${key} must be a string` }]); + } + + const trimmedValue = value.trim(); + + if (options.min !== undefined && trimmedValue.length < options.min) { + throw new ValidationError([{ path: key, message: `${key} must be at least ${options.min} characters` }]); + } + + if (options.max !== undefined && trimmedValue.length > options.max) { + throw new ValidationError([{ path: key, message: `${key} must be at most ${options.max} characters` }]); + } + + return trimmedValue; +}; + +const readEmail = (source: Record, required: boolean): string | undefined => { + const email = readString(source, "email", { required, max: 254 }); + + if (!email) { + return undefined; + } + + const normalizedEmail = email.toLowerCase(); + const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (!emailPattern.test(normalizedEmail)) { + throw new ValidationError([{ path: "email", message: "email must be valid" }]); + } + + return normalizedEmail; +}; + +export const userIdParamsSchema = { + parse(value: unknown): { id: string } { + if (!isRecord(value)) { + throw new ValidationError([{ path: "params", message: "params must be an object" }]); + } + + const id = readString(value, "id", { required: true, min: 1 }); + return { id: id as string }; + }, +}; + +export const createUserBodySchema = { + parse(value: unknown): CreateUserBody { + if (!isRecord(value)) { + throw new ValidationError([{ path: "body", message: "body must be an object" }]); + } + + return { + name: readString(value, "name", { required: true, min: 1, max: 100 }) as string, + email: readEmail(value, true) as string, + password: readString(value, "password", { required: true, min: 12, max: 128 }) as string, + }; + }, +}; + +export const updateUserBodySchema = { + parse(value: unknown): UpdateUserBody { + if (!isRecord(value)) { + throw new ValidationError([{ path: "body", message: "body must be an object" }]); + } + + const update: UpdateUserBody = { + ...(value.name !== undefined + ? { name: readString(value, "name", { min: 1, max: 100 }) as string } + : {}), + ...(value.email !== undefined ? { email: readEmail(value, false) as string } : {}), + ...(value.password !== undefined + ? { password: readString(value, "password", { min: 12, max: 128 }) as string } + : {}), + }; + + if (Object.keys(update).length === 0) { + throw new ValidationError([{ path: "body", message: "At least one field must be provided" }]); + } + + return update; + }, +}; diff --git a/src/modules/users/user.service.ts b/src/modules/users/user.service.ts new file mode 100644 index 0000000..363f582 --- /dev/null +++ b/src/modules/users/user.service.ts @@ -0,0 +1,72 @@ +import { hashPassword } from "./password.js"; +import { ConflictError, NotFoundError } from "@shared/errors/app-error.js"; +import type { CreateUserInput, PublicUser, UpdateUserInput, UserRecord, UserRepository } from "./user.types.js"; + +const toPublicUser = (user: UserRecord): PublicUser => ({ + id: user.id, + name: user.name, + email: user.email, + createdAt: user.createdAt, + updatedAt: user.updatedAt, +}); + +export class UserService { + constructor(private readonly users: UserRepository) {} + + async findAll(): Promise { + const users = await this.users.findAll(); + return users.map(toPublicUser); + } + + async findById(id: string): Promise { + const user = await this.users.findById(id); + + if (!user) { + throw new NotFoundError("User not found"); + } + + return toPublicUser(user); + } + + async create(input: CreateUserInput): Promise { + const existingUser = await this.users.findByEmail(input.email); + + if (existingUser) { + throw new ConflictError("Email is already registered"); + } + + const passwordHash = await hashPassword(input.password); + const user = await this.users.create({ + name: input.name, + email: input.email, + passwordHash, + }); + + return toPublicUser(user); + } + + async update(id: string, input: UpdateUserInput): Promise { + const passwordHash = input.password ? await hashPassword(input.password) : undefined; + const updatedUser = await this.users.update(id, { + ...(input.name ? { name: input.name } : {}), + ...(input.email ? { email: input.email } : {}), + ...(passwordHash ? { passwordHash } : {}), + }); + + if (!updatedUser) { + throw new NotFoundError("User not found"); + } + + return toPublicUser(updatedUser); + } + + async delete(id: string): Promise { + const deletedUser = await this.users.delete(id); + + if (!deletedUser) { + throw new NotFoundError("User not found"); + } + + return toPublicUser(deletedUser); + } +} diff --git a/src/modules/users/user.types.ts b/src/modules/users/user.types.ts new file mode 100644 index 0000000..2499558 --- /dev/null +++ b/src/modules/users/user.types.ts @@ -0,0 +1,29 @@ +export type UserId = string; + +export type UserRecord = { + id: UserId; + name: string; + email: string; + passwordHash: string; + createdAt: Date; + updatedAt: Date; +}; + +export type PublicUser = Omit; + +export type CreateUserInput = { + name: string; + email: string; + password: string; +}; + +export type UpdateUserInput = Partial>; + +export type UserRepository = { + findAll(): Promise; + findById(id: UserId): Promise; + findByEmail(email: string): Promise; + create(input: Omit): Promise; + update(id: UserId, input: Partial>): Promise; + delete(id: UserId): Promise; +}; diff --git a/src/routes/user.ts b/src/routes/user.ts deleted file mode 100644 index 0723122..0000000 --- a/src/routes/user.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Router } from "express"; -import UserController from "@controllers/UserController.js"; - -const router = Router(); - -router.get("/", UserController.getAllUsers); -router.get("/:id", UserController.getUser); -router.post("/", UserController.createUser); -router.put("/:id", UserController.updateUser); -router.delete("/:id", UserController.deleteUser); - -export default router; diff --git a/src/schemas/user.schema.ts b/src/schemas/user.schema.ts deleted file mode 100644 index 92220cb..0000000 --- a/src/schemas/user.schema.ts +++ /dev/null @@ -1,24 +0,0 @@ -import mongoose from "mongoose"; - -const UserSchema = new mongoose.Schema( - { - name: { - type: String, - required: true, - }, - email: { - type: String, - required: true, - unique: true, - }, - password: { - type: String, - required: true, - }, - }, - { timestamps: true }, -); - -const UserModel = mongoose.model("User", UserSchema); - -export default UserModel; diff --git a/src/shared/errors/app-error.ts b/src/shared/errors/app-error.ts new file mode 100644 index 0000000..06267df --- /dev/null +++ b/src/shared/errors/app-error.ts @@ -0,0 +1,29 @@ +export class AppError extends Error { + constructor( + public readonly statusCode: number, + public readonly code: string, + message: string, + public readonly details?: unknown, + ) { + super(message); + this.name = "AppError"; + } +} + +export class ValidationError extends AppError { + constructor(details: unknown, message = "Request validation failed") { + super(400, "VALIDATION_ERROR", message, details); + } +} + +export class NotFoundError extends AppError { + constructor(message = "Resource not found") { + super(404, "NOT_FOUND", message); + } +} + +export class ConflictError extends AppError { + constructor(message = "Resource already exists") { + super(409, "CONFLICT", message); + } +} diff --git a/src/shared/http/request-id.ts b/src/shared/http/request-id.ts new file mode 100644 index 0000000..3fa6555 --- /dev/null +++ b/src/shared/http/request-id.ts @@ -0,0 +1,10 @@ +import { randomUUID } from "node:crypto"; +import type { RequestHandler } from "express"; + +export const requestId: RequestHandler = (req, res, next) => { + const existingRequestId = req.header("x-request-id"); + const id = existingRequestId && existingRequestId.length > 0 ? existingRequestId : randomUUID(); + + res.setHeader("x-request-id", id); + next(); +}; diff --git a/src/shared/middleware/error-handler.ts b/src/shared/middleware/error-handler.ts new file mode 100644 index 0000000..6acc83e --- /dev/null +++ b/src/shared/middleware/error-handler.ts @@ -0,0 +1,25 @@ +import type { ErrorRequestHandler } from "express"; +import { logger } from "@config/logger.js"; +import { AppError } from "@shared/errors/app-error.js"; + +export const errorHandler: ErrorRequestHandler = (err: unknown, req, res, _next) => { + if (err instanceof AppError) { + res.status(err.statusCode).json({ + error: { + code: err.code, + message: err.message, + details: err.details, + }, + }); + return; + } + + logger.error({ err, path: req.path, method: req.method }, "Unhandled request error"); + + res.status(500).json({ + error: { + code: "INTERNAL_SERVER_ERROR", + message: "Internal Server Error", + }, + }); +}; diff --git a/src/shared/middleware/not-found.ts b/src/shared/middleware/not-found.ts new file mode 100644 index 0000000..036d547 --- /dev/null +++ b/src/shared/middleware/not-found.ts @@ -0,0 +1,10 @@ +import type { RequestHandler } from "express"; + +export const notFoundHandler: RequestHandler = (req, res) => { + res.status(404).json({ + error: { + code: "ROUTE_NOT_FOUND", + message: `Route ${req.method} ${req.path} was not found`, + }, + }); +}; diff --git a/src/shared/middleware/rate-limit.ts b/src/shared/middleware/rate-limit.ts new file mode 100644 index 0000000..80a16f2 --- /dev/null +++ b/src/shared/middleware/rate-limit.ts @@ -0,0 +1,35 @@ +import type { RequestHandler } from "express"; +import { env } from "@config/env.js"; + +type RateLimitEntry = { + count: number; + resetAt: number; +}; + +const clients = new Map(); + +export const rateLimit: RequestHandler = (req, res, next) => { + const now = Date.now(); + const key = req.ip ?? "unknown"; + const entry = clients.get(key); + + if (!entry || entry.resetAt <= now) { + clients.set(key, { count: 1, resetAt: now + env.RATE_LIMIT_WINDOW_MS }); + next(); + return; + } + + entry.count += 1; + + if (entry.count > env.RATE_LIMIT_MAX) { + res.status(429).json({ + error: { + code: "RATE_LIMIT_EXCEEDED", + message: "Too many requests", + }, + }); + return; + } + + next(); +}; diff --git a/src/shared/middleware/request-logger.ts b/src/shared/middleware/request-logger.ts new file mode 100644 index 0000000..6a2caf7 --- /dev/null +++ b/src/shared/middleware/request-logger.ts @@ -0,0 +1,23 @@ +import type { RequestHandler } from "express"; +import { logger } from "@config/logger.js"; + +export const requestLogger: RequestHandler = (req, res, next) => { + const start = process.hrtime.bigint(); + + res.on("finish", () => { + const durationMs = Number(process.hrtime.bigint() - start) / 1_000_000; + + logger.info( + { + method: req.method, + path: req.originalUrl, + statusCode: res.statusCode, + durationMs: Number(durationMs.toFixed(3)), + requestId: res.getHeader("x-request-id"), + }, + "Request completed", + ); + }); + + next(); +}; diff --git a/src/shared/middleware/security.ts b/src/shared/middleware/security.ts new file mode 100644 index 0000000..c754149 --- /dev/null +++ b/src/shared/middleware/security.ts @@ -0,0 +1,37 @@ +import type { RequestHandler } from "express"; +import { env } from "@config/env.js"; + +export const securityHeaders: RequestHandler = (_req, res, next) => { + res.setHeader("content-security-policy", "default-src 'self'; object-src 'none'; frame-ancestors 'none'"); + res.setHeader("x-content-type-options", "nosniff"); + res.setHeader("x-frame-options", "DENY"); + res.setHeader("x-dns-prefetch-control", "off"); + res.setHeader("x-download-options", "noopen"); + res.setHeader("x-permitted-cross-domain-policies", "none"); + res.setHeader("referrer-policy", "no-referrer"); + + if (env.NODE_ENV === "production") { + res.setHeader("strict-transport-security", "max-age=15552000; includeSubDomains"); + } + + next(); +}; + +export const cors: RequestHandler = (req, res, next) => { + const origin = req.header("origin"); + + if (origin && env.CORS_ORIGINS.includes(origin)) { + res.setHeader("access-control-allow-origin", origin); + res.setHeader("access-control-allow-credentials", "true"); + } + + res.setHeader("access-control-allow-methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS"); + res.setHeader("access-control-allow-headers", "content-type,authorization,x-request-id"); + + if (req.method === "OPTIONS") { + res.status(204).send(); + return; + } + + next(); +}; diff --git a/src/shared/middleware/validate-request.ts b/src/shared/middleware/validate-request.ts new file mode 100644 index 0000000..74906c3 --- /dev/null +++ b/src/shared/middleware/validate-request.ts @@ -0,0 +1,29 @@ +import type { RequestHandler } from "express"; + +type Parser = { + parse(value: unknown): T; +}; + +type RequestSchema = { + body?: Parser; + params?: Parser; + query?: Parser; +}; + +export const validateRequest = (schema: RequestSchema): RequestHandler => { + return (req, _res, next) => { + if (schema.body) { + req.body = schema.body.parse(req.body); + } + + if (schema.params) { + req.params = schema.params.parse(req.params) as typeof req.params; + } + + if (schema.query) { + req.query = schema.query.parse(req.query) as typeof req.query; + } + + next(); + }; +}; diff --git a/src/utils/types.ts b/src/utils/types.ts deleted file mode 100644 index ac0d7a4..0000000 --- a/src/utils/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type User = { - name: string; - email: string; - password: string; -}; diff --git a/src/utils/util.ts b/src/utils/util.ts deleted file mode 100644 index adf6a2c..0000000 --- a/src/utils/util.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { fileURLToPath } from "node:url"; -import { dirname } from "path"; - -export const __filename = fileURLToPath(import.meta.url); -export const __dirname = dirname(__filename); diff --git a/test/app.test.ts b/test/app.test.ts new file mode 100644 index 0000000..c8102b1 --- /dev/null +++ b/test/app.test.ts @@ -0,0 +1,83 @@ +import { once } from "node:events"; +import { createServer, type Server } from "node:http"; +import assert from "node:assert/strict"; +import { after, before, describe, it } from "node:test"; +import { createApp } from "../src/app.js"; +import { InMemoryUserRepository } from "../src/modules/users/user.repository.js"; + +let server: Server; +let baseUrl: string; + +const request = async (path: string, init?: RequestInit) => { + const response = await fetch(`${baseUrl}${path}`, { + ...init, + headers: { + "content-type": "application/json", + ...(init?.headers ?? {}), + }, + }); + const body = (await response.json()) as unknown; + + return { response, body }; +}; + +before(async () => { + const app = createApp({ userRepository: new InMemoryUserRepository() }); + server = createServer(app); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + const address = server.address(); + + if (!address || typeof address === "string") { + throw new Error("Test server did not bind to a TCP port"); + } + + baseUrl = `http://127.0.0.1:${address.port}`; +}); + +after(async () => { + server.close(); + await once(server, "close"); +}); + +describe("app", () => { + it("returns liveness health", async () => { + const { response, body } = await request("/health/live"); + + assert.equal(response.status, 200); + assert.deepEqual(body, { status: "ok" }); + }); + + it("validates user creation payloads", async () => { + const { response, body } = await request("/api/users", { + method: "POST", + body: JSON.stringify({ + name: "Ada Lovelace", + email: "not-an-email", + password: "short", + }), + }); + + assert.equal(response.status, 400); + assert.equal((body as { error: { code: string } }).error.code, "VALIDATION_ERROR"); + }); + + it("creates users without exposing password hashes", async () => { + const { response, body } = await request("/api/users", { + method: "POST", + body: JSON.stringify({ + name: "Ada Lovelace", + email: "ada@example.com", + password: "a-very-strong-password", + }), + }); + + const data = (body as { data: Record }).data; + + assert.equal(response.status, 201); + assert.equal(data.name, "Ada Lovelace"); + assert.equal(data.email, "ada@example.com"); + assert.equal(data.password, undefined); + assert.equal(data.passwordHash, undefined); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index dab7330..6869815 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "module": "NodeNext", // <-- o'zgartirildi - "moduleResolution": "NodeNext", // <-- o'zgartirildi + "module": "NodeNext", + "moduleResolution": "NodeNext", "target": "ES2022", "rootDir": "./src", "outDir": "./dist", @@ -13,16 +13,31 @@ "forceConsistentCasingInFileNames": true, "noEmit": false, "isolatedModules": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, "baseUrl": ".", "paths": { - "@config/*": ["src/config/*"], - "@controllers/*": ["src/controllers/*"], - "@routes/*": ["src/routes/*"], - "@schemas/*": ["src/schemas/*"], - "@middlewares/*": ["src/middlewares/*"], - "@utils/*": ["src/utils/*"] + "@config/*": [ + "src/config/*" + ], + "@infrastructure/*": [ + "src/infrastructure/*" + ], + "@modules/*": [ + "src/modules/*" + ], + "@shared/*": [ + "src/shared/*" + ] } }, - "include": ["src/**/*"], - "exclude": ["node_modules"] + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] } From 94eccb4e1e562d3a6189b57bb501cada9bef02fc Mon Sep 17 00:00:00 2001 From: Mirsolih Mirzaahmad Date: Mon, 8 Jun 2026 14:23:23 +0500 Subject: [PATCH 2/2] Potential fix for pull request finding 'CodeQL / Database query built from user-controlled sources' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/modules/users/user.repository.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/users/user.repository.ts b/src/modules/users/user.repository.ts index 1f1cea3..0c9ef60 100644 --- a/src/modules/users/user.repository.ts +++ b/src/modules/users/user.repository.ts @@ -24,7 +24,9 @@ export class MongooseUserRepository implements UserRepository { } async findByEmail(email: string): Promise { - const user = await UserModel.findOne({ email } satisfies FilterQuery).select("+passwordHash").exec(); + const user = await UserModel.findOne({ email: { $eq: email } } satisfies FilterQuery) + .select("+passwordHash") + .exec(); return user ? toUserRecord(user) : null; }