diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..01d4beb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint-and-test: + runs-on: ubuntu-latest + + services: + mongo: + image: mongo:7 + ports: + - 27017:27017 + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 8 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Check (lint + format) + run: pnpm check + + - name: Build + run: pnpm build + + - name: Test + run: pnpm test + env: + DATABASE_URL: mongodb://localhost:27017/test?directConnection=true diff --git a/.gitignore b/.gitignore index 2185086..4fae7ee 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ package-lock.json yarn.lock # Environment variables +packages/api/.env .env .env.local .env.development.local diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2a75084 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,72 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Full-stack TypeScript monorepo implementing a TryHackMe-inspired learning platform. Built as a technical challenge project demonstrating modern full-stack capabilities. + +## Common Commands + +```bash +# Development (starts both API on :3001 and Client on :3000) +pnpm dev + +# Code quality +pnpm lint # Run Biome linting +pnpm lint:fix # Fix linting issues +pnpm format # Format code with Biome +pnpm check # Run both linting and formatting checks + +# Build +pnpm build # Build all packages + +# Database (required before running dev) +docker-compose up -d mongo +``` + +### Package-specific commands + +```bash +# API only +cd packages/api && pnpm dev + +# Client only +cd packages/client && pnpm dev +``` + +## Architecture + +**Monorepo Structure:** pnpm workspaces + Turborepo for task orchestration + +- `packages/api` - Express.js backend with MongoDB +- `packages/client` - React 19 + Vite frontend + +**API:** Single endpoint `GET /rooms/:roomCode` returns room data with nested tasks and questions. MongoDB connection via `DATABASE_URL` env var. + +**Frontend:** React Router (2 routes: `/` and `/room/:roomCode`), TanStack Query for data fetching with 5-minute stale time. + +**Data Flow:** RoomData β†’ Tasks[] β†’ Questions[] hierarchy. Question progress tracks correctness and attempts. + +## Code Style + +- **Formatter/Linter:** Biome (not ESLint/Prettier) +- **Indentation:** 2 spaces +- **Line width:** 100 characters +- **Language:** TypeScript only (strict mode enabled) + +Run `pnpm check` before committing. + +## Environment Setup + +API requires `.env` file (see `packages/api/.env.sample`): +``` +DATABASE_URL=mongodb://localhost:27017/technical-challenge?directConnection=true +PORT=3001 +``` + +## Prerequisites + +- Node.js v18+ +- pnpm v8+ +- Docker v24+ (for MongoDB) \ No newline at end of file diff --git a/README.md b/README.md index 69d84b8..1d49c80 100644 --- a/README.md +++ b/README.md @@ -1,202 +1,123 @@ # Technical Challenge - Monorepo Project -A full-stack application built with a Node.js Express API and React frontend, organized as a monorepo using pnpm and Turborepo. +A full-stack TypeScript application implementing a TryHackMe-inspired learning platform, organized as a monorepo using pnpm and Turborepo. -## πŸ—οΈ Project Structure +## Project Structure ``` / β”œβ”€β”€ packages/ β”‚ β”œβ”€β”€ api/ # Express.js API server β”‚ β”‚ β”œβ”€β”€ src/ -β”‚ β”‚ β”œβ”€β”€ db/ -β”‚ β”‚ └── package.json -β”‚ └── client/ # React frontend application -β”‚ β”œβ”€β”€ src/ -β”‚ └── package.json -β”œβ”€β”€ package.json # Root package.json -β”œβ”€β”€ pnpm-workspace.yaml # pnpm workspace configuration -β”œβ”€β”€ turbo.json # Turborepo configuration -β”œβ”€β”€ biome.json # Biome linting/formatting config -β”œβ”€β”€ docker-compose.yml # Docker compose configuration -└── .vscode/ # VS Code settings +β”‚ β”‚ β”‚ β”œβ”€β”€ config/ # Database, env validation +β”‚ β”‚ β”‚ β”œβ”€β”€ common/ # Middleware, utils, logger +β”‚ β”‚ β”‚ └── modules/ # Feature modules (room, health) +β”‚ β”‚ └── tests/ # Unit and integration tests +β”‚ β”œβ”€β”€ client/ # React frontend application +β”‚ β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”‚ β”œβ”€β”€ features/ # Feature-based components (room, home) +β”‚ β”‚ β”‚ └── shared/ # Shared components, hooks, api +β”‚ β”‚ └── tests/ +β”‚ └── shared/ # Shared TypeScript types +β”œβ”€β”€ .github/workflows/ # CI/CD pipeline +β”œβ”€β”€ docker-compose.yml # Docker services +└── biome.json # Linting/formatting config ``` -## πŸš€ Getting Started +## Getting Started ### Prerequisites -- Node.js (v18 or higher) -- pnpm (v8 or higher) -- Docker (v24 or higher) - -### Installation - -1. Clone the repository -2. Start up the MongoDB container with Docker: - ```bash - docker-compose up -d mongo - ``` -3. Install dependencies: - ```bash - pnpm install - ``` +- Node.js (v18+) +- pnpm (v8+) +- Docker (v24+) ### Development -Start both the API and client in development mode: - +**Option 1: Local (requires Node.js)** ```bash +docker-compose up -d mongo +pnpm install pnpm dev ``` -This will start: - -- **API Server**: http://localhost:3001 -- **Client App**: http://localhost:3000 -- **MongoDB**: mongodb://localhost:27017 - -### Individual Services - -Start only the API: - +**Option 2: Full Docker environment** ```bash -cd packages/api -pnpm dev +docker-compose up ``` -Start only the client: - -```bash -cd packages/client -pnpm dev -``` - -Start only the database: - -```bash -docker-compose up -d technical-challenge-mongodb -``` - -## πŸ› οΈ Available Scripts - -### Root Level - -- `pnpm dev` - Start all services in development mode -- `pnpm build` - Build all packages -- `pnpm lint` - Run Biome linting -- `pnpm format` - Format code with Biome -- `pnpm check` - Run both linting and formatting checks - -### API Scripts - -- `pnpm dev` - Start API server with hot reload -- `pnpm build` - Build TypeScript to JavaScript -- `pnpm start` - Start production server - -### Client Scripts - -- `pnpm dev` - Start Vite development server -- `pnpm build` - Build for production -- `pnpm preview` - Preview production build - -## 🎯 Features - -### Frontend (React + TypeScript) - -- **Modern UI**: Built with React 18 and TypeScript -- **Routing**: React Router for navigation -- **Styling**: Custom CSS with Source Sans Pro font -- **Icons**: Font Awesome icons -- **Responsive**: Mobile-friendly design -- **Interactive Accordion**: Collapsible task sections with progress tracking - -### Backend (Express + TypeScript) - -- **RESTful API**: Express.js server with TypeScript -- **Database**: Pre seeded MongoDB database running in a Docker container -- **CORS**: Cross-origin resource sharing enabled -- **Room Data**: `/rooms/:roomCode` endpoint - -### Development Tools - -- **Monorepo**: pnpm workspaces + Turborepo -- **Code Quality**: Biome for linting and formatting -- **Type Safety**: Full TypeScript support -- **Hot Reload**: Fast development experience - -## πŸ“ Key Components - -### Frontend Components - -- **Header**: Shared navigation component -- **Home**: Landing page with main title section -- **About**: Project information page -- **Room**: Interactive room page with accordion -- **Accordion**: Collapsible task display with progress tracking - -### API Endpoints +Both options provide: +- **API**: http://localhost:3001 +- **Client**: http://localhost:3000 +- **MongoDB**: mongodb://localhost:27017 -- `GET /rooms/:roomCode` - Get room data with tasks and questions +Docker environment includes hot reload via volume mounts. -## 🎨 Styling +## Available Scripts -The project uses a custom CSS approach with: +| Command | Description | +|---------|-------------| +| `pnpm dev` | Start all services in dev mode | +| `pnpm build` | Build all packages | +| `pnpm test` | Run all tests | +| `pnpm check` | Lint + format check | +| `pnpm lint:fix` | Auto-fix lint issues | -- **Color Scheme**: Dark theme with green/red progress indicators -- **Typography**: Source Sans Pro font family -- **Layout**: Flexbox and CSS Grid -- **Responsive**: Mobile-first design approach -- **Animations**: Smooth transitions and hover effects +## API Endpoints -## πŸ”§ Configuration +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/v1/health` | Health check | +| `GET` | `/api/v1/rooms/:roomCode` | Get room with tasks/questions | +| `POST` | `/api/v1/rooms/:roomCode/questions/:questionOrder` | Submit answer | -### Biome (Code Quality) +All responses follow `{ success, data }` or `{ success, error }` format. -- **Indentation**: 2 spaces (no tabs) -- **Line Width**: 100 characters -- **Linting**: Recommended rules enabled -- **Formatting**: Automatic on save (VS Code) +## Features -### TypeScript +### Backend +- Express v5 with layered architecture +- Zod-validated environment config +- Winston logging with request tracing +- Jest tests (83% coverage) -- **Strict Mode**: Enabled -- **Module Resolution**: Node.js style -- **Target**: ES2020 +### Frontend +- React 19 + Vite +- TanStack Query for data fetching +- Feature-based folder structure +- Answer submission with feedback -## πŸš€ Deployment +### Security +- Helmet security headers +- CORS whitelist +- Rate limiting (100 req/15min on submissions) +- Input validation (roomCode, answer length) +- XSS protection with DOMPurify -### Build for Production +### Infrastructure +- GitHub Actions CI (lint β†’ build β†’ test) +- Docker dev environment with hot reload +- MongoDB with seed data -```bash -pnpm build -``` +## Environment Variables -### Environment Variables - -Create a `.env` file in the API package: +Create `packages/api/.env`: ``` DATABASE_URL=mongodb://localhost:27017/technical-challenge?directConnection=true PORT=3001 +ALLOWED_ORIGINS=http://localhost:3000 ``` -## πŸ“ Development Notes - -- The project uses pnpm for fast, efficient package management -- Turborepo provides intelligent caching and parallel execution -- Biome ensures consistent code formatting and catches common issues -- The API uses MongoDB with a pre seeded database running in a Docker container for development -- All components are built with accessibility in mind +## Code Quality -## 🀝 Contributing +- **Formatter/Linter**: Biome +- **Indentation**: 2 spaces +- **Line width**: 100 characters +- **TypeScript**: Strict mode -1. Follow the existing code style (2 spaces, Biome formatting) -2. Use TypeScript for all new code -3. Add proper type definitions -4. Test your changes locally with `pnpm dev` -5. Run `pnpm check` before committing +Run `pnpm check` before committing. -## πŸ“„ License +## License -This project is part of a technical challenge. +Technical challenge project. diff --git a/biome.json b/biome.json index 3801d95..a509135 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,8 @@ { "$schema": "https://biomejs.dev/schemas/2.2.5/schema.json", + "files": { + "includes": ["**", "!**/dist", "!**/node_modules", "!**/coverage"] + }, "linter": { "enabled": true, "rules": { diff --git a/docker-compose.yml b/docker-compose.yml index 9ef81d0..3f0ec2d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,48 @@ services: container_name: mongo image: mongo:7.0 restart: unless-stopped + ports: + - "27017:27017" volumes: - ./packages/api/db/seeds:/docker-entrypoint-initdb.d/seeds:ro - ./packages/api/db/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro + + api: + container_name: api + build: + context: . + dockerfile: ./packages/api/Dockerfile.dev + volumes: + - .:/workspace + # These "anonymous" volumes protect the container's installed deps + - /workspace/node_modules + - /workspace/packages/api/node_modules + - /workspace/packages/shared/node_modules + - /workspace/packages/client/node_modules + - pnpm-store:/root/.local/share/pnpm/store + environment: + - PORT=3001 + - DATABASE_URL=mongodb://mongo:27017/technical-challenge?directConnection=true + ports: + - "3001:3001" + depends_on: + - mongo + + client: + container_name: client + build: + context: . + dockerfile: ./packages/client/Dockerfile.dev + volumes: + - .:/workspace + - /workspace/node_modules + - /workspace/packages/client/node_modules + - /workspace/packages/shared/node_modules + - pnpm-store:/root/.local/share/pnpm/store + ports: + - "3000:3000" + depends_on: + - api + +volumes: + pnpm-store: diff --git a/package.json b/package.json index d8d2b65..6cea49e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ ], "scripts": { "build": "turbo run build", + "test": "turbo run test", "dev": "turbo run dev", "lint": "biome lint .", "lint:fix": "biome lint --write .", diff --git a/packages/api/.env.sample b/packages/api/.env.sample index 31d5f78..cf55571 100644 --- a/packages/api/.env.sample +++ b/packages/api/.env.sample @@ -1,2 +1,3 @@ PORT=3001 DATABASE_URL=mongodb://localhost:27017/technical-challenge?directConnection=true +ALLOWED_ORIGINS=http://localhost:3000 diff --git a/packages/api/Dockerfile.dev b/packages/api/Dockerfile.dev new file mode 100644 index 0000000..3b2b122 --- /dev/null +++ b/packages/api/Dockerfile.dev @@ -0,0 +1,26 @@ +FROM node:22-alpine + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@8.10.0 --activate + +WORKDIR /workspace + +# 1. Copy workspace manifests first +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ + +# 2. Copy ALL package.json files from all subpackages +# This allows pnpm to calculate the dependency graph +COPY packages/api/package.json ./packages/api/ +COPY packages/shared/package.json ./packages/shared/ +COPY packages/client/package.json ./packages/client/ + +# 3. Install dependencies +# Using --prefer-offline can speed up builds if using a cache +RUN pnpm install --frozen-lockfile + +# 4. Copy the rest of the source code +COPY . . + +EXPOSE 3001 + +CMD ["pnpm", "--filter", "@technical-challenge/api", "dev"] \ No newline at end of file diff --git a/packages/api/db/seeds/questions.js b/packages/api/db/seeds/questions.js index cd60f5b..5fc510e 100644 --- a/packages/api/db/seeds/questions.js +++ b/packages/api/db/seeds/questions.js @@ -8,7 +8,8 @@ const questions = [ correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "No answer needed", + answer: null, task: 1, }, { @@ -20,7 +21,8 @@ const questions = [ correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "Answer format: ****", + answer: "1991", task: 2, }, { @@ -32,7 +34,8 @@ const questions = [ correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "No answer needed", + answer: null, task: 3, }, { @@ -44,7 +47,8 @@ const questions = [ correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "Answer format: **** **********", + answer: "echo TryHackMe", task: 4, }, { @@ -57,7 +61,8 @@ const questions = [ correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "Answer format: **********", + answer: "tryhackme", task: 4, }, { @@ -69,19 +74,21 @@ const questions = [ correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "Answer format: *", + answer: "4", task: 5, }, { hint: "We've discussed about a certain command that can be used to list contents of directories", - question: "

Which directory contains a file?Β 

", + question: "

Which directory contains a file?

", order: 2, roomCode: "linux-fundamentals-part1", progress: { correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "Answer format: *******", + answer: "folder4", task: 5, }, { @@ -93,7 +100,8 @@ const questions = [ correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "Answer format: ***** *****", + answer: "Hello World", task: 5, }, { @@ -106,7 +114,8 @@ const questions = [ correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "Answer format: /****/**********/*******", + answer: "/home/tryhackme/folder4", task: 5, }, { @@ -120,6 +129,7 @@ const questions = [ attempts: 0, }, answerDescription: "Answer format: ***{*********_**_****}", + answer: "THM{ACCESS_LOG_GREP}", task: 6, }, { @@ -131,7 +141,8 @@ const questions = [ correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "No answer needed", + answer: null, task: 6, }, { @@ -144,7 +155,8 @@ const questions = [ correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "Answer format: *", + answer: "&", task: 7, }, { @@ -157,11 +169,12 @@ const questions = [ correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "Answer format: **** *********** * *********", + answer: "echo password123 > passwords", task: 7, }, { - hint: "echo%20%3Ccontent%3E%20%3E%3E%20%3Cfilename%3E", + hint: "echo >> ", question: '

Now if I wanted to add "tryhackme" to this file named "passwords" but also keep "passwords123", what would my command be

', order: 3, @@ -170,7 +183,8 @@ const questions = [ correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "Answer format: **** ********** ** *********", + answer: "echo tryhackme >> passwords", task: 7, }, { @@ -182,7 +196,8 @@ const questions = [ correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "No answer needed", + answer: null, task: 7, }, { @@ -194,32 +209,35 @@ const questions = [ correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "No answer needed", + answer: null, task: 8, }, { hint: "", - question: "

Terminate the machine deployed in this room from task 3.Β 

", + question: "

Terminate the machine deployed in this room from task 3.

", order: 1, roomCode: "linux-fundamentals-part1", progress: { correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "No answer needed", + answer: null, task: 9, }, { hint: "", question: - '

JoinΒ Linux Fundamentals Part 2!

', + '

Join Linux Fundamentals Part 2!

', order: 2, roomCode: "linux-fundamentals-part1", progress: { correct: false, attempts: 0, }, - answerDescription: "Answer format: ***{*********_**_****}", + answerDescription: "No answer needed", + answer: null, task: 9, }, ]; diff --git a/packages/api/db/seeds/tasks.js b/packages/api/db/seeds/tasks.js index 01c3752..03fc8cb 100644 --- a/packages/api/db/seeds/tasks.js +++ b/packages/api/db/seeds/tasks.js @@ -73,4 +73,4 @@ const tasks = [ }, ]; -module.exports = { tasks }; \ No newline at end of file +module.exports = { tasks }; diff --git a/packages/api/jest.config.js b/packages/api/jest.config.js new file mode 100644 index 0000000..c10d7b4 --- /dev/null +++ b/packages/api/jest.config.js @@ -0,0 +1,24 @@ +/** @type {import('jest').Config} */ +export default { + preset: "ts-jest/presets/default-esm", + testEnvironment: "node", + extensionsToTreatAsEsm: [".ts"], + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + "^@/(.*)$": "/src/$1", + }, + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + useESM: true, + tsconfig: "tsconfig.test.json", + }, + ], + }, + testMatch: ["/tests/**/*.test.ts"], + setupFilesAfterEnv: ["/tests/setup.ts"], + collectCoverageFrom: ["src/**/*.ts", "!src/index.ts", "!src/**/*.d.ts"], + coverageDirectory: "coverage", + verbose: true, +}; diff --git a/packages/api/package.json b/packages/api/package.json index cd226c7..2e07769 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -9,24 +9,38 @@ "dev": "nodemon --exec tsx src/index.ts", "start": "node dist/index.js", "lint": "eslint src --ext .ts", - "clean": "rm -rf dist" + "clean": "rm -rf dist", + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", + "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch", + "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage" }, "dependencies": { + "@live-code-challenge/shared": "workspace:*", "cors": "^2.8.5", "dotenv": "^16.4.5", - "express": "^4.18.2", - "mongodb": "^6.20.0" + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "helmet": "^8.1.0", + "mongodb": "^6.20.0", + "winston": "^3.17.0", + "zod": "^3.24.0" }, "devDependencies": { "@biomejs/biome": "^2.2.5", + "@jest/globals": "^29.7.0", "@types/cors": "^2.8.14", - "@types/express": "^4.17.17", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", "@types/node": "^20.8.0", + "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", "eslint": "^8.50.0", + "jest": "^29.7.0", "mongodb-memory-server": "^10.2.2", "nodemon": "^3.0.2", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", "tsx": "^3.14.0", "typescript": "^5.2.2" } diff --git a/packages/api/scripts/add-answer-field.ts b/packages/api/scripts/add-answer-field.ts new file mode 100644 index 0000000..1b47a3b --- /dev/null +++ b/packages/api/scripts/add-answer-field.ts @@ -0,0 +1,45 @@ +import { MongoClient } from "mongodb"; + +const DATABASE_URL = + process.env.DATABASE_URL || "mongodb://localhost:27017/technical-challenge?directConnection=true"; + +async function main() { + const client = new MongoClient(DATABASE_URL); + + try { + await client.connect(); + console.log("Connected to MongoDB"); + + const db = client.db(); + const questionsCollection = db.collection("questions"); + + // Find all questions without an answer field + const questionsWithoutAnswer = await questionsCollection + .find({ answer: { $exists: false } }) + .toArray(); + + console.log(`Found ${questionsWithoutAnswer.length} questions without answer field`); + + if (questionsWithoutAnswer.length === 0) { + console.log("No questions to update"); + return; + } + + // Update each question with a placeholder answer + const updateResult = await questionsCollection.updateMany( + { answer: { $exists: false } }, + { $set: { answer: "placeholder" } }, + ); + + console.log(`Updated ${updateResult.modifiedCount} questions with placeholder answer`); + console.log("Remember to update the answers with correct values!"); + } catch (error) { + console.error("Error:", error); + process.exit(1); + } finally { + await client.close(); + console.log("Disconnected from MongoDB"); + } +} + +main(); diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts new file mode 100644 index 0000000..df60c00 --- /dev/null +++ b/packages/api/src/app.ts @@ -0,0 +1,58 @@ +import cors from "cors"; +import express, { type Express } from "express"; +import rateLimit from "express-rate-limit"; +import helmet from "helmet"; +import { + errorHandler, + notFoundHandler, + requestId, + requestLogger, +} from "./common/middleware/index.js"; +import { routes } from "./routes.js"; + +const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || []; + +const corsOptions: cors.CorsOptions = { + origin: (origin, callback) => { + // Allow requests with no origin (curl, server-to-server) + if (!origin) return callback(null, true); + if (allowedOrigins.includes(origin)) { + return callback(null, true); + } + return callback(new Error("CORS policy violation")); + }, + credentials: true, +}; + +const apiLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // 100 requests per window + standardHeaders: true, + legacyHeaders: false, + message: { error: "Too many requests, please try again later" }, +}); + +export function createApp(): Express { + const app = express(); + + // Security middleware + app.use(helmet()); + app.use(cors(corsOptions)); + app.use(express.json({ limit: "10kb" })); // Limit body size + + // Request tracking + app.use(requestId); + app.use(requestLogger); + + // Rate limiting on mutation endpoints + app.use("/api/v1/rooms/:roomCode/questions", apiLimiter); + + // Routes + app.use(routes); + + // Error handling + app.use(notFoundHandler); + app.use(errorHandler); + + return app; +} diff --git a/packages/api/src/common/middleware/errorHandler.ts b/packages/api/src/common/middleware/errorHandler.ts new file mode 100644 index 0000000..9c20021 --- /dev/null +++ b/packages/api/src/common/middleware/errorHandler.ts @@ -0,0 +1,34 @@ +import type { ApiErrorResponse } from "@live-code-challenge/shared"; +import type { NextFunction, Request, Response } from "express"; +import { env } from "../../config/env.js"; +import { ApiError } from "../utils/ApiError.js"; +import { logger } from "../utils/logger.js"; + +export function errorHandler(err: Error, req: Request, res: Response, _next: NextFunction): void { + const requestId = req.requestId || "unknown"; + + if (err instanceof ApiError) { + logger.warn(`[${requestId}] ${err.message}`, { statusCode: err.statusCode }); + const response: ApiErrorResponse = { + success: false, + error: { message: err.message }, + }; + res.status(err.statusCode).json(response); + return; + } + + logger.error(`[${requestId}] Unexpected error: ${err.message}`, { stack: err.stack }); + + const response: ApiErrorResponse = { + success: false, + error: { + message: env.NODE_ENV === "production" ? "Internal Server Error" : err.message, + }, + }; + + if (env.NODE_ENV !== "production") { + response.error.stack = err.stack; + } + + res.status(500).json(response); +} diff --git a/packages/api/src/common/middleware/index.ts b/packages/api/src/common/middleware/index.ts new file mode 100644 index 0000000..8310359 --- /dev/null +++ b/packages/api/src/common/middleware/index.ts @@ -0,0 +1,4 @@ +export { errorHandler } from "./errorHandler.js"; +export { notFoundHandler } from "./notFound.js"; +export { requestId } from "./requestId.js"; +export { requestLogger } from "./requestLogger.js"; diff --git a/packages/api/src/common/middleware/notFound.ts b/packages/api/src/common/middleware/notFound.ts new file mode 100644 index 0000000..68e97f7 --- /dev/null +++ b/packages/api/src/common/middleware/notFound.ts @@ -0,0 +1,6 @@ +import type { NextFunction, Request, Response } from "express"; +import { ApiError } from "../utils/ApiError.js"; + +export function notFoundHandler(req: Request, _res: Response, next: NextFunction): void { + next(ApiError.notFound(`Route ${req.method} ${req.path} not found`)); +} diff --git a/packages/api/src/common/middleware/requestId.ts b/packages/api/src/common/middleware/requestId.ts new file mode 100644 index 0000000..6f15866 --- /dev/null +++ b/packages/api/src/common/middleware/requestId.ts @@ -0,0 +1,7 @@ +import crypto from "node:crypto"; +import type { NextFunction, Request, Response } from "express"; + +export function requestId(req: Request, _res: Response, next: NextFunction): void { + req.requestId = crypto.randomUUID(); + next(); +} diff --git a/packages/api/src/common/middleware/requestLogger.ts b/packages/api/src/common/middleware/requestLogger.ts new file mode 100644 index 0000000..87090e5 --- /dev/null +++ b/packages/api/src/common/middleware/requestLogger.ts @@ -0,0 +1,16 @@ +import type { NextFunction, Request, Response } from "express"; +import { logger } from "../utils/logger.js"; + +export function requestLogger(req: Request, res: Response, next: NextFunction): void { + const start = Date.now(); + + req.log = logger.child({ requestId: req.requestId }); + req.log.info(`${req.method} ${req.path}`); + + res.on("finish", () => { + const duration = Date.now() - start; + req.log.info(`${req.method} ${req.path} ${res.statusCode} ${duration}ms`); + }); + + next(); +} diff --git a/packages/api/src/common/types/express.d.ts b/packages/api/src/common/types/express.d.ts new file mode 100644 index 0000000..aeedce9 --- /dev/null +++ b/packages/api/src/common/types/express.d.ts @@ -0,0 +1,10 @@ +import type { Logger } from "winston"; + +declare global { + namespace Express { + interface Request { + requestId: string; + log: Logger; + } + } +} diff --git a/packages/api/src/common/utils/ApiError.ts b/packages/api/src/common/utils/ApiError.ts new file mode 100644 index 0000000..041e5a6 --- /dev/null +++ b/packages/api/src/common/utils/ApiError.ts @@ -0,0 +1,24 @@ +export class ApiError extends Error { + readonly statusCode: number; + readonly isOperational: boolean; + + constructor(statusCode: number, message: string, isOperational = true) { + super(message); + this.statusCode = statusCode; + this.isOperational = isOperational; + Object.setPrototypeOf(this, ApiError.prototype); + Error.captureStackTrace(this, this.constructor); + } + + static badRequest(message: string): ApiError { + return new ApiError(400, message); + } + + static notFound(message: string): ApiError { + return new ApiError(404, message); + } + + static internal(message: string): ApiError { + return new ApiError(500, message, false); + } +} diff --git a/packages/api/src/common/utils/ApiResponse.ts b/packages/api/src/common/utils/ApiResponse.ts new file mode 100644 index 0000000..8e767c0 --- /dev/null +++ b/packages/api/src/common/utils/ApiResponse.ts @@ -0,0 +1,28 @@ +import type { ApiSuccessResponse } from "@live-code-challenge/shared"; +import type { Response } from "express"; + +export function sendResponse( + res: Response, + data: T, + statusCode = 200, + meta?: Record, +): Response { + const response: ApiSuccessResponse = { success: true, data }; + if (meta) response.meta = meta; + return res.status(statusCode).json(response); +} + +export function sendCreated(res: Response, data: T): Response { + return sendResponse(res, data, 201); +} + +export function sendNoContent(res: Response): Response { + return res.status(204).send(); +} + +// Legacy export for backward compat +export const ApiResponse = { + send: sendResponse, + created: sendCreated, + noContent: sendNoContent, +}; diff --git a/packages/api/src/common/utils/index.ts b/packages/api/src/common/utils/index.ts new file mode 100644 index 0000000..8f3d048 --- /dev/null +++ b/packages/api/src/common/utils/index.ts @@ -0,0 +1,3 @@ +export { ApiError } from "./ApiError.js"; +export { ApiResponse } from "./ApiResponse.js"; +export { logger } from "./logger.js"; diff --git a/packages/api/src/common/utils/logger.ts b/packages/api/src/common/utils/logger.ts new file mode 100644 index 0000000..1933024 --- /dev/null +++ b/packages/api/src/common/utils/logger.ts @@ -0,0 +1,20 @@ +import winston from "winston"; + +const { combine, timestamp, printf, colorize } = winston.format; + +const logFormat = printf(({ level, message, timestamp, requestId, ...meta }) => { + const reqId = requestId ? `[${requestId}] ` : ""; + const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : ""; + return `${timestamp} ${level}: ${reqId}${message}${metaStr}`; +}); + +export const logger = winston.createLogger({ + level: process.env.NODE_ENV === "production" ? "info" : "debug", + silent: process.env.NODE_ENV === "test", + format: combine(timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), logFormat), + transports: [ + new winston.transports.Console({ + format: combine(colorize(), timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), logFormat), + }), + ], +}); diff --git a/packages/api/src/config/database.ts b/packages/api/src/config/database.ts new file mode 100644 index 0000000..57310c2 --- /dev/null +++ b/packages/api/src/config/database.ts @@ -0,0 +1,37 @@ +import { type Db, MongoClient } from "mongodb"; +import { logger } from "../common/utils/logger.js"; +import { env } from "./env.js"; + +let client: MongoClient | null = null; +let db: Db | null = null; + +export async function connectDatabase(): Promise { + if (db) return db; + + client = new MongoClient(env.DATABASE_URL); + await client.connect(); + db = client.db(); + + logger.info("Connected to MongoDB"); + return db; +} + +export function getDatabase(): Db { + if (!db) { + throw new Error("Database not initialized. Call connectDatabase() first."); + } + return db; +} + +export function getClient(): MongoClient | null { + return client; +} + +export async function disconnectDatabase(): Promise { + if (client) { + await client.close(); + client = null; + db = null; + logger.info("Disconnected from MongoDB"); + } +} diff --git a/packages/api/src/config/env.ts b/packages/api/src/config/env.ts new file mode 100644 index 0000000..8d23ee8 --- /dev/null +++ b/packages/api/src/config/env.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; + +const envSchema = z.object({ + NODE_ENV: z.enum(["development", "production", "test"]).default("development"), + PORT: z.coerce.number().default(3001), + DATABASE_URL: z.string().url(), +}); + +export type Env = z.infer; + +let cachedEnv: Env | null = null; + +function validateEnv(): Env { + if (cachedEnv) return cachedEnv; + + const result = envSchema.safeParse(process.env); + + if (!result.success) { + console.error("Invalid environment variables:"); + console.error(result.error.flatten().fieldErrors); + process.exit(1); + } + + cachedEnv = result.data; + return cachedEnv; +} + +export const env: Env = new Proxy({} as Env, { + get(_target, prop: keyof Env) { + return validateEnv()[prop]; + }, +}); diff --git a/packages/api/src/config/index.ts b/packages/api/src/config/index.ts new file mode 100644 index 0000000..347c2e9 --- /dev/null +++ b/packages/api/src/config/index.ts @@ -0,0 +1,2 @@ +export { connectDatabase, disconnectDatabase, getClient, getDatabase } from "./database.js"; +export { type Env, env } from "./env.js"; diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index b253117..6c08d8c 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,36 +1,22 @@ -import cors from "cors"; -import { room as roomData } from "db/seeds/data"; -import dotenv from "dotenv"; -import express, { type Request, type Response } from "express"; -import { MongoClient } from "mongodb"; +import "dotenv/config"; +import { createApp } from "./app.js"; +import { logger } from "./common/utils/logger.js"; +import { connectDatabase } from "./config/database.js"; +import { env } from "./config/env.js"; -dotenv.config(); - -const PORT = process.env.PORT || 3001; -if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is not set"); -const DATABASE_URL = process.env.DATABASE_URL; - -// Middleware -const app = express(); -app.use(cors()); -app.use(express.json()); +async function bootstrap(): Promise { + try { + await connectDatabase(); -// Routes -app.get("/rooms/:roomCode", async (req: Request, res: Response) => { - const mongoClient = new MongoClient(DATABASE_URL); - console.log("Connecting to MongoDB..."); + const app = createApp(); - try { - await mongoClient.connect(); - console.log("Successfully connected to MongoDB!"); - const room = roomData["linux-fundamentals-part1"]; - res.send(room); - } finally { - await mongoClient.close(); + app.listen(env.PORT, () => { + logger.info(`API server running on http://localhost:${env.PORT}`); + }); + } catch (error) { + logger.error("Failed to start server:", error); + process.exit(1); } -}); +} -// Start server -app.listen(PORT, () => { - console.log(`πŸš€ API Server running on http://localhost:${PORT}`); -}); +bootstrap(); diff --git a/packages/api/src/modules/health/health.routes.ts b/packages/api/src/modules/health/health.routes.ts new file mode 100644 index 0000000..7479060 --- /dev/null +++ b/packages/api/src/modules/health/health.routes.ts @@ -0,0 +1,27 @@ +import { type Request, type Response, Router } from "express"; +import { ApiResponse } from "../../common/utils/ApiResponse.js"; +import { getClient } from "../../config/database.js"; + +const router = Router(); + +router.get("/health", async (_req: Request, res: Response) => { + const client = getClient(); + let dbStatus = "disconnected"; + + if (client) { + try { + await client.db().command({ ping: 1 }); + dbStatus = "connected"; + } catch { + dbStatus = "error"; + } + } + + ApiResponse.send(res, { + status: "ok", + database: dbStatus, + timestamp: new Date().toISOString(), + }); +}); + +export { router as healthRoutes }; diff --git a/packages/api/src/modules/room/index.ts b/packages/api/src/modules/room/index.ts new file mode 100644 index 0000000..75083b1 --- /dev/null +++ b/packages/api/src/modules/room/index.ts @@ -0,0 +1,4 @@ +export type { Question, QuestionProgress, Room, Task } from "@live-code-challenge/shared"; +export { RoomRepository, roomRepository } from "./room.repository.js"; +export { roomRoutes } from "./room.routes.js"; +export { RoomService, roomService } from "./room.service.js"; diff --git a/packages/api/src/modules/room/room.handlers.ts b/packages/api/src/modules/room/room.handlers.ts new file mode 100644 index 0000000..5bd63bb --- /dev/null +++ b/packages/api/src/modules/room/room.handlers.ts @@ -0,0 +1,50 @@ +import type { NextFunction, Request, Response } from "express"; +import { ApiError } from "../../common/utils/ApiError.js"; +import { ApiResponse } from "../../common/utils/ApiResponse.js"; +import { roomService } from "./room.service.js"; +import { + roomCodeSchema, + submitAnswerBodySchema, + submitAnswerParamsSchema, +} from "./room.validators.js"; + +export async function getRoomByCode( + req: Request, + res: Response, + next: NextFunction, +): Promise { + try { + const result = roomCodeSchema.safeParse(req.params); + + if (!result.success) { + throw ApiError.badRequest("Invalid room code"); + } + + const room = await roomService.getRoomByCode(result.data.roomCode); + ApiResponse.send(res, room); + } catch (error) { + next(error); + } +} + +export async function submitAnswer(req: Request, res: Response, next: NextFunction): Promise { + try { + const paramsResult = submitAnswerParamsSchema.safeParse(req.params); + if (!paramsResult.success) { + throw ApiError.badRequest("Invalid request parameters"); + } + + const bodyResult = submitAnswerBodySchema.safeParse(req.body); + if (!bodyResult.success) { + throw ApiError.badRequest("Invalid request body"); + } + + const { roomCode, questionOrder } = paramsResult.data; + const { taskOrder, answer } = bodyResult.data; + + const result = await roomService.submitAnswer(roomCode, taskOrder, questionOrder, answer); + ApiResponse.send(res, result); + } catch (error) { + next(error); + } +} diff --git a/packages/api/src/modules/room/room.repository.ts b/packages/api/src/modules/room/room.repository.ts new file mode 100644 index 0000000..6606058 --- /dev/null +++ b/packages/api/src/modules/room/room.repository.ts @@ -0,0 +1,92 @@ +import type { Question, Room, Task } from "@live-code-challenge/shared"; +import { getDatabase } from "../../config/database.js"; + +interface QuestionWithAnswer extends Question { + answer: string | null; +} + +export class RoomRepository { + private readonly roomsColl = "rooms"; + private readonly tasksColl = "tasks"; + private readonly questionsColl = "questions"; + + async findByCode(code: string): Promise { + const db = getDatabase(); + + // 1. Fetch the base room + const roomDoc = await db.collection(this.roomsColl).findOne({ code }); + + if (!roomDoc) { + return null; + } + + // 2. Fetch all related Tasks and Questions in parallel for max speed + // We filter both by roomCode to keep the query indexed and fast + const [tasks, rawQuestions] = await Promise.all([ + db.collection(this.tasksColl).find({ roomCode: code }).sort({ order: 1 }).toArray(), + db + .collection(this.questionsColl) + .find({ roomCode: code }) + .sort({ order: 1 }) + .toArray(), + ]); + + // Map questions to include requiresAnswer and exclude answer field + const allQuestions = rawQuestions.map(({ answer, ...rest }) => ({ + ...rest, + requiresAnswer: answer !== null, + })); + + // 3. Stitch the hierarchy together + const assembledTasks = tasks.map((task) => { + return { + ...task, + // Match questions to this specific task + // Logic: Questions must have a 'taskOrder' or 'taskId' in the DB + questions: allQuestions.filter((q: Question) => q.task === task.order), + }; + }); + + // 4. Return the fully nested object + return { + ...roomDoc, + tasks: assembledTasks, + }; + } + + async findQuestion( + roomCode: string, + taskOrder: number, + questionOrder: number, + ): Promise { + const db = getDatabase(); + return db.collection(this.questionsColl).findOne({ + roomCode, + task: taskOrder, + order: questionOrder, + }); + } + + async updateQuestionProgress( + roomCode: string, + taskOrder: number, + questionOrder: number, + correct: boolean, + ): Promise { + const db = getDatabase(); + const update = correct + ? { $inc: { "progress.attempts": 1 }, $set: { "progress.correct": true } } + : { $inc: { "progress.attempts": 1 } }; + + await db.collection(this.questionsColl).updateOne( + { + roomCode, + task: taskOrder, + order: questionOrder, + }, + update, + ); + } +} + +export const roomRepository = new RoomRepository(); diff --git a/packages/api/src/modules/room/room.routes.ts b/packages/api/src/modules/room/room.routes.ts new file mode 100644 index 0000000..bd1f281 --- /dev/null +++ b/packages/api/src/modules/room/room.routes.ts @@ -0,0 +1,9 @@ +import { Router } from "express"; +import { getRoomByCode, submitAnswer } from "./room.handlers.js"; + +const router = Router(); + +router.get("/rooms/:roomCode", getRoomByCode); +router.post("/rooms/:roomCode/questions/:questionOrder", submitAnswer); + +export { router as roomRoutes }; diff --git a/packages/api/src/modules/room/room.service.ts b/packages/api/src/modules/room/room.service.ts new file mode 100644 index 0000000..adbe90d --- /dev/null +++ b/packages/api/src/modules/room/room.service.ts @@ -0,0 +1,51 @@ +import type { AnswerSubmissionResponse, Room } from "@live-code-challenge/shared"; +import { ApiError } from "../../common/utils/ApiError.js"; +import { type RoomRepository, roomRepository } from "./room.repository.js"; + +export class RoomService { + constructor(private readonly repository: RoomRepository) {} + + async getRoomByCode(code: string): Promise { + const room = await this.repository.findByCode(code); + + if (!room) { + throw ApiError.notFound(`Room '${code}' not found`); + } + + return room; + } + + async submitAnswer( + roomCode: string, + taskOrder: number, + questionOrder: number, + answer?: string, + ): Promise { + const question = await this.repository.findQuestion(roomCode, taskOrder, questionOrder); + + if (!question) { + throw ApiError.notFound("Question not found"); + } + + // Questions with null answer are auto-correct + const isCorrect = + question.answer === null || + question.answer.trim().toLowerCase() === (answer ?? "").trim().toLowerCase(); + + await this.repository.updateQuestionProgress(roomCode, taskOrder, questionOrder, isCorrect); + + const newAttempts = question.progress.attempts + 1; + + if (isCorrect) { + return { correct: true, attempts: newAttempts }; + } + + return { + correct: false, + attempts: newAttempts, + hint: question.hint || undefined, + }; + } +} + +export const roomService = new RoomService(roomRepository); diff --git a/packages/api/src/modules/room/room.validators.ts b/packages/api/src/modules/room/room.validators.ts new file mode 100644 index 0000000..8c716cb --- /dev/null +++ b/packages/api/src/modules/room/room.validators.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; + +const roomCodePattern = /^[a-z0-9-]+$/; + +export const roomCodeSchema = z.object({ + roomCode: z.string().min(1).max(100).regex(roomCodePattern, "Invalid room code format"), +}); + +export type RoomCodeParams = z.infer; + +export const submitAnswerParamsSchema = z.object({ + roomCode: z.string().min(1).max(100).regex(roomCodePattern, "Invalid room code format"), + questionOrder: z.coerce.number().int().positive(), +}); + +export type SubmitAnswerParams = z.infer; + +export const submitAnswerBodySchema = z.object({ + taskOrder: z.number().int().positive(), + answer: z.string().min(1).max(500).optional(), +}); + +export type SubmitAnswerBody = z.infer; diff --git a/packages/api/src/routes.ts b/packages/api/src/routes.ts new file mode 100644 index 0000000..810a998 --- /dev/null +++ b/packages/api/src/routes.ts @@ -0,0 +1,10 @@ +import { Router } from "express"; +import { healthRoutes } from "./modules/health/health.routes.js"; +import { roomRoutes } from "./modules/room/room.routes.js"; + +const router = Router(); + +router.use(healthRoutes); +router.use("/api/v1", roomRoutes); + +export { router as routes }; diff --git a/packages/api/tests/integration/room.test.ts b/packages/api/tests/integration/room.test.ts new file mode 100644 index 0000000..5fb5ceb --- /dev/null +++ b/packages/api/tests/integration/room.test.ts @@ -0,0 +1,110 @@ +// Set test environment +process.env.DATABASE_URL = "mongodb://localhost:27017/test"; +process.env.NODE_ENV = "test"; + +import type { Room } from "@live-code-challenge/shared"; +import type { Db } from "mongodb"; + +// ESM mock setup - must use jest.unstable_mockModule before imports +const { jest } = await import("@jest/globals"); + +jest.unstable_mockModule("../../src/config/database.js", () => ({ + getDatabase: () => (globalThis as unknown as { testDb: Db }).testDb, + getClient: () => (globalThis as unknown as { testClient: unknown }).testClient, +})); + +// Dynamic imports after mocking +const { createApp } = await import("../../src/app.js"); +const request = (await import("supertest")).default; + +describe("Room API", () => { + const app = createApp(); + let db: Db; + + const testRoom: Room = { + code: "linux-fundamentals-part1", + title: "Linux Fundamentals Part 1", + describe: "Learn Linux basics", + imageURL: "https://example.com/image.png", + tasks: [ + { + roomCode: "linux-fundamentals-part1", + order: 1, + title: "Introduction", + created: "2021-03-18T12:24:58.588Z", + description: "Welcome to Linux", + questions: [ + { + hint: "", + question: "Let's get started!", + task: 1, + order: 1, + roomCode: "linux-fundamentals-part1", + progress: { correct: true, attempts: 1 }, + answerDescription: "Answer format: ***{*********_**_****}", + requiresAnswer: true, + }, + ], + }, + ], + }; + + beforeAll(() => { + db = (globalThis as unknown as { testDb: Db }).testDb; + }); + + beforeEach(async () => { + const { tasks, ...roomBase } = testRoom; + await db.collection("rooms").insertOne(roomBase); + + for (const task of tasks) { + const { questions, ...taskBase } = task; + await db.collection("tasks").insertOne(taskBase); + + for (const question of questions) { + await db.collection("questions").insertOne(question); + } + } + }); + + describe("GET /api/v1/rooms/:roomCode", () => { + it("should return room when found", async () => { + const response = await request(app).get("/api/v1/rooms/linux-fundamentals-part1"); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.data.code).toBe("linux-fundamentals-part1"); + expect(response.body.data.title).toBe("Linux Fundamentals Part 1"); + expect(response.body.data.tasks).toHaveLength(1); + }); + + it("should return 404 when room not found", async () => { + const response = await request(app).get("/api/v1/rooms/invalid-room"); + + expect(response.status).toBe(404); + expect(response.body.success).toBe(false); + expect(response.body.error.message).toContain("not found"); + }); + }); + + describe("GET /health", () => { + it("should return health status", async () => { + const response = await request(app).get("/health"); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.data.status).toBe("ok"); + expect(response.body.data.database).toBeDefined(); + }); + }); + + describe("404 handler", () => { + it("should return 404 for unknown routes", async () => { + const response = await request(app).get("/unknown-route"); + + expect(response.status).toBe(404); + expect(response.body.success).toBe(false); + expect(response.body.error.message).toContain("not found"); + }); + }); +}); diff --git a/packages/api/tests/setup.ts b/packages/api/tests/setup.ts new file mode 100644 index 0000000..05f8ea0 --- /dev/null +++ b/packages/api/tests/setup.ts @@ -0,0 +1,38 @@ +// Set test env vars before any module imports +process.env.DATABASE_URL = "mongodb://localhost:27017/test"; +process.env.NODE_ENV = "test"; + +import { type Db, MongoClient } from "mongodb"; +import { MongoMemoryServer } from "mongodb-memory-server"; + +declare global { + var testDb: Db; + var testClient: MongoClient; +} + +let mongod: MongoMemoryServer; +let client: MongoClient; +let db: Db; + +beforeAll(async () => { + mongod = await MongoMemoryServer.create(); + const uri = mongod.getUri(); + client = new MongoClient(uri); + await client.connect(); + db = client.db("test"); + + globalThis.testDb = db; + globalThis.testClient = client; +}, 60000); // Allow 60s for MongoMemoryServer download on first run + +afterAll(async () => { + await client.close(); + await mongod.stop(); +}); + +afterEach(async () => { + const collections = await db.collections(); + for (const collection of collections) { + await collection.deleteMany({}); + } +}); diff --git a/packages/api/tests/unit/common/middleware/errorHandler.test.ts b/packages/api/tests/unit/common/middleware/errorHandler.test.ts new file mode 100644 index 0000000..b3e34e2 --- /dev/null +++ b/packages/api/tests/unit/common/middleware/errorHandler.test.ts @@ -0,0 +1,90 @@ +// Set test environment before imports +process.env.NODE_ENV = "test"; + +import { describe, expect, it, jest } from "@jest/globals"; +import type { NextFunction, Request, Response } from "express"; +import { errorHandler } from "../../../../src/common/middleware/errorHandler.js"; +import { ApiError } from "../../../../src/common/utils/ApiError.js"; + +function createMockRequest(requestId?: string): Request { + return { requestId } as Request; +} + +function createMockResponse(): Response & { _status?: number; _json?: unknown } { + const res: Partial & { _status?: number; _json?: unknown } = { + status: jest.fn(function (this: typeof res, code: number) { + this._status = code; + return this as Response; + }), + json: jest.fn(function (this: typeof res, body: unknown) { + this._json = body; + return this as Response; + }), + }; + return res as Response & { _status?: number; _json?: unknown }; +} + +describe("errorHandler", () => { + const mockNext: NextFunction = jest.fn(); + + it("should handle ApiError with correct status and message", () => { + const req = createMockRequest("req-123"); + const res = createMockResponse(); + const error = ApiError.badRequest("Invalid input"); + + errorHandler(error, req, res, mockNext); + + expect(res._status).toBe(400); + expect(res._json).toEqual({ + success: false, + error: { message: "Invalid input" }, + }); + }); + + it("should handle ApiError.notFound", () => { + const req = createMockRequest("req-456"); + const res = createMockResponse(); + const error = ApiError.notFound("Resource not found"); + + errorHandler(error, req, res, mockNext); + + expect(res._status).toBe(404); + expect(res._json).toEqual({ + success: false, + error: { message: "Resource not found" }, + }); + }); + + it("should handle generic Error with 500 status", () => { + const req = createMockRequest("req-789"); + const res = createMockResponse(); + const error = new Error("Something went wrong"); + + errorHandler(error, req, res, mockNext); + + expect(res._status).toBe(500); + expect((res._json as { error: { message: string } }).error.message).toBe( + "Something went wrong", + ); + }); + + it("should include stack trace in non-production", () => { + const req = createMockRequest(); + const res = createMockResponse(); + const error = new Error("Test error"); + + errorHandler(error, req, res, mockNext); + + expect((res._json as { error: { stack?: string } }).error.stack).toBeDefined(); + }); + + it("should handle missing requestId", () => { + const req = createMockRequest(); // no requestId + const res = createMockResponse(); + const error = ApiError.badRequest("Test"); + + // Should not throw + expect(() => errorHandler(error, req, res, mockNext)).not.toThrow(); + expect(res._status).toBe(400); + }); +}); diff --git a/packages/api/tests/unit/common/utils/ApiError.test.ts b/packages/api/tests/unit/common/utils/ApiError.test.ts new file mode 100644 index 0000000..f3acda9 --- /dev/null +++ b/packages/api/tests/unit/common/utils/ApiError.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "@jest/globals"; +import { ApiError } from "../../../../src/common/utils/ApiError.js"; + +describe("ApiError", () => { + describe("constructor", () => { + it("should create error with statusCode and message", () => { + const error = new ApiError(400, "Bad request"); + + expect(error.statusCode).toBe(400); + expect(error.message).toBe("Bad request"); + expect(error.isOperational).toBe(true); + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(ApiError); + }); + + it("should allow non-operational errors", () => { + const error = new ApiError(500, "System failure", false); + + expect(error.isOperational).toBe(false); + }); + }); + + describe("static methods", () => { + it("badRequest should return 400 error", () => { + const error = ApiError.badRequest("Invalid input"); + + expect(error.statusCode).toBe(400); + expect(error.message).toBe("Invalid input"); + expect(error.isOperational).toBe(true); + }); + + it("notFound should return 404 error", () => { + const error = ApiError.notFound("Resource not found"); + + expect(error.statusCode).toBe(404); + expect(error.message).toBe("Resource not found"); + expect(error.isOperational).toBe(true); + }); + + it("internal should return 500 non-operational error", () => { + const error = ApiError.internal("Server error"); + + expect(error.statusCode).toBe(500); + expect(error.message).toBe("Server error"); + expect(error.isOperational).toBe(false); + }); + }); +}); diff --git a/packages/api/tests/unit/common/utils/ApiResponse.test.ts b/packages/api/tests/unit/common/utils/ApiResponse.test.ts new file mode 100644 index 0000000..6cdb888 --- /dev/null +++ b/packages/api/tests/unit/common/utils/ApiResponse.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import type { Response } from "express"; +import { + ApiResponse, + sendCreated, + sendNoContent, + sendResponse, +} from "../../../../src/common/utils/ApiResponse.js"; + +function createMockResponse(): Response { + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + send: jest.fn().mockReturnThis(), + }; + return res as unknown as Response; +} + +describe("ApiResponse", () => { + describe("sendResponse", () => { + it("should send success response with data", () => { + const res = createMockResponse(); + const data = { id: 1, name: "test" }; + + sendResponse(res, data); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ success: true, data }); + }); + + it("should allow custom status code", () => { + const res = createMockResponse(); + + sendResponse(res, { result: "ok" }, 201); + + expect(res.status).toHaveBeenCalledWith(201); + }); + + it("should include meta when provided", () => { + const res = createMockResponse(); + const data = { items: [] }; + const meta = { total: 100, page: 1 }; + + sendResponse(res, data, 200, meta); + + expect(res.json).toHaveBeenCalledWith({ success: true, data, meta }); + }); + }); + + describe("sendCreated", () => { + it("should send 201 response", () => { + const res = createMockResponse(); + const data = { id: 123 }; + + sendCreated(res, data); + + expect(res.status).toHaveBeenCalledWith(201); + expect(res.json).toHaveBeenCalledWith({ success: true, data }); + }); + }); + + describe("sendNoContent", () => { + it("should send 204 response", () => { + const res = createMockResponse(); + + sendNoContent(res); + + expect(res.status).toHaveBeenCalledWith(204); + expect(res.send).toHaveBeenCalled(); + }); + }); + + describe("legacy ApiResponse object", () => { + it("should expose send, created, noContent methods", () => { + expect(ApiResponse.send).toBe(sendResponse); + expect(ApiResponse.created).toBe(sendCreated); + expect(ApiResponse.noContent).toBe(sendNoContent); + }); + }); +}); diff --git a/packages/api/tests/unit/modules/room/room.handlers.test.ts b/packages/api/tests/unit/modules/room/room.handlers.test.ts new file mode 100644 index 0000000..b790f28 --- /dev/null +++ b/packages/api/tests/unit/modules/room/room.handlers.test.ts @@ -0,0 +1,183 @@ +// Set test environment +process.env.DATABASE_URL = "mongodb://localhost:27017/test"; +process.env.NODE_ENV = "test"; + +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; +import type { AnswerSubmissionResponse, Room } from "@live-code-challenge/shared"; +import type { NextFunction, Request, Response } from "express"; +import { ApiError } from "../../../../src/common/utils/ApiError.js"; + +// Mock the service module +const mockGetRoomByCode = jest.fn<(code: string) => Promise>(); +const mockSubmitAnswer = + jest.fn< + ( + roomCode: string, + taskOrder: number, + questionOrder: number, + answer?: string, + ) => Promise + >(); + +jest.unstable_mockModule("../../../../src/modules/room/room.service.js", () => ({ + roomService: { + getRoomByCode: mockGetRoomByCode, + submitAnswer: mockSubmitAnswer, + }, + RoomService: jest.fn(), +})); + +// Import handlers after mocking +const { getRoomByCode, submitAnswer } = await import( + "../../../../src/modules/room/room.handlers.js" +); + +function createMockRequest(params: Record = {}, body: unknown = {}): Request { + return { params, body } as Request; +} + +function createMockResponse(): Response & { _status?: number; _json?: unknown } { + const res: Partial & { _status?: number; _json?: unknown } = { + status: jest.fn(function (this: typeof res, code: number) { + this._status = code; + return this as Response; + }), + json: jest.fn(function (this: typeof res, body: unknown) { + this._json = body; + return this as Response; + }), + }; + return res as Response & { _status?: number; _json?: unknown }; +} + +describe("Room Handlers", () => { + let mockNext: NextFunction; + + beforeEach(() => { + mockNext = jest.fn(); + jest.clearAllMocks(); + }); + + describe("getRoomByCode", () => { + it("should return room data on success", async () => { + const roomData = { + code: "test-room", + title: "Test", + describe: "Desc", + imageURL: "", + tasks: [], + }; + mockGetRoomByCode.mockResolvedValue(roomData); + + const req = createMockRequest({ roomCode: "test-room" }); + const res = createMockResponse(); + + await getRoomByCode(req, res, mockNext); + + expect(mockGetRoomByCode).toHaveBeenCalledWith("test-room"); + expect(res._status).toBe(200); + expect(res._json).toEqual({ success: true, data: roomData }); + }); + + it("should call next with error for invalid roomCode format", async () => { + const req = createMockRequest({ roomCode: "INVALID_CODE!" }); + const res = createMockResponse(); + + await getRoomByCode(req, res, mockNext); + + expect(mockNext).toHaveBeenCalled(); + const error = (mockNext as jest.Mock).mock.calls[0][0] as ApiError; + expect(error).toBeInstanceOf(ApiError); + expect(error.statusCode).toBe(400); + }); + + it("should call next with error when service throws", async () => { + mockGetRoomByCode.mockRejectedValue(ApiError.notFound("Room not found")); + + const req = createMockRequest({ roomCode: "missing-room" }); + const res = createMockResponse(); + + await getRoomByCode(req, res, mockNext); + + expect(mockNext).toHaveBeenCalledWith(expect.any(ApiError)); + }); + }); + + describe("submitAnswer", () => { + it("should return result on correct answer", async () => { + mockSubmitAnswer.mockResolvedValue({ correct: true, attempts: 1 }); + + const req = createMockRequest( + { roomCode: "test-room", questionOrder: "1" }, + { taskOrder: 1, answer: "correct" }, + ); + const res = createMockResponse(); + + await submitAnswer(req, res, mockNext); + + expect(mockSubmitAnswer).toHaveBeenCalledWith("test-room", 1, 1, "correct"); + expect(res._json).toEqual({ success: true, data: { correct: true, attempts: 1 } }); + }); + + it("should return result on wrong answer with hint", async () => { + mockSubmitAnswer.mockResolvedValue({ correct: false, attempts: 2, hint: "Try harder" }); + + const req = createMockRequest( + { roomCode: "test-room", questionOrder: "1" }, + { taskOrder: 1, answer: "wrong" }, + ); + const res = createMockResponse(); + + await submitAnswer(req, res, mockNext); + + expect(res._json).toEqual({ + success: true, + data: { correct: false, attempts: 2, hint: "Try harder" }, + }); + }); + + it("should call next with error for invalid params", async () => { + const req = createMockRequest( + { roomCode: "INVALID!", questionOrder: "abc" }, + { taskOrder: 1 }, + ); + const res = createMockResponse(); + + await submitAnswer(req, res, mockNext); + + expect(mockNext).toHaveBeenCalled(); + const error = (mockNext as jest.Mock).mock.calls[0][0] as ApiError; + expect(error).toBeInstanceOf(ApiError); + expect(error.statusCode).toBe(400); + }); + + it("should call next with error for invalid body", async () => { + const req = createMockRequest( + { roomCode: "test-room", questionOrder: "1" }, + { taskOrder: "not-a-number" }, + ); + const res = createMockResponse(); + + await submitAnswer(req, res, mockNext); + + expect(mockNext).toHaveBeenCalled(); + const error = (mockNext as jest.Mock).mock.calls[0][0] as ApiError; + expect(error).toBeInstanceOf(ApiError); + expect(error.statusCode).toBe(400); + }); + + it("should handle optional answer field", async () => { + mockSubmitAnswer.mockResolvedValue({ correct: true, attempts: 1 }); + + const req = createMockRequest( + { roomCode: "test-room", questionOrder: "1" }, + { taskOrder: 1 }, // no answer + ); + const res = createMockResponse(); + + await submitAnswer(req, res, mockNext); + + expect(mockSubmitAnswer).toHaveBeenCalledWith("test-room", 1, 1, undefined); + }); + }); +}); diff --git a/packages/api/tests/unit/modules/room/room.service.test.ts b/packages/api/tests/unit/modules/room/room.service.test.ts new file mode 100644 index 0000000..458f149 --- /dev/null +++ b/packages/api/tests/unit/modules/room/room.service.test.ts @@ -0,0 +1,148 @@ +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; +import type { Room } from "@live-code-challenge/shared"; +import { ApiError } from "../../../../src/common/utils/ApiError.js"; +import type { RoomRepository } from "../../../../src/modules/room/room.repository.js"; +import { RoomService } from "../../../../src/modules/room/room.service.js"; + +interface QuestionWithAnswer { + hint: string; + question: string; + order: number; + task: number; + roomCode: string; + progress: { correct: boolean; attempts: number }; + answerDescription: string; + requiresAnswer: boolean; + answer: string | null; +} + +type MockedRepository = { + findByCode: jest.Mock<(code: string) => Promise>; + findQuestion: jest.Mock< + ( + roomCode: string, + taskOrder: number, + questionOrder: number, + ) => Promise + >; + updateQuestionProgress: jest.Mock< + (roomCode: string, taskOrder: number, questionOrder: number, correct: boolean) => Promise + >; +}; + +describe("RoomService", () => { + let service: RoomService; + let mockRepository: MockedRepository; + + const mockRoom: Room = { + code: "test-room", + title: "Test Room", + describe: "A test room", + imageURL: "https://example.com/image.png", + tasks: [], + }; + + const mockQuestion: QuestionWithAnswer = { + hint: "Try again", + question: "What is 2+2?", + order: 1, + task: 1, + roomCode: "test-room", + progress: { correct: false, attempts: 0 }, + answerDescription: "A number", + requiresAnswer: true, + answer: "4", + }; + + beforeEach(() => { + mockRepository = { + findByCode: jest.fn(), + findQuestion: jest.fn(), + updateQuestionProgress: jest.fn(), + }; + service = new RoomService(mockRepository as unknown as RoomRepository); + }); + + describe("getRoomByCode", () => { + it("should return room when found", async () => { + mockRepository.findByCode.mockResolvedValue(mockRoom); + + const result = await service.getRoomByCode("test-room"); + + expect(result).toEqual(mockRoom); + expect(mockRepository.findByCode).toHaveBeenCalledWith("test-room"); + }); + + it("should throw ApiError.notFound when room not found", async () => { + mockRepository.findByCode.mockResolvedValue(null); + + await expect(service.getRoomByCode("invalid")).rejects.toThrow(ApiError); + await expect(service.getRoomByCode("invalid")).rejects.toMatchObject({ + statusCode: 404, + message: "Room 'invalid' not found", + }); + }); + }); + + describe("submitAnswer", () => { + it("should return correct=true when answer matches", async () => { + mockRepository.findQuestion.mockResolvedValue(mockQuestion); + mockRepository.updateQuestionProgress.mockResolvedValue(undefined); + + const result = await service.submitAnswer("test-room", 1, 1, "4"); + + expect(result).toEqual({ correct: true, attempts: 1 }); + expect(mockRepository.updateQuestionProgress).toHaveBeenCalledWith("test-room", 1, 1, true); + }); + + it("should return correct=true when answer matches case-insensitively", async () => { + const questionWithTextAnswer = { ...mockQuestion, answer: "Hello" }; + mockRepository.findQuestion.mockResolvedValue(questionWithTextAnswer); + mockRepository.updateQuestionProgress.mockResolvedValue(undefined); + + const result = await service.submitAnswer("test-room", 1, 1, " HELLO "); + + expect(result).toEqual({ correct: true, attempts: 1 }); + }); + + it("should return correct=false with hint when answer is wrong", async () => { + mockRepository.findQuestion.mockResolvedValue(mockQuestion); + mockRepository.updateQuestionProgress.mockResolvedValue(undefined); + + const result = await service.submitAnswer("test-room", 1, 1, "5"); + + expect(result).toEqual({ correct: false, attempts: 1, hint: "Try again" }); + expect(mockRepository.updateQuestionProgress).toHaveBeenCalledWith("test-room", 1, 1, false); + }); + + it("should return correct=false without hint when hint is empty", async () => { + const questionNoHint = { ...mockQuestion, hint: "" }; + mockRepository.findQuestion.mockResolvedValue(questionNoHint); + mockRepository.updateQuestionProgress.mockResolvedValue(undefined); + + const result = await service.submitAnswer("test-room", 1, 1, "wrong"); + + expect(result).toEqual({ correct: false, attempts: 1, hint: undefined }); + }); + + it("should auto-correct questions with null answer", async () => { + const autoCorrectQuestion = { ...mockQuestion, answer: null }; + mockRepository.findQuestion.mockResolvedValue(autoCorrectQuestion); + mockRepository.updateQuestionProgress.mockResolvedValue(undefined); + + const result = await service.submitAnswer("test-room", 1, 1, undefined); + + expect(result).toEqual({ correct: true, attempts: 1 }); + }); + + it("should throw ApiError.notFound when question not found", async () => { + mockRepository.findQuestion.mockResolvedValue(null); + + await expect(service.submitAnswer("test-room", 1, 99)).rejects.toThrow(ApiError); + await expect(service.submitAnswer("test-room", 1, 99)).rejects.toMatchObject({ + statusCode: 404, + message: "Question not found", + }); + }); + }); +}); diff --git a/packages/api/tests/unit/modules/room/room.validators.test.ts b/packages/api/tests/unit/modules/room/room.validators.test.ts new file mode 100644 index 0000000..aa1a754 --- /dev/null +++ b/packages/api/tests/unit/modules/room/room.validators.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from "@jest/globals"; +import { + roomCodeSchema, + submitAnswerBodySchema, + submitAnswerParamsSchema, +} from "../../../../src/modules/room/room.validators.js"; + +describe("Room Validators", () => { + describe("roomCodeSchema", () => { + it("should accept valid room codes", () => { + expect(roomCodeSchema.safeParse({ roomCode: "linux-fundamentals" }).success).toBe(true); + expect(roomCodeSchema.safeParse({ roomCode: "room123" }).success).toBe(true); + expect(roomCodeSchema.safeParse({ roomCode: "a" }).success).toBe(true); + }); + + it("should reject empty room code", () => { + const result = roomCodeSchema.safeParse({ roomCode: "" }); + expect(result.success).toBe(false); + }); + + it("should reject room codes with invalid characters", () => { + expect(roomCodeSchema.safeParse({ roomCode: "UPPERCASE" }).success).toBe(false); + expect(roomCodeSchema.safeParse({ roomCode: "has spaces" }).success).toBe(false); + expect(roomCodeSchema.safeParse({ roomCode: "special!" }).success).toBe(false); + expect(roomCodeSchema.safeParse({ roomCode: "under_score" }).success).toBe(false); + }); + + it("should reject room codes over max length", () => { + const longCode = "a".repeat(101); + const result = roomCodeSchema.safeParse({ roomCode: longCode }); + expect(result.success).toBe(false); + }); + }); + + describe("submitAnswerParamsSchema", () => { + it("should accept valid params", () => { + const result = submitAnswerParamsSchema.safeParse({ + roomCode: "test-room", + questionOrder: "5", + }); + expect(result.success).toBe(true); + expect(result.data?.questionOrder).toBe(5); // coerced to number + }); + + it("should reject invalid room code format", () => { + const result = submitAnswerParamsSchema.safeParse({ + roomCode: "INVALID!", + questionOrder: "1", + }); + expect(result.success).toBe(false); + }); + + it("should reject non-positive questionOrder", () => { + expect( + submitAnswerParamsSchema.safeParse({ roomCode: "test", questionOrder: "0" }).success, + ).toBe(false); + expect( + submitAnswerParamsSchema.safeParse({ roomCode: "test", questionOrder: "-1" }).success, + ).toBe(false); + }); + }); + + describe("submitAnswerBodySchema", () => { + it("should accept valid body with answer", () => { + const result = submitAnswerBodySchema.safeParse({ + taskOrder: 1, + answer: "my answer", + }); + expect(result.success).toBe(true); + }); + + it("should accept body without answer (optional)", () => { + const result = submitAnswerBodySchema.safeParse({ taskOrder: 1 }); + expect(result.success).toBe(true); + }); + + it("should reject non-positive taskOrder", () => { + expect(submitAnswerBodySchema.safeParse({ taskOrder: 0 }).success).toBe(false); + expect(submitAnswerBodySchema.safeParse({ taskOrder: -1 }).success).toBe(false); + }); + + it("should reject answer over max length", () => { + const longAnswer = "a".repeat(501); + const result = submitAnswerBodySchema.safeParse({ + taskOrder: 1, + answer: longAnswer, + }); + expect(result.success).toBe(false); + }); + + it("should accept answer at max length", () => { + const maxAnswer = "a".repeat(500); + const result = submitAnswerBodySchema.safeParse({ + taskOrder: 1, + answer: maxAnswer, + }); + expect(result.success).toBe(true); + }); + + it("should reject empty answer", () => { + const result = submitAnswerBodySchema.safeParse({ + taskOrder: 1, + answer: "", + }); + expect(result.success).toBe(false); + }); + }); +}); diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index 3d46f3e..c93dd1d 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2017", + "target": "ES2022", "module": "ES2022", "moduleResolution": "Bundler", "esModuleInterop": true, @@ -8,6 +8,11 @@ "strict": true, "skipLibCheck": true, "baseUrl": ".", - "outDir": "./dist" - } + "outDir": "./dist", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] } diff --git a/packages/api/tsconfig.test.json b/packages/api/tsconfig.test.json new file mode 100644 index 0000000..b3c4c72 --- /dev/null +++ b/packages/api/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["jest", "node"] + }, + "include": ["src/**/*", "tests/**/*"] +} diff --git a/packages/client/.env.example b/packages/client/.env.example new file mode 100644 index 0000000..8f825ca --- /dev/null +++ b/packages/client/.env.example @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:3001/api/v1 diff --git a/packages/client/Dockerfile.dev b/packages/client/Dockerfile.dev new file mode 100644 index 0000000..1dbccbe --- /dev/null +++ b/packages/client/Dockerfile.dev @@ -0,0 +1,20 @@ +FROM node:22-alpine + +WORKDIR /workspace + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@8.10.0 --activate + +# Copy package files for dependency installation +COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ +COPY packages/client/package.json ./packages/client/ + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Copy source (will be overwritten by volume mount in dev) +COPY . . + +EXPOSE 3000 + +CMD ["pnpm", "--filter", "client", "dev", "--host", "0.0.0.0"] diff --git a/packages/client/eslint.config.js b/packages/client/eslint.config.js index a6d1076..d5f2c0b 100644 --- a/packages/client/eslint.config.js +++ b/packages/client/eslint.config.js @@ -1,9 +1,9 @@ import js from "@eslint/js"; -import globals from "globals"; +import { defineConfig, globalIgnores } from "eslint/config"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; +import globals from "globals"; import tseslint from "typescript-eslint"; -import { defineConfig, globalIgnores } from "eslint/config"; export default defineConfig([ globalIgnores(["dist"]), diff --git a/packages/client/package.json b/packages/client/package.json index 109e43f..df0b3b2 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -8,19 +8,29 @@ "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", + "test": "vitest run", + "test:run": "vitest run", "clean": "rm -rf dist" }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "^0.2.0", + "@live-code-challenge/shared": "workspace:*", "@tanstack/react-query": "^5.90.2", + "dompurify": "^3.3.1", "react": "^19.1.1", "react-dom": "^19.1.1", - "react-router-dom": "^6.20.1" + "react-router-dom": "^6.20.1", + "web-vitals": "^5.1.0" }, "devDependencies": { "@eslint/js": "^9.36.0", + "@tailwindcss/vite": "^4.1.18", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/node": "^24.6.0", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", @@ -29,8 +39,11 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.22", "globals": "^16.4.0", + "jsdom": "^27.4.0", + "tailwindcss": "^4.1.18", "typescript": "~5.9.3", "typescript-eslint": "^8.45.0", - "vite": "^7.1.7" + "vite": "7.1.11", + "vitest": "^4.0.18" } } diff --git a/packages/client/src/App.css b/packages/client/src/App.css deleted file mode 100644 index d2007a2..0000000 --- a/packages/client/src/App.css +++ /dev/null @@ -1,88 +0,0 @@ -@import url("https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600;700&display=swap"); - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: "Source Sans Pro", sans-serif; - background-color: #f8f9fa; -} - -#root { - min-height: 100vh; - min-width: 100vw; -} - -.app { - min-height: 100vh; - display: flex; - flex-direction: column; -} - -.main { - flex: 1; - background-color: #141c2b; -} - -.page { - padding: 2rem 0; -} - -/* Common card styles */ -.card { - background: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - margin: 1rem 0; - padding: 2rem; - text-align: left; - font-family: "Source Sans Pro", sans-serif; -} - -.card h2 { - font-family: "Source Sans Pro", sans-serif; - font-weight: 600; - color: #2c3e50; - margin-top: 0; - margin-bottom: 1rem; -} - -.card p { - font-family: "Source Sans Pro", sans-serif; - color: #34495e; - line-height: 1.6; - margin: 0.5rem 0; -} - -.card ul { - margin: 1rem 0; - padding-left: 1.5rem; -} - -.card li { - margin: 0.5rem 0; - font-family: "Source Sans Pro", sans-serif; -} - -/* Common status styles */ -.status { - font-weight: 600; - color: #27ae60; -} - -/* Common content section */ -.content-section { - padding: 2rem; - max-width: 1200px; - margin: 0 auto; -} - -/* Responsive design */ -@media (max-width: 768px) { - .content-section { - padding: 1rem; - } -} diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 05f9ea3..ae47199 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -1,9 +1,13 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Route, BrowserRouter as Router, Routes } from "react-router-dom"; -import Header from "./components/Header"; -import Home from "./pages/Home"; -import Room from "./pages/Room"; -import "./App.css"; +import { HomePage } from "./features/home/HomePage"; +import { RoomPage } from "./features/room/RoomPage"; +import { ErrorBoundary } from "./shared/components/ErrorBoundary"; +import { Header } from "./shared/components/Header"; +import { logger } from "./shared/lib/logger"; +import { initWebVitals } from "./shared/lib/monitoring"; + +initWebVitals(); const queryClient = new QueryClient({ defaultOptions: { @@ -11,25 +15,43 @@ const queryClient = new QueryClient({ staleTime: 5 * 60 * 1000, // 5 minutes retry: 1, }, + mutations: { + onError: (error) => { + logger.error("Mutation error", { + error: error instanceof Error ? error.message : "Unknown error", + }); + }, + }, }, }); +queryClient.getQueryCache().subscribe((event) => { + if (event.type === "updated" && event.query.state.status === "error") { + const error = event.query.state.error; + logger.error("Query error", { + queryKey: event.query.queryKey, + error: error instanceof Error ? error.message : "Unknown error", + }); + } +}); + function App() { return ( - - -
-
- -
- - } /> - } /> - -
-
-
-
+ + + +
+
+
+ + } /> + } /> + +
+
+
+
+
); } diff --git a/packages/client/src/components/Accordion.css b/packages/client/src/components/Accordion.css deleted file mode 100644 index 043ba91..0000000 --- a/packages/client/src/components/Accordion.css +++ /dev/null @@ -1,240 +0,0 @@ -.accordion { - background-color: #141c2b; - min-height: 100vh; - padding: 2rem; -} - -.accordion-item { - background-color: #212c42; - border-radius: 8px; - margin-bottom: 1rem; - overflow: hidden; - transition: all 0.3s ease; - border-left: 4px solid transparent; -} - -.accordion-item.completed { - border-left-color: #a3ea2a; -} - -.accordion-item.incomplete { - border-left-color: #ff5b67; -} - -.accordion-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1.2rem 2rem; - cursor: pointer; - transition: background-color 0.2s ease; - font-family: "Source Sans Pro", sans-serif; - background: none; - border: none; - width: 100%; - text-align: left; -} - -.task-info { - display: flex; - align-items: center; - gap: 1rem; -} - -.task-number { - font-weight: 600; - font-size: 1.1rem; -} - -.accordion-item.completed .task-number { - color: #a3ea2a; -} - -.accordion-item.incomplete .task-number { - color: #ff5b67; -} - -.check-icon { - color: #a3ea2a; - font-size: 1.2rem; -} - -.task-title { - color: #fff; - font-weight: 500; - font-size: 1.1rem; -} - -.chevron-icon { - color: #ccc; - font-size: 1.2rem; - transition: transform 0.3s ease; -} - -.accordion-content { - padding: 0 2rem; - background-color: #1c2539; -} - -.task-description { - color: #e0e0e0; - line-height: 1.6; - font-family: "Source Sans Pro", sans-serif; - margin-bottom: 2rem; - padding-bottom: 1rem; -} - -.task-description h1, -.task-description h2, -.task-description h3 { - color: #fff; - margin: 1.5rem 0 1rem 0; -} - -.task-description p { - margin: 1rem 0; -} - -.task-description ul { - margin: 1rem 0; - padding-left: 2rem; -} - -.task-description li { - margin: 0.5rem 0; -} - -.task-description img { - max-width: 100%; - height: auto; - border-radius: 4px; - margin: 1rem 0; -} - -.questions-section { - margin-top: 2rem; - padding-bottom: 1rem; -} - -.questions-divider { - height: 1px; - background-color: #444; - margin: 1.5rem 0; -} - -.questions-title { - color: #fff; - font-size: 1.2rem; - font-weight: 600; - margin-bottom: 1.5rem; - font-family: "Source Sans Pro", sans-serif; -} - -.question-item { - margin-bottom: 2rem; -} - -.question-text { - color: #e0e0e0; - font-size: 1.1rem; - margin-bottom: 1rem; - font-family: "Source Sans Pro", sans-serif; -} - -.question-input-container { - display: flex; - gap: 1rem; - align-items: center; -} - -.question-input { - flex: 1; - padding: 0.5rem 1rem; - background-color: #1a1a1a; - border: 1px solid #444; - border-radius: 4px; - color: #fff; - font-family: "Source Sans Pro", sans-serif; - font-size: 1rem; -} - -.question-input:disabled { - background-color: #525a6a; - color: #878fa2; -} - -.question-input:disabled:hover { - cursor: not-allowed; -} - -.question-input:focus { - outline: none; - border-color: #a3ea2a; -} - -.submit-button { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1.5rem; - border-radius: 4px; - font-family: "Source Sans Pro", sans-serif; - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; - border: 2px solid; -} - -.submit-button.outlined { - background-color: transparent; - color: #a3ea2a; - border-color: #a3ea2a; -} - -.submit-button.outlined:hover { - background-color: #a3ea2a; - color: #151c2b; -} - -.submit-button.answered { - background-color: #a3ea2a; - color: #151c2b; - border-color: #a3ea2a; -} - -.submit-button svg { - font-size: 1rem; -} - -/* Responsive design */ -@media (max-width: 768px) { - .accordion { - padding: 1rem; - } - - .accordion-header { - padding: 1rem; - } - - .accordion-content { - padding: 0 1rem 1rem 1rem; - } - - .task-info { - gap: 0.5rem; - } - - .task-number, - .task-title { - font-size: 1rem; - } - - .question-input-container { - flex-direction: column; - align-items: stretch; - } - - .correct-button { - justify-content: center; - } -} diff --git a/packages/client/src/components/Accordion.tsx b/packages/client/src/components/Accordion.tsx deleted file mode 100644 index c54d08c..0000000 --- a/packages/client/src/components/Accordion.tsx +++ /dev/null @@ -1,141 +0,0 @@ -/** biome-ignore-all lint/security/noDangerouslySetInnerHtml: The content from the API is HTML unfortunatly */ -import { faCheck, faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useEffect, useState } from "react"; -import "./Accordion.css"; - -interface Question { - hint: string; - question: string; - order: number; - roomCode: string; - task: number; - answerDescription: string; - progress: { - correct: boolean; - }; -} - -interface Task { - order: number; - title: string; - created: string; - description: string; - questions: Question[]; -} - -interface AccordionProps { - tasks: Task[]; -} - -const Accordion = ({ tasks }: AccordionProps) => { - const [expandedTask, setExpandedTask] = useState(null); - const [answers, setAnswers] = useState<{ [key: string]: string }>({}); - - useEffect(() => { - if (tasks.length > 0 && expandedTask === null) { - setExpandedTask(tasks[0].order); - } - }, [tasks]); - - const toggleTask = (taskOrder: number) => { - setExpandedTask(expandedTask === taskOrder ? null : taskOrder); - }; - - const handleAnswerChange = (questionKey: string, value: string) => { - setAnswers((prev) => ({ - ...prev, - [questionKey]: value, - })); - }; - - const getQuestionKey = (taskOrder: number, questionOrder: number) => { - return `${taskOrder}-${questionOrder}`; - }; - - const isTaskCompleted = (task: Task) => { - return task.questions.some((question) => question.progress.correct); - }; - - return ( -
- {tasks.map((task) => { - const isCompleted = isTaskCompleted(task); - return ( -
- - - {expandedTask === task.order && ( -
-
- - {task.questions.length > 0 && ( -
-
-

Answer the questions below

- {task.questions.map((question) => { - const questionKey = getQuestionKey(task.order, question.order); - const hasAnswered = question.progress.correct; - - return ( -
-
{question.question}
-
- handleAnswerChange(questionKey, e.target.value)} - /> - -
-
- ); - })} -
- )} -
- )} -
- ); - })} -
- ); -}; - -export default Accordion; diff --git a/packages/client/src/components/Header.css b/packages/client/src/components/Header.css deleted file mode 100644 index 8d6a36b..0000000 --- a/packages/client/src/components/Header.css +++ /dev/null @@ -1,66 +0,0 @@ -.header { - background-color: #212c42; - width: 100%; - position: fixed; - top: 0; - left: 0; - z-index: 1000; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.header-content { - display: flex; - align-items: center; - justify-content: flex-start; - padding: 0.8rem 2rem; - height: 90px; - max-width: 1200px; - margin: 0 auto; - gap: 2rem; -} - -.logo-container { - display: flex; - align-items: center; - text-decoration: none; - color: white; -} - -.logo { - height: 70px; - width: auto; -} - -.header-nav { - display: flex; - align-items: center; - gap: 1rem; -} - -.nav-item { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.25rem; - padding: 0.5rem 1rem; - text-decoration: none; - color: #bdc3c7; - border-radius: 4px; - transition: all 0.2s ease; - font-family: "Source Sans Pro", sans-serif; - font-weight: 400; -} - -.nav-item:hover { - background-color: rgba(255, 255, 255, 0.1); - color: white; -} - -.nav-icon { - width: 2rem; - height: 1.5rem; -} - -.nav-text { - font-size: 1.25rem; -} diff --git a/packages/client/src/components/Header.tsx b/packages/client/src/components/Header.tsx deleted file mode 100644 index edc67fd..0000000 --- a/packages/client/src/components/Header.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { faBookOpen } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Link } from "react-router-dom"; -import thmLogo from "/thm-logo-full.svg"; -import "./Header.css"; - -const Header = () => { - return ( -
-
- - Try Hack Me - - - -
-
- ); -}; - -export default Header; diff --git a/packages/client/src/features/home/HomePage.tsx b/packages/client/src/features/home/HomePage.tsx new file mode 100644 index 0000000..05b29ce --- /dev/null +++ b/packages/client/src/features/home/HomePage.tsx @@ -0,0 +1,67 @@ +export function HomePage() { + return ( +
+
+
+

+ Live coding challenge +

+
+

+ Practice your skills with live coding challenges. +

+
+ +
+
+

+ This coding challenge is designed to assess your ability to work with a full-stack + TypeScript application. The application should already running locally on the candidates + machine and they'll spend the session extending and improving existing functionality. +

+ +

+ The application is a TryHackMe-inspired learning platform that displays + interactive rooms with tasks and questions. It's built as a monorepo using: +

+ +
    +
  • + Backend: Node.js + Express + + TypeScript + MongoDB +
  • +
  • + Frontend: React + TypeScript + + Vite + React Query +
  • +
  • + Tooling: pnpm + Turborepo + + Biome +
  • +
+ +

+ Database +

+

+ The project has been setup with a MongoDB docker container instance and + has been pre-seeded with data. +

+ +
+

+ Disclaimer +

+

+ This application is a demonstration project created for educational + and interview purposes only. It is not affiliated with or part of TryHackMe's official + codebase. This dummy application replicates some of TryHackMe's functionality for + learning purposes, and none of the code contained herein will be incorporated into + TryHackMe's actual platform. +

+
+
+
+
+ ); +} diff --git a/packages/client/src/features/room/RoomPage.tsx b/packages/client/src/features/room/RoomPage.tsx new file mode 100644 index 0000000..c9b972b --- /dev/null +++ b/packages/client/src/features/room/RoomPage.tsx @@ -0,0 +1,111 @@ +import { useParams } from "react-router-dom"; +import { SafeHTML } from "../../shared/components/SafeHTML"; +import { Accordion } from "./components/Accordion"; +import { useRoomData } from "./hooks/useRoomData"; + +function LoadingSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+
+ ); +} + +function ErrorState({ message, onRetry }: { message: string; onRetry: () => void }) { + return ( +
+
+
!
+

Failed to load room

+

{message}

+ +
+
+ ); +} + +export function RoomPage() { + const { roomCode } = useParams<{ roomCode: string }>(); + const { data: roomData, isPending, isError, error, refetch } = useRoomData(roomCode || ""); + + if (isPending) { + return ; + } + + if (isError) { + return ( + refetch()} + /> + ); + } + + const totalQuestions = roomData?.tasks.reduce((sum, task) => sum + task.questions.length, 0) ?? 0; + const correctQuestions = + roomData?.tasks.reduce( + (sum, task) => sum + task.questions.filter((q) => q.progress.correct).length, + 0, + ) ?? 0; + const progress = totalQuestions > 0 ? (correctQuestions / totalQuestions) * 100 : 0; + + return ( +
+
+
+ {roomData?.title} +
+ + +
+
+
+ +
+
+
+ {Math.round(progress)}% Complete +
+
+ +
+ +
+
+ ); +} diff --git a/packages/client/src/features/room/components/Accordion.tsx b/packages/client/src/features/room/components/Accordion.tsx new file mode 100644 index 0000000..6814e35 --- /dev/null +++ b/packages/client/src/features/room/components/Accordion.tsx @@ -0,0 +1,104 @@ +import { faCheck, faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import type { Task } from "@live-code-challenge/shared"; +import { useEffect, useState } from "react"; +import { SafeHTML } from "../../../shared/components/SafeHTML"; +import { useQuestionSubmission } from "../hooks/useQuestionSubmission"; +import { QuestionRow } from "./QuestionRow"; + +interface AccordionProps { + tasks: Task[]; + roomCode: string; +} + +export function Accordion({ tasks, roomCode }: AccordionProps) { + const [expandedTask, setExpandedTask] = useState(null); + const submission = useQuestionSubmission(roomCode); + + useEffect(() => { + if (tasks.length > 0 && expandedTask === null) { + setExpandedTask(tasks[0].order); + } + }, [tasks, expandedTask]); + + const toggleTask = (taskOrder: number) => { + setExpandedTask(expandedTask === taskOrder ? null : taskOrder); + }; + + const isTaskCompleted = (task: Task) => { + return task.questions.some((question) => question.progress.correct); + }; + + return ( +
+ {tasks.map((task) => { + const isCompleted = isTaskCompleted(task); + const isExpanded = expandedTask === task.order; + + return ( +
+ + + {isExpanded && ( +
+ + + {task.questions.length > 0 && ( +
+
+

+ Answer the questions below +

+ {task.questions.map((question) => ( + + submission.setAnswer(task.order, question.order, value) + } + onSubmit={() => + submission.submit(task.order, question.order, question.requiresAnswer) + } + /> + ))} +
+ )} +
+ )} +
+ ); + })} +
+ ); +} diff --git a/packages/client/src/features/room/components/QuestionRow.tsx b/packages/client/src/features/room/components/QuestionRow.tsx new file mode 100644 index 0000000..fbffb37 --- /dev/null +++ b/packages/client/src/features/room/components/QuestionRow.tsx @@ -0,0 +1,86 @@ +import { faCheck, faSpinner } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import type { Question } from "@live-code-challenge/shared"; +import { SafeHTML } from "../../../shared/components/SafeHTML"; + +interface QuestionRowProps { + question: Question; + answer: string; + isSubmitting: boolean; + result?: { correct: boolean; hint?: string }; + onAnswerChange: (value: string) => void; + onSubmit: () => void; +} + +export function QuestionRow({ + question, + answer, + isSubmitting, + result, + onAnswerChange, + onSubmit, +}: QuestionRowProps) { + const hasAnswered = question.progress.correct; + const showError = result && !result.correct; + const noAnswerNeeded = !question.requiresAnswer; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !hasAnswered && !isSubmitting) { + onSubmit(); + } + }; + + return ( +
+ +
+ onAnswerChange(e.target.value)} + onKeyDown={handleKeyDown} + /> + +
+ {showError && ( +
+ Incorrect, try again + {result.hint && ( + Hint: {result.hint} + )} +
+ )} +
+ ); +} diff --git a/packages/client/src/features/room/components/__tests__/Accordion.test.tsx b/packages/client/src/features/room/components/__tests__/Accordion.test.tsx new file mode 100644 index 0000000..e339496 --- /dev/null +++ b/packages/client/src/features/room/components/__tests__/Accordion.test.tsx @@ -0,0 +1,111 @@ +import type { Task } from "@live-code-challenge/shared"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it } from "vitest"; +import { Accordion } from "../Accordion"; + +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, +}); + +const renderWithProviders = (ui: React.ReactElement) => { + return render({ui}); +}; + +const mockTasks: Task[] = [ + { + roomCode: "test-room", + order: 1, + title: "Task 1", + created: "2024-01-01", + description: "

Task 1 description

", + questions: [ + { + hint: "", + question: "What is 1+1?", + order: 1, + task: 1, + roomCode: "test-room", + answerDescription: "Enter a number", + progress: { correct: false, attempts: 0 }, + requiresAnswer: true, + }, + ], + }, + { + roomCode: "test-room", + order: 2, + title: "Task 2", + created: "2024-01-01", + description: "

Task 2 description

", + questions: [ + { + hint: "", + question: "What is 2+2?", + order: 1, + task: 2, + roomCode: "test-room", + answerDescription: "Enter a number", + progress: { correct: true, attempts: 1 }, + requiresAnswer: true, + }, + ], + }, +]; + +describe("Accordion", () => { + it("renders all tasks", () => { + renderWithProviders(); + + // Check that both task buttons are rendered + expect(screen.getByRole("button", { name: /task 1/i })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /task 2/i })).toBeInTheDocument(); + }); + + it("expands first task by default", () => { + renderWithProviders(); + + expect(screen.getByText("Task 1 description")).toBeInTheDocument(); + }); + + it("toggles task expansion on click", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + // Task 1 expanded by default + expect(screen.getByText("Task 1 description")).toBeInTheDocument(); + + // Click Task 2 + await user.click(screen.getByRole("button", { name: /task 2/i })); + + // Task 2 now expanded + expect(screen.getByText("Task 2 description")).toBeInTheDocument(); + }); + + it("shows completed state for tasks with correct answers", () => { + renderWithProviders(); + + // Task 2 has correct answer, should show check icon + const task2Button = screen.getByRole("button", { name: /task 2/i }); + expect(task2Button.querySelector("svg")).toBeInTheDocument(); + }); + + it("sanitizes HTML content", () => { + const maliciousTasks: Task[] = [ + { + roomCode: "test-room", + order: 1, + title: "XSS Task", + created: "2024-01-01", + description: '

Safe content

', + questions: [], + }, + ]; + + renderWithProviders(); + + expect(screen.getByText("Safe content")).toBeInTheDocument(); + expect(document.querySelector("script")).not.toBeInTheDocument(); + }); +}); diff --git a/packages/client/src/features/room/hooks/__tests__/useRoomData.test.tsx b/packages/client/src/features/room/hooks/__tests__/useRoomData.test.tsx new file mode 100644 index 0000000..47a093c --- /dev/null +++ b/packages/client/src/features/room/hooks/__tests__/useRoomData.test.tsx @@ -0,0 +1,67 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { renderHook, waitFor } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { describe, expect, it, vi } from "vitest"; +import { useRoomData } from "../useRoomData"; + +const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + }, + }); + return ({ children }: { children: ReactNode }) => ( + {children} + ); +}; + +describe("useRoomData", () => { + it("does not fetch when roomCode is empty", () => { + const { result } = renderHook(() => useRoomData(""), { + wrapper: createWrapper(), + }); + + // When enabled is false, isPending is true but isFetching is false + expect(result.current.isFetching).toBe(false); + expect(result.current.fetchStatus).toBe("idle"); + }); + + it("fetches room data when roomCode is provided", async () => { + const mockRoom = { + data: { + code: "test-room", + title: "Test Room", + describe: "Test description", + tasks: [], + }, + }; + + globalThis.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockRoom), + }); + + const { result } = renderHook(() => useRoomData("test-room"), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data?.code).toBe("test-room"); + expect(result.current.data?.title).toBe("Test Room"); + }); + + it("handles fetch errors", async () => { + globalThis.fetch = vi.fn().mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: "Not Found", + }); + + const { result } = renderHook(() => useRoomData("nonexistent"), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + }); +}); diff --git a/packages/client/src/features/room/hooks/useQuestionSubmission.ts b/packages/client/src/features/room/hooks/useQuestionSubmission.ts new file mode 100644 index 0000000..93f9392 --- /dev/null +++ b/packages/client/src/features/room/hooks/useQuestionSubmission.ts @@ -0,0 +1,54 @@ +import { useState } from "react"; +import { useSubmitAnswer } from "./useSubmitAnswer"; + +interface SubmissionResult { + correct: boolean; + hint?: string; +} + +export function useQuestionSubmission(roomCode: string) { + const [answers, setAnswers] = useState>({}); + const [results, setResults] = useState>({}); + const [submittingKey, setSubmittingKey] = useState(null); + const submitMutation = useSubmitAnswer(roomCode); + + const getKey = (taskOrder: number, questionOrder: number) => `${taskOrder}-${questionOrder}`; + + const getAnswer = (taskOrder: number, questionOrder: number) => + answers[getKey(taskOrder, questionOrder)] || ""; + + const setAnswer = (taskOrder: number, questionOrder: number, value: string) => { + setAnswers((prev) => ({ ...prev, [getKey(taskOrder, questionOrder)]: value })); + }; + + const getResult = (taskOrder: number, questionOrder: number) => + results[getKey(taskOrder, questionOrder)]; + + const isSubmitting = (taskOrder: number, questionOrder: number) => + submittingKey === getKey(taskOrder, questionOrder); + + const submit = async (taskOrder: number, questionOrder: number, requiresAnswer: boolean) => { + const key = getKey(taskOrder, questionOrder); + const answer = answers[key]; + + if (requiresAnswer && !answer?.trim()) return; + + setSubmittingKey(key); + setResults((prev) => ({ ...prev, [key]: undefined as unknown as SubmissionResult })); + + try { + const result = await submitMutation.mutateAsync({ + taskOrder, + questionOrder, + answer: requiresAnswer ? answer : undefined, + }); + setResults((prev) => ({ ...prev, [key]: { correct: result.correct, hint: result.hint } })); + } catch { + setResults((prev) => ({ ...prev, [key]: { correct: false, hint: undefined } })); + } finally { + setSubmittingKey(null); + } + }; + + return { getAnswer, setAnswer, getResult, isSubmitting, submit }; +} diff --git a/packages/client/src/features/room/hooks/useRoomData.ts b/packages/client/src/features/room/hooks/useRoomData.ts new file mode 100644 index 0000000..7f76182 --- /dev/null +++ b/packages/client/src/features/room/hooks/useRoomData.ts @@ -0,0 +1,16 @@ +import type { ApiSuccessResponse, Room } from "@live-code-challenge/shared"; +import { useQuery } from "@tanstack/react-query"; +import { apiClient } from "../../../shared/lib/api-client"; + +const fetchRoomData = async (roomCode: string): Promise => { + const response = await apiClient>(`/rooms/${roomCode}`); + return response.data; +}; + +export const useRoomData = (roomCode: string) => { + return useQuery({ + queryKey: ["room", roomCode], + queryFn: () => fetchRoomData(roomCode), + enabled: !!roomCode, + }); +}; diff --git a/packages/client/src/features/room/hooks/useSubmitAnswer.ts b/packages/client/src/features/room/hooks/useSubmitAnswer.ts new file mode 100644 index 0000000..b101ba0 --- /dev/null +++ b/packages/client/src/features/room/hooks/useSubmitAnswer.ts @@ -0,0 +1,37 @@ +import type { AnswerSubmissionResponse, ApiSuccessResponse } from "@live-code-challenge/shared"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { apiClient } from "../../../shared/lib/api-client"; + +interface SubmitAnswerParams { + taskOrder: number; + questionOrder: number; + answer?: string; +} + +const submitAnswer = async ( + roomCode: string, + params: SubmitAnswerParams, +): Promise => { + const response = await apiClient>( + `/rooms/${roomCode}/questions/${params.questionOrder}`, + { + method: "POST", + body: JSON.stringify({ + taskOrder: params.taskOrder, + ...(params.answer !== undefined && { answer: params.answer }), + }), + }, + ); + return response.data; +}; + +export const useSubmitAnswer = (roomCode: string) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (params: SubmitAnswerParams) => submitAnswer(roomCode, params), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["room", roomCode] }); + }, + }); +}; diff --git a/packages/client/src/hooks/useRoomData.ts b/packages/client/src/hooks/useRoomData.ts deleted file mode 100644 index bafe358..0000000 --- a/packages/client/src/hooks/useRoomData.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -interface Question { - hint: string; - question: string; - order: number; - roomCode: string; - task: number; - answerDescription: string; - progress: { - correct: boolean; - }; -} - -interface Task { - order: number; - title: string; - created: string; - description: string; - questions: Question[]; -} - -interface RoomData { - code: string; - title: string; - describe: string; - imageURL: string; - tasks: Task[]; -} - -const fetchRoomData = async (roomCode: string): Promise => { - const response = await fetch(`http://localhost:3001/rooms/${roomCode}`); - - if (!response.ok) { - throw new Error(`Failed to fetch room data: ${response.statusText}`); - } - - return response.json(); -}; - -export const useRoomData = (roomCode: string) => { - return useQuery({ - queryKey: ["room", roomCode], - queryFn: () => fetchRoomData(roomCode), - enabled: !!roomCode, - }); -}; diff --git a/packages/client/src/index.css b/packages/client/src/index.css index 08a3ac9..71e1d7d 100644 --- a/packages/client/src/index.css +++ b/packages/client/src/index.css @@ -1,68 +1,2 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} +@import url("https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600;700&display=swap"); +@import "tailwindcss"; diff --git a/packages/client/src/main.tsx b/packages/client/src/main.tsx index eff7ccc..79dcdfe 100644 --- a/packages/client/src/main.tsx +++ b/packages/client/src/main.tsx @@ -3,7 +3,8 @@ import { createRoot } from "react-dom/client"; import "./index.css"; import App from "./App.tsx"; -createRoot(document.getElementById("root")!).render( +// @ts-expect-error root is always present in dom +createRoot(document.getElementById("root")).render( , diff --git a/packages/client/src/pages/Home.css b/packages/client/src/pages/Home.css deleted file mode 100644 index d6bc3ea..0000000 --- a/packages/client/src/pages/Home.css +++ /dev/null @@ -1,178 +0,0 @@ -.home-page { - margin-top: 90px; /* Account for fixed header */ - min-height: calc(100vh - 70px); - position: relative; -} - -.main-title-section { - background: #141c2b; - padding: 4rem 2rem; - text-align: center; - position: relative; - overflow: hidden; -} - -.main-title-section::before { - content: ""; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-image: - radial-gradient(circle at 20% 50%, rgba(52, 73, 94, 0.3) 0%, transparent 50%), - radial-gradient(circle at 80% 50%, rgba(52, 73, 94, 0.3) 0%, transparent 50%); - pointer-events: none; -} - -.main-title { - font-family: "Source Sans Pro", sans-serif; - font-size: 3.5rem; - font-weight: 700; - color: white; - margin: 0; - position: relative; - z-index: 1; -} - -.title-underline { - width: 120px; - height: 4px; - background-color: #a3ea2a; - margin: 1rem auto; - position: relative; - z-index: 1; -} - -.main-subtitle { - font-family: "Source Sans Pro", sans-serif; - font-size: 1.2rem; - font-weight: 400; - color: #bdc3c7; - margin: 1rem 0 0 0; - position: relative; - z-index: 1; -} - -.challenge-description { - max-width: 800px; - margin: 0 auto; - padding: 2rem; - background: rgba(255, 255, 255, 0.05); - border-radius: 12px; - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); -} - -.challenge-description p { - font-family: "Source Sans Pro", sans-serif; - font-size: 1.1rem; - line-height: 1.7; - color: #e8e8e8; - margin: 0 0 1.5rem 0; -} - -.challenge-description h3 { - font-family: "Source Sans Pro", sans-serif; - font-size: 1.4rem; - font-weight: 600; - color: #fff; - margin: 2rem 0 1rem 0; -} - -.tech-stack { - margin: 1.5rem 0; - padding-left: 0; - list-style: none; -} - -.tech-stack li { - font-family: "Source Sans Pro", sans-serif; - font-size: 1.1rem; - line-height: 1.6; - color: #e8e8e8; - margin: 0.8rem 0; - padding-left: 1.5rem; - position: relative; -} - -.tech-stack li::before { - content: "β–Έ"; - color: #a3ea2a; - font-weight: bold; - position: absolute; - left: 0; -} - -.tech-stack strong { - color: #a3ea2a; - font-weight: 600; -} - -.disclaimer { - margin-top: 2rem; - padding: 1.5rem; - background: rgba(255, 193, 7, 0.1); - border: 1px solid rgba(255, 193, 7, 0.3); - border-radius: 8px; - border-left: 4px solid #ffc107; -} - -.disclaimer h3 { - font-family: "Source Sans Pro", sans-serif; - font-size: 1.2rem; - font-weight: 600; - color: #ffc107; - margin: 0 0 1rem 0; -} - -.disclaimer p { - font-family: "Source Sans Pro", sans-serif; - font-size: 1rem; - line-height: 1.6; - color: #f8f9fa; - margin: 0; -} - -/* Responsive design */ -@media (max-width: 768px) { - .main-title { - font-size: 2.5rem; - } - - .main-subtitle { - font-size: 1rem; - } - - .main-title-section { - padding: 3rem 1rem; - } - - .challenge-description { - padding: 1.5rem; - } - - .challenge-description p { - font-size: 1rem; - } - - .challenge-description h3 { - font-size: 1.2rem; - } - - .tech-stack li { - font-size: 1rem; - } - - .disclaimer { - padding: 1rem; - } - - .disclaimer h3 { - font-size: 1.1rem; - } - - .disclaimer p { - font-size: 0.9rem; - } -} diff --git a/packages/client/src/pages/Home.tsx b/packages/client/src/pages/Home.tsx deleted file mode 100644 index c06329e..0000000 --- a/packages/client/src/pages/Home.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import "./Home.css"; - -const Home = () => { - return ( -
-
-

Live coding challenge

-
-

Practice your skills with live coding challenges.

-
- -
-
-

- This coding challenge is designed to assess your ability to work with a full-stack - TypeScript application. The application should already running locally on the candidates - machine and they'll spend the session extending and improving existing functionality. -

- -

- The application is a TryHackMe-inspired learning platform that displays - interactive rooms with tasks and questions. It's built as a monorepo using: -

- -
    -
  • - Backend: Node.js + Express + TypeScript + MongoDB -
  • -
  • - Frontend: React + TypeScript + Vite + React Query -
  • -
  • - Tooling: pnpm + Turborepo + Biome -
  • -
- -

Database

-

- The project has been setup with a MongoDB docker container instance and - has been pre-seeded with data. -

- -
-

Disclaimer

-

- This application is a demonstration project created for educational - and interview purposes only. It is not affiliated with or part of TryHackMe's official - codebase. This dummy application replicates some of TryHackMe's functionality for - learning purposes, and none of the code contained herein will be incorporated into - TryHackMe's actual platform. -

-
-
-
-
- ); -}; - -export default Home; diff --git a/packages/client/src/pages/Room.css b/packages/client/src/pages/Room.css deleted file mode 100644 index c39ce69..0000000 --- a/packages/client/src/pages/Room.css +++ /dev/null @@ -1,105 +0,0 @@ -.room-page { - margin-top: 70px; /* Account for fixed header */ - min-height: calc(100vh - 70px); -} -.room-content { - max-width: 1200px; - margin: 0 auto; -} - -.page-header { - background: #141c2b; - padding: 3rem 2rem; -} - -.room-header-content { - display: flex; - align-items: flex-start; - gap: 2rem; - margin-bottom: 2rem; -} - -.room-image { - width: 120px; - height: 120px; - border-radius: 12px; - object-fit: cover; - flex-shrink: 0; -} - -.room-header-text { - flex: 1; - text-align: left; -} - -.room-header-text h1 { - font-family: "Source Sans Pro", sans-serif; - font-size: 2.5rem; - font-weight: 700; - color: white; - margin: 0 0 1rem 0; -} - -.room-description { - font-family: "Source Sans Pro", sans-serif; - font-size: 1.2rem; - color: #bdc3c7; - margin: 0; - line-height: 1.6; -} - -.progress-bar-container { - width: 100%; - height: 15px; - background-color: rgba(255, 255, 255, 0.2); - overflow: hidden; - position: relative; -} - -.progress-bar { - height: 100%; - background: linear-gradient(90deg, #a3ea2a 0%, #27ae60 100%); - transition: width 0.3s ease; -} - -.progress-text { - font-family: "Source Sans Pro", sans-serif; - font-size: 0.8rem; - color: #bdc3c7; - text-align: center; - font-weight: 600; - position: absolute; - top: -3px; - width: 100vw; -} - -/* Responsive design */ -@media (max-width: 768px) { - .room-header-content { - flex-direction: column; - align-items: center; - text-align: center; - gap: 1rem; - } - - .room-image { - width: 80px; - height: 80px; - } - - .room-header-text h1 { - font-size: 2rem; - } - - .room-description { - font-size: 1rem; - } - - .page-header { - padding: 2rem 1rem; - } - - .room-header-text { - text-align: center; - } -} diff --git a/packages/client/src/pages/Room.tsx b/packages/client/src/pages/Room.tsx deleted file mode 100644 index 2815f25..0000000 --- a/packages/client/src/pages/Room.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useParams } from "react-router-dom"; -import Accordion from "../components/Accordion"; -import { useRoomData } from "../hooks/useRoomData"; -import "./Room.css"; - -const Room = () => { - const { roomCode } = useParams<{ roomCode: string }>(); - const { data: roomData } = useRoomData(roomCode || ""); - - const progress = 10; - - return ( -
-
-
- {roomData?.title} -
-

{roomData?.title}

-

{roomData?.describe}

-
-
-
-
-
-
-
{Math.round(progress)}% Complete
-
-
-
- -
- -
-
- ); -}; - -export default Room; diff --git a/packages/client/src/shared/components/ErrorBoundary.tsx b/packages/client/src/shared/components/ErrorBoundary.tsx new file mode 100644 index 0000000..6da08a7 --- /dev/null +++ b/packages/client/src/shared/components/ErrorBoundary.tsx @@ -0,0 +1,64 @@ +import { Component, type ErrorInfo, type ReactNode } from "react"; +import { logger } from "../lib/logger"; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + logger.error("React error boundary caught error", { + error: error.message, + stack: error.stack, + componentStack: errorInfo.componentStack, + }); + } + + handleRetry = () => { + this.setState({ hasError: false, error: undefined }); + }; + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( +
+
+
!
+

Something went wrong

+

+ {this.state.error?.message || "An unexpected error occurred"} +

+ +
+
+ ); + } + + return this.props.children; + } +} diff --git a/packages/client/src/shared/components/Header.tsx b/packages/client/src/shared/components/Header.tsx new file mode 100644 index 0000000..32a759c --- /dev/null +++ b/packages/client/src/shared/components/Header.tsx @@ -0,0 +1,26 @@ +import { faBookOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Link } from "react-router-dom"; +import thmLogo from "/thm-logo-full.svg"; + +export function Header() { + return ( +
+
+ + Try Hack Me + + + +
+
+ ); +} diff --git a/packages/client/src/shared/components/SafeHTML.tsx b/packages/client/src/shared/components/SafeHTML.tsx new file mode 100644 index 0000000..80606fe --- /dev/null +++ b/packages/client/src/shared/components/SafeHTML.tsx @@ -0,0 +1,20 @@ +import DOMPurify from "dompurify"; + +interface SafeHTMLProps { + html: string; + className?: string; + allowedTags?: string[]; +} + +export function SafeHTML({ html, className, allowedTags }: SafeHTMLProps) { + const config = allowedTags ? { ALLOWED_TAGS: allowedTags } : undefined; + const sanitizedHtml = DOMPurify.sanitize(html, config); + + return ( +
+ ); +} diff --git a/packages/client/src/shared/components/__tests__/Header.test.tsx b/packages/client/src/shared/components/__tests__/Header.test.tsx new file mode 100644 index 0000000..bf3182a --- /dev/null +++ b/packages/client/src/shared/components/__tests__/Header.test.tsx @@ -0,0 +1,28 @@ +import { render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { describe, expect, it } from "vitest"; +import { Header } from "../Header"; + +describe("Header", () => { + it("renders logo link to home", () => { + render( + +
+ , + ); + + const logoLink = screen.getByRole("link", { name: /try hack me/i }); + expect(logoLink).toHaveAttribute("href", "/"); + }); + + it("renders Room navigation link", () => { + render( + +
+ , + ); + + const roomLink = screen.getByRole("link", { name: /room/i }); + expect(roomLink).toHaveAttribute("href", "/room/linux-fundamentals-part1"); + }); +}); diff --git a/packages/client/src/shared/lib/api-client.ts b/packages/client/src/shared/lib/api-client.ts new file mode 100644 index 0000000..f932f5f --- /dev/null +++ b/packages/client/src/shared/lib/api-client.ts @@ -0,0 +1,54 @@ +import { config } from "./config"; +import { logger } from "./logger"; + +export class ApiError extends Error { + status: number; + statusText: string; + + constructor(message: string, status: number, statusText: string) { + super(message); + this.name = "ApiError"; + this.status = status; + this.statusText = statusText; + } +} + +export async function apiClient(endpoint: string, options?: RequestInit): Promise { + const url = `${config.apiUrl}${endpoint}`; + + try { + const response = await fetch(url, { + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers, + }, + }); + + if (!response.ok) { + const error = new ApiError( + `API request failed: ${response.statusText}`, + response.status, + response.statusText, + ); + logger.error("API request failed", { + url, + status: response.status, + statusText: response.statusText, + }); + throw error; + } + + const data = await response.json(); + return data; + } catch (error) { + if (error instanceof ApiError) { + throw error; + } + logger.error("API request error", { + url, + error: error instanceof Error ? error.message : "Unknown error", + }); + throw error; + } +} diff --git a/packages/client/src/shared/lib/config.ts b/packages/client/src/shared/lib/config.ts new file mode 100644 index 0000000..b1e3465 --- /dev/null +++ b/packages/client/src/shared/lib/config.ts @@ -0,0 +1,5 @@ +export const config = { + apiUrl: import.meta.env.VITE_API_URL || "http://localhost:3001/api/v1", + isDev: import.meta.env.DEV, + isProd: import.meta.env.PROD, +} as const; diff --git a/packages/client/src/shared/lib/logger.ts b/packages/client/src/shared/lib/logger.ts new file mode 100644 index 0000000..4923dd9 --- /dev/null +++ b/packages/client/src/shared/lib/logger.ts @@ -0,0 +1,43 @@ +type LogLevel = "debug" | "info" | "warn" | "error"; + +const levels: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, +}; + +const currentLevel: LogLevel = import.meta.env.DEV ? "debug" : "info"; + +const shouldLog = (level: LogLevel): boolean => { + return levels[level] >= levels[currentLevel]; +}; + +const formatMessage = (level: LogLevel, message: string, context?: Record) => { + const timestamp = new Date().toISOString(); + const contextStr = context ? ` ${JSON.stringify(context)}` : ""; + return `[${timestamp}] [${level.toUpperCase()}] ${message}${contextStr}`; +}; + +export const logger = { + debug: (message: string, context?: Record) => { + if (shouldLog("debug")) { + console.debug(formatMessage("debug", message, context)); + } + }, + info: (message: string, context?: Record) => { + if (shouldLog("info")) { + console.info(formatMessage("info", message, context)); + } + }, + warn: (message: string, context?: Record) => { + if (shouldLog("warn")) { + console.warn(formatMessage("warn", message, context)); + } + }, + error: (message: string, context?: Record) => { + if (shouldLog("error")) { + console.error(formatMessage("error", message, context)); + } + }, +}; diff --git a/packages/client/src/shared/lib/monitoring.ts b/packages/client/src/shared/lib/monitoring.ts new file mode 100644 index 0000000..d3ddcb3 --- /dev/null +++ b/packages/client/src/shared/lib/monitoring.ts @@ -0,0 +1,24 @@ +import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals"; +import { logger } from "./logger"; + +export function initWebVitals() { + onCLS((metric) => { + logger.info("Web Vital: CLS", { value: metric.value, rating: metric.rating }); + }); + + onFCP((metric) => { + logger.info("Web Vital: FCP", { value: metric.value, rating: metric.rating }); + }); + + onINP((metric) => { + logger.info("Web Vital: INP", { value: metric.value, rating: metric.rating }); + }); + + onLCP((metric) => { + logger.info("Web Vital: LCP", { value: metric.value, rating: metric.rating }); + }); + + onTTFB((metric) => { + logger.info("Web Vital: TTFB", { value: metric.value, rating: metric.rating }); + }); +} diff --git a/packages/client/src/test/setup.ts b/packages/client/src/test/setup.ts new file mode 100644 index 0000000..f149f27 --- /dev/null +++ b/packages/client/src/test/setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/packages/client/tsconfig.node.json b/packages/client/tsconfig.node.json index 8a67f62..dff6435 100644 --- a/packages/client/tsconfig.node.json +++ b/packages/client/tsconfig.node.json @@ -4,7 +4,7 @@ "target": "ES2023", "lib": ["ES2023"], "module": "ESNext", - "types": ["node"], + "types": ["node", "vitest/globals"], "skipLibCheck": true, /* Bundler mode */ @@ -22,5 +22,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["vite.config.ts"] + "include": ["vite.config.ts", "vitest.config.ts"] } diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts index 73a4b65..3910137 100644 --- a/packages/client/vite.config.ts +++ b/packages/client/vite.config.ts @@ -1,10 +1,12 @@ -import { defineConfig } from "vite"; +import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), tailwindcss()], server: { port: 3000, + host: true, }, }); diff --git a/packages/client/vitest.config.ts b/packages/client/vitest.config.ts new file mode 100644 index 0000000..2056ee3 --- /dev/null +++ b/packages/client/vitest.config.ts @@ -0,0 +1,11 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: "jsdom", + setupFiles: "./src/test/setup.ts", + }, +}); diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..90a1420 --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,21 @@ +{ + "name": "@live-code-challenge/shared", + "version": "0.0.1", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist" + }, + "devDependencies": { + "typescript": "^5.2.2" + } +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..2f88e30 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1 @@ +export * from "./types/index.js"; diff --git a/packages/shared/src/types/api.ts b/packages/shared/src/types/api.ts new file mode 100644 index 0000000..5b902c7 --- /dev/null +++ b/packages/shared/src/types/api.ts @@ -0,0 +1,15 @@ +export interface ApiSuccessResponse { + success: true; + data: T; + meta?: Record; +} + +export interface ApiErrorResponse { + success: false; + error: { + message: string; + stack?: string; + }; +} + +export type ApiResponse = ApiSuccessResponse | ApiErrorResponse; diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts new file mode 100644 index 0000000..da10d9e --- /dev/null +++ b/packages/shared/src/types/index.ts @@ -0,0 +1,2 @@ +export * from "./api.js"; +export * from "./room.js"; diff --git a/packages/shared/src/types/room.ts b/packages/shared/src/types/room.ts new file mode 100644 index 0000000..9e8f0f0 --- /dev/null +++ b/packages/shared/src/types/room.ts @@ -0,0 +1,38 @@ +export interface QuestionProgress { + correct: boolean; + attempts: number; +} + +export interface Question { + hint: string; + question: string; + order: number; + task: number; + roomCode: string; + progress: QuestionProgress; + answerDescription: string; + requiresAnswer: boolean; +} + +export interface Task { + roomCode: string; + order: number; + title: string; + created: string; + description: string; + questions: Question[]; +} + +export interface Room { + code: string; + title: string; + describe: string; + imageURL: string; + tasks: Task[]; +} + +export interface AnswerSubmissionResponse { + correct: boolean; + attempts: number; + hint?: string; +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..2b3d968 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b96f7a..01996f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: packages/api: dependencies: + '@live-code-challenge/shared': + specifier: workspace:* + version: link:../shared cors: specifier: ^2.8.5 version: 2.8.5 @@ -30,24 +33,45 @@ importers: specifier: ^16.4.5 version: 16.6.1 express: - specifier: ^4.18.2 - version: 4.21.2 + specifier: ^5.2.1 + version: 5.2.1 + express-rate-limit: + specifier: ^8.2.1 + version: 8.2.1(express@5.2.1) + helmet: + specifier: ^8.1.0 + version: 8.1.0 mongodb: specifier: ^6.20.0 version: 6.20.0 + winston: + specifier: ^3.17.0 + version: 3.19.0 + zod: + specifier: ^3.24.0 + version: 3.25.76 devDependencies: '@biomejs/biome': specifier: ^2.2.5 version: 2.2.5 + '@jest/globals': + specifier: ^29.7.0 + version: 29.7.0 '@types/cors': specifier: ^2.8.14 version: 2.8.19 '@types/express': - specifier: ^4.17.17 - version: 4.17.23 + specifier: ^5.0.0 + version: 5.0.6 + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 '@types/node': specifier: ^20.8.0 version: 20.19.19 + '@types/supertest': + specifier: ^6.0.2 + version: 6.0.3 '@typescript-eslint/eslint-plugin': specifier: ^6.7.0 version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1)(typescript@5.9.3) @@ -57,12 +81,21 @@ importers: eslint: specifier: ^8.50.0 version: 8.57.1 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.19.19) mongodb-memory-server: specifier: ^10.2.2 version: 10.2.2 nodemon: specifier: ^3.0.2 version: 3.1.10 + supertest: + specifier: ^7.0.0 + version: 7.2.2 + ts-jest: + specifier: ^29.2.5 + version: 29.4.6(@babel/core@7.28.4)(jest@29.7.0)(typescript@5.9.3) tsx: specifier: ^3.14.0 version: 3.14.0 @@ -81,9 +114,15 @@ importers: '@fortawesome/react-fontawesome': specifier: ^0.2.0 version: 0.2.6(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.2.0) + '@live-code-challenge/shared': + specifier: workspace:* + version: link:../shared '@tanstack/react-query': specifier: ^5.90.2 version: 5.90.2(react@19.2.0) + dompurify: + specifier: ^3.3.1 + version: 3.3.1 react: specifier: ^19.1.1 version: 19.2.0 @@ -93,10 +132,28 @@ importers: react-router-dom: specifier: ^6.20.1 version: 6.30.1(react-dom@19.2.0)(react@19.2.0) + web-vitals: + specifier: ^5.1.0 + version: 5.1.0 devDependencies: '@eslint/js': specifier: ^9.36.0 version: 9.37.0 + '@tailwindcss/vite': + specifier: ^4.1.18 + version: 4.1.18(vite@7.1.11) + '@testing-library/dom': + specifier: ^10.4.1 + version: 10.4.1 + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.0)(@types/react@19.2.0)(react-dom@19.2.0)(react@19.2.0) + '@testing-library/user-event': + specifier: ^14.6.1 + version: 14.6.1(@testing-library/dom@10.4.1) '@types/node': specifier: ^24.6.0 version: 24.7.0 @@ -108,7 +165,7 @@ importers: version: 19.2.0(@types/react@19.2.0) '@vitejs/plugin-react': specifier: ^5.0.4 - version: 5.0.4(vite@7.1.9) + version: 5.0.4(vite@7.1.11) eslint: specifier: ^9.36.0 version: 9.37.0 @@ -121,6 +178,12 @@ importers: globals: specifier: ^16.4.0 version: 16.4.0 + jsdom: + specifier: ^27.4.0 + version: 27.4.0 + tailwindcss: + specifier: ^4.1.18 + version: 4.1.18 typescript: specifier: ~5.9.3 version: 5.9.3 @@ -128,11 +191,52 @@ importers: specifier: ^8.45.0 version: 8.45.0(eslint@9.37.0)(typescript@5.9.3) vite: - specifier: ^7.1.7 - version: 7.1.9(@types/node@24.7.0) + specifier: 7.1.11 + version: 7.1.11(@types/node@24.7.0) + vitest: + specifier: ^4.0.18 + version: 4.0.18(@types/node@24.7.0)(jsdom@27.4.0) + + packages/shared: + devDependencies: + typescript: + specifier: ^5.2.2 + version: 5.9.3 packages: + /@acemir/cssom@0.9.31: + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + dev: true + + /@adobe/css-tools@4.4.4: + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + dev: true + + /@asamuzakjp/css-color@4.1.1: + resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==} + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5)(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5)(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.5 + dev: true + + /@asamuzakjp/dom-selector@6.7.6: + resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==} + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.5 + dev: true + + /@asamuzakjp/nwsapi@2.3.9: + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + dev: true + /@babel/code-frame@7.27.1: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -226,6 +330,11 @@ packages: engines: {node: '>=6.9.0'} dev: true + /@babel/helper-plugin-utils@7.28.6: + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helper-string-parser@7.27.1: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -257,6 +366,165 @@ packages: '@babel/types': 7.28.4 dev: true + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.4): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.4): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.4): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.4): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.28.4): + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.28.6 + dev: true + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.4): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.28.4): + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.28.6 + dev: true + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.4): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.4): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.4): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.4): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.4): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.4): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.4): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.4): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.28.4): + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.28.6 + dev: true + /@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4): resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -277,6 +545,11 @@ packages: '@babel/helper-plugin-utils': 7.27.1 dev: true + /@babel/runtime@7.28.6: + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/template@7.27.2: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -309,6 +582,10 @@ packages: '@babel/helper-validator-identifier': 7.27.1 dev: true + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + /@biomejs/biome@2.2.5: resolution: {integrity: sha512-zcIi+163Rc3HtyHbEO7CjeHq8DjQRs40HsGbW6vx2WI0tg8mYQOPouhvHSyEnCBAorfYNnKdR64/IxO7xQ5faw==} engines: {node: '>=14.21.3'} @@ -396,6 +673,66 @@ packages: dev: true optional: true + /@colors/colors@1.6.0: + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + dev: false + + /@csstools/color-helpers@5.1.0: + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + dev: true + + /@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5)(@csstools/css-tokenizer@3.0.4): + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + dev: true + + /@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5)(@csstools/css-tokenizer@3.0.4): + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5)(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + dev: true + + /@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4): + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + dependencies: + '@csstools/css-tokenizer': 3.0.4 + dev: true + + /@csstools/css-syntax-patches-for-csstree@1.0.26: + resolution: {integrity: sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==} + dev: true + + /@csstools/css-tokenizer@3.0.4: + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + dev: true + + /@dabh/diagnostics@2.0.8: + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + dev: false + /@esbuild/aix-ppc64@0.25.10: resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} engines: {node: '>=18'} @@ -935,6 +1272,16 @@ packages: levn: 0.4.1 dev: true + /@exodus/bytes@1.10.0: + resolution: {integrity: sha512-tf8YdcbirXdPnJ+Nd4UN1EXnz+IP2DI45YVEr3vvzcVTOyrApkmIB4zvOQVd3XPr7RXnfBtAx+PXImXOIU0Ajg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + dev: true + /@fortawesome/fontawesome-common-types@6.7.2: resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==} engines: {node: '>=6'} @@ -1005,66 +1352,307 @@ packages: engines: {node: '>=18.18'} dev: true - /@jridgewell/gen-mapping@0.3.13: - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + /@istanbuljs/load-nyc-config@1.1.0: + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.2 + resolve-from: 5.0.0 dev: true - /@jridgewell/remapping@2.3.5: - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + + /@jest/console@29.7.0: + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + '@jest/types': 29.6.3 + '@types/node': 24.7.0 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 dev: true - /@jridgewell/resolve-uri@3.1.2: - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} + /@jest/core@29.7.0: + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 24.7.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@24.7.0) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node dev: true - /@jridgewell/sourcemap-codec@1.5.5: - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 24.7.0 + jest-mock: 29.7.0 dev: true - /@jridgewell/trace-mapping@0.3.31: - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + jest-get-type: 29.6.3 dev: true - /@mongodb-js/saslprep@1.3.1: - resolution: {integrity: sha512-6nZrq5kfAz0POWyhljnbWQQJQ5uT8oE2ddX303q1uY0tWsivWKgBDXBBvuFPwOqRRalXJuVO9EjOdVtuhLX0zg==} + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - sparse-bitfield: 3.0.3 + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 24.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 dev: true - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color dev: true - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} + /@jest/reporters@29.7.0: + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 24.7.0 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.2.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color dev: true - /@remix-run/router@1.23.0: - resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} - engines: {node: '>=14.0.0'} - dev: false + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + dev: true + + /@jest/test-result@29.7.0: + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + dev: true + + /@jest/test-sequencer@29.7.0: + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + dev: true + + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.28.4 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 24.7.0 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + dev: true + + /@jridgewell/gen-mapping@0.3.13: + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + dev: true + + /@jridgewell/remapping@2.3.5: + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + dev: true + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.5.5: + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + dev: true + + /@jridgewell/trace-mapping@0.3.31: + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + dev: true + + /@mongodb-js/saslprep@1.3.1: + resolution: {integrity: sha512-6nZrq5kfAz0POWyhljnbWQQJQ5uT8oE2ddX303q1uY0tWsivWKgBDXBBvuFPwOqRRalXJuVO9EjOdVtuhLX0zg==} + dependencies: + sparse-bitfield: 3.0.3 + + /@noble/hashes@1.8.0: + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + dev: true + + /@paralleldrive/cuid2@2.3.1: + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + dependencies: + '@noble/hashes': 1.8.0 + dev: true + + /@remix-run/router@1.23.0: + resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} + engines: {node: '>=14.0.0'} + dev: false /@rolldown/pluginutils@1.0.0-beta.38: resolution: {integrity: sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==} @@ -1246,6 +1834,188 @@ packages: dev: true optional: true + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + + /@sinonjs/commons@3.0.1: + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.1 + dev: true + + /@so-ric/colorspace@1.1.6: + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + dev: false + + /@standard-schema/spec@1.1.0: + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + dev: true + + /@tailwindcss/node@4.1.18: + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.4 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + dev: true + + /@tailwindcss/oxide-android-arm64@4.1.18: + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@tailwindcss/oxide-darwin-arm64@4.1.18: + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@tailwindcss/oxide-darwin-x64@4.1.18: + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@tailwindcss/oxide-freebsd-x64@4.1.18: + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18: + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tailwindcss/oxide-linux-arm64-gnu@4.1.18: + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tailwindcss/oxide-linux-arm64-musl@4.1.18: + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tailwindcss/oxide-linux-x64-gnu@4.1.18: + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tailwindcss/oxide-linux-x64-musl@4.1.18: + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tailwindcss/oxide-wasm32-wasi@4.1.18: + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + requiresBuild: true + dev: true + optional: true + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + /@tailwindcss/oxide-win32-arm64-msvc@4.1.18: + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@tailwindcss/oxide-win32-x64-msvc@4.1.18: + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@tailwindcss/oxide@4.1.18: + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + dev: true + + /@tailwindcss/vite@4.1.18(vite@7.1.11): + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + dependencies: + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + tailwindcss: 4.1.18 + vite: 7.1.11(@types/node@24.7.0) + dev: true + /@tanstack/query-core@5.90.2: resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} dev: false @@ -1259,6 +2029,68 @@ packages: react: 19.2.0 dev: false + /@testing-library/dom@10.4.1: + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.28.6 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + dev: true + + /@testing-library/jest-dom@6.9.1: + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + dev: true + + /@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.0)(@types/react@19.2.0)(react-dom@19.2.0)(react@19.2.0): + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.28.6 + '@testing-library/dom': 10.4.1 + '@types/react': 19.2.0 + '@types/react-dom': 19.2.0(@types/react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + dev: true + + /@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1): + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + dependencies: + '@testing-library/dom': 10.4.1 + dev: true + + /@types/aria-query@5.0.4: + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + dev: true + /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: @@ -1292,13 +2124,24 @@ packages: resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} dependencies: '@types/connect': 3.4.38 - '@types/node': 20.19.19 + '@types/node': 24.7.0 + dev: true + + /@types/chai@5.2.3: + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 dev: true /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@types/node': 20.19.19 + '@types/node': 24.7.0 + dev: true + + /@types/cookiejar@2.1.5: + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} dev: true /@types/cors@2.8.19: @@ -1307,38 +2150,70 @@ packages: '@types/node': 20.19.19 dev: true + /@types/deep-eql@4.0.2: + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + dev: true + /@types/estree@1.0.8: resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} dev: true - /@types/express-serve-static-core@4.19.6: - resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + /@types/express-serve-static-core@5.1.1: + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} dependencies: - '@types/node': 20.19.19 + '@types/node': 24.7.0 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.0 dev: true - /@types/express@4.17.23: - resolution: {integrity: sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==} + /@types/express@5.0.6: + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} dependencies: '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 4.19.6 - '@types/qs': 6.14.0 - '@types/serve-static': 1.15.9 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + dev: true + + /@types/graceful-fs@4.1.9: + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + dependencies: + '@types/node': 24.7.0 dev: true /@types/http-errors@2.0.5: resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} dev: true + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + dev: true + + /@types/istanbul-lib-report@3.0.3: + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + dev: true + + /@types/istanbul-reports@3.0.4: + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + dependencies: + '@types/istanbul-lib-report': 3.0.3 + dev: true + + /@types/jest@29.5.14: + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + dev: true + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true - /@types/mime@1.3.5: - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + /@types/methods@1.1.4: + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} dev: true /@types/node@20.19.19: @@ -1379,27 +2254,49 @@ packages: resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} dev: true - /@types/send@0.17.5: - resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} - dependencies: - '@types/mime': 1.3.5 - '@types/node': 20.19.19 - dev: true - /@types/send@1.2.0: resolution: {integrity: sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==} dependencies: - '@types/node': 20.19.19 + '@types/node': 24.7.0 dev: true - /@types/serve-static@1.15.9: - resolution: {integrity: sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==} + /@types/serve-static@2.2.0: + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} dependencies: '@types/http-errors': 2.0.5 - '@types/node': 20.19.19 - '@types/send': 0.17.5 + '@types/node': 24.7.0 dev: true + /@types/stack-utils@2.0.3: + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + dev: true + + /@types/superagent@8.1.9: + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 24.7.0 + form-data: 4.0.5 + dev: true + + /@types/supertest@6.0.3: + resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.9 + dev: true + + /@types/triple-beam@1.3.5: + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + dev: false + + /@types/trusted-types@2.0.7: + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + requiresBuild: true + dev: false + optional: true + /@types/webidl-conversions@7.0.3: resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} @@ -1408,6 +2305,16 @@ packages: dependencies: '@types/webidl-conversions': 7.0.3 + /@types/yargs-parser@21.0.3: + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + dev: true + + /@types/yargs@17.0.35: + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + dependencies: + '@types/yargs-parser': 21.0.3 + dev: true + /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1)(typescript@5.9.3): resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1686,7 +2593,7 @@ packages: resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} dev: true - /@vitejs/plugin-react@5.0.4(vite@7.1.9): + /@vitejs/plugin-react@5.0.4(vite@7.1.11): resolution: {integrity: sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: @@ -1698,17 +2605,77 @@ packages: '@rolldown/pluginutils': 1.0.0-beta.38 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.1.9(@types/node@24.7.0) + vite: 7.1.11(@types/node@24.7.0) transitivePeerDependencies: - supports-color dev: true - /accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + /@vitest/expect@4.0.18: + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + chai: 6.2.2 + tinyrainbow: 3.0.3 + dev: true + + /@vitest/mocker@4.0.18(vite@7.1.11): + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + dependencies: + '@vitest/spy': 4.0.18 + estree-walker: 3.0.3 + magic-string: 0.30.21 + vite: 7.1.11(@types/node@24.7.0) + dev: true + + /@vitest/pretty-format@4.0.18: + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + dependencies: + tinyrainbow: 3.0.3 + dev: true + + /@vitest/runner@4.0.18: + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + dependencies: + '@vitest/utils': 4.0.18 + pathe: 2.0.3 + dev: true + + /@vitest/snapshot@4.0.18: + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + dependencies: + '@vitest/pretty-format': 4.0.18 + magic-string: 0.30.21 + pathe: 2.0.3 + dev: true + + /@vitest/spy@4.0.18: + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + dev: true + + /@vitest/utils@4.0.18: + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + dependencies: + '@vitest/pretty-format': 4.0.18 + tinyrainbow: 3.0.3 + dev: true + + /accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 + mime-types: 3.0.2 + negotiator: 1.0.0 dev: false /acorn-jsx@5.3.2(acorn@8.15.0): @@ -1739,6 +2706,13 @@ packages: uri-js: 4.4.1 dev: true + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + dev: true + /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1751,6 +2725,11 @@ packages: color-convert: 2.0.1 dev: true + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + /anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -1759,25 +2738,50 @@ packages: picomatch: 2.3.1 dev: true + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true + /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true - /array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - dev: false + /aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + dependencies: + dequal: 2.0.3 + dev: true /array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} dev: true + /asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + dev: true + + /assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + dev: true + /async-mutex@0.5.0: resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} dependencies: tslib: 2.8.1 dev: true + /async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + dev: false + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: true + /b4a@1.7.3: resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} peerDependencies: @@ -1787,6 +2791,81 @@ packages: optional: true dev: true + /babel-jest@29.7.0(@babel/core@7.28.4): + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.28.4 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.28.4) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + dev: true + + /babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.4): + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.4) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.4) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.4) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.28.4) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.4) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.4) + dev: true + + /babel-preset-jest@29.6.3(@babel/core@7.28.4): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.28.4 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) + dev: true + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true @@ -1800,27 +2879,30 @@ packages: hasBin: true dev: true + /bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + dependencies: + require-from-string: 2.0.2 + dev: true + /binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} dev: true - /body-parser@1.20.3: - resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + /body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 + debug: 4.4.3(supports-color@5.5.0) http-errors: 2.0.0 - iconv-lite: 0.4.24 + iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.13.0 - raw-body: 2.5.2 - type-is: 1.6.18 - unpipe: 1.0.0 + qs: 6.14.1 + raw-body: 3.0.2 + type-is: 2.0.1 transitivePeerDependencies: - supports-color dev: false @@ -1857,6 +2939,19 @@ packages: update-browserslist-db: 1.1.3(browserslist@4.26.3) dev: true + /bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + dependencies: + fast-json-stable-stringify: 2.1.0 + dev: true + + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + dependencies: + node-int64: 0.4.0 + dev: true + /bson@6.10.4: resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} engines: {node: '>=16.20.1'} @@ -1880,7 +2975,6 @@ packages: dependencies: es-errors: 1.3.0 function-bind: 1.1.2 - dev: false /call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} @@ -1888,13 +2982,17 @@ packages: dependencies: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - dev: false /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} dev: true + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: true + /camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} @@ -1904,6 +3002,11 @@ packages: resolution: {integrity: sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==} dev: true + /chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + dev: true + /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1912,6 +3015,11 @@ packages: supports-color: 7.2.0 dev: true + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + dev: true + /chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -1927,6 +3035,33 @@ packages: fsevents: 2.3.3 dev: true + /ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + dev: true + + /cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + dev: true + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: true + + /collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + dev: true + /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1934,23 +3069,59 @@ packages: color-name: 1.1.4 dev: true + /color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + dependencies: + color-name: 2.1.0 + dev: false + /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} + dev: false + + /color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + dependencies: + color-name: 2.1.0 + dev: false + + /color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + dev: false + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: true + /commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} dev: true + /component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + dev: true + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true - /content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - dependencies: - safe-buffer: 5.2.1 + /content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} dev: false /content-type@1.0.5: @@ -1962,15 +3133,19 @@ packages: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true - /cookie-signature@1.0.6: - resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - dev: false + /cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} /cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} dev: false + /cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + dev: true + /cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -1979,6 +3154,25 @@ packages: vary: 1.1.2 dev: false + /create-jest@29.7.0(@types/node@20.19.19): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.19.19) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1988,20 +3182,39 @@ packages: which: 2.0.2 dev: true + /css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + dev: true + + /css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + dev: true + + /cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + engines: {node: '>=20'} + dependencies: + '@asamuzakjp/css-color': 4.1.1 + '@csstools/css-syntax-patches-for-csstree': 1.0.26 + css-tree: 3.1.0 + lru-cache: 11.2.5 + dev: true + /csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} dev: true - /debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + /data-urls@6.0.1: + resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==} + engines: {node: '>=20'} dependencies: - ms: 2.0.0 - dev: false + whatwg-mimetype: 5.0.0 + whatwg-url: 15.1.0 + dev: true /debug@4.4.3(supports-color@5.5.0): resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -2014,21 +3227,65 @@ packages: dependencies: ms: 2.1.3 supports-color: 5.5.0 + + /decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + dev: true + + /dedent@1.7.1: + resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true dev: true /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dev: true + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: true + /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} dev: false - /destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dev: false + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: true + + /detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dev: true + + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + dev: true + + /dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + dev: true + + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} @@ -2044,6 +3301,20 @@ packages: esutils: 2.0.3 dev: true + /dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dev: true + + /dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dev: true + + /dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + optionalDependencies: + '@types/trusted-types': 2.0.7 + dev: false + /dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -2056,7 +3327,6 @@ packages: call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 - dev: false /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -2066,9 +3336,17 @@ packages: resolution: {integrity: sha512-A6A6Fd3+gMdaed9wX83CvHYJb4UuapPD5X5SLq72VZJzxHSY0/LUweGXRWmQlh2ln7KV7iw7jnwXK7dlPoOnHQ==} dev: true - /encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} + /emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} dev: false /encodeurl@2.0.0: @@ -2076,22 +3354,52 @@ packages: engines: {node: '>= 0.8'} dev: false + /enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + dev: true + + /entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + dev: true + + /error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + dependencies: + is-arrayish: 0.2.1 + dev: true + /es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} - dev: false /es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - dev: false + + /es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + dev: true /es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} dependencies: es-errors: 1.3.0 - dev: false + + /es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + dev: true /esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} @@ -2166,6 +3474,11 @@ packages: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} dev: false + /escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + dev: true + /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -2329,7 +3642,13 @@ packages: eslint-visitor-keys: 3.4.3 dev: true - /esquery@1.6.0: + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} dependencies: @@ -2348,6 +3667,12 @@ packages: engines: {node: '>=4.0'} dev: true + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.8 + dev: true + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2364,40 +3689,83 @@ packages: bare-events: 2.7.0 dev: true - /express@4.21.2: - resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} - engines: {node: '>= 0.10.0'} + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + dev: true + + /expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + dev: true + + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + dev: true + + /express-rate-limit@8.2.1(express@5.2.1): + resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + dependencies: + express: 5.2.1 + ip-address: 10.0.1 + dev: false + + /express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.3 - content-disposition: 0.5.4 + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 content-type: 1.0.5 cookie: 0.7.1 - cookie-signature: 1.0.6 - debug: 2.6.9 + cookie-signature: 1.2.2 + debug: 4.4.3(supports-color@5.5.0) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.3.1 - fresh: 0.5.2 + finalhandler: 2.1.1 + fresh: 2.0.0 http-errors: 2.0.0 - merge-descriptors: 1.0.3 - methods: 1.1.2 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 on-finished: 2.4.1 + once: 1.4.0 parseurl: 1.3.3 - path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.13.0 + qs: 6.14.1 range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.0 - serve-static: 1.16.2 - setprototypeof: 1.2.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 + type-is: 2.0.1 vary: 1.1.2 transitivePeerDependencies: - supports-color @@ -2430,12 +3798,22 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true + /fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + dev: true + /fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} dependencies: reusify: 1.1.0 dev: true + /fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + dependencies: + bser: 2.1.1 + dev: true + /fdir@6.5.0(picomatch@4.0.3): resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -2448,6 +3826,10 @@ packages: picomatch: 4.0.3 dev: true + /fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + dev: false + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -2469,17 +3851,16 @@ packages: to-regex-range: 5.0.1 dev: true - /finalhandler@1.3.1: - resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} - engines: {node: '>= 0.8'} + /finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} dependencies: - debug: 2.6.9 + debug: 4.4.3(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 statuses: 2.0.1 - unpipe: 1.0.0 transitivePeerDependencies: - supports-color dev: false @@ -2530,6 +3911,10 @@ packages: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} dev: true + /fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + dev: false + /follow-redirects@1.15.11(debug@4.4.3): resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -2542,14 +3927,34 @@ packages: debug: 4.4.3(supports-color@5.5.0) dev: true + /form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + dev: true + + /formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + dependencies: + '@paralleldrive/cuid2': 2.3.1 + dezalgo: 1.0.4 + once: 1.4.0 + dev: true + /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} dev: false - /fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} + /fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} dev: false /fs.realpath@1.0.0: @@ -2566,13 +3971,17 @@ packages: /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - dev: false /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} dev: true + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + /get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2587,7 +3996,11 @@ packages: has-symbols: 1.1.0 hasown: 2.0.2 math-intrinsics: 1.1.0 - dev: false + + /get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + dev: true /get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} @@ -2595,7 +4008,11 @@ packages: dependencies: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - dev: false + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true /get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} @@ -2661,16 +4078,31 @@ packages: /gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - dev: false + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true + /handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + dev: true + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -2680,15 +4112,38 @@ packages: /has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} - dev: false + + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.1.0 + dev: true /hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} dependencies: function-bind: 1.1.2 + + /helmet@8.1.0: + resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==} + engines: {node: '>=18.0.0'} dev: false + /html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + dependencies: + '@exodus/bytes': 1.10.0 + transitivePeerDependencies: + - '@noble/hashes' + dev: true + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + /http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -2700,6 +4155,27 @@ packages: toidentifier: 1.0.1 dev: false + /http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + dev: false + + /http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: true + /https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -2710,8 +4186,13 @@ packages: - supports-color dev: true - /iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: true + + /iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} dependencies: safer-buffer: 2.1.2 @@ -2739,11 +4220,25 @@ packages: resolve-from: 4.0.0 dev: true + /import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + dev: true + /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} dev: true + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -2755,11 +4250,20 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + /ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + dev: false + /ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} dev: false + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -2767,11 +4271,28 @@ packages: binary-extensions: 2.3.0 dev: true + /is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + dependencies: + hasown: 2.0.2 + dev: true + /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} dev: true + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + dev: true + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2784,18 +4305,552 @@ packages: engines: {node: '>=0.12.0'} dev: true - /is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: true + + /is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + dev: false + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + dependencies: + '@babel/core': 7.28.4 + '@babel/parser': 7.28.4 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.28.4 + '@babel/parser': 7.28.4 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.4.3(supports-color@5.5.0) + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + + /jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + dev: true + + /jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 24.7.0 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.1 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + + /jest-cli@29.7.0(@types/node@20.19.19): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.19.19) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.19.19) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /jest-config@29.7.0(@types/node@20.19.19): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.28.4 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.19 + babel-jest: 29.7.0(@babel/core@7.28.4) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + + /jest-config@29.7.0(@types/node@24.7.0): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.28.4 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 24.7.0 + babel-jest: 29.7.0(@babel/core@7.28.4) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 + dev: true + + /jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + dev: true + + /jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 24.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 24.7.0 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: true + + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 24.7.0 + jest-util: 29.7.0 + dev: true + + /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.7.0 + dev: true + + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.11 + resolve.exports: 2.0.3 + slash: 3.0.0 + dev: true + + /jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 24.7.0 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 24.7.0 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.3 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.28.4 + '@babel/generator': 7.28.3 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.28.4) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.28.4) + '@babel/types': 7.28.4 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 24.7.0 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: true + + /jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + dev: true + + /jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 24.7.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + dev: true + + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 24.7.0 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /jest@29.7.0(@types/node@20.19.19): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.19.19) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node dev: true - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true dev: true /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + /js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: true + /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -2803,6 +4858,42 @@ packages: argparse: 2.0.1 dev: true + /jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.7.6 + '@exodus/bytes': 1.10.0 + cssstyle: 5.3.7 + data-urls: 6.0.1 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - supports-color + - utf-8-validate + dev: true + /jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -2813,6 +4904,10 @@ packages: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} dev: true + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true @@ -2833,6 +4928,20 @@ packages: json-buffer: 3.0.1 dev: true + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + dev: true + + /kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + dev: false + + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + dev: true + /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -2841,6 +4950,128 @@ packages: type-check: 0.4.0 dev: true + /lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -2855,10 +5086,26 @@ packages: p-locate: 5.0.0 dev: true + /lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + dev: true + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + dev: false + /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2866,12 +5113,28 @@ packages: js-tokens: 4.0.0 dev: false + /lru-cache@11.2.5: + resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} + engines: {node: 20 || >=22} + dev: true + /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: yallist: 3.1.1 dev: true + /lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + dev: true + + /magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + dev: true + /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -2879,23 +5142,48 @@ packages: semver: 6.3.1 dev: true + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.7.2 + dev: true + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 + dev: true + /math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - dev: false - /media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} + /mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + dev: true + + /media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} dev: false /memory-pager@1.5.0: resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} - /merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + /merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} dev: false + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2904,7 +5192,7 @@ packages: /methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} - dev: false + dev: true /micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} @@ -2917,6 +5205,11 @@ packages: /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + dev: true + + /mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} dev: false /mime-types@2.1.35: @@ -2924,13 +5217,30 @@ packages: engines: {node: '>= 0.6'} dependencies: mime-db: 1.52.0 + dev: true + + /mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + dependencies: + mime-db: 1.54.0 dev: false - /mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} + /mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} hasBin: true - dev: false + dev: true + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + + /min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2952,6 +5262,10 @@ packages: brace-expansion: 2.0.2 dev: true + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + /mongodb-connection-string-url@3.0.2: resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} dependencies: @@ -3036,10 +5350,6 @@ packages: bson: 6.10.4 mongodb-connection-string-url: 3.0.2 - /ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - dev: false - /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3053,11 +5363,15 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true - /negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + /negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} dev: false + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: true + /new-find-package-json@2.0.0: resolution: {integrity: sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==} engines: {node: '>=12.22.0'} @@ -3067,6 +5381,10 @@ packages: - supports-color dev: true + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + dev: true + /node-releases@2.0.23: resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} dev: true @@ -3093,6 +5411,13 @@ packages: engines: {node: '>=0.10.0'} dev: true + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + dev: true + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3101,7 +5426,10 @@ packages: /object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - dev: false + + /obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + dev: true /on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -3114,6 +5442,18 @@ packages: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 + + /one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + dependencies: + fn.name: 1.1.0 + dev: false + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 dev: true /optionator@0.9.4: @@ -3168,6 +5508,22 @@ packages: callsites: 3.1.0 dev: true + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + + /parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + dependencies: + entities: 6.0.1 + dev: true + /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -3188,8 +5544,12 @@ packages: engines: {node: '>=8'} dev: true - /path-to-regexp@0.1.12: - resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} dev: false /path-type@4.0.0: @@ -3197,6 +5557,10 @@ packages: engines: {node: '>=8'} dev: true + /pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + dev: true + /pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} dev: true @@ -3215,6 +5579,11 @@ packages: engines: {node: '>=12'} dev: true + /pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + dev: true + /pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -3236,6 +5605,32 @@ packages: engines: {node: '>= 0.8.0'} dev: true + /pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + dev: true + + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + dev: true + + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + dev: true + /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} dependencies: @@ -3260,12 +5655,15 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - /qs@6.13.0: - resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + /pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + dev: true + + /qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} dependencies: side-channel: 1.1.0 - dev: false /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -3276,13 +5674,13 @@ packages: engines: {node: '>= 0.6'} dev: false - /raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} - engines: {node: '>= 0.8'} + /raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} dependencies: bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 + http-errors: 2.0.1 + iconv-lite: 0.7.2 unpipe: 1.0.0 dev: false @@ -3293,12 +5691,19 @@ packages: dependencies: react: 19.2.0 scheduler: 0.27.0 - dev: false /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: false + /react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: true + + /react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + dev: true + /react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -3330,6 +5735,14 @@ packages: /react@19.2.0: resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 dev: false /readdirp@3.6.0: @@ -3339,15 +5752,60 @@ packages: picomatch: 2.3.1 dev: true + /redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: true + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} dev: true + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + /resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} dev: true + /resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + dev: true + + /resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + /reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3393,6 +5851,19 @@ packages: fsevents: 2.3.3 dev: true + /router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + dependencies: + debug: 4.4.3(supports-color@5.5.0) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + dev: false + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -3403,13 +5874,24 @@ packages: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} dev: false + /safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + dev: false + /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: false + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: true + /scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - dev: false /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} @@ -3422,35 +5904,39 @@ packages: hasBin: true dev: true - /send@0.19.0: - resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} - engines: {node: '>= 0.8.0'} + /semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + dev: true + + /send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 + debug: 4.4.3(supports-color@5.5.0) + encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color dev: false - /serve-static@1.16.2: - resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} - engines: {node: '>= 0.8.0'} + /serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.19.0 + send: 1.2.1 transitivePeerDependencies: - supports-color dev: false @@ -3477,7 +5963,6 @@ packages: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 - dev: false /side-channel-map@1.0.1: resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} @@ -3487,7 +5972,6 @@ packages: es-errors: 1.3.0 get-intrinsic: 1.3.0 object-inspect: 1.13.4 - dev: false /side-channel-weakmap@1.0.2: resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} @@ -3498,7 +5982,6 @@ packages: get-intrinsic: 1.3.0 object-inspect: 1.13.4 side-channel-map: 1.0.1 - dev: false /side-channel@1.1.0: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} @@ -3509,7 +5992,14 @@ packages: side-channel-list: 1.0.0 side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - dev: false + + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true /simple-update-notifier@2.0.0: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} @@ -3518,6 +6008,10 @@ packages: semver: 7.7.2 dev: true + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: true + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -3528,6 +6022,13 @@ packages: engines: {node: '>=0.10.0'} dev: true + /source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} dependencies: @@ -3545,11 +6046,39 @@ packages: dependencies: memory-pager: 1.5.0 + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + + /stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + dev: false + + /stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + dev: true + + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} dev: false + /statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + dev: false + + /std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + dev: true + /streamx@2.23.0: resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} dependencies: @@ -3560,6 +6089,29 @@ packages: - react-native-b4a dev: true + /string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3567,17 +6119,61 @@ packages: ansi-regex: 5.0.1 dev: true + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + dev: true + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: true + + /strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 + dev: true + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} dev: true + /superagent@10.3.0: + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} + engines: {node: '>=14.18.0'} + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3(supports-color@5.5.0) + fast-safe-stringify: 2.1.1 + form-data: 4.0.5 + formidable: 3.5.4 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.14.1 + transitivePeerDependencies: + - supports-color + dev: true + + /supertest@7.2.2: + resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} + engines: {node: '>=14.18.0'} + dependencies: + cookie-signature: 1.2.2 + methods: 1.1.2 + superagent: 10.3.0 + transitivePeerDependencies: + - supports-color + dev: true + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -3586,6 +6182,31 @@ packages: has-flag: 4.0.0 dev: true + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: true + + /tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + dev: true + + /tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + dev: true + /tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} dependencies: @@ -3596,6 +6217,15 @@ packages: - react-native-b4a dev: true + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + /text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} dependencies: @@ -3604,10 +6234,23 @@ packages: - react-native-b4a dev: true + /text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + dev: false + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + dev: true + + /tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + dev: true + /tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -3616,6 +6259,26 @@ packages: picomatch: 4.0.3 dev: true + /tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + dev: true + + /tldts-core@7.0.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} + dev: true + + /tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} + hasBin: true + dependencies: + tldts-core: 7.0.19 + dev: true + + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + dev: true + /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3633,12 +6296,31 @@ packages: hasBin: true dev: true + /tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + dependencies: + tldts: 7.0.19 + dev: true + /tr46@5.1.1: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} dependencies: punycode: 2.3.1 + /tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + dependencies: + punycode: 2.3.1 + dev: true + + /triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + dev: false + /ts-api-utils@1.4.3(typescript@5.9.3): resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -3657,6 +6339,47 @@ packages: typescript: 5.9.3 dev: true + /ts-jest@29.4.6(@babel/core@7.28.4)(jest@29.7.0)(typescript@5.9.3): + resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + dependencies: + '@babel/core': 7.28.4 + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@20.19.19) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.3 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + dev: true + /tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} dev: true @@ -3739,17 +6462,33 @@ packages: prelude-ls: 1.2.1 dev: true + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} dev: true - /type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + dev: true + + /type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + dev: true + + /type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 dev: false /typescript-eslint@8.45.0(eslint@9.37.0)(typescript@5.9.3): @@ -3775,6 +6514,14 @@ packages: hasBin: true dev: true + /uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + requiresBuild: true + dev: true + optional: true + /undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} dev: true @@ -3809,18 +6556,26 @@ packages: punycode: 2.3.1 dev: true - /utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: false + /v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + dev: true + /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} dev: false - /vite@7.1.9(@types/node@24.7.0): - resolution: {integrity: sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==} + /vite@7.1.11(@types/node@24.7.0): + resolution: {integrity: sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3870,10 +6625,112 @@ packages: fsevents: 2.3.3 dev: true + /vitest@4.0.18(@types/node@24.7.0)(jsdom@27.4.0): + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.18 + '@vitest/browser-preview': 4.0.18 + '@vitest/browser-webdriverio': 4.0.18 + '@vitest/ui': 4.0.18 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 24.7.0 + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(vite@7.1.11) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + jsdom: 27.4.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.1.11(@types/node@24.7.0) + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + dev: true + + /w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + dependencies: + xml-name-validator: 5.0.0 + dev: true + + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + dependencies: + makeerror: 1.0.12 + dev: true + + /web-vitals@5.1.0: + resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==} + dev: false + /webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + /webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + dev: true + + /whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + dev: true + + /whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + dev: true + /whatwg-url@14.2.0: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} @@ -3881,6 +6738,14 @@ packages: tr46: 5.1.1 webidl-conversions: 7.0.0 + /whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.1 + dev: true + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3889,19 +6754,119 @@ packages: isexe: 2.0.0 dev: true + /why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + + /winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + dev: false + + /winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + dev: false + /word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} dev: true + /wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + /write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + dev: true + + /ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + dev: true + + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} dev: true /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} dev: true + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: true + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true + /yauzl@3.2.0: resolution: {integrity: sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==} engines: {node: '>=12'} @@ -3914,3 +6879,7 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + /zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + dev: false diff --git a/turbo.json b/turbo.json index c78dad4..5305ae0 100644 --- a/turbo.json +++ b/turbo.json @@ -10,6 +10,10 @@ "cache": false, "persistent": true }, + "test": { + "cache": false, + "persistent": false + }, "lint": { "dependsOn": ["^lint"] },