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 (
-
-
toggleTask(task.order)}
- >
-
- Task {task.order}
- {isCompleted && }
- {task.title}
-
-
-
-
- {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)}
- />
-
- {hasAnswered ? (
- <>
-
- Correct Answer
- >
- ) : (
- "Submit"
- )}
-
-
-
- );
- })}
-
- )}
-
- )}
-
- );
- })}
-
- );
-};
-
-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 (
-
-
-
-
-
-
-
-
-
- Room
-
-
-
-
- );
-};
-
-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}
+
+ Try again
+
+
+
+ );
+}
+
+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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {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 (
+
+
toggleTask(task.order)}
+ >
+
+
+ Task {task.order}
+
+ {isCompleted && (
+
+ )}
+ {task.title}
+
+
+
+
+ {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}
+ />
+
+ {hasAnswered ? (
+ <>
+
+ Correct Answer
+ >
+ ) : isSubmitting ? (
+ <>
+
+ Checking...
+ >
+ ) : noAnswerNeeded ? (
+ "Continue"
+ ) : (
+ "Submit"
+ )}
+
+
+ {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?.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"}
+
+
+ Try again
+
+
+
+ );
+ }
+
+ 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 (
+
+
+
+
+
+
+
+
+
+ Room
+
+
+
+
+ );
+}
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"]
},