diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0bce5d4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +.env +.venv +venv/ +data/*.db +logs/*.log +.git +.gitignore +.DS_Store +*.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7410f33 --- /dev/null +++ b/.env.example @@ -0,0 +1,95 @@ +# Container Spawner Konfiguration +# Kopiere diese Datei nach .env und passe die Werte an +# cp .env.example .env + +# ============================================================ +# PFLICHT - Diese Werte MUESSEN angepasst werden +# ============================================================ + +# Flask Session Secret - Generiere mit: +# python3 -c "import secrets; print(secrets.token_hex(32))" +SECRET_KEY=HIER_DEINEN_GEHEIMEN_SCHLUESSEL_EINTRAGEN + +# Deine Domain (z.B. example.com, wieland.org) +BASE_DOMAIN=example.com + +# Subdomain fuer den Spawner (z.B. coder -> coder.example.com) +SPAWNER_SUBDOMAIN=coder + +# Name des Traefik-Netzwerks (muss existieren oder wird erstellt) +TRAEFIK_NETWORK=web + +# ============================================================ +# TRAEFIK - Anpassen an deine Traefik-Konfiguration +# ============================================================ + +# Name des Traefik Certificate Resolvers (aus deiner traefik.yml) +# Typische Namen: lets-encrypt, letsencrypt, hetzner, cloudflare, default +TRAEFIK_CERTRESOLVER=lets-encrypt + +# Traefik Entrypoint fuer HTTPS (Standard: websecure) +TRAEFIK_ENTRYPOINT=websecure + +# ============================================================ +# DOCKER - Normalerweise keine Aenderung noetig +# ============================================================ + +# Docker Socket Pfad (Standard fuer Linux) +# Fuer Windows: npipe:////./pipe/docker_engine +# Fuer TCP: tcp://localhost:2375 +DOCKER_HOST=unix:///var/run/docker.sock + +# Docker-Image fuer User-Container +# Verfuegbare Templates: +# - user-service-template:latest (nginx, einfache Willkommensseite) +# - user-template-next:latest (Next.js, moderne React-App) +USER_TEMPLATE_IMAGE=user-service-template:latest + +# ============================================================ +# RESSOURCEN - Container-Limits +# ============================================================ + +# RAM-Limit pro User-Container +DEFAULT_MEMORY_LIMIT=512m + +# CPU-Quota (50000 = 0.5 CPU, 100000 = 1 CPU) +DEFAULT_CPU_QUOTA=50000 + +# ============================================================ +# JWT - Authentifizierung +# ============================================================ + +# JWT Secret (verwendet SECRET_KEY wenn nicht gesetzt) +# JWT_SECRET_KEY= + +# JWT Token Gueltigkeitsdauer in Sekunden (Standard: 1 Stunde) +JWT_ACCESS_TOKEN_EXPIRES=3600 + +# ============================================================ +# CORS - Cross-Origin Resource Sharing +# ============================================================ + +# CORS erlaubte Origins (kommasepariert) +# WICHTIG: Wird automatisch aus SPAWNER_SUBDOMAIN und BASE_DOMAIN generiert +# Nur setzen wenn du zusaetzliche Origins brauchst +# CORS_ORIGINS=https://coder.example.com,http://localhost:3000 + +# ============================================================ +# OPTIONAL - Logging und Debugging +# ============================================================ + +# Spawner-Port (intern, nur fuer direkten Zugriff) +SPAWNER_PORT=5000 + +# Log-Level (DEBUG, INFO, WARNING, ERROR) +LOG_LEVEL=INFO + +# Container-Timeout in Sekunden (fuer Auto-Shutdown, noch nicht implementiert) +CONTAINER_IDLE_TIMEOUT=3600 + +# ============================================================ +# PRODUKTION - Erweiterte Einstellungen +# ============================================================ + +# PostgreSQL statt SQLite (empfohlen fuer Produktion) +# DATABASE_URL=postgresql://spawner:password@postgres:5432/spawner diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0aed684 --- /dev/null +++ b/.gitignore @@ -0,0 +1,174 @@ +# ============================================================ +# Container Spawner - .gitignore +# ============================================================ + +# ============================================================ +# Umgebung und Secrets +# ============================================================ +.env +.env.local +.env.*.local +!.env.example + +# ============================================================ +# Python +# ============================================================ +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual Environments +venv/ +.venv/ +ENV/ +env/ + +# PyInstaller +*.manifest +*.spec + +# Pytest +.pytest_cache/ +.coverage +htmlcov/ + +# mypy +.mypy_cache/ + +# ============================================================ +# Node.js / Next.js +# ============================================================ +node_modules/ +.next/ +out/ +.pnp +.pnp.js +.yarn/install-state.gz + +# Build Output +build/ +dist/ + +# Package Manager Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.npm/ + +# Lock Files (optional - auskommentieren wenn gewuenscht) +# package-lock.json +# yarn.lock +# pnpm-lock.yaml + +# TypeScript +*.tsbuildinfo + +# Vercel +.vercel/ + +# ============================================================ +# Generierte Daten (Server) +# ============================================================ +data/ +logs/ +*.db +*.sqlite +*.sqlite3 + +# Backups +*.bak +*.backup +backup-*.db + +# ============================================================ +# IDE / Editoren +# ============================================================ +.idea/ +.vscode/ +*.swp +*.swo +*~ +.project +.classpath +.settings/ +*.sublime-project +*.sublime-workspace + +# ============================================================ +# Betriebssystem +# ============================================================ +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +desktop.ini + +# ============================================================ +# Logs und temporaere Dateien +# ============================================================ +*.log +*.tmp +*.temp +*.cache + +# ============================================================ +# Sicherheit +# ============================================================ +*.pem +*.key +*.crt +*.p12 +*.pfx +secrets/ +credentials/ + +# ============================================================ +# Docker (nur generierte Dateien) +# ============================================================ +# Keine Docker-Dateien ignorieren - diese werden benoetigt +# Aber lokale Override-Dateien ignorieren +docker-compose.override.yml +docker-compose.local.yml + +# ============================================================ +# Test und Coverage +# ============================================================ +coverage/ +.nyc_output/ +test-results/ +junit.xml + +# ============================================================ +# Projekt-spezifisch +# ============================================================ +# CLAUDE.md enthaelt projektspezifische Instruktionen fuer Claude Code +# Kann bei Bedarf auskommentiert werden um es zu behalten +CLAUDE.md + +# ============================================================ +# Misc +# ============================================================ +*.pid +*.seed +*.pid.lock diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3bd5532 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM python:3.11-slim + +WORKDIR /app + +# System-Dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# Python-Dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Application-Code +COPY . . + +# Daten-Verzeichnisse +RUN mkdir -p /app/data /app/logs && \ + chmod 755 /app/data /app/logs + +EXPOSE 5000 + +# Health-Check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:5000/health || exit 1 + +CMD ["python", "app.py"] diff --git a/README.md b/README.md index 750cda4..462b4a0 100644 --- a/README.md +++ b/README.md @@ -1,2599 +1,83 @@ ---- -tags: SPAWNER unter Docker ---- +# Container Spawner -# DAS SPAWNER-PROJEKT V0.1 -*27.01.2026 rwd* -![](https://hedgedoc.wieland.org/uploads/7aaace93-f7d9-4fe9-9553-94b2fa2e7031.png) +Ein Flask-basierter Service zur automatischen Bereitstellung von isolierten Docker-Containern pro Benutzer mit Traefik-Integration. Benutzer registrieren sich, erhalten einen eigenen Container und eine personalisierte Subdomain. - ---- - -## Inhaltsverzeichnis - -1. [Projektübersicht](#projektübersicht) -2. [Architektur](#architektur) -3. [Voraussetzungen](#voraussetzungen) -4. [Installation](#installation) -5. [Konfiguration](#konfiguration) -6. [Dateistruktur](#dateistruktur) -7. [Komponenten im Detail](#komponenten-im-detail) -8. [Workflow](#workflow) -9. [Traefik-Integration](#traefik-integration) -10. [Sicherheit](#sicherheit) -11. [Deployment](#deployment) -12. [Troubleshooting](#troubleshooting) -13. [Erweiterungen](#erweiterungen) -14. [Best Practices](#best-practices) - ---- - -## Projektübersicht - -Der **Docker Container Spawner** ist eine leichtgewichtige Lösung zum automatischen Bereitstellen von isolierten Docker-Containern für einzelne Benutzer. Nach erfolgreicher Authentifizierung erhält jeder Benutzer einen eigenen Container mit einem dedizierten Webdienst, der über eine personalisierte Subdomain erreichbar ist. - -Das System basiert auf einer Flask-Architektur, die nach einer erfolgreichen Anmeldung automatisch dedizierte Container aus vordefinierten Vorlagen erstellt. Die Anbindung erfolgt über den **Reverse-Proxy Traefik**, der den Datenverkehr dynamisch über personalisierte Subdomains an die jeweiligen Dienste weiterleitet. Zu den Sicherheitsmerkmalen gehören strikte Ressourcenlimits für RAM und CPU sowie eine verschlüsselte Nutzerverwaltung via SQLite. Die Dokumentation beschreibt zudem umfassende Wartungsfunktionen wie das Lifecycle-Management von Containern und Best Practices für den produktiven Einsatz. - -Anwendungsgebiete finden sich vor allem in der Bereitstellung von -- Lernumgebungen -- Sandboxes -- SaaS-Plattformen - ---- - - -### Hauptfunktionen +## Features - **User-Management**: Registrierung und Login mit sicherer Passwort-Speicherung -- **Automatisches Container-Spawning**: Jeder User erhält einen eigenen Docker-Container -- **Dynamisches Routing**: Traefik routet automatisch zu den User-Containern +- **Automatisches Container-Spawning**: Jeder User erhaelt einen eigenen Docker-Container +- **Dynamisches Routing**: Traefik routet automatisch zu den User-Containern via Subdomain - **Resource-Management**: CPU- und RAM-Limits pro Container - **Lifecycle-Management**: Starten, Stoppen und Neustarten von User-Containern - **Template-basiert**: Neue User-Container aus vorgefertigten Images -### Use Cases +## Schnellstart -- **Entwicklungsumgebungen**: Isolierte Dev-Spaces für Entwickler -- **SaaS-Anwendungen**: Multi-Tenant-Webservices -- **Lernplattformen**: Übungsumgebungen für Schulungen -- **CI/CD-Pipelines**: On-Demand Build-Umgebungen -- **Sandbox-Umgebungen**: Sichere Test-Environments - ---- - -## Architektur - -### Komponenten-Übersicht - -```flow -st=>start: Browser -e=>end: End -op=>operation: Traefik -:80 / :443 -op2=>operation: Spawner Service -Flask + Docker SDK -:5000 -op3=>operation: Docker Daemon -op4=>operation: User Containers -USER-I | USER-II | USER-III | USER-nnn| - -st->op->op2->op3->op4 +```bash +# Installation mit einem Befehl +curl -sSL https://gitea.iotxs.de/RainerWieland/spawner/raw/branch/main/install.sh | bash ``` -### Datenfluss - -1. **Login**: User meldet sich über Web-UI an -2. **Authentication**: Flask validiert Credentials gegen SQLite-DB -3. **Container-Spawn**: Docker SDK startet neuen Container aus Template -4. **Label-Injection**: Traefik-Labels werden beim Container-Start gesetzt -5. **Auto-Discovery**: Traefik erkennt neuen Container und erstellt Route -6. **Redirect**: User wird zu persönlicher Subdomain weitergeleitet - -### Netzwerk-Architektur - -Alle Services laufen im gleichen Docker-Netzwerk (\`traefik-network\`), damit Traefik die User-Container erreichen kann: +Nach der Installation `.env` anpassen und erneut ausfuehren: +```bash +cp .env.example .env +nano .env # Werte anpassen +bash install.sh ``` -traefik-network (bridge) -├── traefik (Reverse Proxy) -├── spawner (Management Service) -├── user-alice-1 (User Container) -├── user-bob-2 (User Container) -└── user-charlie-3 (User Container) -``` - ---- ## Voraussetzungen -### Hardware +- Docker 20.10+ +- Docker Compose 2.0+ +- Traefik 2.x oder 3.x (laufend) +- Bestehendes Docker-Netzwerk fuer Traefik -- **Min. 2 GB RAM**: Für Spawner + mehrere User-Container -- **Min. 20 GB Disk**: Für Images und Container-Volumes -- **Multi-Core CPU**: Empfohlen für parallele Container +## Dokumentation -### Software +| Dokument | Beschreibung | +|----------|--------------| +| [Installation](docs/install/README.md) | Installationsanleitung und Updates | +| [Architektur](docs/architecture/README.md) | Technische Architektur und Komponenten | +| [Sicherheit](docs/security/README.md) | Sicherheitsrisiken und Massnahmen | +| [Versionen](docs/versions/README.md) | Changelog und Versionierung | +| [Bekannte Bugs](docs/bugs/README.md) | Bekannte Probleme und Workarounds | +| [Best Practices](docs/dos-n-donts/README.md) | Dos and Don'ts | -- **Docker**: Version 20.10+ -- **Docker Compose**: Version 2.0+ -- **Python**: 3.11+ (im Container enthalten) -- **Traefik**: Version 2.x oder 3.x (optional, aber empfohlen) +## Projektstruktur -### Netzwerk - -- **Port 5000**: Spawner Web-UI (oder via Traefik) -- **Port 80/443**: Traefik (für User-Container-Routing) -- **Wildcard-DNS** oder \`/etc/hosts\`-Einträge für Subdomains - ---- - -## Installation - -### Schritt 1: Projekt-Setup - -```bash -# Repository erstellen -mkdir docker-spawner -cd docker-spawner - -# Verzeichnisstruktur anlegen -mkdir -p spawner/{templates,user-template,data} -cd spawner ``` - -### Schritt 2: Dateien erstellen - -Erstelle alle Dateien aus der Projektstruktur (siehe [Dateistruktur](#dateistruktur)). - -### Schritt 3: Traefik-Netzwerk erstellen - -```bash -docker network create traefik-network +spawner/ +├── app.py # Flask-Hauptanwendung +├── auth.py # Authentifizierungs-Blueprint +├── container_manager.py # Docker-Container-Management +├── models.py # SQLAlchemy User-Modell +├── config.py # Konfigurationsklassen +├── templates/ # Jinja2-Templates (Legacy) +├── frontend/ # Next.js Frontend +├── user-template/ # Docker-Template fuer User-Container +└── docs/ # Dokumentation ``` -### Schritt 4: User-Template-Image bauen - -```bash -cd user-template -docker build -t user-service-template:latest . -cd .. -``` - -### Schritt 5: Spawner starten - -```bash -docker-compose up -d --build -``` - -### Schritt 6: Traefik starten (falls noch nicht vorhanden) - -```bash -# Minimal Traefik docker-compose.yml -cat > traefik-compose.yml <100 User: Kubernetes mit Horizontal Pod Autoscaling empfohlen. - -### Kann ich verschiedene Services pro User bereitstellen? - -Ja! Erweitere das User-Modell um \`service_type\` und verwende verschiedene Template-Images. - -### Funktioniert das auch ohne Traefik? - -Ja! Alternativen: - -- **Nginx mit dynamischer Config-Generation** -- **HAProxy mit Runtime-API** -- **Caddy mit JSON-API** - -### Wie sichere ich den Docker-Socket ab? - -Verwende \`tecnativa/docker-socket-proxy\` mit eingeschränkten Permissions (siehe [Sicherheit](#sicherheit)). - -### Kann ich existierende User-Daten migrieren? - -Ja! Volume-Mounts verwenden und bei Migration Volumes kopieren: - -```bash -docker run --rm -v old-user-data:/from -v new-user-data:/to alpine sh -c "cp -av /from/* /to/" -``` - ---- - -## Ressourcen - -### Docker SDK Documentation - -- [Docker SDK for Python](https://docker-py.readthedocs.io/) -- [Docker Engine API](https://docs.docker.com/engine/api/) - -### Flask & Security - -- [Flask Documentation](https://flask.palletsprojects.com/) -- [Flask-Login](https://flask-login.readthedocs.io/) -- [OWASP Security Guidelines](https://owasp.org/www-project-web-security-testing-guide/) - -### Traefik - -- [Traefik Documentation](https://doc.traefik.io/traefik/) -- [Docker Provider](https://doc.traefik.io/traefik/providers/docker/) - -### Alternatives & Inspiration - -- [JupyterHub](https://github.com/jupyterhub/jupyterhub) -- [Code-Server](https://github.com/coder/code-server) -- [Gitpod](https://www.gitpod.io/) - ---- - -## Lizenz & Support - -Dieses Projekt ist ein Beispiel-Setup. Für Produktions-Einsatz: - -- **Security-Audit** durchführen -- **Load-Tests** mit erwarteter User-Anzahl -- **Backup-Strategie** implementieren -- **Monitoring** mit Prometheus/Grafana - ---- - -**Version**: 1.0.0 -**Erstellt**: Januar 2026 -**Zielgruppe**: DevOps, Platform Engineers, SaaS-Entwickler - - ---- - - -## Verzeichnisstruktur - -``` -spawner/ -├── Dockerfile -├── docker-compose.yml -├── requirements.txt -├── app.py -├── auth.py -├── container_manager.py -├── models.py -├── config.py -├── templates/ -│ ├── login.html -│ └── dashboard.html -└── user-template/ - └── Dockerfile -``` - -## Dockerfile (Spawner-Service) - -```dockerfile -FROM python:3.11-slim - -WORKDIR /app - -# System-Dependencies -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - curl \ - ca-certificates && \ - rm -rf /var/lib/apt/lists/* - -# Python-Dependencies -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# Application-Code -COPY . . - -# Daten-Verzeichnisse -RUN mkdir -p /app/data /app/logs && \ - chmod 755 /app/data /app/logs - -EXPOSE 5000 - -# Health-Check -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:5000/health || exit 1 - -CMD ["python", "app.py"] -``` - -## requirements.txt - -```txt -flask==3.0.0 -flask-login==0.6.3 -flask-sqlalchemy==3.1.1 -werkzeug==3.0.1 -docker==7.0.0 -PyJWT==2.8.0 -python-dotenv==1.0.0 -``` - -## config.py - -```python -import os -from dotenv import load_dotenv - -load_dotenv() - -class Config: - # ======================================== - # Sicherheit - # ======================================== - SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') - - # Session-Sicherheit - SESSION_COOKIE_SECURE = os.getenv('BASE_DOMAIN', 'localhost') != 'localhost' - SESSION_COOKIE_HTTPONLY = True - SESSION_COOKIE_SAMESITE = 'Lax' - PERMANENT_SESSION_LIFETIME = 3600 # 1 Stunde - - # ======================================== - # Datenbank - # ======================================== - SQLALCHEMY_DATABASE_URI = os.getenv( - 'DATABASE_URL', - 'sqlite:///data/users.db' - ) - SQLALCHEMY_TRACK_MODIFICATIONS = False - - # ======================================== - # Docker-Konfiguration - # ======================================== - DOCKER_SOCKET = os.getenv('DOCKER_SOCKET', 'unix://var/run/docker.sock') - USER_TEMPLATE_IMAGE = os.getenv('USER_TEMPLATE_IMAGE', 'user-service-template:latest') - - # ======================================== - # Traefik/Domain-Konfiguration - # ======================================== - BASE_DOMAIN = os.getenv('BASE_DOMAIN', 'localhost') - SPAWNER_SUBDOMAIN = os.getenv('SPAWNER_SUBDOMAIN', 'spawner') # ← FEHLTE! - TRAEFIK_NETWORK = os.getenv('TRAEFIK_NETWORK', 'web') - - # Vollständige Spawner-URL - SPAWNER_URL = f"{SPAWNER_SUBDOMAIN}.{BASE_DOMAIN}" - - # ======================================== - # Application-Settings - # ======================================== - # HTTPS automatisch für Nicht-Localhost - PREFERRED_URL_SCHEME = 'https' if BASE_DOMAIN != 'localhost' else 'http' - - # Spawner-Port (nur für Debugging wichtig) - SPAWNER_PORT = int(os.getenv('SPAWNER_PORT', 5000)) - - # ======================================== - # Optionale Einstellungen - # ======================================== - # Logging - LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') - LOG_FILE = os.getenv('LOG_FILE', '/app/logs/spawner.log') - - # Container-Limits (für container_manager.py) - DEFAULT_MEMORY_LIMIT = os.getenv('DEFAULT_MEMORY_LIMIT', '512m') - DEFAULT_CPU_QUOTA = int(os.getenv('DEFAULT_CPU_QUOTA', 50000)) # 0.5 CPU - - # Container-Cleanup - CONTAINER_IDLE_TIMEOUT = int(os.getenv('CONTAINER_IDLE_TIMEOUT', 3600)) # 1h in Sekunden - - -class DevelopmentConfig(Config): - """Konfiguration für Entwicklung""" - DEBUG = True - TESTING = False - - -class ProductionConfig(Config): - """Konfiguration für Produktion""" - DEBUG = False - TESTING = False - - # Strikte Session-Sicherheit - SESSION_COOKIE_SECURE = True - - # Optional: PostgreSQL statt SQLite - # SQLALCHEMY_DATABASE_URI = os.getenv( - # 'DATABASE_URL', - # 'postgresql://spawner:password@postgres:5432/spawner' - # ) - - -class TestingConfig(Config): - """Konfiguration für Tests""" - TESTING = True - SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' - WTF_CSRF_ENABLED = False - - -# Config-Dict für einfaches Laden -config = { - 'development': DevelopmentConfig, - 'production': ProductionConfig, - 'testing': TestingConfig, - 'default': DevelopmentConfig -} - -``` - -## models.py - -```python -from flask_sqlalchemy import SQLAlchemy -from flask_login import UserMixin -from werkzeug.security import generate_password_hash, check_password_hash -from datetime import datetime - -db = SQLAlchemy() - -class User(UserMixin, db.Model): - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(80), unique=True, nullable=False) - email = db.Column(db.String(120), unique=True, nullable=False) - password_hash = db.Column(db.String(200), nullable=False) - container_id = db.Column(db.String(100), nullable=True) - container_port = db.Column(db.Integer, nullable=True) - created_at = db.Column(db.DateTime, default=datetime.utcnow) - - def set_password(self, password): - self.password_hash = generate_password_hash(password) - - def check_password(self, password): - return check_password_hash(self.password_hash, password) -``` - -## container_manager.py - -```python -import docker -from config import Config - -class ContainerManager: - def __init__(self): - self.client = docker.from_env() - - def spawn_container(self, user_id, username): - """Spawnt einen neuen Container für den User""" - try: - existing = self._get_user_container(username) - if existing and existing.status == 'running': - return existing.id, self._get_container_port(existing) - - # User-Container-Subdomain (OHNE spawner-subdomain!) - user_domain = f"{username}.{Config.BASE_DOMAIN}" - - container = self.client.containers.run( - Config.USER_TEMPLATE_IMAGE, - name=f"user-{username}-{user_id}", - detach=True, - network=Config.TRAEFIK_NETWORK, - labels={ - 'traefik.enable': 'true', - - # HTTP Router - f'traefik.http.routers.user{user_id}.rule': - f'Host(`{user_domain}`)', - f'traefik.http.routers.user{user_id}.entrypoints': 'web', - - # HTTPS Router (auskommentiert für initiale Tests) - # f'traefik.http.routers.user{user_id}-secure.rule': - # f'Host(`{user_domain}`)', - # f'traefik.http.routers.user{user_id}-secure.entrypoints': 'websecure', - # f'traefik.http.routers.user{user_id}-secure.tls.certresolver': 'hetzner', - - # Service - f'traefik.http.services.user{user_id}.loadbalancer.server.port': '8080', - - # Metadata - 'spawner.user_id': str(user_id), - 'spawner.username': username, - 'spawner.managed': 'true' - }, - environment={ - 'USER_ID': str(user_id), - 'USERNAME': username - }, - restart_policy={'Name': 'unless-stopped'}, - mem_limit=Config.DEFAULT_MEMORY_LIMIT, - cpu_quota=Config.DEFAULT_CPU_QUOTA - ) - - return container.id, 8080 - - except docker.errors.ImageNotFound: - raise Exception(f"Template-Image '{Config.USER_TEMPLATE_IMAGE}' nicht gefunden") - except docker.errors.APIError as e: - raise Exception(f"Docker API Fehler: {str(e)}") - - def stop_container(self, container_id): - """Stoppt einen User-Container""" - try: - container = self.client.containers.get(container_id) - container.stop(timeout=10) - return True - except docker.errors.NotFound: - return False - - def remove_container(self, container_id): - """Entfernt einen User-Container komplett""" - try: - container = self.client.containers.get(container_id) - container.remove(force=True) - return True - except docker.errors.NotFound: - return False - - def get_container_status(self, container_id): - """Gibt Status eines Containers zurück""" - try: - container = self.client.containers.get(container_id) - return container.status - except docker.errors.NotFound: - return 'not_found' - - def _get_user_container(self, username): - """Findet existierenden Container für User""" - filters = {'label': f'spawner.username={username}'} - containers = self.client.containers.list(all=True, filters=filters) - return containers[0] if containers else None - - def _get_container_port(self, container): - """Extrahiert Port aus Container-Config""" - return 8080 -``` - -## auth.py - -```python -from flask import Blueprint, render_template, redirect, url_for, request, flash -from flask_login import login_user, logout_user, login_required, current_user -from models import db, User -from container_manager import ContainerManager - -auth_bp = Blueprint('auth', __name__) -container_mgr = ContainerManager() - -@auth_bp.route('/login', methods=['GET', 'POST']) -def login(): - """User-Login""" - if request.method == 'POST': - username = request.form.get('username') - password = request.form.get('password') - - user = User.query.filter_by(username=username).first() - - if user and user.check_password(password): - login_user(user) - - # Container spawnen wenn noch nicht vorhanden - if not user.container_id: - try: - container_id, port = container_mgr.spawn_container(user.id, user.username) - user.container_id = container_id - user.container_port = port - db.session.commit() - except Exception as e: - flash(f'Container-Start fehlgeschlagen: {str(e)}', 'error') - return redirect(url_for('auth.login')) - - flash('Login erfolgreich!', 'success') - return redirect(url_for('dashboard')) - else: - flash('Ungültige Anmeldedaten', 'error') - - return render_template('login.html') - -@auth_bp.route('/signup', methods=['GET', 'POST']) -def signup(): - """User-Registrierung""" - if request.method == 'POST': - username = request.form.get('username') - email = request.form.get('email') - password = request.form.get('password') - - # Prüfe ob User existiert - if User.query.filter_by(username=username).first(): - flash('Username bereits vergeben', 'error') - return redirect(url_for('auth.signup')) - - if User.query.filter_by(email=email).first(): - flash('Email bereits registriert', 'error') - return redirect(url_for('auth.signup')) - - # Neuen User anlegen - user = User(username=username, email=email) - user.set_password(password) - db.session.add(user) - db.session.commit() - - # Container aus Template bauen und starten - try: - container_id, port = container_mgr.spawn_container(user.id, user.username) - user.container_id = container_id - user.container_port = port - db.session.commit() - - flash('Registrierung erfolgreich! Container wird gestartet...', 'success') - login_user(user) - return redirect(url_for('dashboard')) - - except Exception as e: - db.session.delete(user) - db.session.commit() - flash(f'Registrierung fehlgeschlagen: {str(e)}', 'error') - - return render_template('signup.html') - -@auth_bp.route('/logout') -@login_required -def logout(): - """User-Logout""" - logout_user() - flash('Erfolgreich abgemeldet', 'success') - return redirect(url_for('auth.login')) -``` - -## app.py - -```python -from flask import Flask, render_template, redirect, url_for -from flask_login import LoginManager, login_required, current_user -from models import db, User -from auth import auth_bp -from config import Config -from container_manager import ContainerManager - -# Flask-App initialisieren -app = Flask(__name__) -app.config.from_object(Config) - -# Datenbank initialisieren -db.init_app(app) - -# Flask-Login initialisieren -login_manager = LoginManager() -login_manager.init_app(app) -login_manager.login_view = 'auth.login' -login_manager.login_message = 'Bitte melde dich an, um auf diese Seite zuzugreifen.' -login_manager.login_message_category = 'error' - -# Blueprint registrieren -app.register_blueprint(auth_bp) - -@login_manager.user_loader -def load_user(user_id): - """Lädt User für Flask-Login""" - return User.query.get(int(user_id)) - -@app.route('/') -def index(): - """Startseite - Redirect zu Dashboard oder Login""" - if current_user.is_authenticated: - return redirect(url_for('dashboard')) - return redirect(url_for('auth.login')) - -@app.route('/dashboard') -@login_required -def dashboard(): - """Dashboard - zeigt Container-Status und Service-URL""" - container_mgr = ContainerManager() - container_status = 'unknown' - - if current_user.container_id: - container_status = container_mgr.get_container_status(current_user.container_id) - - # Service-URL für den User - scheme = app.config['PREFERRED_URL_SCHEME'] - service_url = f"{scheme}://{current_user.username}.{app.config['BASE_DOMAIN']}" - - return render_template('dashboard.html', - user=current_user, - service_url=service_url, - container_status=container_status) - -@app.route('/container/restart') -@login_required -def restart_container(): - """Startet Container des Users neu""" - container_mgr = ContainerManager() - - # Alten Container stoppen falls vorhanden - if current_user.container_id: - container_mgr.stop_container(current_user.container_id) - container_mgr.remove_container(current_user.container_id) - - # Neuen Container starten - try: - container_id, port = container_mgr.spawn_container(current_user.id, current_user.username) - current_user.container_id = container_id - current_user.container_port = port - db.session.commit() - except Exception as e: - app.logger.error(f"Container-Restart fehlgeschlagen: {str(e)}") - - return redirect(url_for('dashboard')) - -@app.route('/health') -def health(): - """Health-Check für Docker und Monitoring""" - try: - # DB-Check - db.session.execute('SELECT 1') - db_status = 'ok' - except Exception as e: - db_status = f'error: {str(e)}' - - try: - # Docker-Check - container_mgr = ContainerManager() - container_mgr.client.ping() - docker_status = 'ok' - except Exception as e: - docker_status = f'error: {str(e)}' - - status_code = 200 if db_status == 'ok' and docker_status == 'ok' else 503 - - return { - 'status': 'healthy' if status_code == 200 else 'unhealthy', - 'database': db_status, - 'docker': docker_status, - 'version': '1.0.0' - }, status_code - -# Datenbank-Tabellen erstellen beim ersten Start -with app.app_context(): - db.create_all() - app.logger.info('Datenbank-Tabellen erstellt') - -if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, debug=False) -``` - -## docker-compose.yml - -```yaml -version: '3.8' - -services: - spawner: - build: . - container_name: spawner - restart: unless-stopped - - ports: - - "5000:5000" # Optional: Direktzugriff für Debugging - - volumes: - # Docker-Socket für Container-Management - - /var/run/docker.sock:/var/run/docker.sock:rw - # Persistente Daten - - ./data:/app/data - # Logs - - ./logs:/app/logs - - environment: - # Aus .env-Datei - - SECRET_KEY=${SECRET_KEY} - - BASE_DOMAIN=${BASE_DOMAIN} - - TRAEFIK_NETWORK=${TRAEFIK_NETWORK} - - USER_TEMPLATE_IMAGE=${USER_TEMPLATE_IMAGE:-user-service-template:latest} - - SPAWNER_SUBDOMAIN=${SPAWNER_SUBDOMAIN:-spawner} - - networks: - - web # ⚠️ Dein bestehendes Traefik-Netzwerk! - - labels: - # Traefik aktivieren - - "traefik.enable=true" - - # HTTP Router - - "traefik.http.routers.spawner.rule=Host(`${SPAWNER_SUBDOMAIN}.${BASE_DOMAIN}`)" - - "traefik.http.routers.spawner.entrypoints=web" - - "traefik.http.services.spawner.loadbalancer.server.port=5000" - - # Metadata für Management - - "spawner.managed=true" - - "spawner.version=1.0.0" - - "spawner.type=management-service" - - # Health-Check - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:5000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - -# Externes Netzwerk (von deinem Traefik bereits vorhanden) -networks: - web: - external: true -``` - -## user-template/Dockerfile (Template für User-Container) - -```dockerfile -FROM nginxinc/nginx-unprivileged:alpine - -# Beispiel: Einfacher Webserver pro User -# HTML direkt in den Container schreiben -RUN echo '

Dein persönlicher Service

' > /usr/share/nginx/html/index.html - -EXPOSE 8080 - -CMD ["nginx", "-g", "daemon off;"] -``` - -**Hinweis**: Verwende `nginx-unprivileged` statt `nginx` für bessere Sicherheit (kein root-Prozess). -Container läuft auf Port 8080 (als unprivileged user). - -## templates/login.html - -```html - - - - Login - Spawner - - -

Login

- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -

{{ message }}

- {% endfor %} - {% endif %} - {% endwith %} - -
-
-
- -
-

Noch kein Account? Registrieren

- - -``` - -## templates/dashboard.html - -```html - - - - Dashboard - {{ user.username }} - - -

Willkommen, {{ user.username }}!

- -

Container-Status: {{ container_status }}

-

Dein Service: {{ service_url }}

- - Container neu starten
- Logout - - -``` - -## Starten - -```bash -# Im spawner/ Verzeichnis: -docker-compose up --build -``` - -Die Lösung enthält: -- **Vollständige Authentifizierung** mit Flask-Login und gehashten Passwörtern -- **Automatisches Container-Spawning** via Docker SDK [docs.docker](https://docs.docker.com/reference/api/engine/sdk/examples/) -- **Traefik-Integration** über Labels [brunoscheufler](https://brunoscheufler.com/blog/2022-04-17-routing-traffic-for-dynamic-deployments-using-traefik) -- **Resource-Limits** (RAM/CPU) pro User-Container -- **Persistente Datenbank** für User-Management -- **Template-System** für neue User-Container - -Der Spawner-Service benötigt Zugriff auf `/var/run/docker.sock`, um Container zu steuern. - ---- - -# SPAWNER Integration in bestehende Traefik-Umgebung -## Step-by-Step Implementierungsplan - ---- - -## 🎯 Zielsetzung - -Integration des SPAWNER-Systems in eine bestehende Docker-Infrastruktur mit Traefik als Reverse Proxy, ohne bestehende Services zu beeinträchtigen. - ---- - -## 📋 Voraussetzungen prüfen - -### ✅ Checkliste vor Start - -- [ ] Traefik läuft und ist erreichbar -- [ ] Docker-Version ≥ 20.10 -- [ ] Freier Port für Spawner (Standard: 5000) -- [ ] Mindestens 2 GB freier RAM -- [ ] Wildcard-DNS oder manuelle DNS-Einträge möglich -- [ ] Zugriff auf `/var/run/docker.sock` -- [ ] Git installiert (zum Klonen/Erstellen der Dateien) - ---- - -## Phase 1: Analyse der bestehenden Umgebung - -### Step 1.1: Traefik-Konfiguration ermitteln - -**Aktion**: Bestehende Traefik-Setup analysieren - -```bash -# Traefik-Container finden -docker ps | grep traefik - -# Traefik-Konfiguration anzeigen -docker inspect | jq '.[0].Config.Labels' -docker inspect | jq '.[0].HostConfig.Binds' - -# Verwendetes Netzwerk ermitteln -docker inspect | jq '.[0].NetworkSettings.Networks' -``` - -**Dokumentieren**: -- Traefik-Version: _______________ -- Netzwerk-Name: _______________ -- EntryPoints: _______________ -- Zertifikats-Resolver (falls HTTPS): _______________ - -```bash=1 - docker ps | grep traefik -81e0f2d0f8c0 traefik:v3.6.5 -``` -```config=1 -docker inspect 81e0f2d0f8c0 | jq '.[0].Config.Labels' -{ - "com.docker.compose.config-hash": "aeb95d30dd87fd499dd7207ef416f97a6c325227615a2ccdae20278b5f70f51c", - "com.docker.compose.container-number": "1", - "com.docker.compose.depends_on": "", - "com.docker.compose.image": "sha256:0fb158a64eaac3b411525e180705dbb4e120d078150b6a795e120e6b80e81b02", - "com.docker.compose.oneoff": "False", - "com.docker.compose.project": "traefik", - "com.docker.compose.project.config_files": "/volume1/docker/traefik/docker-compose.yml", - "com.docker.compose.project.working_dir": "/volume1/docker/traefik", - "com.docker.compose.service": "traefik", - "com.docker.compose.version": "2.20.1", - "org.opencontainers.image.description": "A modern reverse-proxy", - "org.opencontainers.image.documentation": "https://docs.traefik.io", - "org.opencontainers.image.source": "https://github.com/traefik/traefik", - "org.opencontainers.image.title": "Traefik", - "org.opencontainers.image.url": "https://traefik.io", - "org.opencontainers.image.vendor": "Traefik Labs", - "org.opencontainers.image.version": "v3.6.5" -} -``` - -```config=1 -docker inspect 81e0f2d0f8c0 | jq '.[0].HostConfig.Binds' -[ - "/var/run/docker.sock:/var/run/docker.sock:rw", - "/volume1/docker/traefik/traefik.toml:/traefik.toml:rw", - "/volume1/docker/traefik/traefik_dynamic.toml:/traefik_dynamic.toml:rw", - "/volume1/docker/traefik/acme.json:/acme.json:rw" -] -``` - -```config=1 -docker inspect 81e0f2d0f8c0 | jq '.[0].NetworkSettings.Networks' -{ - "web": { - "IPAMConfig": null, - "Links": null, - "Aliases": [ - "traefik", - "traefik", - "81e0f2d0f8c0" - ], - "NetworkID": "79c8e53a1b0d38b655e769918c2ecfccf049461f0e1fe276362ccc1c13869aa3", - "EndpointID": "95e639cc48ced9bb06d58fd501bbf850bbe64e6050d5de75700ded13bdb1c4d4", - "Gateway": "192.168.16.1", - "IPAddress": "192.168.16.6", - "IPPrefixLen": 24, - "IPv6Gateway": "", - "GlobalIPv6Address": "", - "GlobalIPv6PrefixLen": 0, - "MacAddress": "02:42:c0:a8:10:06", - "DriverOpts": null - } -} -``` - -### Step 1.2: Netzwerk-Topologie verstehen - -```bash -# Alle Docker-Netzwerke auflisten -docker network ls - -# Netzwerk-Details des Traefik-Netzwerks -docker network inspect - -# Welche Container sind bereits angeschlossen? -docker network inspect | jq '.[0].Containers' -``` - -```bash=1 -docker network ls -NETWORK ID NAME DRIVER SCOPE -37f47c9e1943 bridge bridge local -3ab114f137fa dokploy_dokploy_internal bridge local -04ae90e99953 host host local -208dfb8d38b0 jupyterhub bridge local -ed620451f21c none null local -79c8e53a1b0d web bridge local -``` - -**Entscheidung**: -- Existierendes Netzwerk nutzen: **Ja** ☐ / **Nein** ☐ -- Netzwerk-Name: `_______________` - -:::success -**Existierendes Netzwerk nutzen:** ✔ -**Netzwerk-Name: web** -::: -### Step 1.3: Domain-Strategie festlegen - -**Optionen**: - -**A) Subdomains pro User** (empfohlen) -``` -alice.spawner.example.com -bob.spawner.example.com -charlie.spawner.example.com -``` - -**B) Path-basiert** -``` -spawner.example.com/alice -spawner.example.com/bob -spawner.example.com/charlie -``` - - -**Gewählte Strategie**: ☐ A ☐ B -:::success -**B) Path-basiert** -::: -**Base-Domain**: `_______________` -:::success -**Base-Domain: coder.wieland.org** -::: -### Step 1.4: Traefik-Dashboard prüfen - -```bash -# Ist Dashboard aktiviert? -docker exec cat /etc/traefik/traefik.yml | grep -A5 "api:" - -# Dashboard-URL (Standard: Port 8080) -firefox http://:8080 -``` - -**Notiz**: Dashboard-URL für Monitoring: `_______________` - ---- - -## Phase 2: Projekt-Setup - -### Step 2.1: Projektverzeichnis erstellen - -```bash -# Zu deinem Docker-Projekten-Verzeichnis wechseln -cd /path/to/docker/projects # z.B. ~/docker oder /opt/docker - -# Spawner-Verzeichnis erstellen -mkdir -p spawner/{templates,user-template,data,logs} -cd spawner - -# Berechtigungen setzen -chmod 755 . -``` - -**Pfad dokumentieren**: `_______________` - -### Step 2.2: Core-Dateien erstellen - -```bash -# Python-Dateien -touch app.py auth.py container_manager.py models.py config.py - -# Docker-Dateien -touch Dockerfile docker-compose.yml .env .dockerignore - -# Templates -touch templates/login.html templates/signup.html templates/dashboard.html - -# User-Template -touch user-template/Dockerfile - -# README -touch README.md -``` - -### Step 2.3: .dockerignore erstellen - -```bash -cat > .dockerignore << 'EOF' -__pycache__ -*.pyc -*.pyo -*.pyd -.Python -.env -.venv -venv/ -data/*.db -logs/*.log -.git -.gitignore -.DS_Store -*.md -EOF -``` - -### Step 2.4: requirements.txt erstellen - -```bash -cat > requirements.txt << 'EOF' -flask==3.0.0 -flask-login==0.6.3 -flask-sqlalchemy==3.1.1 -werkzeug==3.0.1 -docker==7.0.0 -PyJWT==2.8.0 -python-dotenv==1.0.0 -EOF -``` - ---- - -## Phase 3: Anpassung an deine Umgebung - -### Step 3.1: .env-Datei konfigurieren - -Erstelle `.env` mit folgenden Variablen (anpassen!): - -``` -SECRET_KEY=ÄNDERE_MICH_ZU_RANDOM_STRING -BASE_DOMAIN=spawner.example.com -TRAEFIK_NETWORK=traefik-network -USER_TEMPLATE_IMAGE=user-service-template:latest -SPAWNER_PORT=5000 -``` - -SECRET_KEY generieren: -```bash -python3 -c "import secrets; print(secrets.token_hex(32))" -``` - -### Step 3.2: docker-compose.yml erstellen - -Vollständiges Beispiel mit Traefik-Labels - siehe Dokumentation. - -**Wichtige Anpassungen**: -- Netzwerk-Name -- EntryPoint-Name -- Domain-Name -- Port-Konflikte prüfen - -### Step 3.3: Traefik-Konfiguration erweitern - -Prüfe ob Docker-Provider aktiviert: - -```bash -docker exec cat /etc/traefik/traefik.yml -``` - -Falls Docker-Provider fehlt, ergänzen und Traefik neu starten. - ---- - ---- - -## Phase 4: User-Template vorbereiten - -### Step 4.1: Template-Dockerfile erstellen - -Im Verzeichnis `user-template/`: - -```dockerfile -FROM nginxinc/nginx-unprivileged:alpine - -# HTML kopieren UND Ownership setzen -COPY --chown=nginx:nginx index.html /usr/share/nginx/html/index.html - -EXPOSE 8080 -``` - -### Step 4.2: Beispiel index.html - -Erstelle eine einfache HTML-Seite für User-Container. - -### Step 4.3: Template-Image bauen - -```bash -cd user-template -docker build -t user-service-template:latest . - -# Test -docker run -d -p 8080:8080 --name test-user user-service-template:latest -curl http://localhost:8080 -docker stop test-user && docker rm test-user - -cd .. -``` - ---- - -## Phase 5: Spawner bauen und testen - -### Step 5.1: Alle Python-Dateien erstellen - -Kopiere die Code-Beispiele aus der Dokumentation: -- config.py -- models.py -- container_manager.py -- auth.py -- app.py - -### Step 5.2: Dockerfile erstellen - -```dockerfile -FROM python:3.11-slim - -WORKDIR /app - -RUN apt-get update && \ - apt-get install -y --no-install-recommends curl ca-certificates && \ - rm -rf /var/lib/apt/lists/* - -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -COPY . . - -RUN mkdir -p /app/data /app/logs && chmod 755 /app/data /app/logs - -EXPOSE 5000 - -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:5000/health || exit 1 - -CMD ["python", "app.py"] -``` - -### Step 5.3: Spawner bauen - -```bash -docker-compose build -``` - -### Step 5.4: Test-Start - -```bash -docker-compose up -d -docker-compose logs -f spawner - -# Health-Check -curl http://localhost:5000/health - -# Login-Seite -curl http://localhost:5000/login -``` - -### Step 5.5: Erste Test-Registrierung - -```bash -firefox http://localhost:5000 -``` - -Registriere einen Test-User und prüfe ob Container spawnt: - -```bash -docker ps | grep user- -docker inspect user-testuser-1 | jq '.[0].Config.Labels' -``` - ---- - -## Phase 6: Traefik-Integration aktivieren - -### Step 6.1: Netzwerk verbinden - -```bash -# Falls noch nicht verbunden -docker network connect traefik-network spawner - -# Verifizieren -docker network inspect traefik-network | grep spawner -``` - -### Step 6.2: DNS konfigurieren - -**Lokal testen** (Option A): -```bash -sudo nano /etc/hosts - -# Hinzufügen: -127.0.0.1 spawner.localhost -127.0.0.1 testuser.localhost -``` - -**Produktion** (Option B): -- Wildcard-DNS-Eintrag erstellen: `*.spawner.example.com → ` -- DNS-Propagation abwarten - -### Step 6.3: Traefik-Routing testen - -```bash -# Traefik-Dashboard öffnen -firefox http://:8080 - -# Routes prüfen unter HTTP → Routers - -# Mit curl testen -curl -H "Host: spawner.localhost" http://localhost/ -curl -H "Host: testuser.localhost" http://localhost/ -``` - -### Step 6.4: End-to-End Test - -1. Spawner-UI aufrufen -2. Neuen User registrieren -3. Zum Dashboard navigieren -4. Service-Link klicken → User-Container sollte erreichbar sein - ---- - -## Phase 7: HTTPS aktivieren (optional) - -### Step 7.1: Let's Encrypt konfigurieren - -Prüfe Traefik-Config für certificatesResolvers. - -### Step 7.2: Labels für HTTPS anpassen - -In `docker-compose.yml` und `container_manager.py` HTTPS-Labels ergänzen: -- entrypoints: websecure -- tls.certresolver: letsencrypt - -### Step 7.3: Spawner neu starten - -```bash -docker-compose down -docker-compose up -d --build -``` - -### Step 7.4: HTTPS testen - -```bash -firefox https://spawner.example.com - -# Zertifikat prüfen -openssl s_client -connect spawner.example.com:443 -``` - ---- - -## Phase 8: Monitoring & Observability - -### Step 8.1: Logging aktivieren - -Strukturiertes Logging in app.py implementieren. - -### Step 8.2: Monitoring-Script - -```bash -cat > monitor.sh << 'EOF' -#!/bin/bash -echo "=== SPAWNER Statistics ===" -docker stats spawner --no-stream -docker ps --filter "label=spawner.user_id" -docker stats $(docker ps --filter "label=spawner.user_id" -q) --no-stream -EOF - -chmod +x monitor.sh -./monitor.sh -``` - -### Step 8.3: Backup-Strategie - -```bash -cat > backup.sh << 'EOF' -#!/bin/bash -BACKUP_DIR="/backup/spawner" -DATE=$(date +%Y%m%d_%H%M%S) -mkdir -p $BACKUP_DIR -docker exec spawner sqlite3 /app/data/users.db ".backup '/app/data/backup_${DATE}.db'" -cp data/backup_${DATE}.db $BACKUP_DIR/ -find $BACKUP_DIR -name "backup_*.db" -mtime +7 -delete -EOF - -chmod +x backup.sh - -# Cronjob -crontab -e -# 0 2 * * * /path/to/spawner/backup.sh -``` - ---- - -## Phase 9: Produktions-Optimierung - -### Step 9.1: Ressourcen-Limits anpassen - -In `container_manager.py` basierend auf deiner Hardware. - -### Step 9.2: Container-Cleanup - -Script für automatisches Aufräumen alter Container. - -### Step 9.3: PostgreSQL statt SQLite - -Für Produktion docker-compose.yml um PostgreSQL erweitern. - -### Step 9.4: Rate-Limiting - -Flask-Limiter installieren und konfigurieren. - ---- - -## Phase 10: Go-Live - -### Step 10.1: Load-Test - -```bash -ab -n 1000 -c 10 http://spawner.example.com/login - -# Multi-User Test -for i in {1..10}; do - curl -X POST http://spawner.example.com/signup \ - -d "username=loadtest${i}&email=test${i}@example.com&password=test123" - sleep 2 -done -``` - -### Step 10.2: Security-Audit - -- SECRET_KEY stark -- HTTPS erzwungen -- Rate-Limiting aktiv -- Container-Isolation -- Non-Root-User - -```bash -# Vulnerability-Scan -docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ - aquasec/trivy image spawner:latest -``` - -### Step 10.3: Go-Live Checklist - -- [ ] Alle Services erreichbar -- [ ] HTTPS funktioniert -- [ ] DNS korrekt -- [ ] Backups laufen -- [ ] Monitoring aktiv -- [ ] Logs werden geschrieben -- [ ] Load-Tests bestanden -- [ ] Security-Audit durchgeführt -- [ ] Team informiert - -```bash -# Finaler Health-Check -curl -f https://spawner.example.com/health - -# Container-Count -docker ps --filter 'label=spawner.managed=true' -q | wc -l -``` - -**🎉 GO-LIVE!** - ---- - -## 🚨 Troubleshooting - -### Traefik findet Spawner nicht -- Netzwerk-Verbindung prüfen -- Labels verifizieren -- Traefik-Logs checken - -### User-Container startet nicht -- Template-Image existiert? -- Docker-Socket-Permissions -- Netzwerk vorhanden? - -### DNS funktioniert nicht -- Wildcard-DNS konfiguriert? -- /etc/hosts für lokale Tests -- DNS-Propagation abwarten - -### Container-Spawn schlägt fehl -- Docker-API-Zugriff testen -- Socket-Mount prüfen -- Permissions checken - ---- - -## 📊 Post-Integration - -### Wöchentliches Monitoring - -```bash -# Container-Anzahl -docker ps --filter "label=spawner.managed=true" - -# Ressourcen -docker stats --no-stream - -# Disk-Space -docker system df -``` - -### Metriken - -- Anzahl User -- Aktive Container -- CPU/RAM-Auslastung -- Netzwerk-Traffic - ---- - -## 🎓 Next Steps - -1. User-Feedback sammeln -2. Templates erweitern (Python, Node.js) -3. Admin-Dashboard entwickeln -4. Auto-Shutdown implementieren -5. Volume-Persistenz aktivieren -6. Multi-Region deployment - ---- - -**Integration abgeschlossen!** 🚀 - -Bei Fragen: -- Logs: `docker-compose logs -f` -- Traefik: `http://:8080` -- Health: `curl https://spawner.example.com/health` +**Version**: 0.1.0 +**Repository**: https://gitea.iotxs.de/RainerWieland/spawner diff --git a/api.py b/api.py new file mode 100644 index 0000000..dc16e59 --- /dev/null +++ b/api.py @@ -0,0 +1,251 @@ +from flask import Blueprint, jsonify, request, current_app +from flask_jwt_extended import ( + create_access_token, + jwt_required, + get_jwt_identity, + get_jwt +) +from datetime import timedelta +from models import db, User +from container_manager import ContainerManager + +api_bp = Blueprint('api', __name__, url_prefix='/api') + +# Token-Blacklist für Logout +token_blacklist = set() + + +@api_bp.route('/auth/login', methods=['POST']) +def api_login(): + """API-Login - gibt JWT-Token zurück""" + data = request.get_json() + + if not data: + return jsonify({'error': 'Keine Daten übermittelt'}), 400 + + username = data.get('username') + password = data.get('password') + + if not username or not password: + return jsonify({'error': 'Username und Passwort erforderlich'}), 400 + + user = User.query.filter_by(username=username).first() + + if not user or not user.check_password(password): + return jsonify({'error': 'Ungültige Anmeldedaten'}), 401 + + # Container spawnen wenn noch nicht vorhanden + if not user.container_id: + try: + container_mgr = ContainerManager() + container_id, port = container_mgr.spawn_container(user.id, user.username) + user.container_id = container_id + user.container_port = port + db.session.commit() + except Exception as e: + current_app.logger.error(f"Container-Start fehlgeschlagen: {str(e)}") + return jsonify({'error': f'Container-Start fehlgeschlagen: {str(e)}'}), 500 + + # JWT-Token erstellen + expires = timedelta(seconds=current_app.config.get('JWT_ACCESS_TOKEN_EXPIRES', 3600)) + access_token = create_access_token( + identity=str(user.id), + expires_delta=expires, + additional_claims={'username': user.username} + ) + + return jsonify({ + 'access_token': access_token, + 'token_type': 'Bearer', + 'expires_in': int(expires.total_seconds()), + 'user': { + 'id': user.id, + 'username': user.username, + 'email': user.email + } + }), 200 + + +@api_bp.route('/auth/signup', methods=['POST']) +def api_signup(): + """API-Registrierung - erstellt User, spawnt Container, gibt JWT zurück""" + data = request.get_json() + + if not data: + return jsonify({'error': 'Keine Daten übermittelt'}), 400 + + username = data.get('username') + email = data.get('email') + password = data.get('password') + + if not username or not email or not password: + return jsonify({'error': 'Username, Email und Passwort erforderlich'}), 400 + + # Validierung + if len(username) < 3: + return jsonify({'error': 'Username muss mindestens 3 Zeichen lang sein'}), 400 + + if len(password) < 6: + return jsonify({'error': 'Passwort muss mindestens 6 Zeichen lang sein'}), 400 + + # Prüfe ob User existiert + if User.query.filter_by(username=username).first(): + return jsonify({'error': 'Username bereits vergeben'}), 409 + + if User.query.filter_by(email=email).first(): + return jsonify({'error': 'Email bereits registriert'}), 409 + + # Neuen User anlegen + user = User(username=username, email=email) + user.set_password(password) + db.session.add(user) + db.session.commit() + + # Container spawnen + try: + container_mgr = ContainerManager() + container_id, port = container_mgr.spawn_container(user.id, user.username) + user.container_id = container_id + user.container_port = port + db.session.commit() + except Exception as e: + db.session.delete(user) + db.session.commit() + current_app.logger.error(f"Registrierung fehlgeschlagen: {str(e)}") + return jsonify({'error': f'Container-Erstellung fehlgeschlagen: {str(e)}'}), 500 + + # JWT-Token erstellen + expires = timedelta(seconds=current_app.config.get('JWT_ACCESS_TOKEN_EXPIRES', 3600)) + access_token = create_access_token( + identity=str(user.id), + expires_delta=expires, + additional_claims={'username': user.username} + ) + + return jsonify({ + 'access_token': access_token, + 'token_type': 'Bearer', + 'expires_in': int(expires.total_seconds()), + 'user': { + 'id': user.id, + 'username': user.username, + 'email': user.email + } + }), 201 + + +@api_bp.route('/auth/logout', methods=['POST']) +@jwt_required() +def api_logout(): + """API-Logout - invalidiert Token""" + jti = get_jwt()['jti'] + token_blacklist.add(jti) + return jsonify({'message': 'Erfolgreich abgemeldet'}), 200 + + +@api_bp.route('/user/me', methods=['GET']) +@jwt_required() +def api_user_me(): + """Gibt aktuellen User und Container-Info zurück""" + user_id = get_jwt_identity() + user = User.query.get(int(user_id)) + + if not user: + return jsonify({'error': 'User nicht gefunden'}), 404 + + # Service-URL berechnen + scheme = current_app.config['PREFERRED_URL_SCHEME'] + spawner_domain = f"{current_app.config['SPAWNER_SUBDOMAIN']}.{current_app.config['BASE_DOMAIN']}" + service_url = f"{scheme}://{spawner_domain}/{user.username}" + + # Container-Status abrufen + container_status = 'unknown' + if user.container_id: + try: + container_mgr = ContainerManager() + container_status = container_mgr.get_container_status(user.container_id) + except Exception: + container_status = 'error' + + return jsonify({ + 'user': { + 'id': user.id, + 'username': user.username, + 'email': user.email, + 'created_at': user.created_at.isoformat() if user.created_at else None + }, + 'container': { + 'id': user.container_id, + 'port': user.container_port, + 'status': container_status, + 'service_url': service_url + } + }), 200 + + +@api_bp.route('/container/status', methods=['GET']) +@jwt_required() +def api_container_status(): + """Gibt Container-Status zurück""" + user_id = get_jwt_identity() + user = User.query.get(int(user_id)) + + if not user: + return jsonify({'error': 'User nicht gefunden'}), 404 + + container_status = 'no_container' + if user.container_id: + try: + container_mgr = ContainerManager() + container_status = container_mgr.get_container_status(user.container_id) + except Exception as e: + container_status = f'error: {str(e)}' + + return jsonify({ + 'container_id': user.container_id, + 'status': container_status + }), 200 + + +@api_bp.route('/container/restart', methods=['POST']) +@jwt_required() +def api_container_restart(): + """Startet Container neu""" + user_id = get_jwt_identity() + user = User.query.get(int(user_id)) + + if not user: + return jsonify({'error': 'User nicht gefunden'}), 404 + + container_mgr = ContainerManager() + + # Alten Container stoppen falls vorhanden + if user.container_id: + try: + container_mgr.stop_container(user.container_id) + container_mgr.remove_container(user.container_id) + except Exception as e: + current_app.logger.warning(f"Alter Container konnte nicht gestoppt werden: {str(e)}") + + # Neuen Container starten + try: + container_id, port = container_mgr.spawn_container(user.id, user.username) + user.container_id = container_id + user.container_port = port + db.session.commit() + + return jsonify({ + 'message': 'Container erfolgreich neugestartet', + 'container_id': container_id, + 'status': 'running' + }), 200 + + except Exception as e: + current_app.logger.error(f"Container-Restart fehlgeschlagen: {str(e)}") + return jsonify({'error': f'Container-Restart fehlgeschlagen: {str(e)}'}), 500 + + +def check_if_token_revoked(jwt_header, jwt_payload): + """Callback für flask-jwt-extended um revoked Tokens zu prüfen""" + jti = jwt_payload['jti'] + return jti in token_blacklist diff --git a/app.py b/app.py new file mode 100644 index 0000000..2aff97b --- /dev/null +++ b/app.py @@ -0,0 +1,158 @@ +from flask import Flask, render_template, redirect, url_for, jsonify +from flask_login import LoginManager, login_required, current_user +from flask_jwt_extended import JWTManager +from flask_cors import CORS +from sqlalchemy import text +from models import db, User +from auth import auth_bp +from api import api_bp, check_if_token_revoked +from config import Config +from container_manager import ContainerManager + +# Flask-App initialisieren +app = Flask(__name__) +app.config.from_object(Config) + +# Datenbank initialisieren +db.init_app(app) + +# CORS initialisieren +CORS(app, resources={ + r"/api/*": { + "origins": app.config.get('CORS_ORIGINS', ['http://localhost:3000']), + "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + "allow_headers": ["Content-Type", "Authorization"], + "supports_credentials": True + } +}) + +# JWT initialisieren +jwt = JWTManager(app) + +@jwt.token_in_blocklist_loader +def check_if_token_in_blocklist(jwt_header, jwt_payload): + return check_if_token_revoked(jwt_header, jwt_payload) + +@jwt.expired_token_loader +def expired_token_callback(jwt_header, jwt_payload): + return jsonify({'error': 'Token abgelaufen'}), 401 + +@jwt.invalid_token_loader +def invalid_token_callback(error): + return jsonify({'error': 'Ungültiger Token'}), 401 + +@jwt.unauthorized_loader +def missing_token_callback(error): + return jsonify({'error': 'Authentifizierung erforderlich'}), 401 + +# Flask-Login initialisieren +login_manager = LoginManager() +login_manager.init_app(app) +login_manager.login_view = 'auth.login' +login_manager.login_message = 'Bitte melde dich an, um auf diese Seite zuzugreifen.' +login_manager.login_message_category = 'error' + +# Blueprints registrieren +app.register_blueprint(auth_bp) +app.register_blueprint(api_bp) + +@login_manager.user_loader +def load_user(user_id): + """Lädt User für Flask-Login""" + return User.query.get(int(user_id)) + +@app.route('/') +def index(): + """Startseite - Redirect zu Dashboard oder Login""" + if current_user.is_authenticated: + return redirect(url_for('dashboard')) + return redirect(url_for('auth.login')) + +@app.route('/dashboard') +@login_required +def dashboard(): + """Dashboard - zeigt Container-Status und Service-URL""" + container_mgr = ContainerManager() + container_status = 'unknown' + + if current_user.container_id: + container_status = container_mgr.get_container_status(current_user.container_id) + + # Service-URL für den User (pfad-basiert) + scheme = app.config['PREFERRED_URL_SCHEME'] + spawner_domain = f"{app.config['SPAWNER_SUBDOMAIN']}.{app.config['BASE_DOMAIN']}" + service_url = f"{scheme}://{spawner_domain}/{current_user.username}" + + return render_template('dashboard.html', + user=current_user, + service_url=service_url, + container_status=container_status) + +@app.route('/container/restart') +@login_required +def restart_container(): + """Startet Container des Users neu""" + container_mgr = ContainerManager() + + # Alten Container stoppen falls vorhanden + if current_user.container_id: + container_mgr.stop_container(current_user.container_id) + container_mgr.remove_container(current_user.container_id) + + # Neuen Container starten + try: + container_id, port = container_mgr.spawn_container(current_user.id, current_user.username) + current_user.container_id = container_id + current_user.container_port = port + db.session.commit() + except Exception as e: + app.logger.error(f"Container-Restart fehlgeschlagen: {str(e)}") + + return redirect(url_for('dashboard')) + +@app.route('/health') +def health(): + """Health-Check für Docker und Monitoring""" + db_status = 'ok' + docker_status = 'warning' + + try: + # DB-Check (KRITISCH) + db.session.execute(text('SELECT 1')) + except Exception as e: + db_status = f'error: {str(e)}' + app.logger.error(f"Database health check failed: {str(e)}") + + try: + # Docker-Check (OPTIONAL) + container_mgr = ContainerManager() + container_mgr._get_client().ping() + docker_status = 'ok' + except Exception as e: + docker_status = f'warning: {str(e)}' + app.logger.warning(f"Docker health check failed (non-critical): {str(e)}") + + # Status 503 nur wenn DATABASE down ist, nicht wenn Docker down ist + status_code = 200 if db_status == 'ok' else 503 + + response = { + 'status': 'healthy' if status_code == 200 else 'unhealthy', + 'database': db_status, + 'docker': docker_status, + 'version': '1.0.0' + } + + if status_code != 200: + app.logger.error(f"Health check CRITICAL: {response}") + else: + app.logger.info(f"Health check OK") + + return response, status_code + +# Datenbank-Tabellen erstellen beim ersten Start +with app.app_context(): + db.create_all() + app.logger.info('Datenbank-Tabellen erstellt') + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=False) diff --git a/auth.py b/auth.py new file mode 100644 index 0000000..d6cf093 --- /dev/null +++ b/auth.py @@ -0,0 +1,87 @@ +from flask import Blueprint, render_template, redirect, url_for, request, flash +from flask_login import login_user, logout_user, login_required, current_user +from models import db, User +from container_manager import ContainerManager + +auth_bp = Blueprint('auth', __name__) + +@auth_bp.route('/login', methods=['GET', 'POST']) +def login(): + """User-Login""" + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + + user = User.query.filter_by(username=username).first() + + if user and user.check_password(password): + login_user(user) + + # Container spawnen wenn noch nicht vorhanden + if not user.container_id: + try: + container_mgr = ContainerManager() + container_id, port = container_mgr.spawn_container(user.id, user.username) + user.container_id = container_id + user.container_port = port + db.session.commit() + except Exception as e: + flash(f'Container-Start fehlgeschlagen: {str(e)}', 'error') + return redirect(url_for('auth.login')) + + flash('Login erfolgreich!', 'success') + return redirect(url_for('dashboard')) + else: + flash('Ungültige Anmeldedaten', 'error') + + return render_template('login.html') + +@auth_bp.route('/signup', methods=['GET', 'POST']) +def signup(): + """User-Registrierung""" + if request.method == 'POST': + username = request.form.get('username') + email = request.form.get('email') + password = request.form.get('password') + + # Prüfe ob User existiert + if User.query.filter_by(username=username).first(): + flash('Username bereits vergeben', 'error') + return redirect(url_for('auth.signup')) + + if User.query.filter_by(email=email).first(): + flash('Email bereits registriert', 'error') + return redirect(url_for('auth.signup')) + + # Neuen User anlegen + user = User(username=username, email=email) + user.set_password(password) + db.session.add(user) + db.session.commit() + + # Container aus Template bauen und starten + try: + container_mgr = ContainerManager() + container_id, port = container_mgr.spawn_container(user.id, user.username) + user.container_id = container_id + user.container_port = port + db.session.commit() + + flash('Registrierung erfolgreich! Container wird gestartet...', 'success') + login_user(user) + return redirect(url_for('dashboard')) + + except Exception as e: + db.session.delete(user) + db.session.commit() + flash(f'Registrierung fehlgeschlagen: {str(e)}', 'error') + + return render_template('signup.html') + +@auth_bp.route('/logout') +@login_required +def logout(): + """User-Logout""" + logout_user() + flash('Erfolgreich abgemeldet', 'success') + return redirect(url_for('auth.login')) diff --git a/config.py b/config.py new file mode 100644 index 0000000..92616fb --- /dev/null +++ b/config.py @@ -0,0 +1,113 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +class Config: + # ======================================== + # Sicherheit + # ======================================== + SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') + + # JWT-Konfiguration + JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', SECRET_KEY) + JWT_ACCESS_TOKEN_EXPIRES = int(os.getenv('JWT_ACCESS_TOKEN_EXPIRES', 3600)) # 1 Stunde + JWT_TOKEN_LOCATION = ['headers'] + JWT_HEADER_NAME = 'Authorization' + JWT_HEADER_TYPE = 'Bearer' + + # CORS-Konfiguration + CORS_ORIGINS = os.getenv('CORS_ORIGINS', 'http://localhost:3000').split(',') + + # Session-Sicherheit + SESSION_COOKIE_SECURE = os.getenv('BASE_DOMAIN', 'localhost') != 'localhost' + SESSION_COOKIE_HTTPONLY = True + SESSION_COOKIE_SAMESITE = 'Lax' + PERMANENT_SESSION_LIFETIME = 3600 # 1 Stunde + + # ======================================== + # Datenbank + # ======================================== + SQLALCHEMY_DATABASE_URI = os.getenv( + 'DATABASE_URL', + 'sqlite:////app/data/users.db' # 4 slashes: sqlite:// + /app/data/users.db + ) + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # ======================================== + # Docker-Konfiguration + # ======================================== + DOCKER_SOCKET = os.getenv('DOCKER_SOCKET', 'unix://var/run/docker.sock') + USER_TEMPLATE_IMAGE = os.getenv('USER_TEMPLATE_IMAGE', 'user-service-template:latest') + + # ======================================== + # Traefik/Domain-Konfiguration + # ======================================== + BASE_DOMAIN = os.getenv('BASE_DOMAIN', 'localhost') + SPAWNER_SUBDOMAIN = os.getenv('SPAWNER_SUBDOMAIN', 'spawner') + TRAEFIK_NETWORK = os.getenv('TRAEFIK_NETWORK', 'web') + TRAEFIK_CERTRESOLVER = os.getenv('TRAEFIK_CERTRESOLVER', 'lets-encrypt') + TRAEFIK_ENTRYPOINT = os.getenv('TRAEFIK_ENTRYPOINT', 'websecure') + + # Vollständige Spawner-URL + SPAWNER_URL = f"{SPAWNER_SUBDOMAIN}.{BASE_DOMAIN}" + + # ======================================== + # Application-Settings + # ======================================== + # HTTPS automatisch für Nicht-Localhost + PREFERRED_URL_SCHEME = 'https' if BASE_DOMAIN != 'localhost' else 'http' + + # Spawner-Port (nur für Debugging wichtig) + SPAWNER_PORT = int(os.getenv('SPAWNER_PORT', 5000)) + + # ======================================== + # Optionale Einstellungen + # ======================================== + # Logging + LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') + LOG_FILE = os.getenv('LOG_FILE', '/app/logs/spawner.log') + + # Container-Limits (für container_manager.py) + DEFAULT_MEMORY_LIMIT = os.getenv('DEFAULT_MEMORY_LIMIT', '512m') + DEFAULT_CPU_QUOTA = int(os.getenv('DEFAULT_CPU_QUOTA', 50000)) # 0.5 CPU + + # Container-Cleanup + CONTAINER_IDLE_TIMEOUT = int(os.getenv('CONTAINER_IDLE_TIMEOUT', 3600)) # 1h in Sekunden + + +class DevelopmentConfig(Config): + """Konfiguration für Entwicklung""" + DEBUG = True + TESTING = False + + +class ProductionConfig(Config): + """Konfiguration für Produktion""" + DEBUG = False + TESTING = False + + # Strikte Session-Sicherheit + SESSION_COOKIE_SECURE = True + + # Optional: PostgreSQL statt SQLite + # SQLALCHEMY_DATABASE_URI = os.getenv( + # 'DATABASE_URL', + # 'postgresql://spawner:password@postgres:5432/spawner' + # ) + + +class TestingConfig(Config): + """Konfiguration für Tests""" + TESTING = True + SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' + WTF_CSRF_ENABLED = False + + +# Config-Dict für einfaches Laden +config = { + 'development': DevelopmentConfig, + 'production': ProductionConfig, + 'testing': TestingConfig, + 'default': DevelopmentConfig +} diff --git a/container_manager.py b/container_manager.py new file mode 100644 index 0000000..0af9c89 --- /dev/null +++ b/container_manager.py @@ -0,0 +1,128 @@ +import requests_unixsocket +import docker +from config import Config + +class ContainerManager: + def __init__(self): + self.client = None + + def _get_client(self): + """Lazy initialization of Docker client""" + if self.client is None: + try: + # Nutze from_env() - DOCKER_HOST aus Umgebungsvariable + self.client = docker.from_env() + + except Exception as e: + raise Exception(f"Docker connection failed: {str(e)}") + return self.client + + def spawn_container(self, user_id, username): + """Spawnt einen neuen Container für den User""" + try: + existing = self._get_user_container(username) + if existing and existing.status == 'running': + return existing.id, self._get_container_port(existing) + + # Pfad-basiertes Routing: User unter coder.wieland.org/username + base_host = f"{Config.SPAWNER_SUBDOMAIN}.{Config.BASE_DOMAIN}" + + # Labels vorbereiten + labels = { + 'traefik.enable': 'true', + 'traefik.docker.network': Config.TRAEFIK_NETWORK, + + # HTTPS Router mit PathPrefix + f'traefik.http.routers.user{user_id}.rule': + f'Host(`{base_host}`) && PathPrefix(`/{username}`)', + f'traefik.http.routers.user{user_id}.entrypoints': Config.TRAEFIK_ENTRYPOINT, + f'traefik.http.routers.user{user_id}.priority': '100', + # StripPrefix Middleware - entfernt /{username} bevor Container Request erhält + f'traefik.http.routers.user{user_id}.middlewares': f'user{user_id}-strip', + f'traefik.http.middlewares.user{user_id}-strip.stripprefix.prefixes': f'/{username}', + # TLS für HTTPS + f'traefik.http.routers.user{user_id}.tls': 'true', + f'traefik.http.routers.user{user_id}.tls.certresolver': Config.TRAEFIK_CERTRESOLVER, + + # Service + f'traefik.http.services.user{user_id}.loadbalancer.server.port': '8080', + + # Metadata + 'spawner.user_id': str(user_id), + 'spawner.username': username, + 'spawner.managed': 'true' + } + + # Logging: Traefik-Labels ausgeben + print(f"[SPAWNER] Creating container user-{username}-{user_id}") + print(f"[SPAWNER] Traefik Labels:") + for key, value in labels.items(): + if 'traefik' in key: + print(f"[SPAWNER] {key}: {value}") + + container = self._get_client().containers.run( + Config.USER_TEMPLATE_IMAGE, + name=f"user-{username}-{user_id}", + detach=True, + network=Config.TRAEFIK_NETWORK, + labels=labels, + environment={ + 'USER_ID': str(user_id), + 'USERNAME': username + }, + restart_policy={'Name': 'unless-stopped'}, + mem_limit=Config.DEFAULT_MEMORY_LIMIT, + cpu_quota=Config.DEFAULT_CPU_QUOTA + ) + + print(f"[SPAWNER] Container created: {container.id[:12]}") + print(f"[SPAWNER] URL: https://{base_host}/{username}") + return container.id, 8080 + + except docker.errors.ImageNotFound as e: + error_msg = f"Template-Image '{Config.USER_TEMPLATE_IMAGE}' nicht gefunden" + print(f"[SPAWNER] ERROR: {error_msg}") + raise Exception(error_msg) + except docker.errors.APIError as e: + error_msg = f"Docker API Fehler: {str(e)}" + print(f"[SPAWNER] ERROR: {error_msg}") + raise Exception(error_msg) + except Exception as e: + print(f"[SPAWNER] ERROR: {str(e)}") + raise + + def stop_container(self, container_id): + """Stoppt einen User-Container""" + try: + container = self._get_client().containers.get(container_id) + container.stop(timeout=10) + return True + except docker.errors.NotFound: + return False + + def remove_container(self, container_id): + """Entfernt einen User-Container komplett""" + try: + container = self._get_client().containers.get(container_id) + container.remove(force=True) + return True + except docker.errors.NotFound: + return False + + def get_container_status(self, container_id): + """Gibt Status eines Containers zurück""" + try: + container = self._get_client().containers.get(container_id) + return container.status + except docker.errors.NotFound: + return 'not_found' + + def _get_user_container(self, username): + """Findet existierenden Container für User""" + filters = {'label': f'spawner.username={username}'} + containers = self._get_client().containers.list(all=True, filters=filters) + return containers[0] if containers else None + + def _get_container_port(self, container): + """Extrahiert Port aus Container-Config""" + return 8080 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..83db463 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,119 @@ +version: '3.8' + +services: + # Flask API Backend + spawner: + build: . + container_name: spawner + restart: unless-stopped + + env_file: + - .env + + ports: + - "5000:5000" # Optional: Direktzugriff für Debugging + + volumes: + # Docker-Socket für Container-Management + - /var/run/docker.sock:/var/run/docker.sock:rw + # Persistente Daten + - ./data:/app/data + # Logs + - ./logs:/app/logs + + environment: + # Aus .env-Datei + - SECRET_KEY=${SECRET_KEY} + - JWT_SECRET_KEY=${JWT_SECRET_KEY:-${SECRET_KEY}} + - BASE_DOMAIN=${BASE_DOMAIN} + - TRAEFIK_NETWORK=${TRAEFIK_NETWORK} + - USER_TEMPLATE_IMAGE=${USER_TEMPLATE_IMAGE:-user-service-template:latest} + - SPAWNER_SUBDOMAIN=${SPAWNER_SUBDOMAIN:-coder} + - CORS_ORIGINS=https://${SPAWNER_SUBDOMAIN:-coder}.${BASE_DOMAIN},http://localhost:3000 + # Traefik-Konfiguration + - TRAEFIK_CERTRESOLVER=${TRAEFIK_CERTRESOLVER:-lets-encrypt} + - TRAEFIK_ENTRYPOINT=${TRAEFIK_ENTRYPOINT:-websecure} + # Docker-Verbindung + - DOCKER_HOST=${DOCKER_HOST:-unix:///var/run/docker.sock} + + networks: + - web + + labels: + # Traefik aktivieren + - "traefik.enable=true" + - "traefik.docker.network=web" + + # API-Router (hoehere Prioritaet fuer /api/*) + - "traefik.http.routers.spawner-api.rule=Host(`${SPAWNER_SUBDOMAIN:-coder}.${BASE_DOMAIN}`) && PathPrefix(`/api`)" + - "traefik.http.routers.spawner-api.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" + - "traefik.http.routers.spawner-api.tls.certresolver=${TRAEFIK_CERTRESOLVER:-lets-encrypt}" + - "traefik.http.routers.spawner-api.priority=200" + - "traefik.http.routers.spawner-api.service=spawner-api-service" + - "traefik.http.services.spawner-api-service.loadbalancer.server.port=5000" + + # Legacy-Router fuer alte Flask-Templates (niedrige Prioritaet) + - "traefik.http.routers.spawner-legacy.rule=Host(`${SPAWNER_SUBDOMAIN:-coder}.${BASE_DOMAIN}`) && (PathPrefix(`/login`) || PathPrefix(`/signup`) || PathPrefix(`/logout`) || PathPrefix(`/dashboard`) || PathPrefix(`/container`) || PathPrefix(`/health`))" + - "traefik.http.routers.spawner-legacy.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" + - "traefik.http.routers.spawner-legacy.tls.certresolver=${TRAEFIK_CERTRESOLVER:-lets-encrypt}" + - "traefik.http.routers.spawner-legacy.priority=100" + - "traefik.http.routers.spawner-legacy.service=spawner-api-service" + + # Metadata + - "spawner.managed=true" + - "spawner.version=2.0.0" + - "spawner.type=api-service" + + # Health-Check + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + + # Next.js Frontend + frontend: + build: ./frontend + container_name: spawner-frontend + restart: unless-stopped + + environment: + - NEXT_PUBLIC_API_URL= + + networks: + - web + + labels: + # Traefik aktivieren + - "traefik.enable=true" + - "traefik.docker.network=web" + + # Frontend-Router (niedrigere Prioritaet - Catch-All) + - "traefik.http.routers.spawner-frontend.rule=Host(`${SPAWNER_SUBDOMAIN:-coder}.${BASE_DOMAIN}`)" + - "traefik.http.routers.spawner-frontend.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" + - "traefik.http.routers.spawner-frontend.tls.certresolver=${TRAEFIK_CERTRESOLVER:-lets-encrypt}" + - "traefik.http.routers.spawner-frontend.priority=50" + - "traefik.http.routers.spawner-frontend.service=spawner-frontend-service" + - "traefik.http.services.spawner-frontend-service.loadbalancer.server.port=3000" + + # Metadata + - "spawner.managed=true" + - "spawner.version=2.0.0" + - "spawner.type=frontend-service" + + # Health-Check + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + depends_on: + - spawner + +# Externes Netzwerk (von deinem Traefik bereits vorhanden) +networks: + web: + external: true diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..62af5a3 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,44 @@ +# Container Spawner - Dokumentation + +Willkommen zur Dokumentation des Container Spawner Projekts. + +## Inhaltsverzeichnis + +| Dokument | Beschreibung | +|----------|--------------| +| [Installation](install/README.md) | Neu- und Update-Installation, Umgebungsvariablen | +| [Architektur](architecture/README.md) | Vollstaendige technische Dokumentation | +| [Sicherheit](security/README.md) | Sicherheitsrisiken und Gegenmassnahmen | +| [Versionen](versions/README.md) | Versionierung und Changelog | +| [Bekannte Bugs](bugs/README.md) | Bekannte Probleme und Workarounds | +| [Best Practices](dos-n-donts/README.md) | Dos and Don'ts fuer Produktion | + +## Schnellnavigation + +### Fuer Einsteiger + +1. [Voraussetzungen](install/README.md#voraussetzungen) pruefen +2. [Neuinstallation](install/README.md#neuinstallation) durchfuehren +3. [Konfiguration](install/README.md#umgebungsvariablen) anpassen + +### Fuer Entwickler + +- [Architektur-Uebersicht](architecture/README.md#architektur) +- [Komponenten im Detail](architecture/README.md#komponenten-im-detail) +- [Traefik-Integration](architecture/README.md#traefik-integration) + +### Fuer Administratoren + +- [Sicherheits-Checkliste](security/README.md) +- [Produktions-Empfehlungen](dos-n-donts/README.md) +- [Troubleshooting](architecture/README.md#troubleshooting) + +## Projekt-Links + +- **Repository**: https://gitea.iotxs.de/RainerWieland/spawner +- **Issue Tracker**: https://gitea.iotxs.de/RainerWieland/spawner/issues + +--- + +**Aktuelle Version**: 0.1.0 +**Letzte Aktualisierung**: Januar 2026 diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..750cda4 --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,2599 @@ +--- +tags: SPAWNER unter Docker +--- + +# DAS SPAWNER-PROJEKT V0.1 +*27.01.2026 rwd* +![](https://hedgedoc.wieland.org/uploads/7aaace93-f7d9-4fe9-9553-94b2fa2e7031.png) + + +--- + +## Inhaltsverzeichnis + +1. [Projektübersicht](#projektübersicht) +2. [Architektur](#architektur) +3. [Voraussetzungen](#voraussetzungen) +4. [Installation](#installation) +5. [Konfiguration](#konfiguration) +6. [Dateistruktur](#dateistruktur) +7. [Komponenten im Detail](#komponenten-im-detail) +8. [Workflow](#workflow) +9. [Traefik-Integration](#traefik-integration) +10. [Sicherheit](#sicherheit) +11. [Deployment](#deployment) +12. [Troubleshooting](#troubleshooting) +13. [Erweiterungen](#erweiterungen) +14. [Best Practices](#best-practices) + +--- + +## Projektübersicht + +Der **Docker Container Spawner** ist eine leichtgewichtige Lösung zum automatischen Bereitstellen von isolierten Docker-Containern für einzelne Benutzer. Nach erfolgreicher Authentifizierung erhält jeder Benutzer einen eigenen Container mit einem dedizierten Webdienst, der über eine personalisierte Subdomain erreichbar ist. + +Das System basiert auf einer Flask-Architektur, die nach einer erfolgreichen Anmeldung automatisch dedizierte Container aus vordefinierten Vorlagen erstellt. Die Anbindung erfolgt über den **Reverse-Proxy Traefik**, der den Datenverkehr dynamisch über personalisierte Subdomains an die jeweiligen Dienste weiterleitet. Zu den Sicherheitsmerkmalen gehören strikte Ressourcenlimits für RAM und CPU sowie eine verschlüsselte Nutzerverwaltung via SQLite. Die Dokumentation beschreibt zudem umfassende Wartungsfunktionen wie das Lifecycle-Management von Containern und Best Practices für den produktiven Einsatz. + +Anwendungsgebiete finden sich vor allem in der Bereitstellung von +- Lernumgebungen +- Sandboxes +- SaaS-Plattformen + +--- + + +### Hauptfunktionen + +- **User-Management**: Registrierung und Login mit sicherer Passwort-Speicherung +- **Automatisches Container-Spawning**: Jeder User erhält einen eigenen Docker-Container +- **Dynamisches Routing**: Traefik routet automatisch zu den User-Containern +- **Resource-Management**: CPU- und RAM-Limits pro Container +- **Lifecycle-Management**: Starten, Stoppen und Neustarten von User-Containern +- **Template-basiert**: Neue User-Container aus vorgefertigten Images + +### Use Cases + +- **Entwicklungsumgebungen**: Isolierte Dev-Spaces für Entwickler +- **SaaS-Anwendungen**: Multi-Tenant-Webservices +- **Lernplattformen**: Übungsumgebungen für Schulungen +- **CI/CD-Pipelines**: On-Demand Build-Umgebungen +- **Sandbox-Umgebungen**: Sichere Test-Environments + +--- + +## Architektur + +### Komponenten-Übersicht + +```flow +st=>start: Browser +e=>end: End +op=>operation: Traefik +:80 / :443 +op2=>operation: Spawner Service +Flask + Docker SDK +:5000 +op3=>operation: Docker Daemon +op4=>operation: User Containers +USER-I | USER-II | USER-III | USER-nnn| + +st->op->op2->op3->op4 +``` + +### Datenfluss + +1. **Login**: User meldet sich über Web-UI an +2. **Authentication**: Flask validiert Credentials gegen SQLite-DB +3. **Container-Spawn**: Docker SDK startet neuen Container aus Template +4. **Label-Injection**: Traefik-Labels werden beim Container-Start gesetzt +5. **Auto-Discovery**: Traefik erkennt neuen Container und erstellt Route +6. **Redirect**: User wird zu persönlicher Subdomain weitergeleitet + +### Netzwerk-Architektur + +Alle Services laufen im gleichen Docker-Netzwerk (\`traefik-network\`), damit Traefik die User-Container erreichen kann: + +``` +traefik-network (bridge) +├── traefik (Reverse Proxy) +├── spawner (Management Service) +├── user-alice-1 (User Container) +├── user-bob-2 (User Container) +└── user-charlie-3 (User Container) +``` + +--- + +## Voraussetzungen + +### Hardware + +- **Min. 2 GB RAM**: Für Spawner + mehrere User-Container +- **Min. 20 GB Disk**: Für Images und Container-Volumes +- **Multi-Core CPU**: Empfohlen für parallele Container + +### Software + +- **Docker**: Version 20.10+ +- **Docker Compose**: Version 2.0+ +- **Python**: 3.11+ (im Container enthalten) +- **Traefik**: Version 2.x oder 3.x (optional, aber empfohlen) + +### Netzwerk + +- **Port 5000**: Spawner Web-UI (oder via Traefik) +- **Port 80/443**: Traefik (für User-Container-Routing) +- **Wildcard-DNS** oder \`/etc/hosts\`-Einträge für Subdomains + +--- + +## Installation + +### Schritt 1: Projekt-Setup + +```bash +# Repository erstellen +mkdir docker-spawner +cd docker-spawner + +# Verzeichnisstruktur anlegen +mkdir -p spawner/{templates,user-template,data} +cd spawner +``` + +### Schritt 2: Dateien erstellen + +Erstelle alle Dateien aus der Projektstruktur (siehe [Dateistruktur](#dateistruktur)). + +### Schritt 3: Traefik-Netzwerk erstellen + +```bash +docker network create traefik-network +``` + +### Schritt 4: User-Template-Image bauen + +```bash +cd user-template +docker build -t user-service-template:latest . +cd .. +``` + +### Schritt 5: Spawner starten + +```bash +docker-compose up -d --build +``` + +### Schritt 6: Traefik starten (falls noch nicht vorhanden) + +```bash +# Minimal Traefik docker-compose.yml +cat > traefik-compose.yml <100 User: Kubernetes mit Horizontal Pod Autoscaling empfohlen. + +### Kann ich verschiedene Services pro User bereitstellen? + +Ja! Erweitere das User-Modell um \`service_type\` und verwende verschiedene Template-Images. + +### Funktioniert das auch ohne Traefik? + +Ja! Alternativen: + +- **Nginx mit dynamischer Config-Generation** +- **HAProxy mit Runtime-API** +- **Caddy mit JSON-API** + +### Wie sichere ich den Docker-Socket ab? + +Verwende \`tecnativa/docker-socket-proxy\` mit eingeschränkten Permissions (siehe [Sicherheit](#sicherheit)). + +### Kann ich existierende User-Daten migrieren? + +Ja! Volume-Mounts verwenden und bei Migration Volumes kopieren: + +```bash +docker run --rm -v old-user-data:/from -v new-user-data:/to alpine sh -c "cp -av /from/* /to/" +``` + +--- + +## Ressourcen + +### Docker SDK Documentation + +- [Docker SDK for Python](https://docker-py.readthedocs.io/) +- [Docker Engine API](https://docs.docker.com/engine/api/) + +### Flask & Security + +- [Flask Documentation](https://flask.palletsprojects.com/) +- [Flask-Login](https://flask-login.readthedocs.io/) +- [OWASP Security Guidelines](https://owasp.org/www-project-web-security-testing-guide/) + +### Traefik + +- [Traefik Documentation](https://doc.traefik.io/traefik/) +- [Docker Provider](https://doc.traefik.io/traefik/providers/docker/) + +### Alternatives & Inspiration + +- [JupyterHub](https://github.com/jupyterhub/jupyterhub) +- [Code-Server](https://github.com/coder/code-server) +- [Gitpod](https://www.gitpod.io/) + +--- + +## Lizenz & Support + +Dieses Projekt ist ein Beispiel-Setup. Für Produktions-Einsatz: + +- **Security-Audit** durchführen +- **Load-Tests** mit erwarteter User-Anzahl +- **Backup-Strategie** implementieren +- **Monitoring** mit Prometheus/Grafana + +--- + +**Version**: 1.0.0 +**Erstellt**: Januar 2026 +**Zielgruppe**: DevOps, Platform Engineers, SaaS-Entwickler + + +--- + + +## Verzeichnisstruktur + +``` +spawner/ +├── Dockerfile +├── docker-compose.yml +├── requirements.txt +├── app.py +├── auth.py +├── container_manager.py +├── models.py +├── config.py +├── templates/ +│ ├── login.html +│ └── dashboard.html +└── user-template/ + └── Dockerfile +``` + +## Dockerfile (Spawner-Service) + +```dockerfile +FROM python:3.11-slim + +WORKDIR /app + +# System-Dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# Python-Dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Application-Code +COPY . . + +# Daten-Verzeichnisse +RUN mkdir -p /app/data /app/logs && \ + chmod 755 /app/data /app/logs + +EXPOSE 5000 + +# Health-Check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:5000/health || exit 1 + +CMD ["python", "app.py"] +``` + +## requirements.txt + +```txt +flask==3.0.0 +flask-login==0.6.3 +flask-sqlalchemy==3.1.1 +werkzeug==3.0.1 +docker==7.0.0 +PyJWT==2.8.0 +python-dotenv==1.0.0 +``` + +## config.py + +```python +import os +from dotenv import load_dotenv + +load_dotenv() + +class Config: + # ======================================== + # Sicherheit + # ======================================== + SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') + + # Session-Sicherheit + SESSION_COOKIE_SECURE = os.getenv('BASE_DOMAIN', 'localhost') != 'localhost' + SESSION_COOKIE_HTTPONLY = True + SESSION_COOKIE_SAMESITE = 'Lax' + PERMANENT_SESSION_LIFETIME = 3600 # 1 Stunde + + # ======================================== + # Datenbank + # ======================================== + SQLALCHEMY_DATABASE_URI = os.getenv( + 'DATABASE_URL', + 'sqlite:///data/users.db' + ) + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # ======================================== + # Docker-Konfiguration + # ======================================== + DOCKER_SOCKET = os.getenv('DOCKER_SOCKET', 'unix://var/run/docker.sock') + USER_TEMPLATE_IMAGE = os.getenv('USER_TEMPLATE_IMAGE', 'user-service-template:latest') + + # ======================================== + # Traefik/Domain-Konfiguration + # ======================================== + BASE_DOMAIN = os.getenv('BASE_DOMAIN', 'localhost') + SPAWNER_SUBDOMAIN = os.getenv('SPAWNER_SUBDOMAIN', 'spawner') # ← FEHLTE! + TRAEFIK_NETWORK = os.getenv('TRAEFIK_NETWORK', 'web') + + # Vollständige Spawner-URL + SPAWNER_URL = f"{SPAWNER_SUBDOMAIN}.{BASE_DOMAIN}" + + # ======================================== + # Application-Settings + # ======================================== + # HTTPS automatisch für Nicht-Localhost + PREFERRED_URL_SCHEME = 'https' if BASE_DOMAIN != 'localhost' else 'http' + + # Spawner-Port (nur für Debugging wichtig) + SPAWNER_PORT = int(os.getenv('SPAWNER_PORT', 5000)) + + # ======================================== + # Optionale Einstellungen + # ======================================== + # Logging + LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') + LOG_FILE = os.getenv('LOG_FILE', '/app/logs/spawner.log') + + # Container-Limits (für container_manager.py) + DEFAULT_MEMORY_LIMIT = os.getenv('DEFAULT_MEMORY_LIMIT', '512m') + DEFAULT_CPU_QUOTA = int(os.getenv('DEFAULT_CPU_QUOTA', 50000)) # 0.5 CPU + + # Container-Cleanup + CONTAINER_IDLE_TIMEOUT = int(os.getenv('CONTAINER_IDLE_TIMEOUT', 3600)) # 1h in Sekunden + + +class DevelopmentConfig(Config): + """Konfiguration für Entwicklung""" + DEBUG = True + TESTING = False + + +class ProductionConfig(Config): + """Konfiguration für Produktion""" + DEBUG = False + TESTING = False + + # Strikte Session-Sicherheit + SESSION_COOKIE_SECURE = True + + # Optional: PostgreSQL statt SQLite + # SQLALCHEMY_DATABASE_URI = os.getenv( + # 'DATABASE_URL', + # 'postgresql://spawner:password@postgres:5432/spawner' + # ) + + +class TestingConfig(Config): + """Konfiguration für Tests""" + TESTING = True + SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' + WTF_CSRF_ENABLED = False + + +# Config-Dict für einfaches Laden +config = { + 'development': DevelopmentConfig, + 'production': ProductionConfig, + 'testing': TestingConfig, + 'default': DevelopmentConfig +} + +``` + +## models.py + +```python +from flask_sqlalchemy import SQLAlchemy +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash +from datetime import datetime + +db = SQLAlchemy() + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) + password_hash = db.Column(db.String(200), nullable=False) + container_id = db.Column(db.String(100), nullable=True) + container_port = db.Column(db.Integer, nullable=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) +``` + +## container_manager.py + +```python +import docker +from config import Config + +class ContainerManager: + def __init__(self): + self.client = docker.from_env() + + def spawn_container(self, user_id, username): + """Spawnt einen neuen Container für den User""" + try: + existing = self._get_user_container(username) + if existing and existing.status == 'running': + return existing.id, self._get_container_port(existing) + + # User-Container-Subdomain (OHNE spawner-subdomain!) + user_domain = f"{username}.{Config.BASE_DOMAIN}" + + container = self.client.containers.run( + Config.USER_TEMPLATE_IMAGE, + name=f"user-{username}-{user_id}", + detach=True, + network=Config.TRAEFIK_NETWORK, + labels={ + 'traefik.enable': 'true', + + # HTTP Router + f'traefik.http.routers.user{user_id}.rule': + f'Host(`{user_domain}`)', + f'traefik.http.routers.user{user_id}.entrypoints': 'web', + + # HTTPS Router (auskommentiert für initiale Tests) + # f'traefik.http.routers.user{user_id}-secure.rule': + # f'Host(`{user_domain}`)', + # f'traefik.http.routers.user{user_id}-secure.entrypoints': 'websecure', + # f'traefik.http.routers.user{user_id}-secure.tls.certresolver': 'hetzner', + + # Service + f'traefik.http.services.user{user_id}.loadbalancer.server.port': '8080', + + # Metadata + 'spawner.user_id': str(user_id), + 'spawner.username': username, + 'spawner.managed': 'true' + }, + environment={ + 'USER_ID': str(user_id), + 'USERNAME': username + }, + restart_policy={'Name': 'unless-stopped'}, + mem_limit=Config.DEFAULT_MEMORY_LIMIT, + cpu_quota=Config.DEFAULT_CPU_QUOTA + ) + + return container.id, 8080 + + except docker.errors.ImageNotFound: + raise Exception(f"Template-Image '{Config.USER_TEMPLATE_IMAGE}' nicht gefunden") + except docker.errors.APIError as e: + raise Exception(f"Docker API Fehler: {str(e)}") + + def stop_container(self, container_id): + """Stoppt einen User-Container""" + try: + container = self.client.containers.get(container_id) + container.stop(timeout=10) + return True + except docker.errors.NotFound: + return False + + def remove_container(self, container_id): + """Entfernt einen User-Container komplett""" + try: + container = self.client.containers.get(container_id) + container.remove(force=True) + return True + except docker.errors.NotFound: + return False + + def get_container_status(self, container_id): + """Gibt Status eines Containers zurück""" + try: + container = self.client.containers.get(container_id) + return container.status + except docker.errors.NotFound: + return 'not_found' + + def _get_user_container(self, username): + """Findet existierenden Container für User""" + filters = {'label': f'spawner.username={username}'} + containers = self.client.containers.list(all=True, filters=filters) + return containers[0] if containers else None + + def _get_container_port(self, container): + """Extrahiert Port aus Container-Config""" + return 8080 +``` + +## auth.py + +```python +from flask import Blueprint, render_template, redirect, url_for, request, flash +from flask_login import login_user, logout_user, login_required, current_user +from models import db, User +from container_manager import ContainerManager + +auth_bp = Blueprint('auth', __name__) +container_mgr = ContainerManager() + +@auth_bp.route('/login', methods=['GET', 'POST']) +def login(): + """User-Login""" + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + + user = User.query.filter_by(username=username).first() + + if user and user.check_password(password): + login_user(user) + + # Container spawnen wenn noch nicht vorhanden + if not user.container_id: + try: + container_id, port = container_mgr.spawn_container(user.id, user.username) + user.container_id = container_id + user.container_port = port + db.session.commit() + except Exception as e: + flash(f'Container-Start fehlgeschlagen: {str(e)}', 'error') + return redirect(url_for('auth.login')) + + flash('Login erfolgreich!', 'success') + return redirect(url_for('dashboard')) + else: + flash('Ungültige Anmeldedaten', 'error') + + return render_template('login.html') + +@auth_bp.route('/signup', methods=['GET', 'POST']) +def signup(): + """User-Registrierung""" + if request.method == 'POST': + username = request.form.get('username') + email = request.form.get('email') + password = request.form.get('password') + + # Prüfe ob User existiert + if User.query.filter_by(username=username).first(): + flash('Username bereits vergeben', 'error') + return redirect(url_for('auth.signup')) + + if User.query.filter_by(email=email).first(): + flash('Email bereits registriert', 'error') + return redirect(url_for('auth.signup')) + + # Neuen User anlegen + user = User(username=username, email=email) + user.set_password(password) + db.session.add(user) + db.session.commit() + + # Container aus Template bauen und starten + try: + container_id, port = container_mgr.spawn_container(user.id, user.username) + user.container_id = container_id + user.container_port = port + db.session.commit() + + flash('Registrierung erfolgreich! Container wird gestartet...', 'success') + login_user(user) + return redirect(url_for('dashboard')) + + except Exception as e: + db.session.delete(user) + db.session.commit() + flash(f'Registrierung fehlgeschlagen: {str(e)}', 'error') + + return render_template('signup.html') + +@auth_bp.route('/logout') +@login_required +def logout(): + """User-Logout""" + logout_user() + flash('Erfolgreich abgemeldet', 'success') + return redirect(url_for('auth.login')) +``` + +## app.py + +```python +from flask import Flask, render_template, redirect, url_for +from flask_login import LoginManager, login_required, current_user +from models import db, User +from auth import auth_bp +from config import Config +from container_manager import ContainerManager + +# Flask-App initialisieren +app = Flask(__name__) +app.config.from_object(Config) + +# Datenbank initialisieren +db.init_app(app) + +# Flask-Login initialisieren +login_manager = LoginManager() +login_manager.init_app(app) +login_manager.login_view = 'auth.login' +login_manager.login_message = 'Bitte melde dich an, um auf diese Seite zuzugreifen.' +login_manager.login_message_category = 'error' + +# Blueprint registrieren +app.register_blueprint(auth_bp) + +@login_manager.user_loader +def load_user(user_id): + """Lädt User für Flask-Login""" + return User.query.get(int(user_id)) + +@app.route('/') +def index(): + """Startseite - Redirect zu Dashboard oder Login""" + if current_user.is_authenticated: + return redirect(url_for('dashboard')) + return redirect(url_for('auth.login')) + +@app.route('/dashboard') +@login_required +def dashboard(): + """Dashboard - zeigt Container-Status und Service-URL""" + container_mgr = ContainerManager() + container_status = 'unknown' + + if current_user.container_id: + container_status = container_mgr.get_container_status(current_user.container_id) + + # Service-URL für den User + scheme = app.config['PREFERRED_URL_SCHEME'] + service_url = f"{scheme}://{current_user.username}.{app.config['BASE_DOMAIN']}" + + return render_template('dashboard.html', + user=current_user, + service_url=service_url, + container_status=container_status) + +@app.route('/container/restart') +@login_required +def restart_container(): + """Startet Container des Users neu""" + container_mgr = ContainerManager() + + # Alten Container stoppen falls vorhanden + if current_user.container_id: + container_mgr.stop_container(current_user.container_id) + container_mgr.remove_container(current_user.container_id) + + # Neuen Container starten + try: + container_id, port = container_mgr.spawn_container(current_user.id, current_user.username) + current_user.container_id = container_id + current_user.container_port = port + db.session.commit() + except Exception as e: + app.logger.error(f"Container-Restart fehlgeschlagen: {str(e)}") + + return redirect(url_for('dashboard')) + +@app.route('/health') +def health(): + """Health-Check für Docker und Monitoring""" + try: + # DB-Check + db.session.execute('SELECT 1') + db_status = 'ok' + except Exception as e: + db_status = f'error: {str(e)}' + + try: + # Docker-Check + container_mgr = ContainerManager() + container_mgr.client.ping() + docker_status = 'ok' + except Exception as e: + docker_status = f'error: {str(e)}' + + status_code = 200 if db_status == 'ok' and docker_status == 'ok' else 503 + + return { + 'status': 'healthy' if status_code == 200 else 'unhealthy', + 'database': db_status, + 'docker': docker_status, + 'version': '1.0.0' + }, status_code + +# Datenbank-Tabellen erstellen beim ersten Start +with app.app_context(): + db.create_all() + app.logger.info('Datenbank-Tabellen erstellt') + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=False) +``` + +## docker-compose.yml + +```yaml +version: '3.8' + +services: + spawner: + build: . + container_name: spawner + restart: unless-stopped + + ports: + - "5000:5000" # Optional: Direktzugriff für Debugging + + volumes: + # Docker-Socket für Container-Management + - /var/run/docker.sock:/var/run/docker.sock:rw + # Persistente Daten + - ./data:/app/data + # Logs + - ./logs:/app/logs + + environment: + # Aus .env-Datei + - SECRET_KEY=${SECRET_KEY} + - BASE_DOMAIN=${BASE_DOMAIN} + - TRAEFIK_NETWORK=${TRAEFIK_NETWORK} + - USER_TEMPLATE_IMAGE=${USER_TEMPLATE_IMAGE:-user-service-template:latest} + - SPAWNER_SUBDOMAIN=${SPAWNER_SUBDOMAIN:-spawner} + + networks: + - web # ⚠️ Dein bestehendes Traefik-Netzwerk! + + labels: + # Traefik aktivieren + - "traefik.enable=true" + + # HTTP Router + - "traefik.http.routers.spawner.rule=Host(`${SPAWNER_SUBDOMAIN}.${BASE_DOMAIN}`)" + - "traefik.http.routers.spawner.entrypoints=web" + - "traefik.http.services.spawner.loadbalancer.server.port=5000" + + # Metadata für Management + - "spawner.managed=true" + - "spawner.version=1.0.0" + - "spawner.type=management-service" + + # Health-Check + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + +# Externes Netzwerk (von deinem Traefik bereits vorhanden) +networks: + web: + external: true +``` + +## user-template/Dockerfile (Template für User-Container) + +```dockerfile +FROM nginxinc/nginx-unprivileged:alpine + +# Beispiel: Einfacher Webserver pro User +# HTML direkt in den Container schreiben +RUN echo '

Dein persönlicher Service

' > /usr/share/nginx/html/index.html + +EXPOSE 8080 + +CMD ["nginx", "-g", "daemon off;"] +``` + +**Hinweis**: Verwende `nginx-unprivileged` statt `nginx` für bessere Sicherheit (kein root-Prozess). +Container läuft auf Port 8080 (als unprivileged user). + +## templates/login.html + +```html + + + + Login - Spawner + + +

Login

+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +

{{ message }}

+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+
+ +
+

Noch kein Account? Registrieren

+ + +``` + +## templates/dashboard.html + +```html + + + + Dashboard - {{ user.username }} + + +

Willkommen, {{ user.username }}!

+ +

Container-Status: {{ container_status }}

+

Dein Service: {{ service_url }}

+ + Container neu starten
+ Logout + + +``` + +## Starten + +```bash +# Im spawner/ Verzeichnis: +docker-compose up --build +``` + +Die Lösung enthält: +- **Vollständige Authentifizierung** mit Flask-Login und gehashten Passwörtern +- **Automatisches Container-Spawning** via Docker SDK [docs.docker](https://docs.docker.com/reference/api/engine/sdk/examples/) +- **Traefik-Integration** über Labels [brunoscheufler](https://brunoscheufler.com/blog/2022-04-17-routing-traffic-for-dynamic-deployments-using-traefik) +- **Resource-Limits** (RAM/CPU) pro User-Container +- **Persistente Datenbank** für User-Management +- **Template-System** für neue User-Container + +Der Spawner-Service benötigt Zugriff auf `/var/run/docker.sock`, um Container zu steuern. + +--- + +# SPAWNER Integration in bestehende Traefik-Umgebung +## Step-by-Step Implementierungsplan + +--- + +## 🎯 Zielsetzung + +Integration des SPAWNER-Systems in eine bestehende Docker-Infrastruktur mit Traefik als Reverse Proxy, ohne bestehende Services zu beeinträchtigen. + +--- + +## 📋 Voraussetzungen prüfen + +### ✅ Checkliste vor Start + +- [ ] Traefik läuft und ist erreichbar +- [ ] Docker-Version ≥ 20.10 +- [ ] Freier Port für Spawner (Standard: 5000) +- [ ] Mindestens 2 GB freier RAM +- [ ] Wildcard-DNS oder manuelle DNS-Einträge möglich +- [ ] Zugriff auf `/var/run/docker.sock` +- [ ] Git installiert (zum Klonen/Erstellen der Dateien) + +--- + +## Phase 1: Analyse der bestehenden Umgebung + +### Step 1.1: Traefik-Konfiguration ermitteln + +**Aktion**: Bestehende Traefik-Setup analysieren + +```bash +# Traefik-Container finden +docker ps | grep traefik + +# Traefik-Konfiguration anzeigen +docker inspect | jq '.[0].Config.Labels' +docker inspect | jq '.[0].HostConfig.Binds' + +# Verwendetes Netzwerk ermitteln +docker inspect | jq '.[0].NetworkSettings.Networks' +``` + +**Dokumentieren**: +- Traefik-Version: _______________ +- Netzwerk-Name: _______________ +- EntryPoints: _______________ +- Zertifikats-Resolver (falls HTTPS): _______________ + +```bash=1 + docker ps | grep traefik +81e0f2d0f8c0 traefik:v3.6.5 +``` +```config=1 +docker inspect 81e0f2d0f8c0 | jq '.[0].Config.Labels' +{ + "com.docker.compose.config-hash": "aeb95d30dd87fd499dd7207ef416f97a6c325227615a2ccdae20278b5f70f51c", + "com.docker.compose.container-number": "1", + "com.docker.compose.depends_on": "", + "com.docker.compose.image": "sha256:0fb158a64eaac3b411525e180705dbb4e120d078150b6a795e120e6b80e81b02", + "com.docker.compose.oneoff": "False", + "com.docker.compose.project": "traefik", + "com.docker.compose.project.config_files": "/volume1/docker/traefik/docker-compose.yml", + "com.docker.compose.project.working_dir": "/volume1/docker/traefik", + "com.docker.compose.service": "traefik", + "com.docker.compose.version": "2.20.1", + "org.opencontainers.image.description": "A modern reverse-proxy", + "org.opencontainers.image.documentation": "https://docs.traefik.io", + "org.opencontainers.image.source": "https://github.com/traefik/traefik", + "org.opencontainers.image.title": "Traefik", + "org.opencontainers.image.url": "https://traefik.io", + "org.opencontainers.image.vendor": "Traefik Labs", + "org.opencontainers.image.version": "v3.6.5" +} +``` + +```config=1 +docker inspect 81e0f2d0f8c0 | jq '.[0].HostConfig.Binds' +[ + "/var/run/docker.sock:/var/run/docker.sock:rw", + "/volume1/docker/traefik/traefik.toml:/traefik.toml:rw", + "/volume1/docker/traefik/traefik_dynamic.toml:/traefik_dynamic.toml:rw", + "/volume1/docker/traefik/acme.json:/acme.json:rw" +] +``` + +```config=1 +docker inspect 81e0f2d0f8c0 | jq '.[0].NetworkSettings.Networks' +{ + "web": { + "IPAMConfig": null, + "Links": null, + "Aliases": [ + "traefik", + "traefik", + "81e0f2d0f8c0" + ], + "NetworkID": "79c8e53a1b0d38b655e769918c2ecfccf049461f0e1fe276362ccc1c13869aa3", + "EndpointID": "95e639cc48ced9bb06d58fd501bbf850bbe64e6050d5de75700ded13bdb1c4d4", + "Gateway": "192.168.16.1", + "IPAddress": "192.168.16.6", + "IPPrefixLen": 24, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:c0:a8:10:06", + "DriverOpts": null + } +} +``` + +### Step 1.2: Netzwerk-Topologie verstehen + +```bash +# Alle Docker-Netzwerke auflisten +docker network ls + +# Netzwerk-Details des Traefik-Netzwerks +docker network inspect + +# Welche Container sind bereits angeschlossen? +docker network inspect | jq '.[0].Containers' +``` + +```bash=1 +docker network ls +NETWORK ID NAME DRIVER SCOPE +37f47c9e1943 bridge bridge local +3ab114f137fa dokploy_dokploy_internal bridge local +04ae90e99953 host host local +208dfb8d38b0 jupyterhub bridge local +ed620451f21c none null local +79c8e53a1b0d web bridge local +``` + +**Entscheidung**: +- Existierendes Netzwerk nutzen: **Ja** ☐ / **Nein** ☐ +- Netzwerk-Name: `_______________` + +:::success +**Existierendes Netzwerk nutzen:** ✔ +**Netzwerk-Name: web** +::: +### Step 1.3: Domain-Strategie festlegen + +**Optionen**: + +**A) Subdomains pro User** (empfohlen) +``` +alice.spawner.example.com +bob.spawner.example.com +charlie.spawner.example.com +``` + +**B) Path-basiert** +``` +spawner.example.com/alice +spawner.example.com/bob +spawner.example.com/charlie +``` + + +**Gewählte Strategie**: ☐ A ☐ B +:::success +**B) Path-basiert** +::: +**Base-Domain**: `_______________` +:::success +**Base-Domain: coder.wieland.org** +::: +### Step 1.4: Traefik-Dashboard prüfen + +```bash +# Ist Dashboard aktiviert? +docker exec cat /etc/traefik/traefik.yml | grep -A5 "api:" + +# Dashboard-URL (Standard: Port 8080) +firefox http://:8080 +``` + +**Notiz**: Dashboard-URL für Monitoring: `_______________` + +--- + +## Phase 2: Projekt-Setup + +### Step 2.1: Projektverzeichnis erstellen + +```bash +# Zu deinem Docker-Projekten-Verzeichnis wechseln +cd /path/to/docker/projects # z.B. ~/docker oder /opt/docker + +# Spawner-Verzeichnis erstellen +mkdir -p spawner/{templates,user-template,data,logs} +cd spawner + +# Berechtigungen setzen +chmod 755 . +``` + +**Pfad dokumentieren**: `_______________` + +### Step 2.2: Core-Dateien erstellen + +```bash +# Python-Dateien +touch app.py auth.py container_manager.py models.py config.py + +# Docker-Dateien +touch Dockerfile docker-compose.yml .env .dockerignore + +# Templates +touch templates/login.html templates/signup.html templates/dashboard.html + +# User-Template +touch user-template/Dockerfile + +# README +touch README.md +``` + +### Step 2.3: .dockerignore erstellen + +```bash +cat > .dockerignore << 'EOF' +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +.env +.venv +venv/ +data/*.db +logs/*.log +.git +.gitignore +.DS_Store +*.md +EOF +``` + +### Step 2.4: requirements.txt erstellen + +```bash +cat > requirements.txt << 'EOF' +flask==3.0.0 +flask-login==0.6.3 +flask-sqlalchemy==3.1.1 +werkzeug==3.0.1 +docker==7.0.0 +PyJWT==2.8.0 +python-dotenv==1.0.0 +EOF +``` + +--- + +## Phase 3: Anpassung an deine Umgebung + +### Step 3.1: .env-Datei konfigurieren + +Erstelle `.env` mit folgenden Variablen (anpassen!): + +``` +SECRET_KEY=ÄNDERE_MICH_ZU_RANDOM_STRING +BASE_DOMAIN=spawner.example.com +TRAEFIK_NETWORK=traefik-network +USER_TEMPLATE_IMAGE=user-service-template:latest +SPAWNER_PORT=5000 +``` + +SECRET_KEY generieren: +```bash +python3 -c "import secrets; print(secrets.token_hex(32))" +``` + +### Step 3.2: docker-compose.yml erstellen + +Vollständiges Beispiel mit Traefik-Labels - siehe Dokumentation. + +**Wichtige Anpassungen**: +- Netzwerk-Name +- EntryPoint-Name +- Domain-Name +- Port-Konflikte prüfen + +### Step 3.3: Traefik-Konfiguration erweitern + +Prüfe ob Docker-Provider aktiviert: + +```bash +docker exec cat /etc/traefik/traefik.yml +``` + +Falls Docker-Provider fehlt, ergänzen und Traefik neu starten. + +--- + +--- + +## Phase 4: User-Template vorbereiten + +### Step 4.1: Template-Dockerfile erstellen + +Im Verzeichnis `user-template/`: + +```dockerfile +FROM nginxinc/nginx-unprivileged:alpine + +# HTML kopieren UND Ownership setzen +COPY --chown=nginx:nginx index.html /usr/share/nginx/html/index.html + +EXPOSE 8080 +``` + +### Step 4.2: Beispiel index.html + +Erstelle eine einfache HTML-Seite für User-Container. + +### Step 4.3: Template-Image bauen + +```bash +cd user-template +docker build -t user-service-template:latest . + +# Test +docker run -d -p 8080:8080 --name test-user user-service-template:latest +curl http://localhost:8080 +docker stop test-user && docker rm test-user + +cd .. +``` + +--- + +## Phase 5: Spawner bauen und testen + +### Step 5.1: Alle Python-Dateien erstellen + +Kopiere die Code-Beispiele aus der Dokumentation: +- config.py +- models.py +- container_manager.py +- auth.py +- app.py + +### Step 5.2: Dockerfile erstellen + +```dockerfile +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && \ + apt-get install -y --no-install-recommends curl ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN mkdir -p /app/data /app/logs && chmod 755 /app/data /app/logs + +EXPOSE 5000 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:5000/health || exit 1 + +CMD ["python", "app.py"] +``` + +### Step 5.3: Spawner bauen + +```bash +docker-compose build +``` + +### Step 5.4: Test-Start + +```bash +docker-compose up -d +docker-compose logs -f spawner + +# Health-Check +curl http://localhost:5000/health + +# Login-Seite +curl http://localhost:5000/login +``` + +### Step 5.5: Erste Test-Registrierung + +```bash +firefox http://localhost:5000 +``` + +Registriere einen Test-User und prüfe ob Container spawnt: + +```bash +docker ps | grep user- +docker inspect user-testuser-1 | jq '.[0].Config.Labels' +``` + +--- + +## Phase 6: Traefik-Integration aktivieren + +### Step 6.1: Netzwerk verbinden + +```bash +# Falls noch nicht verbunden +docker network connect traefik-network spawner + +# Verifizieren +docker network inspect traefik-network | grep spawner +``` + +### Step 6.2: DNS konfigurieren + +**Lokal testen** (Option A): +```bash +sudo nano /etc/hosts + +# Hinzufügen: +127.0.0.1 spawner.localhost +127.0.0.1 testuser.localhost +``` + +**Produktion** (Option B): +- Wildcard-DNS-Eintrag erstellen: `*.spawner.example.com → ` +- DNS-Propagation abwarten + +### Step 6.3: Traefik-Routing testen + +```bash +# Traefik-Dashboard öffnen +firefox http://:8080 + +# Routes prüfen unter HTTP → Routers + +# Mit curl testen +curl -H "Host: spawner.localhost" http://localhost/ +curl -H "Host: testuser.localhost" http://localhost/ +``` + +### Step 6.4: End-to-End Test + +1. Spawner-UI aufrufen +2. Neuen User registrieren +3. Zum Dashboard navigieren +4. Service-Link klicken → User-Container sollte erreichbar sein + +--- + +## Phase 7: HTTPS aktivieren (optional) + +### Step 7.1: Let's Encrypt konfigurieren + +Prüfe Traefik-Config für certificatesResolvers. + +### Step 7.2: Labels für HTTPS anpassen + +In `docker-compose.yml` und `container_manager.py` HTTPS-Labels ergänzen: +- entrypoints: websecure +- tls.certresolver: letsencrypt + +### Step 7.3: Spawner neu starten + +```bash +docker-compose down +docker-compose up -d --build +``` + +### Step 7.4: HTTPS testen + +```bash +firefox https://spawner.example.com + +# Zertifikat prüfen +openssl s_client -connect spawner.example.com:443 +``` + +--- + +## Phase 8: Monitoring & Observability + +### Step 8.1: Logging aktivieren + +Strukturiertes Logging in app.py implementieren. + +### Step 8.2: Monitoring-Script + +```bash +cat > monitor.sh << 'EOF' +#!/bin/bash +echo "=== SPAWNER Statistics ===" +docker stats spawner --no-stream +docker ps --filter "label=spawner.user_id" +docker stats $(docker ps --filter "label=spawner.user_id" -q) --no-stream +EOF + +chmod +x monitor.sh +./monitor.sh +``` + +### Step 8.3: Backup-Strategie + +```bash +cat > backup.sh << 'EOF' +#!/bin/bash +BACKUP_DIR="/backup/spawner" +DATE=$(date +%Y%m%d_%H%M%S) +mkdir -p $BACKUP_DIR +docker exec spawner sqlite3 /app/data/users.db ".backup '/app/data/backup_${DATE}.db'" +cp data/backup_${DATE}.db $BACKUP_DIR/ +find $BACKUP_DIR -name "backup_*.db" -mtime +7 -delete +EOF + +chmod +x backup.sh + +# Cronjob +crontab -e +# 0 2 * * * /path/to/spawner/backup.sh +``` + +--- + +## Phase 9: Produktions-Optimierung + +### Step 9.1: Ressourcen-Limits anpassen + +In `container_manager.py` basierend auf deiner Hardware. + +### Step 9.2: Container-Cleanup + +Script für automatisches Aufräumen alter Container. + +### Step 9.3: PostgreSQL statt SQLite + +Für Produktion docker-compose.yml um PostgreSQL erweitern. + +### Step 9.4: Rate-Limiting + +Flask-Limiter installieren und konfigurieren. + +--- + +## Phase 10: Go-Live + +### Step 10.1: Load-Test + +```bash +ab -n 1000 -c 10 http://spawner.example.com/login + +# Multi-User Test +for i in {1..10}; do + curl -X POST http://spawner.example.com/signup \ + -d "username=loadtest${i}&email=test${i}@example.com&password=test123" + sleep 2 +done +``` + +### Step 10.2: Security-Audit + +- SECRET_KEY stark +- HTTPS erzwungen +- Rate-Limiting aktiv +- Container-Isolation +- Non-Root-User + +```bash +# Vulnerability-Scan +docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy image spawner:latest +``` + +### Step 10.3: Go-Live Checklist + +- [ ] Alle Services erreichbar +- [ ] HTTPS funktioniert +- [ ] DNS korrekt +- [ ] Backups laufen +- [ ] Monitoring aktiv +- [ ] Logs werden geschrieben +- [ ] Load-Tests bestanden +- [ ] Security-Audit durchgeführt +- [ ] Team informiert + +```bash +# Finaler Health-Check +curl -f https://spawner.example.com/health + +# Container-Count +docker ps --filter 'label=spawner.managed=true' -q | wc -l +``` + +**🎉 GO-LIVE!** + +--- + +## 🚨 Troubleshooting + +### Traefik findet Spawner nicht +- Netzwerk-Verbindung prüfen +- Labels verifizieren +- Traefik-Logs checken + +### User-Container startet nicht +- Template-Image existiert? +- Docker-Socket-Permissions +- Netzwerk vorhanden? + +### DNS funktioniert nicht +- Wildcard-DNS konfiguriert? +- /etc/hosts für lokale Tests +- DNS-Propagation abwarten + +### Container-Spawn schlägt fehl +- Docker-API-Zugriff testen +- Socket-Mount prüfen +- Permissions checken + +--- + +## 📊 Post-Integration + +### Wöchentliches Monitoring + +```bash +# Container-Anzahl +docker ps --filter "label=spawner.managed=true" + +# Ressourcen +docker stats --no-stream + +# Disk-Space +docker system df +``` + +### Metriken + +- Anzahl User +- Aktive Container +- CPU/RAM-Auslastung +- Netzwerk-Traffic + +--- + +## 🎓 Next Steps + +1. User-Feedback sammeln +2. Templates erweitern (Python, Node.js) +3. Admin-Dashboard entwickeln +4. Auto-Shutdown implementieren +5. Volume-Persistenz aktivieren +6. Multi-Region deployment + +--- + +**Integration abgeschlossen!** 🚀 + +Bei Fragen: +- Logs: `docker-compose logs -f` +- Traefik: `http://:8080` +- Health: `curl https://spawner.example.com/health` diff --git a/docs/bugs/README.md b/docs/bugs/README.md new file mode 100644 index 0000000..86ab06b --- /dev/null +++ b/docs/bugs/README.md @@ -0,0 +1,248 @@ +# Bekannte Bugs und Limitationen + +Aktuelle bekannte Probleme und moegliche Workarounds. + +## Inhaltsverzeichnis + +- [Bekannte Limitationen](#bekannte-limitationen) +- [Bekannte Bugs](#bekannte-bugs) +- [Workarounds](#workarounds) +- [Issue Tracker](#issue-tracker) + +--- + +## Bekannte Limitationen + +### Container Auto-Shutdown nicht implementiert + +**Status**: Geplant fuer v0.2.0 + +**Beschreibung**: Die Variable `CONTAINER_IDLE_TIMEOUT` ist definiert, aber die Logik zum automatischen Stoppen inaktiver Container fehlt noch. + +**Auswirkung**: Container laufen unbegrenzt weiter, auch bei Inaktivitaet. + +**Workaround**: Manuelles Aufraumen mit Cron-Job: + +```bash +# cleanup-idle.sh +#!/bin/bash +# Container die aelter als 24h sind und keinen Traffic haben +docker ps --filter 'label=spawner.managed=true' \ + --format '{{.ID}} {{.RunningFor}}' | \ + grep -E 'days|weeks' | \ + awk '{print $1}' | \ + xargs -r docker stop +``` + +--- + +### Keine Volume-Persistenz + +**Status**: Geplant fuer v0.2.0 + +**Beschreibung**: User-Daten in Containern gehen bei Neustart verloren. + +**Auswirkung**: Alle Dateien die ein User im Container erstellt werden bei Restart geloescht. + +**Workaround**: Volume-Mounts manuell in `container_manager.py` hinzufuegen: + +```python +# In spawn_container() +volumes = { + f'/data/users/{username}': { + 'bind': '/app/data', + 'mode': 'rw' + } +} +``` + +--- + +### Kein Multi-Template-Support + +**Status**: Geplant fuer v1.0.0 + +**Beschreibung**: Alle User erhalten das gleiche Container-Template. + +**Auswirkung**: Keine Moeglichkeit verschiedene Umgebungen anzubieten (z.B. Python, Node.js). + +**Workaround**: Mehrere Spawner-Instanzen mit unterschiedlichen `USER_TEMPLATE_IMAGE` Werten. + +--- + +### Minimale Input-Validierung + +**Status**: Bekannt + +**Beschreibung**: Username und Email werden nur minimal validiert. + +**Auswirkung**: Potenzielle Injection-Risiken bei speziellen Zeichen. + +**Workaround**: Siehe [Sicherheits-Dokumentation](../security/README.md#input-validierung) + +--- + +### Kein Rate-Limiting + +**Status**: Geplant fuer v1.0.0 + +**Beschreibung**: Keine Begrenzung von Login-Versuchen oder API-Aufrufen. + +**Auswirkung**: Anfaellig fuer Brute-Force-Angriffe. + +**Workaround**: Rate-Limiting via Traefik: + +```yaml +# In docker-compose.yml Labels +labels: + - "traefik.http.middlewares.ratelimit.ratelimit.average=10" + - "traefik.http.middlewares.ratelimit.ratelimit.burst=20" + - "traefik.http.routers.spawner.middlewares=ratelimit" +``` + +--- + +### Kein Admin-Dashboard + +**Status**: Geplant fuer v0.2.0 + +**Beschreibung**: Keine Web-UI zum Verwalten von Usern und Containern. + +**Workaround**: Direkte Datenbank-/Docker-Befehle: + +```bash +# User auflisten +docker exec spawner sqlite3 /app/data/users.db "SELECT id, username, email FROM user" + +# Container auflisten +docker ps --filter 'label=spawner.managed=true' + +# Container eines Users stoppen +docker stop user-- +``` + +--- + +## Bekannte Bugs + +### BUG-001: Health-Check schlaegt bei erstem Start fehl + +**Schweregrad**: Niedrig + +**Beschreibung**: Der Health-Check kann beim allerersten Start fehlschlagen, bevor die Datenbank initialisiert ist. + +**Schritte zum Reproduzieren**: +1. Frische Installation ohne existierende DB +2. `docker-compose up -d` +3. Sofortiger Health-Check: `curl http://localhost:5000/health` + +**Erwartetes Verhalten**: 200 OK + +**Tatsaechliches Verhalten**: 503 oder Connection Refused + +**Workaround**: 5-10 Sekunden warten nach Start. + +**Status**: Akzeptiert (normales Verhalten bei Kaltstart) + +--- + +### BUG-002: Container-Neustart loescht Container-ID nicht bei Fehler + +**Schweregrad**: Mittel + +**Beschreibung**: Wenn ein Container-Spawn fehlschlaegt, bleibt die alte Container-ID im User-Record. + +**Schritte zum Reproduzieren**: +1. User hat laufenden Container +2. Admin loescht Container manuell: `docker rm -f user-xxx` +3. User klickt "Neustart" im Dashboard +4. Spawn schlaegt fehl (z.B. Image nicht gefunden) +5. Container-ID zeigt auf nicht-existierenden Container + +**Workaround**: Container-ID manuell zuruecksetzen: + +```bash +docker exec spawner sqlite3 /app/data/users.db \ + "UPDATE user SET container_id=NULL WHERE username=''" +``` + +**Status**: Fix geplant + +--- + +## Workarounds + +### Alle User-Container auflisten + +```bash +docker ps --filter 'label=spawner.managed=true' \ + --format 'table {{.Names}}\t{{.Status}}\t{{.RunningFor}}' +``` + +### Container-Ressourcen pruefen + +```bash +docker stats --filter 'label=spawner.managed=true' --no-stream +``` + +### Verwaiste Container aufraumen + +```bash +# Container ohne zugehoerigen User finden +for container in $(docker ps -q --filter 'label=spawner.managed=true'); do + username=$(docker inspect $container --format '{{index .Config.Labels "spawner.username"}}') + exists=$(docker exec spawner sqlite3 /app/data/users.db \ + "SELECT COUNT(*) FROM user WHERE username='$username'") + if [ "$exists" = "0" ]; then + echo "Verwaist: $container ($username)" + # docker rm -f $container # Zum Loeschen auskommentieren + fi +done +``` + +### Datenbank-Backup manuell erstellen + +```bash +docker exec spawner sqlite3 /app/data/users.db ".backup '/app/data/backup-$(date +%Y%m%d).db'" +docker cp spawner:/app/data/backup-*.db ./backups/ +``` + +--- + +## Issue Tracker + +Neue Bugs oder Feature-Requests bitte hier melden: + +**Repository Issues**: https://gitea.iotxs.de/RainerWieland/spawner/issues + +### Issue erstellen - Vorlage + +```markdown +## Beschreibung +[Kurze Beschreibung des Problems] + +## Schritte zum Reproduzieren +1. [Erster Schritt] +2. [Zweiter Schritt] +3. [...] + +## Erwartetes Verhalten +[Was sollte passieren] + +## Tatsaechliches Verhalten +[Was passiert stattdessen] + +## Umgebung +- Spawner Version: [z.B. 0.1.0] +- Docker Version: [z.B. 24.0.5] +- OS: [z.B. Ubuntu 22.04] + +## Logs +\`\`\` +[Relevante Log-Ausgaben] +\`\`\` +``` + +--- + +Zurueck zur [Dokumentations-Uebersicht](../README.md) diff --git a/docs/dos-n-donts/README.md b/docs/dos-n-donts/README.md new file mode 100644 index 0000000..bfc90e4 --- /dev/null +++ b/docs/dos-n-donts/README.md @@ -0,0 +1,411 @@ +# Best Practices - Dos and Don'ts + +Empfehlungen fuer den sicheren und effizienten Betrieb des Container Spawners. + +## Inhaltsverzeichnis + +- [Produktions-Checkliste](#produktions-checkliste) +- [Dos - Empfohlene Praktiken](#dos---empfohlene-praktiken) +- [Don'ts - Zu vermeiden](#donts---zu-vermeiden) +- [Haeufige Fehler](#haeufige-fehler) + +--- + +## Produktions-Checkliste + +### Vor dem Go-Live + +| Kategorie | Aufgabe | Status | +|-----------|---------|--------| +| **Sicherheit** | SECRET_KEY generiert (min. 32 Bytes) | [ ] | +| | `.env` nicht im Repository | [ ] | +| | HTTPS aktiviert | [ ] | +| | Docker Socket Proxy konfiguriert | [ ] | +| **Konfiguration** | BASE_DOMAIN korrekt | [ ] | +| | TRAEFIK_NETWORK existiert | [ ] | +| | Resource-Limits angemessen | [ ] | +| **Infrastruktur** | DNS-Eintraege (Wildcard) | [ ] | +| | Traefik laeuft stabil | [ ] | +| | Firewall konfiguriert | [ ] | +| **Monitoring** | Health-Check funktioniert | [ ] | +| | Logs werden geschrieben | [ ] | +| | Backup-Strategie | [ ] | +| **Testing** | Login/Signup funktioniert | [ ] | +| | Container wird erstellt | [ ] | +| | Subdomain erreichbar | [ ] | + +--- + +## Dos - Empfohlene Praktiken + +### Konfiguration + +**DO: SECRET_KEY sicher generieren** + +```bash +# Guter Key (32 Bytes = 64 Hex-Zeichen) +python3 -c "import secrets; print(secrets.token_hex(32))" +``` + +**DO: Umgebungsvariablen fuer sensible Daten** + +```bash +# In .env (nie committen!) +SECRET_KEY=abc123... +DATABASE_URL=postgresql://... +``` + +**DO: Resource-Limits setzen** + +```bash +# In .env +DEFAULT_MEMORY_LIMIT=512m +DEFAULT_CPU_QUOTA=50000 +``` + +--- + +### Deployment + +**DO: Docker Compose fuer Orchestrierung** + +```bash +# Nicht einzelne docker run Befehle +docker-compose up -d +docker-compose logs -f +docker-compose down +``` + +**DO: Images taggen** + +```bash +# Versionierte Tags statt :latest in Produktion +docker build -t spawner:0.1.0 . +docker build -t spawner:latest . +``` + +**DO: Health-Checks nutzen** + +```yaml +healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/health"] + interval: 30s + timeout: 10s + retries: 3 +``` + +--- + +### Monitoring + +**DO: Logs zentralisieren** + +```yaml +# docker-compose.yml +logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" +``` + +**DO: Regelmaessige Backups** + +```bash +# Cronjob fuer taegliches Backup +0 2 * * * /pfad/zu/spawner/backup.sh +``` + +**DO: Disk-Space ueberwachen** + +```bash +# Docker-Ressourcen pruefen +docker system df +docker system prune -f # Vorsicht in Produktion! +``` + +--- + +### Sicherheit + +**DO: HTTPS erzwingen** + +```yaml +# Traefik-Labels +- "traefik.http.routers.spawner.entrypoints=websecure" +- "traefik.http.routers.spawner.tls=true" +``` + +**DO: Minimale Berechtigungen** + +```yaml +# Docker Socket Proxy statt direktem Zugriff +DOCKER_HOST: tcp://docker-proxy:2375 +``` + +**DO: Container als Non-Root** + +```dockerfile +# Im User-Template +USER nginx +# oder +RUN useradd -m appuser && chown -R appuser /app +USER appuser +``` + +--- + +## Don'ts - Zu vermeiden + +### Konfiguration + +**DON'T: Hardcoded Secrets** + +```python +# NIEMALS! +SECRET_KEY = "supersecret123" +``` + +**DON'T: Debug-Mode in Produktion** + +```python +# NIEMALS in Produktion! +app.run(debug=True) +FLASK_DEBUG=1 +``` + +**DON'T: Schwache Keys** + +```bash +# ZU KURZ / ZU EINFACH +SECRET_KEY=test +SECRET_KEY=12345 +``` + +--- + +### Deployment + +**DON'T: Manuelles Container-Management** + +```bash +# Vermeiden - besser docker-compose +docker run -d --name spawner ... +docker stop spawner +docker rm spawner +``` + +**DON'T: Unversionierte Images in Produktion** + +```bash +# Vermeiden +USER_TEMPLATE_IMAGE=user-service-template:latest + +# Besser +USER_TEMPLATE_IMAGE=user-service-template:0.1.0 +``` + +**DON'T: Ohne Health-Checks deployen** + +--- + +### Sicherheit + +**DON'T: Docker Socket direkt exponieren** + +```yaml +# NIEMALS! +ports: + - "2375:2375" # Docker API oeffentlich +``` + +**DON'T: .env committen** + +```bash +# .gitignore MUSS enthalten: +.env +*.db +``` + +**DON'T: Container ohne Resource-Limits** + +```python +# Vermeiden - DoS-Risiko +container = client.containers.run(image) + +# Besser +container = client.containers.run( + image, + mem_limit='512m', + cpu_quota=50000 +) +``` + +**DON'T: Root-Container in Produktion** + +--- + +### Wartung + +**DON'T: Logs ignorieren** + +```bash +# Regelmaessig pruefen! +docker-compose logs --tail=100 spawner +``` + +**DON'T: Backups vergessen** + +**DON'T: Updates aufschieben** + +```bash +# Regelmaessig aktualisieren +git pull origin main +docker-compose build +docker-compose up -d +``` + +--- + +## Haeufige Fehler + +### 1. "Connection refused" beim Health-Check + +**Ursache**: Container noch nicht bereit + +**Loesung**: Warten oder start_period erhoehen: + +```yaml +healthcheck: + start_period: 30s +``` + +--- + +### 2. "Network not found" + +**Ursache**: Traefik-Netzwerk existiert nicht + +**Loesung**: + +```bash +docker network create web +# Oder TRAEFIK_NETWORK in .env anpassen +``` + +--- + +### 3. "Permission denied" bei Docker Socket + +**Ursache**: Spawner-Container hat keinen Zugriff + +**Loesung**: + +```yaml +volumes: + - /var/run/docker.sock:/var/run/docker.sock:rw +``` + +Oder Docker-Gruppe: + +```bash +sudo usermod -aG docker $USER +``` + +--- + +### 4. "Image not found" + +**Ursache**: User-Template nicht gebaut + +**Loesung**: + +```bash +docker build -t user-service-template:latest ./user-template/ +``` + +--- + +### 5. Subdomain nicht erreichbar + +**Ursache**: DNS oder Traefik-Konfiguration + +**Diagnose**: + +```bash +# DNS pruefen +nslookup username.example.com + +# Traefik-Routes pruefen (Dashboard) +# Container-Labels pruefen +docker inspect user-xxx | jq '.[0].Config.Labels' +``` + +--- + +### 6. "Database locked" + +**Ursache**: SQLite-Konkurrenzzugriff + +**Loesung fuer Produktion**: PostgreSQL verwenden: + +```bash +DATABASE_URL=postgresql://spawner:pass@postgres:5432/spawner +``` + +--- + +### 7. Container startet, aber Service nicht erreichbar + +**Ursache**: Falscher Port in Labels + +**Loesung**: Port in `container_manager.py` pruefen: + +```python +# Muss mit EXPOSE im Dockerfile uebereinstimmen +f'traefik.http.services.user{user_id}.loadbalancer.server.port': '8080' +``` + +--- + +## Quick Reference + +### Nuetzliche Befehle + +```bash +# Status +docker-compose ps +docker ps --filter 'label=spawner.managed=true' + +# Logs +docker-compose logs -f spawner +docker logs user-xxx-1 + +# Neustart +docker-compose restart spawner + +# Komplett neu +docker-compose down +docker-compose up -d --build + +# Cleanup +docker system prune -f +docker volume prune -f +``` + +### Debugging + +```bash +# In Container einsteigen +docker exec -it spawner bash + +# Python-Shell +docker exec -it spawner python + +# Datenbank +docker exec spawner sqlite3 /app/data/users.db ".tables" +``` + +--- + +Zurueck zur [Dokumentations-Uebersicht](../README.md) diff --git a/docs/images/info.png b/docs/images/info.png new file mode 100644 index 0000000..f5cdbe6 Binary files /dev/null and b/docs/images/info.png differ diff --git a/docs/images/spawner-logo.png b/docs/images/spawner-logo.png new file mode 100644 index 0000000..758c593 Binary files /dev/null and b/docs/images/spawner-logo.png differ diff --git a/docs/install/README.md b/docs/install/README.md new file mode 100644 index 0000000..915ff46 --- /dev/null +++ b/docs/install/README.md @@ -0,0 +1,350 @@ +# Installation + +Anleitung zur Installation und Aktualisierung des Container Spawner. + +## Inhaltsverzeichnis + +- [Voraussetzungen](#voraussetzungen) +- [Neuinstallation](#neuinstallation) +- [Update/Upgrade](#updateupgrade) +- [Umgebungsvariablen](#umgebungsvariablen) +- [Manuelle Installation](#manuelle-installation) +- [Troubleshooting](#troubleshooting) + +--- + +## Voraussetzungen + +### Hardware + +| Komponente | Minimum | Empfohlen | +|------------|---------|-----------| +| RAM | 2 GB | 4+ GB | +| Disk | 20 GB | 50+ GB | +| CPU | 2 Cores | 4+ Cores | + +### Software + +- **Docker**: Version 20.10+ +- **Docker Compose**: Version 2.0+ +- **Git**: Fuer Repository-Clone +- **Traefik**: Version 2.x oder 3.x (laufend) +- **curl** oder **wget**: Fuer Installationsskript + +### Netzwerk + +- **Port 5000**: Spawner-Service (intern) +- **Port 80/443**: Traefik Entrypoints +- **Docker-Netzwerk**: Traefik-Netzwerk muss existieren (Standard: `web`) +- **DNS**: Wildcard-DNS fuer Subdomains oder manuelle Eintraege + +--- + +## Neuinstallation + +### Schnellstart (Ein-Befehl-Installation) + +```bash +# In ein leeres Verzeichnis wechseln +mkdir spawner && cd spawner + +# Installationsskript ausfuehren +curl -sSL https://gitea.iotxs.de/RainerWieland/spawner/raw/branch/main/install.sh | bash +``` + +Das Skript erkennt automatisch, dass keine `.env` existiert und: +1. Laedt `.env.example` herunter +2. Gibt Anweisungen zur Konfiguration + +### Konfiguration anpassen + +```bash +# Vorlage kopieren +cp .env.example .env + +# Werte anpassen +nano .env +``` + +**Wichtig**: Mindestens diese Werte anpassen: + +```bash +# Secret-Key generieren +python3 -c "import secrets; print(secrets.token_hex(32))" + +# In .env eintragen: +SECRET_KEY= +BASE_DOMAIN=deine-domain.de +SPAWNER_SUBDOMAIN=coder +TRAEFIK_NETWORK=web # Name deines Traefik-Netzwerks +``` + +### Installation abschliessen + +```bash +bash install.sh +``` + +Das Skript: +1. Klont das Repository +2. Erstellt Verzeichnisse und setzt Berechtigungen (`data/`, `logs/`, `.env`) +3. Prueft/erstellt Docker-Netzwerk +4. Baut alle Docker-Images +5. Startet die Container + +--- + +## Update/Upgrade + +### Automatisches Update + +```bash +cd /pfad/zu/spawner +bash install.sh +``` + +Das Skript erkennt automatisch ein bestehendes Git-Repository und: +1. Holt neueste Aenderungen (`git pull`) +2. Prueft/aktualisiert Verzeichnisrechte +3. Baut Images neu +4. Startet Container neu + +### Manuelles Update + +```bash +cd /pfad/zu/spawner + +# Aenderungen holen +git fetch origin +git pull origin main + +# Images neu bauen +docker-compose down +docker build --no-cache -t user-service-template:latest ./user-template/ +docker-compose build + +# Container starten +docker-compose up -d +``` + +### Rollback + +```bash +# Zu spezifischer Version zurueck +git checkout v0.1.0 +docker-compose down +docker-compose build +docker-compose up -d +``` + +--- + +## Umgebungsvariablen + +### Pflicht-Variablen + +| Variable | Beschreibung | Beispiel | +|----------|--------------|----------| +| `SECRET_KEY` | Flask Session Secret | `python3 -c "import secrets; print(secrets.token_hex(32))"` | +| `BASE_DOMAIN` | Haupt-Domain | `example.com` | +| `SPAWNER_SUBDOMAIN` | Subdomain fuer Spawner-UI | `coder` | +| `TRAEFIK_NETWORK` | Docker-Netzwerk fuer Traefik | `web` | + +### Traefik-Variablen + +| Variable | Standard | Beschreibung | +|----------|----------|--------------| +| `TRAEFIK_CERTRESOLVER` | `lets-encrypt` | Name des Certificate Resolvers aus traefik.yml | +| `TRAEFIK_ENTRYPOINT` | `websecure` | HTTPS Entrypoint Name | + +### Optionale Variablen + +| Variable | Standard | Beschreibung | +|----------|----------|--------------| +| `USER_TEMPLATE_IMAGE` | `user-service-template:latest` | Docker-Image fuer User-Container | +| `DEFAULT_MEMORY_LIMIT` | `512m` | RAM-Limit pro Container | +| `DEFAULT_CPU_QUOTA` | `50000` | CPU-Quota (50000 = 0.5 CPU) | +| `SPAWNER_PORT` | `5000` | Interner Port des Spawners | +| `LOG_LEVEL` | `INFO` | Log-Level (DEBUG, INFO, WARNING, ERROR) | +| `JWT_ACCESS_TOKEN_EXPIRES` | `3600` | JWT Token Gueltigkeitsdauer (Sekunden) | +| `CONTAINER_IDLE_TIMEOUT` | `3600` | Timeout in Sekunden (noch nicht implementiert) | + +### User-Templates + +Es stehen zwei Templates fuer User-Container zur Verfuegung: + +| Image | Verzeichnis | Beschreibung | +|-------|-------------|--------------| +| `user-service-template:latest` | `user-template/` | Einfache nginx-Willkommensseite (Standard) | +| `user-template-next:latest` | `user-template-next/` | Moderne Next.js React-Anwendung | + +Um ein anderes Template zu verwenden, aendere `USER_TEMPLATE_IMAGE` in `.env`: + +```bash +USER_TEMPLATE_IMAGE=user-template-next:latest +``` + +### Produktions-Variablen + +| Variable | Beschreibung | +|----------|--------------| +| `DATABASE_URL` | PostgreSQL-Verbindung (statt SQLite) | +| `JWT_SECRET_KEY` | Separater JWT-Secret | +| `CORS_ORIGINS` | Erlaubte CORS-Origins | +| `DOCKER_HOST` | Docker Socket Pfad (Standard: unix:///var/run/docker.sock) | + +--- + +## Manuelle Installation + +Falls das Installationsskript nicht verwendet werden kann: + +```bash +# 1. Repository klonen +git clone https://gitea.iotxs.de/RainerWieland/spawner.git +cd spawner + +# 2. Konfiguration erstellen +cp .env.example .env +nano .env # Werte anpassen + +# 3. Verzeichnisse und Rechte setzen +mkdir -p data logs +chmod 755 data logs +chmod 600 .env + +# 4. Docker-Netzwerk pruefen +docker network ls | grep web +# Falls nicht vorhanden: +docker network create web + +# 5. User-Template Images bauen +docker build -t user-service-template:latest ./user-template/ +docker build -t user-template-next:latest ./user-template-next/ # Optional + +# 6. Spawner bauen und starten +docker-compose build +docker-compose up -d + +# 7. Logs pruefen +docker-compose logs -f spawner +``` + +--- + +## Troubleshooting + +### Spawner startet nicht + +```bash +# Logs pruefen +docker-compose logs spawner + +# Health-Check +curl http://localhost:5000/health +``` + +**Haeufige Ursachen**: +- `.env` fehlt oder hat falsche Werte +- Docker-Socket nicht gemountet +- Netzwerk existiert nicht + +### Container wird nicht erstellt + +```bash +# Docker-Verbindung testen +docker ps + +# Template-Image pruefen +docker images | grep user-service-template +``` + +**Haeufige Ursachen**: +- Template-Image nicht gebaut +- Netzwerk-Name falsch in `.env` + +### Traefik routet nicht + +```bash +# Traefik-Dashboard pruefen (falls aktiviert) +# Container-Labels pruefen +docker inspect spawner | jq '.[0].Config.Labels' + +# Netzwerk-Verbindung pruefen +docker network inspect web | grep spawner +``` + +**Haeufige Ursachen**: +- Container nicht im Traefik-Netzwerk +- Labels falsch konfiguriert +- DNS nicht konfiguriert + +### Datenbank-Fehler + +```bash +# In Container einsteigen +docker exec -it spawner bash + +# DB manuell initialisieren +python -c "from app import app, db; app.app_context().push(); db.create_all()" +``` + +--- + +## Verzeichnisrechte + +Das Installationsskript setzt die Berechtigungen automatisch. Bei manueller Installation muessen diese selbst gesetzt werden. + +### Vom Skript automatisch gesetzt + +| Pfad | Berechtigung | Zweck | +|------|--------------|-------| +| `./data/` | `755` | SQLite-Datenbank | +| `./logs/` | `755` | Log-Dateien | +| `./.env` | `600` | Sensible Konfiguration (nur Owner) | +| `./install.sh` | `+x` | Ausfuehrbar | + +### Vom System benoetigt + +| Pfad | Berechtigung | Zweck | +|------|--------------|-------| +| `/var/run/docker.sock` | `rw` | Docker-API-Zugriff | + +### Manuelle Rechte setzen + +```bash +# Verzeichnisse erstellen +mkdir -p data logs + +# Berechtigungen setzen +chmod 755 data logs +chmod 600 .env +chmod +x install.sh +``` + +### Non-Root Container + +Falls der Spawner-Container als non-root User laeuft, muessen die Verzeichnisse fuer diesen beschreibbar sein: + +```bash +# Option 1: Volle Schreibrechte (einfach, aber weniger sicher) +chmod 777 data logs + +# Option 2: Owner auf Container-UID setzen (empfohlen) +# UID des Container-Users ermitteln und setzen +chown 1000:1000 data logs +``` + +### Synology NAS Hinweis + +Auf Synology NAS (DSM) kann es noetig sein, die Verzeichnisse dem Docker-User zuzuweisen: + +```bash +# Als root auf der Synology +chown -R 1000:1000 /volume1/docker/spawner/data +chown -R 1000:1000 /volume1/docker/spawner/logs +``` + +--- + +Zurueck zur [Dokumentations-Uebersicht](../README.md) diff --git a/docs/security/README.md b/docs/security/README.md new file mode 100644 index 0000000..72f7e00 --- /dev/null +++ b/docs/security/README.md @@ -0,0 +1,312 @@ +# Sicherheit + +Sicherheitsrisiken und Gegenmassnahmen fuer den Container Spawner. + +## Inhaltsverzeichnis + +- [Docker Socket Risiko](#docker-socket-risiko) +- [Container Isolation](#container-isolation) +- [Session-Sicherheit](#session-sicherheit) +- [Input-Validierung](#input-validierung) +- [Secrets Management](#secrets-management) +- [Netzwerksicherheit](#netzwerksicherheit) +- [Sicherheits-Checkliste](#sicherheits-checkliste) + +--- + +## Docker Socket Risiko + +### Problem + +Der Spawner benoetigt Zugriff auf `/var/run/docker.sock` um Container zu erstellen. Dies entspricht Root-Privilegien auf dem Host-System. + +### Risiken + +- Ein kompromittierter Spawner kann alle Container kontrollieren +- Potenzieller Container-Escape moeglich +- Zugriff auf Host-Dateisystem via Volume-Mounts + +### Gegenmassnahmen + +**Option 1: Docker Socket Proxy (Empfohlen fuer Produktion)** + +```yaml +services: + docker-proxy: + image: tecnativa/docker-socket-proxy + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + environment: + CONTAINERS: 1 # Container-Operationen erlauben + NETWORKS: 1 # Netzwerk-Operationen erlauben + SERVICES: 0 # Swarm-Services blockieren + SWARM: 0 # Swarm-Operationen blockieren + VOLUMES: 0 # Volume-Operationen blockieren + IMAGES: 1 # Image-Operationen erlauben + networks: + - internal + + spawner: + environment: + DOCKER_HOST: tcp://docker-proxy:2375 + networks: + - internal + - web + +networks: + internal: + internal: true # Kein externer Zugriff +``` + +**Option 2: Minimale Permissions** + +```bash +# User-Namespace aktivieren (in /etc/docker/daemon.json) +{ + "userns-remap": "default" +} +``` + +--- + +## Container Isolation + +### Aktuelle Massnahmen + +| Massnahme | Status | Beschreibung | +|-----------|--------|--------------| +| Memory-Limit | Aktiv | Standard 512m pro Container | +| CPU-Quota | Aktiv | Standard 0.5 CPU pro Container | +| Restart-Policy | Aktiv | `unless-stopped` | +| Network-Isolation | Teilweise | Alle im gleichen Traefik-Netzwerk | + +### Empfehlungen + +**Read-Only Filesystem** + +```python +# In container_manager.py +container = client.containers.run( + read_only=True, + tmpfs={'/tmp': 'size=100M,noexec'} +) +``` + +**Security Options** + +```python +container = client.containers.run( + security_opt=['no-new-privileges:true'], + cap_drop=['ALL'], + cap_add=['NET_BIND_SERVICE'] # Nur wenn noetig +) +``` + +**Separate Netzwerke pro User** (Fuer hohe Isolation) + +```python +# Dediziertes Netzwerk pro User +user_network = f"user-{username}-network" +client.networks.create(user_network, driver='bridge', internal=True) +``` + +--- + +## Session-Sicherheit + +### Aktuelle Konfiguration + +```python +SESSION_COOKIE_SECURE = True # Nur HTTPS (Produktion) +SESSION_COOKIE_HTTPONLY = True # Kein JavaScript-Zugriff +SESSION_COOKIE_SAMESITE = 'Lax' # CSRF-Schutz +PERMANENT_SESSION_LIFETIME = 3600 # 1h Timeout +``` + +### Empfehlungen + +- **SECRET_KEY**: Mindestens 32 Bytes, zufaellig generiert +- **HTTPS erzwingen**: Immer in Produktion +- **Session-Rotation**: Nach Login neue Session-ID + +```python +# Session-Rotation nach Login +from flask import session +session.regenerate() +``` + +--- + +## Input-Validierung + +### Aktuelle Risiken + +- Username wird direkt in Container-Namen verwendet +- Minimale Validierung bei Registrierung + +### Empfohlene Validierung + +```python +import re + +def validate_username(username): + """Validiert Username gegen Injection-Angriffe""" + if not username: + return False, "Username erforderlich" + + if len(username) < 3 or len(username) > 20: + return False, "Username muss 3-20 Zeichen lang sein" + + if not re.match(r'^[a-zA-Z0-9_]+$', username): + return False, "Nur Buchstaben, Zahlen und Unterstriche erlaubt" + + # Reservierte Namen + reserved = ['admin', 'root', 'system', 'spawner', 'traefik'] + if username.lower() in reserved: + return False, "Dieser Username ist reserviert" + + return True, None + +# Container-Name sicher erstellen +def safe_container_name(username, user_id): + """Erstellt sicheren Container-Namen""" + safe_name = re.sub(r'[^a-zA-Z0-9_-]', '', username) + return f"user-{safe_name}-{user_id}" +``` + +--- + +## Secrets Management + +### Entwicklung vs. Produktion + +| Umgebung | Methode | +|----------|---------| +| Entwicklung | `.env` Datei (nie committen!) | +| Produktion | Docker Secrets oder Vault | + +### Docker Secrets (Produktion) + +```bash +# Secret erstellen +echo "supersecretkey" | docker secret create flask_secret - + +# In docker-compose.yml +services: + spawner: + secrets: + - flask_secret + environment: + SECRET_KEY_FILE: /run/secrets/flask_secret + +secrets: + flask_secret: + external: true +``` + +### Environment-Variable Sicherheit + +```bash +# .env NIEMALS committen +echo ".env" >> .gitignore + +# Sensible Werte nicht in Logs +LOG_LEVEL=INFO # Nicht DEBUG in Produktion +``` + +--- + +## Netzwerksicherheit + +### Traefik-Konfiguration + +**HTTPS erzwingen** + +```yaml +# In container_manager.py Labels +labels={ + 'traefik.http.routers.user{id}.entrypoints': 'websecure', + 'traefik.http.routers.user{id}.tls': 'true', + 'traefik.http.routers.user{id}.tls.certresolver': 'letsencrypt', + + # HTTP zu HTTPS Redirect + 'traefik.http.middlewares.redirect-https.redirectscheme.scheme': 'https', + 'traefik.http.routers.user{id}-http.middlewares': 'redirect-https' +} +``` + +**Rate-Limiting via Traefik** + +```yaml +labels={ + 'traefik.http.middlewares.ratelimit.ratelimit.average': '100', + 'traefik.http.middlewares.ratelimit.ratelimit.burst': '50', + 'traefik.http.routers.spawner.middlewares': 'ratelimit' +} +``` + +### Firewall-Empfehlungen + +```bash +# Nur Traefik-Ports oeffentlich +ufw allow 80/tcp +ufw allow 443/tcp + +# Docker-Socket NIE oeffentlich! +# Port 5000 nur intern (ueber Traefik) +``` + +--- + +## Sicherheits-Checkliste + +### Vor Go-Live + +- [ ] SECRET_KEY generiert (32+ Bytes) +- [ ] `.env` nicht im Repository +- [ ] HTTPS konfiguriert und getestet +- [ ] Docker Socket Proxy aktiviert (Produktion) +- [ ] Resource-Limits angemessen +- [ ] Input-Validierung implementiert +- [ ] Rate-Limiting aktiv +- [ ] Logs auf sensible Daten geprueft +- [ ] Backup-Strategie implementiert + +### Regelmaessig pruefen + +- [ ] Container auf Vulnerabilities scannen +- [ ] Dependencies aktualisieren +- [ ] Logs auf verdaechtige Aktivitaeten pruefen +- [ ] Unbefugte Container entfernen + +### Vulnerability Scanning + +```bash +# Mit Trivy scannen +docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy image spawner:latest + +docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy image user-service-template:latest +``` + +--- + +## Incident Response + +### Bei Verdacht auf Kompromittierung + +1. **Spawner stoppen**: `docker-compose down` +2. **Alle User-Container stoppen**: `docker stop $(docker ps -q --filter 'label=spawner.managed=true')` +3. **Logs sichern**: `docker-compose logs > incident-logs.txt` +4. **Secrets rotieren**: Neue SECRET_KEY generieren +5. **Analyse durchfuehren** +6. **Behobene Version deployen** + +### Kontakt + +Bei Sicherheitsproblemen: Issue im Repository erstellen (privat falls sensibel) + +--- + +Zurueck zur [Dokumentations-Uebersicht](../README.md) diff --git a/docs/versions/CHANGELOG.md b/docs/versions/CHANGELOG.md new file mode 100644 index 0000000..ca115ae --- /dev/null +++ b/docs/versions/CHANGELOG.md @@ -0,0 +1,63 @@ +# Changelog + +Alle nennenswerten Aenderungen werden in dieser Datei dokumentiert. + +Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/). + +--- + +## [Unreleased] + +### Hinzugefuegt +- Dokumentationsstruktur mit mehreren Kategorien +- Automatisches Installationsskript (`install.sh`) +- `.env.example` Vorlage + +### Geaendert +- `doc/` Verzeichnis umbenannt zu `docs/` +- README.md ueberarbeitet mit Schnellstart-Anleitung + +--- + +## [0.1.0] - 2026-01-30 + +### Hinzugefuegt +- Flask-Anwendung mit User-Management +- Docker-Container-Spawning pro User +- Traefik-Integration via Labels +- SQLite-Datenbank fuer User-Daten +- Next.js Frontend +- REST-API fuer Container-Management +- User-Template (nginx-basiert) +- Health-Check Endpoint +- Docker-Compose Setup + +### Sicherheit +- Passwort-Hashing mit Werkzeug +- Session-Cookies mit HttpOnly/Secure +- Resource-Limits fuer Container + +--- + +## [0.0.1] - 2026-01-27 + +### Hinzugefuegt +- Initiales Projekt-Setup +- Grundlegende Dokumentation + +--- + +## Legende + +- **Hinzugefuegt**: Neue Features +- **Geaendert**: Aenderungen an bestehenden Features +- **Veraltet**: Features die bald entfernt werden +- **Entfernt**: Entfernte Features +- **Behoben**: Bugfixes +- **Sicherheit**: Sicherheitsrelevante Aenderungen + +--- + +[Unreleased]: https://gitea.iotxs.de/RainerWieland/spawner/compare/v0.1.0...HEAD +[0.1.0]: https://gitea.iotxs.de/RainerWieland/spawner/releases/tag/v0.1.0 +[0.0.1]: https://gitea.iotxs.de/RainerWieland/spawner/releases/tag/v0.0.1 diff --git a/docs/versions/README.md b/docs/versions/README.md new file mode 100644 index 0000000..4236027 --- /dev/null +++ b/docs/versions/README.md @@ -0,0 +1,68 @@ +# Versionierung + +Dieses Projekt verwendet [Semantic Versioning](https://semver.org/lang/de/). + +## Aktuelle Version + +**Version**: 0.1.0 (Entwicklung) +**Release-Datum**: Januar 2026 +**Status**: Alpha + +## Versionierungsschema + +``` +MAJOR.MINOR.PATCH + +MAJOR: Inkompatible API-Aenderungen +MINOR: Neue Features (rueckwaertskompatibel) +PATCH: Bugfixes (rueckwaertskompatibel) +``` + +### Pre-Release Versionen + +- `0.x.x` - Entwicklungsphase, API kann sich aendern +- `1.0.0` - Erstes stabiles Release + +## Changelog + +Siehe [CHANGELOG.md](CHANGELOG.md) fuer detaillierte Aenderungen. + +## Upgrade-Pfade + +### Von 0.0.x auf 0.1.0 + +Keine Breaking Changes. Einfaches Update moeglich: + +```bash +bash install.sh +``` + +## Git-Tags + +Releases werden mit Git-Tags markiert: + +```bash +# Alle Tags anzeigen +git tag -l + +# Zu spezifischer Version wechseln +git checkout v0.1.0 +``` + +## Geplante Features + +### Version 0.2.0 (geplant) + +- [ ] Container Auto-Shutdown nach Inaktivitaet +- [ ] Volume-Persistenz fuer User-Daten +- [ ] Admin-Dashboard + +### Version 1.0.0 (geplant) + +- [ ] Multi-Template-Support +- [ ] API-Rate-Limiting +- [ ] PostgreSQL-Support (Standard) + +--- + +Zurueck zur [Dokumentations-Uebersicht](../README.md) diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..a1e3965 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,5 @@ +# API-URL (leer lassen für lokale Entwicklung mit Proxy) +NEXT_PUBLIC_API_URL= + +# Fuer Produktion: +# NEXT_PUBLIC_API_URL=https://coder.wieland.org diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a81ec1d --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,35 @@ +# Dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# Testing +/coverage + +# Next.js +/.next/ +/out/ + +# Production +/build + +# Misc +.DS_Store +*.pem + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Local env files +.env*.local +.env + +# Vercel +.vercel + +# TypeScript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..1003b35 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,40 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Dependencies +COPY package.json package-lock.json* ./ +RUN npm ci + +# Source code +COPY . . + +# Build +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npm run build + +# Production stage +FROM node:20-alpine AS runner + +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Copy standalone build +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs new file mode 100644 index 0000000..25ee352 --- /dev/null +++ b/frontend/next.config.mjs @@ -0,0 +1,16 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', + async rewrites() { + return [ + { + source: '/api/:path*', + destination: process.env.NEXT_PUBLIC_API_URL + ? `${process.env.NEXT_PUBLIC_API_URL}/api/:path*` + : 'http://localhost:5000/api/:path*', + }, + ]; + }, +}; + +export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..892a387 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,7337 @@ +{ + "name": "container-spawner-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "container-spawner-frontend", + "version": "1.0.0", + "dependencies": { + "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "lucide-react": "^0.408.0", + "next": "14.2.5", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwind-merge": "^2.4.0" + }, + "devDependencies": { + "@types/node": "^20.14.10", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-config-next": "14.2.5", + "postcss": "^8.4.39", + "tailwindcss": "^3.4.6", + "typescript": "^5.5.3" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", + "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.5" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz", + "integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.5.tgz", + "integrity": "sha512-LY3btOpPh+OTIpviNojDpUdIbHW9j0JBYBjsIp8IxtDFfYFyORvw3yNq6N231FVqQA7n7lwaf7xHbVJlA1ED7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "10.3.10" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz", + "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz", + "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz", + "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz", + "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz", + "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz", + "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz", + "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz", + "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz", + "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", + "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", + "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", + "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.283", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", + "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-next": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.5.tgz", + "integrity": "sha512-zogs9zlOiZ7ka+wgUnmcM0KBEDjo4Jis7kxN1jvC0N4wynQ2MIx/KBkg4mVF63J5EK4W0QMCn7xO3vNisjaAoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "14.2.5", + "@rushstack/eslint-patch": "^1.3.3", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.0.0-canary-7118f5dd7-20230705", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz", + "integrity": "sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lucide-react": { + "version": "0.408.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.408.0.tgz", + "integrity": "sha512-8kETAAeWmOvtGIr7HPHm51DXoxlfkNncQ5FZWXR+abX8saQwMYXANWIkUstaYtcKSo/imOe/q+tVFA8ANzdSVA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz", + "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==", + "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.5", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.5", + "@next/swc-darwin-x64": "14.2.5", + "@next/swc-linux-arm64-gnu": "14.2.5", + "@next/swc-linux-arm64-musl": "14.2.5", + "@next/swc-linux-x64-gnu": "14.2.5", + "@next/swc-linux-x64-musl": "14.2.5", + "@next/swc-win32-arm64-msvc": "14.2.5", + "@next/swc-win32-ia32-msvc": "14.2.5", + "@next/swc-win32-x64-msvc": "14.2.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..139e854 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,36 @@ +{ + "name": "container-spawner-frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "14.2.5", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "tailwind-merge": "^2.4.0", + "lucide-react": "^0.408.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-dropdown-menu": "^2.1.1" + }, + "devDependencies": { + "@types/node": "^20.14.10", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-config-next": "14.2.5", + "postcss": "^8.4.39", + "tailwindcss": "^3.4.6", + "typescript": "^5.5.3" + } +} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..2ef30fc --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,9 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + +export default config; diff --git a/frontend/public/.gitkeep b/frontend/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx new file mode 100644 index 0000000..33a5314 --- /dev/null +++ b/frontend/src/app/dashboard/page.tsx @@ -0,0 +1,314 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/hooks/use-auth"; +import { api, UserResponse } from "@/lib/api"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { + Container, + ExternalLink, + RefreshCw, + LogOut, + Loader2, + CheckCircle2, + XCircle, + AlertCircle, +} from "lucide-react"; + +export default function DashboardPage() { + const { user, logout, isLoading: authLoading } = useAuth(); + const router = useRouter(); + + const [userData, setUserData] = useState(null); + const [isRefreshing, setIsRefreshing] = useState(false); + const [isRestarting, setIsRestarting] = useState(false); + const [error, setError] = useState(""); + + const fetchUserData = useCallback(async () => { + const { data, error } = await api.getUser(); + if (data) { + setUserData(data); + } else if (error) { + setError(error); + } + }, []); + + useEffect(() => { + if (!authLoading && !user) { + router.replace("/login"); + } else if (user) { + fetchUserData(); + } + }, [user, authLoading, router, fetchUserData]); + + const handleRefresh = async () => { + setIsRefreshing(true); + await fetchUserData(); + setIsRefreshing(false); + }; + + const handleRestart = async () => { + setIsRestarting(true); + setError(""); + + const { data, error } = await api.restartContainer(); + + if (error) { + setError(error); + } else { + await fetchUserData(); + } + + setIsRestarting(false); + }; + + const handleLogout = async () => { + await logout(); + router.push("/login"); + }; + + const getStatusBadge = (status: string) => { + switch (status) { + case "running": + return ( + + + Lauft + + ); + case "exited": + case "stopped": + return ( + + + Gestoppt + + ); + case "no_container": + return ( + + + Kein Container + + ); + default: + return ( + + + {status} + + ); + } + }; + + if (authLoading || !user) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+ + Container Spawner +
+
+
+ + + {user.username.slice(0, 2).toUpperCase()} + + + {user.username} +
+ +
+
+
+ + {/* Main Content */} +
+
+

Dashboard

+

+ Verwalte deinen personlichen Container +

+
+ + {error && ( +
+ {error} +
+ )} + +
+ {/* Container Status Card */} + + + + + Container Status + + + Informationen zu deinem personlichen Container + + + +
+ Status + {userData ? ( + getStatusBadge(userData.container.status) + ) : ( + + )} +
+ +
+ + Container ID + + + {userData?.container.id?.slice(0, 12) || "-"} + +
+ +
+ + +
+
+
+ + {/* Service URL Card */} + + + + + Dein Service + + + Zugriff auf deinen personlichen Bereich + + + +
+

+ Deine Service-URL: +

+ {userData?.container.service_url ? ( + + {userData.container.service_url} + + + ) : ( + Laden... + )} +
+ + {userData?.container.status !== "running" && ( +

+ Container muss laufen, um den Service zu nutzen +

+ )} +
+
+ + {/* User Info Card */} + + + Kontoinformationen + Deine personlichen Daten + + +
+
+

Benutzername

+

{user.username}

+
+
+

E-Mail

+

{user.email}

+
+
+

Registriert

+

+ {userData?.user.created_at + ? new Date(userData.user.created_at).toLocaleDateString( + "de-DE" + ) + : "-"} +

+
+
+
+
+
+
+
+ ); +} diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..10c2d37 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,59 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..6f0fb3d --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import { AuthProvider } from "@/hooks/use-auth"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Container Spawner", + description: "Dein persönlicher Container-Service", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx new file mode 100644 index 0000000..e15835f --- /dev/null +++ b/frontend/src/app/login/page.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { useAuth } from "@/hooks/use-auth"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Container, Loader2 } from "lucide-react"; + +export default function LoginPage() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + const { login, user, isLoading } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (!isLoading && user) { + router.replace("/dashboard"); + } + }, [user, isLoading, router]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + setIsSubmitting(true); + + const result = await login(username, password); + + if (result.success) { + router.push("/dashboard"); + } else { + setError(result.error || "Login fehlgeschlagen"); + setIsSubmitting(false); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ + +
+ +
+ Willkommen + + Melde dich an, um auf deinen Container zuzugreifen + +
+ +
+ {error && ( +
+ {error} +
+ )} +
+ + setUsername(e.target.value)} + required + disabled={isSubmitting} + /> +
+
+ + setPassword(e.target.value)} + required + disabled={isSubmitting} + /> +
+ +
+
+ Noch kein Konto?{" "} + + Registrieren + +
+
+
+
+ ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..711f642 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/hooks/use-auth"; + +export default function Home() { + const { user, isLoading } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (!isLoading) { + if (user) { + router.replace("/dashboard"); + } else { + router.replace("/login"); + } + } + }, [user, isLoading, router]); + + return ( +
+
Laden...
+
+ ); +} diff --git a/frontend/src/app/signup/page.tsx b/frontend/src/app/signup/page.tsx new file mode 100644 index 0000000..6994b01 --- /dev/null +++ b/frontend/src/app/signup/page.tsx @@ -0,0 +1,166 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { useAuth } from "@/hooks/use-auth"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Container, Loader2 } from "lucide-react"; + +export default function SignupPage() { + const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [error, setError] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + const { signup, user, isLoading } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (!isLoading && user) { + router.replace("/dashboard"); + } + }, [user, isLoading, router]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + + if (password !== confirmPassword) { + setError("Passworter stimmen nicht uberein"); + return; + } + + if (password.length < 6) { + setError("Passwort muss mindestens 6 Zeichen lang sein"); + return; + } + + if (username.length < 3) { + setError("Benutzername muss mindestens 3 Zeichen lang sein"); + return; + } + + setIsSubmitting(true); + + const result = await signup(username, email, password); + + if (result.success) { + router.push("/dashboard"); + } else { + setError(result.error || "Registrierung fehlgeschlagen"); + setIsSubmitting(false); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ + +
+ +
+ Konto erstellen + + Registriere dich, um deinen eigenen Container zu erhalten + +
+ +
+ {error && ( +
+ {error} +
+ )} +
+ + setUsername(e.target.value)} + required + disabled={isSubmitting} + /> +

+ Dieser wird Teil deiner Service-URL +

+
+
+ + setEmail(e.target.value)} + required + disabled={isSubmitting} + /> +
+
+ + setPassword(e.target.value)} + required + disabled={isSubmitting} + /> +
+
+ + setConfirmPassword(e.target.value)} + required + disabled={isSubmitting} + /> +
+ +
+
+ Bereits ein Konto?{" "} + + Anmelden + +
+
+
+
+ ); +} diff --git a/frontend/src/components/ui/avatar.tsx b/frontend/src/components/ui/avatar.tsx new file mode 100644 index 0000000..d260623 --- /dev/null +++ b/frontend/src/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client"; + +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; + +import { cn } from "@/lib/utils"; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx new file mode 100644 index 0000000..310e6f7 --- /dev/null +++ b/frontend/src/components/ui/badge.tsx @@ -0,0 +1,40 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + success: + "border-transparent bg-green-500 text-white hover:bg-green-500/80", + warning: + "border-transparent bg-yellow-500 text-white hover:bg-yellow-500/80", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ); +} + +export { Badge, badgeVariants }; diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..de31d90 --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..b375b06 --- /dev/null +++ b/frontend/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..644632d --- /dev/null +++ b/frontend/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = "Input"; + +export { Input }; diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000..40378d4 --- /dev/null +++ b/frontend/src/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/frontend/src/components/ui/separator.tsx b/frontend/src/components/ui/separator.tsx new file mode 100644 index 0000000..5b6774d --- /dev/null +++ b/frontend/src/components/ui/separator.tsx @@ -0,0 +1,31 @@ +"use client"; + +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +import { cn } from "@/lib/utils"; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/frontend/src/hooks/use-auth.tsx b/frontend/src/hooks/use-auth.tsx new file mode 100644 index 0000000..d9d2907 --- /dev/null +++ b/frontend/src/hooks/use-auth.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { + createContext, + useContext, + useState, + useEffect, + ReactNode, +} from "react"; +import { api, LoginResponse, UserResponse } from "@/lib/api"; + +interface User { + id: number; + username: string; + email: string; +} + +interface AuthContextType { + user: User | null; + token: string | null; + isLoading: boolean; + login: ( + username: string, + password: string + ) => Promise<{ success: boolean; error?: string }>; + signup: ( + username: string, + email: string, + password: string + ) => Promise<{ success: boolean; error?: string }>; + logout: () => Promise; + refreshUser: () => Promise; +} + +const AuthContext = createContext(undefined); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [token, setToken] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const storedToken = localStorage.getItem("token"); + if (storedToken) { + setToken(storedToken); + fetchUser(storedToken); + } else { + setIsLoading(false); + } + }, []); + + const fetchUser = async (accessToken?: string) => { + const currentToken = accessToken || token; + if (!currentToken) { + setIsLoading(false); + return; + } + + const { data, error } = await api.getUser(); + if (data && !error) { + setUser(data.user); + } else { + localStorage.removeItem("token"); + setToken(null); + setUser(null); + } + setIsLoading(false); + }; + + const login = async ( + username: string, + password: string + ): Promise<{ success: boolean; error?: string }> => { + const { data, error } = await api.login(username, password); + + if (error || !data) { + return { success: false, error: error || "Login fehlgeschlagen" }; + } + + localStorage.setItem("token", data.access_token); + setToken(data.access_token); + setUser(data.user); + return { success: true }; + }; + + const signup = async ( + username: string, + email: string, + password: string + ): Promise<{ success: boolean; error?: string }> => { + const { data, error } = await api.signup(username, email, password); + + if (error || !data) { + return { success: false, error: error || "Registrierung fehlgeschlagen" }; + } + + localStorage.setItem("token", data.access_token); + setToken(data.access_token); + setUser(data.user); + return { success: true }; + }; + + const logout = async () => { + await api.logout(); + localStorage.removeItem("token"); + setToken(null); + setUser(null); + }; + + const refreshUser = async () => { + await fetchUser(); + }; + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..f81d790 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,57 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + darkMode: ["class"], + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + }, + }, + plugins: [], +}; + +export default config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..d89f267 --- /dev/null +++ b/install.sh @@ -0,0 +1,313 @@ +#!/bin/bash +set -e + +# ============================================================ +# Container Spawner - Installationsskript +# https://gitea.iotxs.de/RainerWieland/spawner +# ============================================================ + +REPO_URL="https://gitea.iotxs.de/RainerWieland/spawner.git" +RAW_URL="https://gitea.iotxs.de/RainerWieland/spawner/raw/branch/main" +INSTALL_DIR="${PWD}" +VERSION="0.1.0" + +# Farben fuer Output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo "" +echo "============================================================" +echo " Container Spawner Installation v${VERSION}" +echo "============================================================" +echo "" + +# ============================================================ +# 1. Pruefe .env +# ============================================================ +if [ ! -f "${INSTALL_DIR}/.env" ]; then + echo -e "${YELLOW}HINWEIS: Keine .env-Datei gefunden!${NC}" + echo "" + + # Erstelle .env.example aus Repository + echo "Lade .env.example herunter..." + if command -v curl >/dev/null 2>&1; then + curl -sSL "${RAW_URL}/.env.example" -o "${INSTALL_DIR}/.env.example" + elif command -v wget >/dev/null 2>&1; then + wget -q "${RAW_URL}/.env.example" -O "${INSTALL_DIR}/.env.example" + else + echo -e "${RED}Fehler: Weder curl noch wget gefunden!${NC}" + exit 1 + fi + + echo -e "${GREEN}Vorlage erstellt: .env.example${NC}" + echo "" + echo "Naechste Schritte:" + echo " 1. Kopiere die Vorlage: cp .env.example .env" + echo " 2. Passe die Werte an: nano .env" + echo " - SECRET_KEY generieren (siehe Kommentar in .env)" + echo " - BASE_DOMAIN setzen" + echo " - TRAEFIK_NETWORK pruefen" + echo " 3. Fuehre erneut aus: bash install.sh" + echo "" + exit 0 +fi + +echo -e "${GREEN}.env-Datei gefunden${NC}" + +# Lade .env Variablen +set -a +source "${INSTALL_DIR}/.env" +set +a + +# ============================================================ +# 2. Pruefe Voraussetzungen +# ============================================================ +echo "" +echo "Pruefe Voraussetzungen..." + +# Docker +if ! command -v docker >/dev/null 2>&1; then + echo -e "${RED}Fehler: Docker nicht gefunden!${NC}" + echo "Installiere Docker: https://docs.docker.com/get-docker/" + exit 1 +fi +echo -e " Docker: ${GREEN}OK${NC}" + +# Docker Compose +if command -v docker-compose >/dev/null 2>&1; then + COMPOSE_CMD="docker-compose" +elif docker compose version >/dev/null 2>&1; then + COMPOSE_CMD="docker compose" +else + echo -e "${RED}Fehler: docker-compose nicht gefunden!${NC}" + exit 1 +fi +echo -e " Docker Compose: ${GREEN}OK${NC} (${COMPOSE_CMD})" + +# Git +if ! command -v git >/dev/null 2>&1; then + echo -e "${RED}Fehler: Git nicht gefunden!${NC}" + exit 1 +fi +echo -e " Git: ${GREEN}OK${NC}" + +# ============================================================ +# 3. Pruefe ob bereits installiert (Update vs. Neuinstallation) +# ============================================================ +echo "" +if [ -d "${INSTALL_DIR}/.git" ]; then + echo -e "${YELLOW}Update erkannt - hole neueste Aenderungen...${NC}" + + # Sichere lokale Aenderungen + if ! git diff --quiet 2>/dev/null; then + echo "Lokale Aenderungen gefunden, erstelle Stash..." + git stash + fi + + git fetch origin + git pull origin main + + echo -e "${GREEN}Repository aktualisiert${NC}" +else + echo "Neuinstallation - klone Repository..." + + # Temporaeres Verzeichnis fuer Clone + TEMP_DIR=$(mktemp -d) + git clone "${REPO_URL}" "${TEMP_DIR}" + + # Kopiere Dateien (ueberschreibt nicht .env) + shopt -s dotglob + for item in "${TEMP_DIR}"/*; do + basename_item=$(basename "$item") + if [ "$basename_item" != ".env" ] && [ "$basename_item" != ".git" ]; then + cp -r "$item" "${INSTALL_DIR}/" + fi + done + + # .git Verzeichnis kopieren fuer Updates + cp -r "${TEMP_DIR}/.git" "${INSTALL_DIR}/" + + rm -rf "${TEMP_DIR}" + echo -e "${GREEN}Repository geklont${NC}" +fi + +# ============================================================ +# 4. Verzeichnisse und Rechte setzen +# ============================================================ +echo "" +echo "Setze Verzeichnisse und Berechtigungen..." + +# Verzeichnisse erstellen falls nicht vorhanden +mkdir -p "${INSTALL_DIR}/data" +mkdir -p "${INSTALL_DIR}/logs" + +# Berechtigungen setzen (rwx fuer Owner, rx fuer Group/Other) +chmod 755 "${INSTALL_DIR}/data" +chmod 755 "${INSTALL_DIR}/logs" + +# Fuer Docker: Verzeichnisse muessen vom Container beschreibbar sein +# Option 1: Wenn Container als root laeuft (Standard) - 755 reicht +# Option 2: Wenn Container als non-root laeuft - 777 oder chown noetig + +# Pruefen ob wir als root laufen (fuer chown) +if [ "$(id -u)" = "0" ]; then + # Als root: Owner auf aktuellen User setzen (oder Docker-User) + # Standard: belassen wie es ist (root kann alles) + echo -e " data/: ${GREEN}OK${NC} (755, root)" + echo -e " logs/: ${GREEN}OK${NC} (755, root)" +else + # Als normaler User: Verzeichnisse muessen beschreibbar sein + # Docker-Container laeuft meist als root, daher 755 ausreichend + # Falls Container als non-root laeuft, auf 777 setzen: + # chmod 777 "${INSTALL_DIR}/data" "${INSTALL_DIR}/logs" + echo -e " data/: ${GREEN}OK${NC} (755)" + echo -e " logs/: ${GREEN}OK${NC} (755)" +fi + +# .env Datei schuetzen (nur Owner kann lesen/schreiben) +if [ -f "${INSTALL_DIR}/.env" ]; then + chmod 600 "${INSTALL_DIR}/.env" + echo -e " .env: ${GREEN}OK${NC} (600, nur Owner)" +fi + +# install.sh ausfuehrbar machen +if [ -f "${INSTALL_DIR}/install.sh" ]; then + chmod +x "${INSTALL_DIR}/install.sh" +fi + +# ============================================================ +# 5. Docker-Netzwerk pruefen/erstellen +# ============================================================ +echo "" +NETWORK="${TRAEFIK_NETWORK:-web}" +echo "Pruefe Docker-Netzwerk: ${NETWORK}" + +if docker network inspect "${NETWORK}" >/dev/null 2>&1; then + echo -e " Netzwerk '${NETWORK}': ${GREEN}existiert${NC}" +else + echo -e " ${YELLOW}Netzwerk '${NETWORK}' nicht gefunden - erstelle...${NC}" + docker network create "${NETWORK}" + echo -e " Netzwerk '${NETWORK}': ${GREEN}erstellt${NC}" +fi + +# ============================================================ +# 6. Docker-Images bauen +# ============================================================ +echo "" +echo "Baue Docker-Images..." + +# Stoppe laufende Container +${COMPOSE_CMD} down 2>/dev/null || true + +# User-Template Image bauen (fuer User-Container) +if [ -d "${INSTALL_DIR}/user-template" ]; then + echo " [1/4] Baue user-service-template (User-Container)..." + if docker build --no-cache -t user-service-template:latest "${INSTALL_DIR}/user-template/" > /dev/null 2>&1; then + echo -e " user-service-template: ${GREEN}OK${NC}" + else + echo -e " user-service-template: ${RED}FEHLER${NC}" + echo " Versuche mit detaillierter Ausgabe..." + docker build --no-cache -t user-service-template:latest "${INSTALL_DIR}/user-template/" + exit 1 + fi +fi + +# User-Template-Next Image bauen (alternatives Template, optional) +if [ -d "${INSTALL_DIR}/user-template-next" ]; then + echo " [2/4] Baue user-template-next (alternatives Template)..." + if docker build -t user-template-next:latest "${INSTALL_DIR}/user-template-next/" > /dev/null 2>&1; then + echo -e " user-template-next: ${GREEN}OK${NC}" + else + echo -e " user-template-next: ${YELLOW}WARNUNG - Build fehlgeschlagen (optional)${NC}" + fi +fi + +# Spawner Backend Image bauen +echo " [3/4] Baue Spawner API (Flask Backend)..." +if docker build -t spawner:latest "${INSTALL_DIR}/" > /dev/null 2>&1; then + echo -e " spawner-api: ${GREEN}OK${NC}" +else + echo -e " spawner-api: ${RED}FEHLER${NC}" + docker build -t spawner:latest "${INSTALL_DIR}/" + exit 1 +fi + +# Frontend Image bauen +if [ -d "${INSTALL_DIR}/frontend" ]; then + echo " [4/4] Baue Frontend (Next.js)..." + echo " Dies kann einige Minuten dauern (npm install + build)..." + if docker build -t spawner-frontend:latest "${INSTALL_DIR}/frontend/" 2>&1 | grep -E "(error|Error|ERROR)" > /dev/null; then + echo -e " spawner-frontend: ${RED}FEHLER${NC}" + echo " Versuche mit detaillierter Ausgabe..." + docker build -t spawner-frontend:latest "${INSTALL_DIR}/frontend/" + exit 1 + else + docker build -t spawner-frontend:latest "${INSTALL_DIR}/frontend/" > /dev/null 2>&1 + echo -e " spawner-frontend: ${GREEN}OK${NC}" + fi +fi + +echo "" +echo "Alle Images erfolgreich gebaut." + +# ============================================================ +# 7. Container starten +# ============================================================ +echo "" +echo "Starte Container..." +${COMPOSE_CMD} up -d + +# Warte auf Health-Check +echo "" +echo "Warte auf Spawner-Start..." +sleep 5 + +# Health-Check fuer API +SPAWNER_URL="http://localhost:${SPAWNER_PORT:-5000}/health" +if curl -sf "${SPAWNER_URL}" >/dev/null 2>&1; then + echo -e " API Health-Check: ${GREEN}OK${NC}" +else + echo -e " API Health-Check: ${YELLOW}Noch nicht bereit (normal beim ersten Start)${NC}" +fi + +# Health-Check fuer Frontend +if curl -sf "http://localhost:3000/" >/dev/null 2>&1; then + echo -e " Frontend Health-Check: ${GREEN}OK${NC}" +else + echo -e " Frontend Health-Check: ${YELLOW}Noch nicht bereit (normal beim ersten Start)${NC}" +fi + +# ============================================================ +# 8. Fertig +# ============================================================ +echo "" +echo "============================================================" +echo -e " ${GREEN}Installation abgeschlossen!${NC}" +echo "============================================================" +echo "" + +# URLs anzeigen +SCHEME="https" +if [ "${BASE_DOMAIN:-localhost}" = "localhost" ]; then + SCHEME="http" +fi +FULL_URL="${SCHEME}://${SPAWNER_SUBDOMAIN:-coder}.${BASE_DOMAIN:-localhost}" + +echo "Zugriff:" +echo " Frontend: ${FULL_URL}" +echo " API: ${FULL_URL}/api" +echo " Health: ${FULL_URL}/health" +echo "" +echo "Lokaler Zugriff (ohne Traefik):" +echo " API: http://localhost:${SPAWNER_PORT:-5000}" +echo " Frontend: http://localhost:3000" +echo "" +echo "Nuetzliche Befehle:" +echo " Status: ${COMPOSE_CMD} ps" +echo " Logs API: ${COMPOSE_CMD} logs -f spawner" +echo " Logs FE: ${COMPOSE_CMD} logs -f frontend" +echo " Neustart: ${COMPOSE_CMD} restart" +echo " Stoppen: ${COMPOSE_CMD} down" +echo "" diff --git a/models.py b/models.py new file mode 100644 index 0000000..e2b20ee --- /dev/null +++ b/models.py @@ -0,0 +1,21 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash +from datetime import datetime + +db = SQLAlchemy() + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) + password_hash = db.Column(db.String(200), nullable=False) + container_id = db.Column(db.String(100), nullable=True) + container_port = db.Column(db.Integer, nullable=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0fd1c90 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +flask==3.0.0 +flask-login==0.6.3 +flask-sqlalchemy==3.1.1 +flask-jwt-extended==4.6.0 +flask-cors==4.0.0 +werkzeug==3.0.1 +docker==6.1.0 +requests==2.28.0 +urllib3==1.26.12 +requests-unixsocket>=0.3.0 +PyJWT==2.8.0 +python-dotenv==1.0.0 diff --git a/user-template-next/.gitignore b/user-template-next/.gitignore new file mode 100644 index 0000000..366f75f --- /dev/null +++ b/user-template-next/.gitignore @@ -0,0 +1,32 @@ +# Dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# Testing +/coverage + +# Next.js +/.next/ +/out/ + +# Production +/build + +# Misc +.DS_Store +*.pem + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Local env files +.env*.local +.env + +# TypeScript +*.tsbuildinfo +next-env.d.ts diff --git a/user-template-next/Dockerfile b/user-template-next/Dockerfile new file mode 100644 index 0000000..62ed91e --- /dev/null +++ b/user-template-next/Dockerfile @@ -0,0 +1,30 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci + +COPY . . +RUN npm run build + +# Production stage - serve static files with nginx +FROM nginx:alpine + +# Copy static export +COPY --from=builder /app/out /usr/share/nginx/html + +# Nginx config for SPA +RUN echo 'server { \ + listen 8080; \ + root /usr/share/nginx/html; \ + index index.html; \ + location / { \ + try_files $uri $uri/ /index.html; \ + } \ +}' > /etc/nginx/conf.d/default.conf + +EXPOSE 8080 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/user-template-next/next.config.mjs b/user-template-next/next.config.mjs new file mode 100644 index 0000000..d7186e3 --- /dev/null +++ b/user-template-next/next.config.mjs @@ -0,0 +1,10 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'export', + trailingSlash: true, + images: { + unoptimized: true, + }, +}; + +export default nextConfig; diff --git a/user-template-next/package.json b/user-template-next/package.json new file mode 100644 index 0000000..6dc3fdb --- /dev/null +++ b/user-template-next/package.json @@ -0,0 +1,32 @@ +{ + "name": "user-template", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "14.2.5", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "tailwind-merge": "^2.4.0", + "lucide-react": "^0.408.0", + "@radix-ui/react-slot": "^1.1.0" + }, + "devDependencies": { + "@types/node": "^20.14.10", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-config-next": "14.2.5", + "postcss": "^8.4.39", + "tailwindcss": "^3.4.6", + "typescript": "^5.5.3" + } +} diff --git a/user-template-next/postcss.config.mjs b/user-template-next/postcss.config.mjs new file mode 100644 index 0000000..2ef30fc --- /dev/null +++ b/user-template-next/postcss.config.mjs @@ -0,0 +1,9 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + +export default config; diff --git a/user-template-next/src/app/globals.css b/user-template-next/src/app/globals.css new file mode 100644 index 0000000..4ade4fb --- /dev/null +++ b/user-template-next/src/app/globals.css @@ -0,0 +1,51 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 221.2 83.2% 53.3%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 224.3 76.3% 48%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/user-template-next/src/app/layout.tsx b/user-template-next/src/app/layout.tsx new file mode 100644 index 0000000..c97d786 --- /dev/null +++ b/user-template-next/src/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Mein Container", + description: "Dein persoenlicher Container-Bereich", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/user-template-next/src/app/page.tsx b/user-template-next/src/app/page.tsx new file mode 100644 index 0000000..6fcbdc2 --- /dev/null +++ b/user-template-next/src/app/page.tsx @@ -0,0 +1,133 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Container, Rocket, Code, Terminal } from "lucide-react"; + +export default function Home() { + return ( +
+ {/* Header */} +
+
+
+ + Mein Container +
+
+
+ + {/* Hero Section */} +
+
+
+ +
+

+ Willkommen in deinem Container! +

+

+ Dies ist dein persoenlicher Bereich. Du kannst diese Seite anpassen + und eigene Anwendungen deployen. +

+
+ + {/* Feature Cards */} +
+ + +
+ +
+ Entwicklung + + Starte hier mit deiner eigenen Anwendung + +
+ +

+ Dieses Template basiert auf Next.js und React. Du kannst es + anpassen oder komplett ersetzen. +

+
+
+ + + +
+ +
+ Technologie + Moderne Tools fuer moderne Apps +
+ +
    +
  • + + Next.js 14 mit App Router +
  • +
  • + + TypeScript fuer Typsicherheit +
  • +
  • + + Tailwind CSS fuer Styling +
  • +
  • + + shadcn/ui Komponenten +
  • +
+
+
+
+ + {/* CTA Section */} +
+ + +

Bereit loszulegen?

+

+ Bearbeite die Dateien im Container, um diese Seite anzupassen. +

+ +
+
+
+
+ + {/* Footer */} +
+
+

+ Container Spawner - Dein persoenlicher Entwicklungsbereich +

+
+
+
+ ); +} diff --git a/user-template-next/src/components/ui/button.tsx b/user-template-next/src/components/ui/button.tsx new file mode 100644 index 0000000..de31d90 --- /dev/null +++ b/user-template-next/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/user-template-next/src/components/ui/card.tsx b/user-template-next/src/components/ui/card.tsx new file mode 100644 index 0000000..b375b06 --- /dev/null +++ b/user-template-next/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/user-template-next/tailwind.config.ts b/user-template-next/tailwind.config.ts new file mode 100644 index 0000000..36f59d4 --- /dev/null +++ b/user-template-next/tailwind.config.ts @@ -0,0 +1,49 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + darkMode: ["class"], + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + }, + }, + plugins: [], +}; + +export default config; diff --git a/user-template-next/tsconfig.json b/user-template-next/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/user-template-next/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/user-template/Dockerfile b/user-template/Dockerfile new file mode 100644 index 0000000..df61e0f --- /dev/null +++ b/user-template/Dockerfile @@ -0,0 +1,23 @@ +FROM nginxinc/nginx-unprivileged:alpine + +# Wechsel zu root um Pakete zu installieren +USER root + +# gettext für envsubst installieren +RUN apk add --no-cache gettext + +# HTML-Template kopieren +COPY index.html /usr/share/nginx/html/index.html.template + +# Nginx-Konfiguration kopieren +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Entrypoint Script kopieren und executable machen +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh && \ + chmod 777 /usr/share/nginx/html + +EXPOSE 8080 + +# Entrypoint Script starten (läuft als root, startet Nginx) +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/user-template/entrypoint.sh b/user-template/entrypoint.sh new file mode 100644 index 0000000..cd340b0 --- /dev/null +++ b/user-template/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +# Umgebungsvariablen in die HTML-Datei einfügen +envsubst < /usr/share/nginx/html/index.html.template > /usr/share/nginx/html/index.html + +# Nginx starten +exec nginx -g "daemon off;" diff --git a/user-template/index.html b/user-template/index.html new file mode 100644 index 0000000..635ef99 --- /dev/null +++ b/user-template/index.html @@ -0,0 +1,284 @@ + + + + + + Dein persönlicher Service | SPAWNER + + + +
+ + ● Online + +

Willkommen bei SPAWNER!

+

Dies ist dein persönlicher Container-Service

+ +
+
+ User-ID + Wird geladen... +
+
+ Username + Wird geladen... +
+
+ Container gestartet + Wird geladen... +
+
+ Container-ID + Wird geladen... +
+
+ +
+

🎯 Deine Container-Features

+
+
+
🔒
+
Isolierte Umgebung
+
+
+
+
Schnelle Performance
+
+
+
🔄
+
Auto-Restart
+
+
+
📊
+
Resource-Limits
+
+
+
+ + +
+ + + + diff --git a/user-template/nginx.conf b/user-template/nginx.conf new file mode 100644 index 0000000..4d8915b --- /dev/null +++ b/user-template/nginx.conf @@ -0,0 +1,15 @@ +server { + listen 8080; + server_name _; + + # Root-Verzeichnis + root /usr/share/nginx/html; + + # Alle Anfragen einfach auf root Location + location / { + try_files $uri $uri/ /index.html; + } + + # Error pages + error_page 404 /index.html; +}