commit 845942f36ccfecbdeb85fcfeeee05f3fb532ee7c Author: Florian Wilde Date: Thu Sep 3 00:11:43 2026 +0200 Phase 0: Fundament der Feuerwehr-Verwaltungsplattform Legt den kompletten Unterbau an, auf dem die Fachmodule aufsetzen. Betrieb: - docker-compose.yml mit Traefik, App, Worker, PostgreSQL 17 + PostGIS, Redis, MinIO und Gotenberg; Entwicklungs-Override ohne Traefik, dafuer mit Mailpit und Adminer - Mehrstufiges Dockerfile: eigener Benutzer, tini als PID 1, Healthcheck - .env.example mit einem Kommentar zu jeder Variable Mandantentrennung: - Row Level Security auf allen mandantenpflichtigen Tabellen - Zwei getrennte Datenbankverbindungen: florian_app mit aktiver RLS fuer die Anwendung, Eigentuemer-Zugang nur fuer Migration, Seed und Anmeldung - fuerOrganisation() als einziger Weg zu Fachdaten Nachvollziehbarkeit: - AuditLog append-only per Trigger; UPDATE ist dauerhaft gesperrt, DELETE nur im gekennzeichneten Aufbewahrungslauf - Geheimnisse werden vor dem Schreiben entfernt Anmeldung: - OIDC gegen ein extern betriebenes authentik, Rollen aus Gruppen nur als Vorbelegung beim ersten Login - Lokaler E-Mail/Passwort-Login als Rueckfallweg - Terminal per PIN und RFID mit zwei Schranken (Geraet, dann Person) Weiteres: - lib/authz.ts mit 9 Rollen und 38 Einzelrechten - lib/storage.ts (MinIO), lib/audit.ts, lib/warteschlange.ts (BullMQ) - Integrationen fuer Mail, Telegram und LLM hinter abschaltbaren Interfaces - tRPC v11, Basis-Layout mit Seitenleiste und dunkler Darstellung - Seed mit einer Beispielwehr: 16 Personen, 3 Fahrzeuge, 40 Geraete - 32 Vitest-Tests (davon 8 gegen eine echte Datenbank), 10 Playwright-Tests - README, Anleitung mit Screenshots, Roadmap, Aufwandsschaetzung, Devlog, Versionierungsregeln und Changelog Co-Authored-By: Claude Opus 5 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2e4406d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +node_modules +.next +.git +.github +coverage +test-results +playwright-report +data +Phase-*.md +*.log +.env +.env.local +.env.*.local +README.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ce12391 --- /dev/null +++ b/.env.example @@ -0,0 +1,231 @@ +# ============================================================================= +# FLORIAN — Feuerwehr-Verwaltungsplattform +# Kopiere diese Datei nach ".env" und passe die Werte an: +# cp .env.example .env +# Alles, was hier steht, wird von docker-compose.yml und von lib/env.ts gelesen. +# lib/env.ts validiert beim Start mit Zod — fehlt eine Pflichtvariable, startet +# die App gar nicht erst, statt später halb zu funktionieren. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Allgemein +# ----------------------------------------------------------------------------- +# development | production | test — steuert Logging, Fehlerseiten und Caching. +NODE_ENV=development +# Öffentliche Basis-URL der Anwendung, ohne abschließenden Schrägstrich. +# Muss exakt der Adresse entsprechen, unter der Nutzer die App aufrufen — +# Auth.js baut daraus die OIDC-Redirect-URI. +APP_URL=http://localhost:3100 +# Zeitzone für alle serverseitigen Fristen- und Terminberechnungen. +# Nicht ändern, außer die Wehr liegt außerhalb Deutschlands. +TZ=Europe/Berlin +# Host-Ports fuer die Entwicklung. Bewusst nicht die Standardports, +# damit FLORIAN neben anderen lokalen Stacks laufen kann. +APP_PORT=3100 +GOTENBERG_PORT=3101 +MAILPIT_UI_PORT=8125 +MAILPIT_SMTP_PORT=1125 +ADMINER_PORT=8081 + +# ----------------------------------------------------------------------------- +# Traefik / Domains (nur für den Produktionsbetrieb relevant) +# In der Entwicklung greift docker-compose.override.yml und veröffentlicht die +# Dienste direkt auf localhost — Traefik wird dort nicht gebraucht. +# ----------------------------------------------------------------------------- +# Hostname der Anwendung hinter Traefik, z. B. florian.beispielwehr.de +APP_DOMAIN=florian.localhost +# Hostname der MinIO-S3-API (Datei-Uploads, presignte URLs). +MINIO_DOMAIN=s3.florian.localhost +# Hostname der MinIO-Weboberfläche (Administration der Buckets). +MINIO_CONSOLE_DOMAIN=minio.florian.localhost +# Hostname des Traefik-Dashboards. Leer lassen, um es nicht zu veröffentlichen. +TRAEFIK_DASHBOARD_DOMAIN=traefik.florian.localhost +# E-Mail-Adresse für die ACME-Zertifikate (Let us Encrypt). +ACME_EMAIL=kommandant@beispielwehr.de +# Traefik-Entrypoint für HTTPS. "websecure" ist der Standard aus dieser Datei. +TRAEFIK_ENTRYPOINT=websecure + +# ----------------------------------------------------------------------------- +# PostgreSQL 17 + PostGIS +# ----------------------------------------------------------------------------- +POSTGRES_USER=florian +# Vor dem ersten Start ersetzen: openssl rand -base64 24 +POSTGRES_PASSWORD=bitte_aendern_postgres +POSTGRES_DB=florian +# Port, unter dem Postgres auf dem Host erreichbar ist (nur Entwicklung). +POSTGRES_PORT=5442 +# Verbindungszeichenfolge der Anwendung. Läuft über den Datenbankbenutzer +# "florian_app", für den Row Level Security AKTIV ist — dieser Zugang sieht nur +# Daten der Organisation, die per SET LOCAL app.current_organisation gesetzt +# wurde (siehe src/lib/db.ts). +DATABASE_URL=postgresql://florian_app:bitte_aendern_app@postgres:5432/florian?schema=public +# Passwort des Anwendungsbenutzers. Muss zu DATABASE_URL passen und wird beim +# ersten Start von docker/postgres/10-init-app-role.sh gesetzt. +POSTGRES_APP_PASSWORD=bitte_aendern_app +# Verbindung für Migrationen und Seed. Nutzt den Eigentümer-Zugang, der RLS +# umgehen darf. Niemals im Anwendungscode verwenden. +DIRECT_DATABASE_URL=postgresql://florian:bitte_aendern_postgres@postgres:5432/florian?schema=public + +# ----------------------------------------------------------------------------- +# Redis (Warteschlangen für BullMQ, Rate-Limiting) +# ----------------------------------------------------------------------------- +# Vor dem ersten Start ersetzen: openssl rand -base64 24 +REDIS_PASSWORD=bitte_aendern_redis +REDIS_URL=redis://:bitte_aendern_redis@redis:6379 +# Port auf dem Host (nur Entwicklung). +REDIS_PORT=6389 +# Anzahl paralleler Jobs je Worker-Prozess. +WORKER_CONCURRENCY=5 + +# ----------------------------------------------------------------------------- +# MinIO (S3-kompatibler Objektspeicher — ALLE Dateien liegen hier, nie im FS) +# ----------------------------------------------------------------------------- +MINIO_ROOT_USER=florian +# Mindestens 8 Zeichen, vor dem ersten Start ersetzen. +MINIO_ROOT_PASSWORD=bitte_aendern_minio +# Endpunkt, den App und Worker intern ansprechen — im Compose-Netz der +# Dienstname, außerhalb der externe Hostname. +S3_ENDPOINT=minio +S3_PORT=9000 +# true, sobald MinIO hinter Traefik mit TLS läuft. +S3_USE_SSL=false +S3_ACCESS_KEY=florian +S3_SECRET_KEY=bitte_aendern_minio +S3_REGION=eu-central-1 +# Bucket für alle Nutzdateien (Prüfprotokolle, Fotos, Rechnungen, Exporte). +S3_BUCKET=florian +# Öffentlich erreichbare Basis-URL für presignte Download-Links. In der +# Entwicklung http://localhost:9010, in Produktion https://$MINIO_DOMAIN +S3_PUBLIC_URL=http://localhost:9010 +# Gültigkeitsdauer presignter Links in Sekunden (Standard: 15 Minuten). +S3_URL_TTL=900 +# Ports auf dem Host (nur Entwicklung): API und Weboberfläche. +MINIO_API_PORT=9010 +MINIO_CONSOLE_PORT=9011 + +# ----------------------------------------------------------------------------- +# Gotenberg (PDF-Erzeugung für Prüfprotokolle, Berichte, Rechnungen) +# ----------------------------------------------------------------------------- +GOTENBERG_URL=http://gotenberg:3000 +# Zeitlimit je PDF-Auftrag in Sekunden. +GOTENBERG_TIMEOUT=30 + +# ----------------------------------------------------------------------------- +# Auth.js — Sitzungsverwaltung +# ----------------------------------------------------------------------------- +# Signiert die Sitzungs-Cookies. Zwingend erforderlich, pro Installation neu: +# openssl rand -base64 32 +AUTH_SECRET=bitte_aendern_mindestens_32_zeichen_lang +# Muss hinter einem Reverse Proxy (Traefik) auf true stehen, damit Auth.js die +# X-Forwarded-Header auswertet und korrekte Redirect-URIs erzeugt. +AUTH_TRUST_HOST=true +# Lebensdauer einer Sitzung in Sekunden (Standard: 8 Stunden). +AUTH_SESSION_MAX_AGE=28800 + +# ----------------------------------------------------------------------------- +# OIDC gegen authentik +# Die authentik-Instanz wird SEPARAT betrieben und ist nicht Teil dieses Stacks. +# In authentik anzulegen: Provider "OAuth2/OpenID" (Client-Typ: confidential), +# Redirect-URI exakt $APP_URL/api/auth/callback/authentik +# Scopes: openid profile email — plus einen Gruppen-Scope, wenn Rollen aus +# authentik-Gruppen vorbelegt werden sollen (siehe AUTH_OIDC_GROUP_CLAIM). +# ----------------------------------------------------------------------------- +# false blendet den OIDC-Knopf aus; dann bleibt nur der lokale Login. Sinnvoll +# für den Erststart, bevor authentik konfiguriert ist. +AUTH_OIDC_ENABLED=false +# Issuer-URL des Providers, OHNE /.well-known/openid-configuration. +# Bei authentik: https:///application/o// +AUTH_OIDC_ISSUER=https://auth.beispiel.de/application/o/florian/ +AUTH_OIDC_CLIENT_ID= +AUTH_OIDC_CLIENT_SECRET= +# Beschriftung des Anmeldeknopfes in der Oberfläche. +AUTH_OIDC_NAME=Anmeldung über authentik +# Claim, aus dem die Gruppenzugehörigkeit gelesen wird. In authentik über ein +# Scope-Mapping bereitzustellen. Leer lassen, um das Mapping abzuschalten — +# dann werden Rollen ausschließlich in FLORIAN gepflegt. +AUTH_OIDC_GROUP_CLAIM=groups +# Zuordnung authentik-Gruppe -> FLORIAN-Rolle, kommagetrennt als "gruppe:ROLLE". +# Wirkt nur als VORBELEGUNG beim ersten Login; danach ist die in FLORIAN +# gepflegte Rolle führend und wird nicht mehr überschrieben. +AUTH_OIDC_GROUP_MAPPING=feuerwehr-kommandant:KOMMANDANT,feuerwehr-geraetewart:GERAETEWART,feuerwehr-atemschutz:ATEMSCHUTZGERAETEWART,feuerwehr-kassier:KASSIER,feuerwehr-schriftfuehrer:SCHRIFTFUEHRER,feuerwehr-ausbilder:AUSBILDER,feuerwehr-mitglied:MITGLIED +# true legt einen in authentik bekannten Nutzer beim ersten Login automatisch +# als Person an. false verlangt, dass die Person vorher in FLORIAN existiert +# und über die E-Mail-Adresse verknüpft wird. +AUTH_OIDC_AUTO_PROVISION=true +# Organisation (Slug), der automatisch angelegte Personen zugeordnet werden. +AUTH_OIDC_STANDARD_ORGANISATION=ff-musterhausen + +# ----------------------------------------------------------------------------- +# Lokaler E-Mail/Passwort-Login (Rückfalloption für Erststart und Notbetrieb) +# ----------------------------------------------------------------------------- +# Nach der Einrichtung von authentik auf false setzen, damit nur noch OIDC geht. +AUTH_LOCAL_ENABLED=true +# Zugangsdaten des ersten Administrators, angelegt vom Seed-Skript. +# Nach dem ersten Login das Passwort in der Oberfläche ändern. +SEED_ADMIN_EMAIL=admin@beispielwehr.de +SEED_ADMIN_PASSWORT=Feuerwehr2026 +# bcrypt-Kostenfaktor. 12 ist ein guter Kompromiss aus Sicherheit und Tempo. +AUTH_BCRYPT_ROUNDS=12 + +# ----------------------------------------------------------------------------- +# Terminal-/Kiosk-Anmeldung im Gerätehaus (PIN und RFID) +# ----------------------------------------------------------------------------- +# Schaltet den Kiosk-Modus samt Terminal-Login frei. +KIOSK_ENABLED=true +# Gemeinsames Geheimnis, mit dem sich ein Terminal-Gerät gegenüber dem Server +# ausweist, bevor überhaupt eine PIN oder ein Token akzeptiert wird: +# openssl rand -hex 32 +KIOSK_DEVICE_SECRET=bitte_aendern_kiosk_geheimnis +# Länge der Personen-PIN. 4 ist bequem, 6 ist sicherer. +KIOSK_PIN_LAENGE=6 +# Nach wie vielen Fehlversuchen eine PIN gesperrt wird. +KIOSK_MAX_FEHLVERSUCHE=5 +# Sperrdauer nach zu vielen Fehlversuchen, in Sekunden. +KIOSK_SPERRDAUER=300 +# Automatische Abmeldung am Terminal nach Untätigkeit, in Sekunden. +KIOSK_IDLE_TIMEOUT=120 + +# ----------------------------------------------------------------------------- +# E-Mail-Versand (optional, hinter lib/integrations/mail.ts abstrahiert) +# Ohne MAIL_ENABLED=true wird nur ins Log geschrieben — praktisch für Tests. +# ----------------------------------------------------------------------------- +MAIL_ENABLED=false +MAIL_HOST=smtp.beispiel.de +MAIL_PORT=587 +MAIL_SECURE=false +MAIL_USER= +MAIL_PASSWORD= +MAIL_FROM=FLORIAN + +# ----------------------------------------------------------------------------- +# Telegram-Benachrichtigungen (optional, abschaltbar) +# ----------------------------------------------------------------------------- +TELEGRAM_ENABLED=false +TELEGRAM_BOT_TOKEN= +TELEGRAM_CHAT_ID= + +# ----------------------------------------------------------------------------- +# LLM-Anbindung (optional; nur selbstgehostete Endpunkte, kein Cloud-Zwang) +# ----------------------------------------------------------------------------- +LLM_ENABLED=false +# OpenAI-kompatibler Endpunkt, z. B. eine lokale Ollama- oder vLLM-Instanz. +LLM_BASE_URL=http://ollama:11434/v1 +LLM_API_KEY= +LLM_MODELL=qwen2.5:14b + +# ----------------------------------------------------------------------------- +# Datenschutz und Aufbewahrung (DSGVO) +# ----------------------------------------------------------------------------- +# Aufbewahrungsfrist für Einsatz- und Prüfdokumentation in Jahren. +AUFBEWAHRUNG_JAHRE=10 +# Aufbewahrungsfrist für den Audit-Log in Tagen. Der Log ist append-only; +# Älteres wird vom Worker exportiert, bevor es entfernt wird. +AUDIT_AUFBEWAHRUNG_TAGE=3650 +# Frist in Tagen, nach der ein ausgetretenes Mitglied anonymisiert wird. +PERSON_ANONYMISIERUNG_TAGE=365 + +# ----------------------------------------------------------------------------- +# Telemetrie bleibt aus. Nicht ändern. +# ----------------------------------------------------------------------------- +NEXT_TELEMETRY_DISABLED=1 +CHECKPOINT_DISABLE=1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f6f37e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Abhängigkeiten +node_modules/ +.pnp +.pnp.js + +# Build-Artefakte +.next/ +out/ +build/ +dist/ +*.tsbuildinfo +next-env.d.ts + +# Tests +coverage/ +test-results/ +playwright-report/ +blob-report/ +.playwright/ + +# Umgebung — niemals einchecken +.env +.env.local +.env.*.local +!.env.example + +# Laufzeitdaten (Docker-Volumes bei Bind-Mounts) +data/ +docker/traefik/acme.json + +# Editor / OS +.vscode/* +!.vscode/extensions.json +.idea/ +.DS_Store +Thumbs.db +debug.log +*.log + +# Prisma +prisma/*.db +prisma/*.db-journal diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..ecc2009 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +# Keine Telemetrie, keine Audit-Calls beim Container-Build. +audit=false +fund=false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..012d1df --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,89 @@ +# Changelog + +Alle nennenswerten Änderungen an FLORIAN. Format nach +[Keep a Changelog](https://keepachangelog.com/de/1.1.0/), Versionierung nach +[Semantic Versioning](https://semver.org/lang/de/). + +Die Regeln dazu stehen in [docs/VERSIONIERUNG.md](docs/VERSIONIERUNG.md). + +## [Unveröffentlicht] + +Noch nichts. Nächster Schritt ist Phase 1 (Stammdatenverwaltung). + +## [0.1.0] — 2026-09-03 + +Phase 0: das Fundament. Ab hier steht ein lauffähiger Stack mit +Mandantentrennung, Anmeldung, Dateiablage und lückenloser Protokollierung. + +### Hinzugefügt + +**Betrieb** + +- `docker-compose.yml` mit Traefik, App, Worker, PostgreSQL 17 + PostGIS, + Redis, MinIO und Gotenberg. +- `docker-compose.override.yml` für die Entwicklung: ohne Traefik, mit + Mailpit als SMTP-Auffangkorb und Adminer für den Blick in die Datenbank. + Host-Ports bewusst abseits der Standardwerte (3100, 5442, 6389, 9010/9011), + damit FLORIAN neben anderen lokalen Stacks laufen kann. +- Mehrstufiges `docker/Dockerfile`: eigener Benutzer `florian` (UID 1001, + kein root), `tini` als PID 1, Healthcheck gegen `/api/health`, + Standalone-Build von Next.js. +- `docker/entrypoint.sh` wendet ausstehende Migrationen an — nur im + App-Container, nicht im Worker. +- `docker/postgres/10-init-app-role.sh` legt beim ersten Start die Rolle + `florian_app` (`NOBYPASSRLS`) samt Vorgabeprivilegien an. +- `.env.example` mit einem Kommentar zu jeder einzelnen Variable. + +**Datenmodell** + +- Prisma-Schema mit `Organisation`, `Person`, `Rolle`, `Berechtigung`, + `RolleBerechtigung`, `PersonRolle`, `AusweisToken`, `AuditLog`, `Datei`. +- `Fahrzeug` und `Geraet` bereits schmal angelegt, damit der Seed eine + realistische Wehr aufbauen kann. Phase 1 erweitert beide. +- Migration `20260902000100_rls_policies`: Row Level Security auf allen + mandantenpflichtigen Tabellen, Funktion `aktuelle_organisation()`, + Append-only-Trigger auf `audit_log`. +- Migration `20260902000200_audit_bereinigung`: `UPDATE` auf `audit_log` + bleibt dauerhaft gesperrt, `DELETE` ist nur im gekennzeichneten + Aufbewahrungslauf möglich. + +**Anwendungskern** + +- `lib/env.ts` — Zod-Validierung der gesamten Umgebung beim Start. +- `lib/db.ts` — getrennte Verbindungen für Anwendung (RLS aktiv) und System + (RLS-frei), `fuerOrganisation()` als einziger Weg zu Fachdaten. +- `lib/authz.ts` — 9 Rollen, 38 Einzelrechte, zentrale Prüffunktionen. +- `lib/storage.ts` — MinIO, presignte Up- und Downloads, SHA-256-Prüfsumme. +- `lib/audit.ts` — Protokolleinträge mit Vorher/Nachher-Vergleich, entfernt + Geheimnisse vor dem Schreiben. +- `lib/auth.ts` — Auth.js mit OIDC gegen authentik, lokalem Passwort-Login + und Terminal-Anmeldung über PIN oder RFID. +- `lib/warteschlange.ts` und `src/worker/` — BullMQ-Gerüst mit den + Auftragsarten für Fristen, Erinnerungen, PDF und Aufbewahrung. +- `lib/integrations/` — Mail, Telegram und LLM hinter abschaltbaren + Schnittstellen. +- tRPC v11 mit den Prozedurarten `oeffentlich`, `angemeldet` und + `mitRecht(...)`. + +**Oberfläche** + +- Basis-Layout mit Seitenleiste, heller und dunkler Darstellung + (Systemvorgabe als Standard) und durchgängig deutschen Texten. +- Anmeldeseite, die genau die eingerichteten Anmeldewege zeigt. +- Übersicht mit Kennzahlen und dem Sicherheitshinweis zur analogen + Atemschutzüberwachung. +- Farbwelt in OKLCH mit festen Statusfarben für Gerätezustände. + +**Daten und Tests** + +- Seed mit einer vollständigen Beispielwehr: 15 Personen mit Rollen und + Terminal-Ausweisen, 3 Fahrzeuge, 40 Geräte mit gemischten Prüffristen. +- Vitest: 32 Tests, darunter acht, die die Mandantentrennung gegen eine echte + Datenbank prüfen. +- Playwright: 10 Tests über Desktop und Tablet. +- `npm run screenshots` erzeugt die Bilder für die Anleitung. + +**Dokumentation** + +- README mit Schnellstart in sechs Zeilen. +- Anleitung, Roadmap, Aufwandsschätzung, Devlog und Versionierungsregeln. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e209539 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,62 @@ +# Projekt: FLORIAN — Feuerwehr-Verwaltungsplattform + +## Kontext +Selbstgehostete Verwaltungssoftware für Freiwillige Feuerwehren in Deutschland +(Bayern). Zielgruppe: Kommandant, Gerätewart, Atemschutzgerätewart, Schriftführer, +Kassier, Mannschaft. Betrieb: Docker Compose auf Proxmox VE, kein Cloud-Zwang, +DSGVO-konform, Daten bleiben im Gerätehaus bzw. auf eigener Hardware. + +## Nicht verhandelbarer Stack +- Next.js 15 App Router, TypeScript strict, React Server Components wo sinnvoll +- Prisma + PostgreSQL 17 mit PostGIS +- tRPC für interne Aufrufe, REST unter /api/v1 für externe Integrationen +- Tailwind CSS + shadcn/ui +- Redis + BullMQ für Hintergrundjobs +- MinIO (S3-API) für alle Dateien — niemals lokale Pfade +- Zod für jede Ein- und Ausgabevalidierung +- Vitest (Unit) + Playwright (E2E) +- Alles auf Deutsch: UI-Texte, Feldnamen in der UI, Kommentare. + Code-Bezeichner auf Englisch, Domänenbegriffe deutsch belassen + (z. B. `Atemschutztrupp`, `Pruefintervall`, `Geraetewart`). + +## Architekturregeln +1. Jede Tabelle hat `organisationId` (Mandant). Postgres RLS ist aktiv. + Kein Query ohne Mandantenfilter. +2. Jede schreibende Operation erzeugt einen Eintrag in `AuditLog` + (Actor, Zeitpunkt, Entität, Vorher/Nachher als JSONB). Append-only. +3. Prüf-, Wartungs- und Buchungsdaten werden nie hart gelöscht (Soft-Delete + + Versionierung). +4. Alle Fristen und Zeitberechnungen serverseitig, Zeitzone Europe/Berlin. +5. Berechtigungen zentral in `lib/authz.ts`. Keine Rollenprüfung in Komponenten. +6. Dateizugriff ausschließlich über `lib/storage.ts`. +7. Externe Dienste (LLM, Mail, Telegram) hinter Interfaces in `lib/integrations/`, + damit sie im Test mockbar und optional abschaltbar sind. + +## Domänenwissen, das zu beachten ist +- DGUV-Prüffristen: Geräte haben Prüfintervalle aus Vorschrift oder + Herstellervorgabe. Status: einsatzbereit | pruefung_faellig | gesperrt | + ausgesondert. +- Lebensdauerkategorien: Helme, Seile, Pressluftatmer, Schutzkleidung haben + eine maximale Nutzungsdauer ab Herstellungs- oder Indienststellungsdatum. +- FwDV 7 (Atemschutz): Überwachung dokumentiert Truppzusammensetzung, + Einsatzbeginn, Druckwerte in Intervallen, Rückzugsdruck, Ende. +- FwDV 3: Einsatzstärke wird als Staffel/Gruppe/Zug erfasst. +- Einsatzzeitstempel: Alarmierung, Ausrücken, Eintreffen, Einsatzende, + Einrücken, Einsatzbereitschaft. + +## Sicherheits- und Rechtsleitplanken +- Atemschutzüberwachung und Alarmmonitor sind sicherheitsrelevant. Jede + entsprechende Ansicht zeigt einen Hinweis, dass die analoge Überwachung + führend bleibt, solange das System nicht freigegeben ist. +- Personenbezogene Daten: Löschkonzept, Export je Person, Aufbewahrungsfristen + konfigurierbar. +- Keine Telemetrie, keine externen CDNs, keine Google Fonts. Alles lokal. + +## Arbeitsweise +- Arbeite phasenweise. Fasse nie mehrere Phasen in einem Schritt zusammen. +- Beginne jede Phase mit dem Prisma-Schema, dann Migration, dann Server-Logik, + dann UI, dann Tests. +- Nach jeder Phase: `docker compose up` muss durchlaufen, `npm run test` + muss grün sein, README-Abschnitt zur Phase ergänzen. +- Wenn eine Anforderung fachlich unklar ist: nachfragen statt raten. + Feuerwehr-Fachlichkeit ist regional unterschiedlich. \ No newline at end of file diff --git a/Phase-0.md b/Phase-0.md new file mode 100644 index 0000000..ece7d54 --- /dev/null +++ b/Phase-0.md @@ -0,0 +1,23 @@ +Lies CLAUDE.md. Erstelle Phase 0 des Projekts: das Fundament. + +Liefere: +1. Monorepo-Struktur (Next.js App, prisma/, docker/, scripts/) +2. docker-compose.yml mit: traefik, app, worker, postgres (postgis/postgis:17), + redis, minio, gotenberg. Dazu docker-compose.override.yml für Entwicklung + und eine vollständige .env.example mit Kommentaren zu jeder Variable. +3. Dockerfile für die App (Multi-Stage, non-root User, Healthcheck). +4. Prisma-Schema mit den Basismodellen: Organisation, Person, Rolle, + Berechtigung, AusweisToken, AuditLog, Datei. Inklusive RLS-Policies als + SQL-Migration. +5. Auth: OIDC gegen authentik (Auth.js) plus lokaler E-Mail/Passwort-Fallback + für den Erststart, plus Terminal-Login per PIN/RFID-Token für den Kiosk. +6. lib/authz.ts mit Rollen: SUPERADMIN, KOMMANDANT, GERAETEWART, + ATEMSCHUTZGERAETEWART, KASSIER, SCHRIFTFUEHRER, AUSBILDER, MITGLIED, KIOSK. +7. lib/storage.ts (MinIO), lib/audit.ts, lib/db.ts (mit Mandanten-Kontext). +8. Basis-Layout mit shadcn/ui, deutschem UI-Text, Dark Mode, Seitenleiste. +9. Seed-Skript: eine Beispielwehr, 15 Personen, 3 Fahrzeuge, 40 Geräte. +10. Vitest- und Playwright-Setup mit je einem Beispieltest. +11. README mit Schnellstart in unter 10 Zeilen. + +Erkläre am Ende kurz, wie ich den Stack hochfahre und mich anmelde. +Frage nach, bevor du Annahmen über meine authentik-Konfiguration triffst. \ No newline at end of file diff --git a/Phase-1.md b/Phase-1.md new file mode 100644 index 0000000..d0d71be --- /dev/null +++ b/Phase-1.md @@ -0,0 +1,19 @@ +Phase 1: Stammdatenverwaltung. + +Modelle: Standort, Fahrzeug (mit Funkrufname, Kennzeichen, Fahrgestellnummer, +Baujahr, Typ nach DIN, TÜV-Termin), Geraeteraum, Geraetetyp, Geraet +(Inventarnummer, Seriennummer, Hersteller, Herstellungsdatum, +Indienststellung, Standort ODER Fahrzeug+Geraeteraum, Status, +Prüfintervall, Lebensdauer-Kategorie), Qualifikationstyp, Qualifikation +(Person, Typ, erworben_am, gueltig_bis). + +Funktionen: +- CRUD mit Tabellen (Sortierung, Filter, Volltextsuche, Spaltenauswahl) +- QR-Code je Gerät generieren, Etikettenbogen als PDF (DIN A4, 65 Etiketten) +- QR-Scan im Browser (Kamera) öffnet direkt die Gerätedetailseite +- CSV-/Excel-Import mit Spaltenzuordnung und Vorschau vor dem Übernehmen +- Gerätehistorie (Standortwechsel, Statuswechsel) aus dem AuditLog gerendert +- Qualifikationen mit Ablaufwarnung im Dashboard + +UI: Deutsch, tastaturfreundlich, Massenbearbeitung für Statuswechsel. +Tests für Import und QR-Route. \ No newline at end of file diff --git a/Phase-2.md b/Phase-2.md new file mode 100644 index 0000000..e7c6804 --- /dev/null +++ b/Phase-2.md @@ -0,0 +1,29 @@ +Phase 2: Wartungs- und Prüfmanagement. Das ist das wichtigste Modul. + +Modelle: Wartungsvorlage (Name, Geraetetyp, Intervall in Monaten, +Vorwarnzeit, Pruefvorschrift-Referenz), Checklistenpunkt (Text, Typ: +ja_nein | messwert | text | foto, Pflichtfeld, Sollwert), Wartungstermin +(Geraet, Vorlage, faellig_am, Status), Pruefung (Termin, Prüfer, +durchgefuehrt_am, Dauer in Minuten, Ergebnis: bestanden | +bestanden_mit_maengeln | nicht_bestanden, extern durchgeführt von, +Kosten), Pruefergebnis (Checklistenpunkt, Wert, Foto), Signatur, +Mangel (Geraet, Melder, Beschreibung, Foto, Prioritaet, Status, +behoben_am, behoben_von), Rueckruf (Hersteller, betroffene Typen/Chargen). + +Funktionen: +- Fristen-Dashboard: überfällig / diese Woche / diesen Monat / nächste 90 Tage, + gruppierbar nach Fahrzeug, Standort, Gerätetyp, Prüfer +- BullMQ-Job (nächtlich): Termine generieren, Erinnerungen bei T-30/T-7/T-0, + Gerätestatus automatisch auf "pruefung_faellig" setzen +- Prüfungs-Wizard: Checkliste Schritt für Schritt, Fotoupload je Punkt, + Zeiterfassung, Unterschrift auf Canvas (Touch und Maus) +- PDF-Prüfprotokoll via Gotenberg: Kopf mit Wehr/Gerät/Prüfer-ID, + Checkliste mit Ergebnissen, eingebettete Fotos, eingebettete Signatur, + Fußzeile mit Prüfvorschrift und Prüfdatum. Ablage in MinIO, unveränderbar. +- Lebensdauerüberwachung: Warnung X Monate vor Ablauf, Vorschlag zur Aussonderung +- Rückrufe: betroffene Geräte automatisch markieren und optional sperren +- Externe Wartung: Fachfirma erfassen, Fremdprotokoll als PDF anhängen +- Mängel-Workflow: melden → zugewiesen → in Arbeit → behoben, mit Benachrichtigung + +Wichtig: Ein abgeschlossenes Prüfprotokoll ist unveränderbar. Korrekturen +erzeugen ein neues Protokoll mit Verweis auf das ursprüngliche. \ No newline at end of file diff --git a/Phase-3.md b/Phase-3.md new file mode 100644 index 0000000..e98bd3a --- /dev/null +++ b/Phase-3.md @@ -0,0 +1,20 @@ +Phase 3: Einsatz- und Übungserfassung. + +Modelle: Einsatz (Einsatznummer, Stichwort, Kategorie nach Landesstatistik, +Adresse, Geokoordinaten, Zeitstempel: alarmiert/ausgerueckt/eingetroffen/ +einsatzende/eingerueckt/einsatzbereit, Beschreibung, Bericht, +Einsatzleiter, Wetter), Einsatzfahrzeug, Einsatzkraft (Person, Funktion, +Zeit von/bis), Materialverbrauch, Lagemeldung (Text, Foto, wichtig-Flag), +Uebung (analog, mit Ausbildungsthema und Lernzielen). + +Funktionen: +- Schnellerfassung: Stichwort + Adresse + Fahrzeuge, Rest später nachtragen +- Geocoding über self-hosted Nominatim (mit Fallback auf manuelle Koordinaten) +- Personalauswahl mit Vorschlag anhand Verfügbarkeit und Qualifikation +- Stärkeberechnung nach FwDV 3 (z. B. 1/2/6/9) automatisch +- Einsatzbericht als PDF, Sammelexport pro Jahr für die Gemeinde +- Statistik: Einsätze nach Kategorie/Monat, Eintreffzeiten als Verteilung, + Beteiligung je Person, Fahrzeugauslastung +- Externes Portal: signierter QR-Link, ohne Login, für Fremdwehren — + nur Statusmeldung und Lagemeldung mit Foto, zeitlich begrenzt gültig, + rate-limited \ No newline at end of file diff --git a/Phase-4.md b/Phase-4.md new file mode 100644 index 0000000..0d82f79 --- /dev/null +++ b/Phase-4.md @@ -0,0 +1,21 @@ +Phase 4: Kiosk-Oberfläche als PWA für Wandtablets in der Fahrzeughalle. + +Anforderungen: +- Eigene Route /kiosk mit eigenem Layout: große Kacheln, mindestens 64px + Touchziele, hoher Kontrast, mit Handschuhen bedienbar, kein Hover-Zwang +- Login: RFID (WebHID/Tastaturemulation des Lesers), 6-stellige PIN, + QR-Code vom Handy. Auto-Logout nach 90 Sekunden Inaktivität. +- Rollenabhängige Kachelauswahl +- Wizards: Wartung durchführen, Fahrtenbuch eintragen, Mangel melden + (QR scannen → Foto → fertig in unter 30 Sekunden), Inventur scannen, + Wäsche abgeben, Anwesenheit ein-/auschecken +- Hallen-Monitor-Modus (/kiosk/monitor): Vollbild ohne Interaktion, zeigt + nächste Termine, überfällige Prüfungen, Fahrzeugstatus, Wetterwarnung + (DWD Open Data), Uhrzeit +- Offline: Service Worker cached App-Shell und Stammdaten. Schreibende + Aktionen gehen in eine IndexedDB-Outbox (Dexie) mit UUID als Idempotenzschlüssel + und werden bei Reconnect synchronisiert. Sichtbare Statusanzeige + "offline / N Änderungen ausstehend". Konflikte: Server gewinnt bei + Stammdaten, Client gewinnt bei neuen Datensätzen, Konfliktliste für Admin. + +Schreibe Playwright-Tests, die den Offline-Fall simulieren. \ No newline at end of file diff --git a/Phase-5.md b/Phase-5.md new file mode 100644 index 0000000..bb1ae47 --- /dev/null +++ b/Phase-5.md @@ -0,0 +1,28 @@ +Phase 5: Atemschutzüberwachung nach FwDV 7. + +WICHTIG: Jede Ansicht dieses Moduls zeigt oben dauerhaft den Hinweis: +"Ergänzendes System — die analoge Atemschutzüberwachung bleibt führend." +Dieser Hinweis ist erst über eine Konfigurationsoption abschaltbar, die +dokumentiert eine Freigabe durch den Kommandanten voraussetzt. + +Modelle: Atemschutzeinsatz (Einsatz, Überwacher, Beginn, Ende), +Trupp (Truppführer, Truppmann, Auftrag, Funkrufname, Status: +bereit|im_einsatz|rueckweg|draussen), Druckmessung (Trupp, Person, +Zeitpunkt, Druck in bar, Art: eingang|zwischen|ausgang), +G26Untersuchung (Person, Datum, gueltig_bis), Belastungsuebung +(Person, Datum, Art, Ort). + +Funktionen: +- Truppanlage per RFID-Scan oder Auswahl, Eingangsdruck erfassen +- Live-Timer je Trupp, serverseitig autoritativ, per Socket.IO verteilt +- Rückzugsdruck-Berechnung (konfigurierbare Formel, Standard: + Eingangsdruck-Verbrauchsrate) mit optischer Ampel und akustischem Alarm + bei Schwellwert +- Erinnerung zur Druckabfrage in konfigurierbarem Intervall (Standard 10 min) +- Notfallknopf je Trupp (Mayday) mit Protokolleintrag +- Lückenloses Protokoll als PDF, angehängt an den Einsatz +- Verwaltung: G26.3-Fristen, Belastungsübungen, Einsatzstunden je AGT, + Tauglichkeitsübersicht mit Ampel, Dashboard-Widget "verfügbare AGT" + +Timer müssen einen Browser-Reload und einen Verbindungsabbruch überstehen. +Das ist die wichtigste Testanforderung. \ No newline at end of file diff --git a/Phase-6.md b/Phase-6.md new file mode 100644 index 0000000..db01846 --- /dev/null +++ b/Phase-6.md @@ -0,0 +1,15 @@ +Phase 6: Kartenmodule. + +- MapLibre GL mit self-hosted Tiles (tileserver-gl, MBTiles fürs Ausrückegebiet) +- Hydranten: PostGIS-Point, Typ (Unterflur/Überflur/Saugstelle/Zisterne/ + offenes Gewaesser), Nennweite, Durchfluss l/min, letzte Prüfung, Status, + Foto, Notiz. Import aus GeoJSON und aus einer CSV der Stadtwerke. + Cluster-Darstellung, Filter nach Status, Umkreissuche. +- Objekte/Feuerwehrpläne: Objekt mit Adresse, Kategorie, Ansprechpartner, + Zufahrt, Gefahren, Schlüsseldepot, angehängte PDF-Pläne nach DIN 14095, + Volltextsuche, Sofortzugriff vom Einsatz aus +- Lagekarte: Zeichenebene mit taktischen Zeichen nach FwDV 102 als SVG-Set, + freie Skizzen, Speicherung je Einsatz, Echtzeit-Sync zwischen Geräten +- Wasserförderung: Strecke aus Wegpunkten, Höhenprofil, Berechnung von + Druckverlust und benötigten Verstärkerpumpen bei gegebener Schlauchlänge, + Durchmesser und Fördermenge. Ausgabe als druckbares Schema. \ No newline at end of file diff --git a/Phase-7.md b/Phase-7.md new file mode 100644 index 0000000..ed8aace --- /dev/null +++ b/Phase-7.md @@ -0,0 +1,14 @@ +Phase 7: Benachrichtigungen und Termine. + +- Notification-Abstraktion mit Kanälen: E-Mail (SMTP), Telegram (Bot API), + ntfy (self-hosted Push), Web-Push. WhatsApp bewusst NICHT — begründe das + im README kurz mit Kosten und Datenschutz. +- Benachrichtigungsvorlagen mit Platzhaltern, pro Ereignistyp und Rolle + konfigurierbar, je Person abonnierbar +- Ereignisse: Prüfung fällig, Mangel gemeldet, Qualifikation läuft ab, + neuer Termin, Termin-Erinnerung, Einsatzbericht freigegeben +- Kalender: Dienste, Übungen, Lehrgänge, Veranstaltungen, Arbeitsdienste. + Serientermine, Selbstanmeldung mit Teilnehmerlimit, Absage mit Grund, + Anwesenheitsliste zum Ausdrucken, CalDAV-/ICS-Feed je Person mit Token. +- Digitaler Dienstausweis: signierter QR mit kurzer Gültigkeit, Prüfseite + für Kontrollen, Anzeige von Name, Wehr, Funktion, Foto, Qualifikationen. \ No newline at end of file diff --git a/Phase-8.md b/Phase-8.md new file mode 100644 index 0000000..8e1da95 --- /dev/null +++ b/Phase-8.md @@ -0,0 +1,16 @@ +Phase 8: Lokale KI-Funktionen. Alles gegen Ollama, keine externen Anbieter. + +- lib/ai/ mit OpenAI-kompatiblem Client, Modell und Endpunkt konfigurierbar, + komplett abschaltbar per Feature-Flag +- Diktat: Audioaufnahme im Browser → faster-whisper Container → Transkript + in das Einsatzberichtsfeld +- Berichtsassistent: aus Stichpunkten, Zeitstempeln, eingesetzten Fahrzeugen + und Materialverbrauch einen strukturierten Einsatzberichtsentwurf erzeugen. + Ausgabe immer als Vorschlag mit Übernehmen/Verwerfen, nie automatisch speichern. +- Wiki mit pgvector: Dienstvorschriften und Ausbildungsunterlagen als + Markdown, semantische Suche, RAG-Antwort mit Quellenangabe der Wiki-Seite +- Belegerfassung: Foto einer Quittung → Vision-Modell → Vorschlag für + Betrag, Datum, Händler, Kategorie. Immer manuell bestätigen. + +Jede KI-Ausgabe ist als solche gekennzeichnet und muss bestätigt werden, +bevor sie in ein Dokument mit Nachweischarakter gelangt. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe94a53 --- /dev/null +++ b/README.md @@ -0,0 +1,216 @@ +# FLORIAN — Feuerwehr-Verwaltungsplattform + +Selbstgehostete Verwaltungssoftware für Freiwillige Feuerwehren. Läuft im +Gerätehaus, nicht in der Cloud. Keine Telemetrie, keine externen CDNs. + +## Schnellstart + +```bash +git clone https://git.wilde-server.de/florian.wilde/FireAdministration.git && cd FireAdministration +cp .env.example .env # Passwörter und AUTH_SECRET ersetzen +npm ci # Abhängigkeiten +docker compose up -d # Postgres, Redis, MinIO, Gotenberg +npm run db:migrate && npm run db:seed # Schema und Beispielwehr +npm run dev # http://localhost:3100 +``` + +Anmelden mit `SEED_ADMIN_EMAIL` und `SEED_ADMIN_PASSWORT` aus der `.env` +(Vorgabe: `admin@beispielwehr.de` / `Feuerwehr2026`). Passwort danach ändern. + +--- + +## Was drin ist + +| Bereich | Stand | +|---|---| +| Phase 0 — Fundament | fertig | +| Phase 1 — Stammdaten | offen | +| Phase 2 — Wartung und Prüfung | offen | +| Phase 3 — Einsätze und Übungen | offen | +| Phase 4 — Kiosk-PWA | offen | +| Phase 5 — Atemschutz (FwDV 7) | offen | +| Phase 6 — Kartenmodule | offen | +| Phase 7 — Benachrichtigungen und Termine | offen | +| Phase 8 — Lokale KI | offen | + +Vollständige Planung mit Aufwänden: [docs/ROADMAP.md](docs/ROADMAP.md) und +[docs/AUFWANDSSCHAETZUNG.md](docs/AUFWANDSSCHAETZUNG.md). + +## Weiterführende Dokumentation + +| Datei | Inhalt | +|---|---| +| [docs/ANLEITUNG.md](docs/ANLEITUNG.md) | Bedienung mit Bildschirmfotos | +| [docs/ROADMAP.md](docs/ROADMAP.md) | Phasenplan und was jeweils entsteht | +| [docs/AUFWANDSSCHAETZUNG.md](docs/AUFWANDSSCHAETZUNG.md) | Aufwand je Phase, Schätzung gegen Ist | +| [docs/DEVLOG.md](docs/DEVLOG.md) | Entwicklungstagebuch mit Zeiterfassung | +| [docs/VERSIONIERUNG.md](docs/VERSIONIERUNG.md) | Versions- und Branch-Regeln | +| [CHANGELOG.md](CHANGELOG.md) | Was sich wann geändert hat | + +--- + +## Phase 0 — Fundament + +### Aufbau + +``` +FireAdministration/ +├─ docker/ Dockerfile, Entrypoint, Postgres-Initialisierung +├─ prisma/ Schema, Migrationen, Seed +├─ scripts/ Hilfsskripte (Bildschirmfotos) +├─ src/ +│ ├─ app/ Next.js App Router +│ │ ├─ (app)/ alles, was Anmeldung verlangt +│ │ ├─ anmelden/ Anmeldeseite +│ │ └─ api/ Auth.js, tRPC, Health +│ ├─ components/ shadcn/ui-Bausteine und Layout +│ ├─ lib/ authz, db, storage, audit, env, auth, warteschlange +│ ├─ server/ tRPC-Router und Kontext +│ └─ worker/ BullMQ-Hintergrundprozess +├─ tests/ Vitest (unit) und Playwright (e2e) +└─ docs/ Anleitung, Roadmap, Devlog, Aufwände +``` + +### Mandantentrennung + +Jede Tabelle trägt `organisationId`, und Postgres setzt die Trennung mit Row +Level Security durch. Es gibt zwei Datenbankverbindungen: + +- **`florian_app`** (`DATABASE_URL`) — RLS aktiv, `NOBYPASSRLS`. Über diese + Verbindung arbeitet die Anwendung. Ohne gesetztes + `app.current_organisation` liefert jede Abfrage **null Zeilen**. +- **Eigentümer** (`DIRECT_DATABASE_URL`) — umgeht RLS. Nur für Migrationen, + Seed, Anmeldung (die E-Mail-Suche muss über alle Wehren gehen) und + Wartungsjobs. + +Fachdaten laufen ausschließlich über `fuerOrganisation()` aus +[`src/lib/db.ts`](src/lib/db.ts): + +```ts +const personen = await fuerOrganisation(orgId, (db) => + db.person.findMany({ where: { aktiv: true } }), +); +``` + +Ein vergessener Mandantenfilter kostet damit im schlimmsten Fall ein leeres +Ergebnis — nicht die Daten einer fremden Wehr. +`tests/unit/mandantentrennung.test.ts` prüft das gegen eine echte Datenbank. + +### Audit-Log + +Jede schreibende Operation erzeugt einen Eintrag mit Akteur, Zeitpunkt, +Entität und Vorher/Nachher als JSONB. Die Tabelle ist append-only: + +- `UPDATE` ist per Trigger **immer** gesperrt. +- `DELETE` nur im gekennzeichneten Aufbewahrungslauf + (`SET LOCAL app.audit_bereinigung = 'an'`), den ausschließlich der Worker + ausführt — nachdem er exportiert hat. Ohne diese Ausnahme ließe sich das + Löschkonzept nach DSGVO nicht umsetzen. + +Geheimnisse (Passwort-Hashes, PINs, Token) werden vor dem Schreiben entfernt. + +### Berechtigungen + +Alles in [`src/lib/authz.ts`](src/lib/authz.ts) — keine Rollenprüfung in +Komponenten. 9 Rollen, 38 Einzelrechte. Der Seed spiegelt den Katalog in die +Datenbank, damit eine Wehr Rechte nachträglich zuschneiden kann, ohne dass +jemand Code ändert. + +```ts +export const geraetSpeichern = mitRecht("geraet.bearbeiten").mutation(/* … */); +``` + +### Anmeldung + +Drei Wege, eine Sitzung: + +1. **OIDC gegen authentik** — der Regelweg. authentik wird **separat** + betrieben und ist nicht Teil dieses Stacks. Im Provider einzutragen: + Redirect-URI `${APP_URL}/api/auth/callback/authentik`, Scopes + `openid profile email`. +2. **E-Mail und Passwort** — Rückfalloption für den Erststart und für den Fall, + dass authentik nicht erreichbar ist. Über `AUTH_LOCAL_ENABLED` abschaltbar. +3. **Terminal (PIN oder RFID)** — für das Kiosk-Gerät im Gerätehaus. Zwei + Schranken: erst weist sich das *Gerät* mit `KIOSK_DEVICE_SECRET` aus, dann + die *Person* mit PIN oder Karte. Die vollständige Oberfläche dazu kommt in + Phase 4. + +Rollen aus authentik-Gruppen wirken als **Vorbelegung beim ersten Login** +(`AUTH_OIDC_GROUP_MAPPING`). Sobald jemand die Rolle in FLORIAN ändert, ist +FLORIAN führend und OIDC überschreibt nicht mehr. + +### Dateien + +Alles liegt in MinIO, nie im Dateisystem und nie in der Datenbank. Zugriff +ausschließlich über [`src/lib/storage.ts`](src/lib/storage.ts). Objektschlüssel: + +``` +///- +``` + +Downloads laufen über presignte Links mit begrenzter Gültigkeit +(`S3_URL_TTL`, Vorgabe 15 Minuten). + +--- + +## Befehle + +```bash +npm run dev # Entwicklungsserver auf Port 3100 +npm run build # Produktionsbuild +npm run worker:dev # Hintergrundprozess mit Neuladen +npm run test # Vitest +npm run test:e2e # Playwright +npm run typecheck # TypeScript ohne Ausgabe +npm run db:migrate # ausstehende Migrationen anwenden +npm run db:seed # Beispielwehr einspielen +npm run db:studio # Prisma Studio +npm run screenshots # Bilder für docs/ANLEITUNG.md erzeugen +``` + +## Dienste und Ports (Entwicklung) + +Bewusst nicht die Standardports, damit FLORIAN neben anderen lokalen Stacks +laufen kann. Alle Werte stehen in der `.env`. + +| Dienst | Adresse | +|---|---| +| Anwendung | http://localhost:3100 | +| PostgreSQL 17 + PostGIS | `localhost:5442` | +| Redis | `localhost:6389` | +| MinIO API | http://localhost:9010 | +| MinIO Weboberfläche | http://localhost:9011 | +| Gotenberg | http://localhost:3101 | +| Mailpit (nur Entwicklung) | http://localhost:8125 | +| Adminer (nur Entwicklung) | http://localhost:8081 | + +## Produktionsbetrieb + +```bash +docker compose -f docker-compose.yml up -d +``` + +Die Angabe von `-f` ist wichtig: ohne sie legt Docker Compose automatisch +`docker-compose.override.yml` darüber, und das ist die Entwicklungsvariante +ohne Traefik. In Produktion terminiert Traefik TLS und verteilt auf +`APP_DOMAIN` und `MINIO_DOMAIN`. + +Der App-Container wendet beim Start ausstehende Migrationen an; der Worker +tut das bewusst nicht, damit nicht zwei Prozesse gleichzeitig migrieren. + +## Datenschutz + +- Keine Telemetrie, keine externen CDNs, keine Google Fonts. Es werden nur + Schriften verwendet, die auf dem Gerät vorhanden sind. +- Aufbewahrungsfristen konfigurierbar (`AUFBEWAHRUNG_JAHRE`, + `AUDIT_AUFBEWAHRUNG_TAGE`, `PERSON_ANONYMISIERUNG_TAGE`). +- Externe Dienste (Mail, Telegram, LLM) sitzen hinter Schnittstellen in + `src/lib/integrations/` und sind einzeln abschaltbar. Ohne Freischaltung + wird nur ins Log geschrieben. + +## Sicherheitshinweis + +Atemschutzüberwachung und Alarmmonitor sind sicherheitsrelevant. Solange die +Wehr das System nicht ausdrücklich freigegeben hat, bleibt die **analoge +Überwachung führend**. Jede entsprechende Ansicht zeigt diesen Hinweis +dauerhaft an. diff --git a/components.json b/components.json new file mode 100644 index 0000000..30fbc61 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..6f555c7 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,116 @@ +# ============================================================================= +# Entwicklungs-Override. Docker Compose lädt diese Datei automatisch zusätzlich +# zu docker-compose.yml — für den Produktionsbetrieb daher explizit nur die +# Hauptdatei angeben: +# docker compose -f docker-compose.yml up -d +# +# Unterschiede zur Produktion: +# - kein Traefik (die Dienste hängen direkt an localhost-Ports) +# - Quellcode als Bind-Mount, Hot Reload über "next dev" +# - Mailpit als lokaler SMTP-Auffangkorb +# - Adminer zum Draufschauen auf die Datenbank +# ============================================================================= + +services: + traefik: + # Wird in der Entwicklung nicht gebraucht. Mit dem Profil "proxy" trotzdem + # startbar: docker compose --profile proxy up -d + profiles: ["proxy"] + + postgres: + ports: + - "${POSTGRES_PORT:-5432}:5432" + + redis: + ports: + - "${REDIS_PORT:-6379}:6379" + + minio: + ports: + - "${MINIO_API_PORT:-9000}:9000" + - "${MINIO_CONSOLE_PORT:-9001}:9001" + environment: + # Ohne TLS-Terminierung zeigt die Konsole auf localhost. + MINIO_BROWSER_REDIRECT_URL: http://localhost:${MINIO_CONSOLE_PORT:-9001} + MINIO_SERVER_URL: http://localhost:${MINIO_API_PORT:-9000} + labels: + - traefik.enable=false + + gotenberg: + ports: + - "${GOTENBERG_PORT:-3101}:3000" + + app: + build: + target: builder + command: ["npm", "run", "dev"] + ports: + # "next dev" laeuft im Container auf 3100 (siehe package.json). + - "${APP_PORT:-3100}:3100" + environment: + NODE_ENV: development + # Datei-Watching funktioniert über Bind-Mounts von Windows aus nur mit + # Polling zuverlässig. + WATCHPACK_POLLING: "true" + CHOKIDAR_USEPOLLING: "true" + volumes: + - ./src:/app/src + - ./prisma:/app/prisma + - ./public:/app/public + - ./tests:/app/tests + - ./next.config.ts:/app/next.config.ts + - ./tsconfig.json:/app/tsconfig.json + - ./postcss.config.mjs:/app/postcss.config.mjs + # Anonyme Volumes: node_modules und .next bleiben im Container. + - /app/node_modules + - /app/.next + labels: + - traefik.enable=false + + worker: + build: + target: builder + command: ["npm", "run", "worker:dev"] + environment: + NODE_ENV: development + ROLLE: worker + volumes: + - ./src:/app/src + - ./prisma:/app/prisma + - /app/node_modules + + # --------------------------------------------------------------------------- + # Nur Entwicklung: SMTP-Auffangkorb. Alle Mails landen hier statt beim + # Empfänger. Weboberfläche: http://localhost:${MAILPIT_UI_PORT:-8125} + # --------------------------------------------------------------------------- + mailpit: + image: axllent/mailpit:latest + container_name: florian-mailpit + restart: unless-stopped + environment: + MP_MAX_MESSAGES: 500 + TZ: ${TZ:-Europe/Berlin} + ports: + - "${MAILPIT_UI_PORT:-8125}:8025" + - "${MAILPIT_SMTP_PORT:-1125}:1025" + networks: + - intern + + # --------------------------------------------------------------------------- + # Nur Entwicklung: schlanke Datenbankoberfläche. http://localhost:${ADMINER_PORT:-8081} + # Anmeldung: System PostgreSQL, Server "postgres", Benutzer/Passwort aus .env + # --------------------------------------------------------------------------- + adminer: + image: adminer:latest + container_name: florian-adminer + restart: unless-stopped + environment: + ADMINER_DEFAULT_SERVER: postgres + ADMINER_DESIGN: dracula + ports: + - "${ADMINER_PORT:-8081}:8080" + depends_on: + postgres: + condition: service_healthy + networks: + - intern diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1d1441d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,335 @@ +# ============================================================================= +# FLORIAN — Produktionsnaher Stack. +# Für die Entwicklung wird docker-compose.override.yml automatisch daraufgelegt; +# die veröffentlicht die Dienste direkt auf localhost und braucht kein Traefik. +# Produktion starten: docker compose -f docker-compose.yml up -d +# Entwicklung starten: docker compose up -d +# ============================================================================= + +name: florian + +x-app-env: &app-env + NODE_ENV: ${NODE_ENV:-production} + TZ: ${TZ:-Europe/Berlin} + APP_URL: ${APP_URL:-http://localhost:3000} + DATABASE_URL: ${DATABASE_URL} + DIRECT_DATABASE_URL: ${DIRECT_DATABASE_URL} + REDIS_URL: ${REDIS_URL} + WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-5} + S3_ENDPOINT: ${S3_ENDPOINT:-minio} + S3_PORT: ${S3_PORT:-9000} + S3_USE_SSL: ${S3_USE_SSL:-false} + S3_ACCESS_KEY: ${S3_ACCESS_KEY} + S3_SECRET_KEY: ${S3_SECRET_KEY} + S3_REGION: ${S3_REGION:-eu-central-1} + S3_BUCKET: ${S3_BUCKET:-florian} + S3_PUBLIC_URL: ${S3_PUBLIC_URL} + S3_URL_TTL: ${S3_URL_TTL:-900} + GOTENBERG_URL: ${GOTENBERG_URL:-http://gotenberg:3000} + GOTENBERG_TIMEOUT: ${GOTENBERG_TIMEOUT:-30} + AUTH_SECRET: ${AUTH_SECRET} + AUTH_TRUST_HOST: ${AUTH_TRUST_HOST:-true} + AUTH_SESSION_MAX_AGE: ${AUTH_SESSION_MAX_AGE:-28800} + AUTH_OIDC_ENABLED: ${AUTH_OIDC_ENABLED:-false} + AUTH_OIDC_ISSUER: ${AUTH_OIDC_ISSUER:-} + AUTH_OIDC_CLIENT_ID: ${AUTH_OIDC_CLIENT_ID:-} + AUTH_OIDC_CLIENT_SECRET: ${AUTH_OIDC_CLIENT_SECRET:-} + AUTH_OIDC_NAME: ${AUTH_OIDC_NAME:-authentik} + AUTH_OIDC_GROUP_CLAIM: ${AUTH_OIDC_GROUP_CLAIM:-groups} + AUTH_OIDC_GROUP_MAPPING: ${AUTH_OIDC_GROUP_MAPPING:-} + AUTH_OIDC_AUTO_PROVISION: ${AUTH_OIDC_AUTO_PROVISION:-true} + AUTH_OIDC_STANDARD_ORGANISATION: ${AUTH_OIDC_STANDARD_ORGANISATION:-} + AUTH_LOCAL_ENABLED: ${AUTH_LOCAL_ENABLED:-true} + AUTH_BCRYPT_ROUNDS: ${AUTH_BCRYPT_ROUNDS:-12} + KIOSK_ENABLED: ${KIOSK_ENABLED:-true} + KIOSK_DEVICE_SECRET: ${KIOSK_DEVICE_SECRET} + KIOSK_PIN_LAENGE: ${KIOSK_PIN_LAENGE:-6} + KIOSK_MAX_FEHLVERSUCHE: ${KIOSK_MAX_FEHLVERSUCHE:-5} + KIOSK_SPERRDAUER: ${KIOSK_SPERRDAUER:-300} + KIOSK_IDLE_TIMEOUT: ${KIOSK_IDLE_TIMEOUT:-120} + MAIL_ENABLED: ${MAIL_ENABLED:-false} + MAIL_HOST: ${MAIL_HOST:-} + MAIL_PORT: ${MAIL_PORT:-587} + MAIL_SECURE: ${MAIL_SECURE:-false} + MAIL_USER: ${MAIL_USER:-} + MAIL_PASSWORD: ${MAIL_PASSWORD:-} + MAIL_FROM: ${MAIL_FROM:-} + TELEGRAM_ENABLED: ${TELEGRAM_ENABLED:-false} + TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-} + TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-} + LLM_ENABLED: ${LLM_ENABLED:-false} + LLM_BASE_URL: ${LLM_BASE_URL:-} + LLM_API_KEY: ${LLM_API_KEY:-} + LLM_MODELL: ${LLM_MODELL:-} + AUFBEWAHRUNG_JAHRE: ${AUFBEWAHRUNG_JAHRE:-10} + AUDIT_AUFBEWAHRUNG_TAGE: ${AUDIT_AUFBEWAHRUNG_TAGE:-3650} + PERSON_ANONYMISIERUNG_TAGE: ${PERSON_ANONYMISIERUNG_TAGE:-365} + NEXT_TELEMETRY_DISABLED: "1" + +x-logging: &logging + driver: json-file + options: + max-size: "10m" + max-file: "5" + +services: + # --------------------------------------------------------------------------- + # Reverse Proxy. Terminiert TLS, verteilt auf App und MinIO. + # --------------------------------------------------------------------------- + traefik: + image: traefik:v3.3 + container_name: florian-traefik + restart: unless-stopped + command: + - --providers.docker=true + - --providers.docker.exposedByDefault=false + - --providers.docker.network=florian-edge + - --entrypoints.web.address=:80 + - --entrypoints.web.http.redirections.entryPoint.to=websecure + - --entrypoints.web.http.redirections.entryPoint.scheme=https + - --entrypoints.websecure.address=:443 + - --certificatesresolvers.le.acme.email=${ACME_EMAIL} + - --certificatesresolvers.le.acme.storage=/acme/acme.json + - --certificatesresolvers.le.acme.tlschallenge=true + # Kein anonymes Nutzungs-Reporting. + - --global.sendAnonymousUsage=false + - --global.checkNewVersion=false + - --log.level=INFO + - --accesslog=true + ports: + - "80:80" + - "443:443" + volumes: + # Nur lesend — Traefik braucht die Socket-Verbindung zur Diensterkennung. + - /var/run/docker.sock:/var/run/docker.sock:ro + - traefik-acme:/acme + networks: + - edge + logging: *logging + + # --------------------------------------------------------------------------- + # PostgreSQL 17 mit PostGIS. Einziger Ort für relationale Daten. + # --------------------------------------------------------------------------- + postgres: + image: postgis/postgis:17-3.5 + container_name: florian-postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-florian} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB:-florian} + POSTGRES_APP_PASSWORD: ${POSTGRES_APP_PASSWORD} + # Deutsche Sortierung ueber ICU — das Debian-Basisimage hat keine + # de_DE-Locale erzeugt, ICU liefert sie unabhaengig vom Betriebssystem. + POSTGRES_INITDB_ARGS: "--locale-provider=icu --icu-locale=de-DE --locale=C.UTF-8 --encoding=UTF8" + TZ: ${TZ:-Europe/Berlin} + volumes: + - postgres-data:/var/lib/postgresql/data + # Legt beim ERSTEN Start den RLS-pflichtigen Anwendungsbenutzer an. + - ./docker/postgres:/docker-entrypoint-initdb.d:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-florian} -d ${POSTGRES_DB:-florian}"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + networks: + - intern + logging: *logging + + # --------------------------------------------------------------------------- + # Redis. Warteschlangen (BullMQ) und Rate-Limiting. Keine Nutzdaten. + # --------------------------------------------------------------------------- + redis: + image: redis:7-alpine + container_name: florian-redis + restart: unless-stopped + command: + - redis-server + - --requirepass + - ${REDIS_PASSWORD} + - --appendonly + - "yes" + - --maxmemory-policy + - noeviction + volumes: + - redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 10s + networks: + - intern + logging: *logging + + # --------------------------------------------------------------------------- + # MinIO. Ablage ALLER Dateien — Fotos, Prüfprotokolle, Rechnungen, Exporte. + # In Produktion auf ein festes RELEASE-Tag pinnen. + # --------------------------------------------------------------------------- + minio: + image: minio/minio:latest + container_name: florian-minio + restart: unless-stopped + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + MINIO_BROWSER_REDIRECT_URL: https://${MINIO_CONSOLE_DOMAIN} + MINIO_SERVER_URL: https://${MINIO_DOMAIN} + MINIO_UPDATE: "off" + TZ: ${TZ:-Europe/Berlin} + volumes: + - minio-data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + networks: + - intern + - edge + labels: + - traefik.enable=true + # S3-API + - traefik.http.routers.minio-api.rule=Host(`${MINIO_DOMAIN}`) + - traefik.http.routers.minio-api.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure} + - traefik.http.routers.minio-api.tls.certresolver=le + - traefik.http.routers.minio-api.service=minio-api + - traefik.http.services.minio-api.loadbalancer.server.port=9000 + # Weboberfläche + - traefik.http.routers.minio-console.rule=Host(`${MINIO_CONSOLE_DOMAIN}`) + - traefik.http.routers.minio-console.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure} + - traefik.http.routers.minio-console.tls.certresolver=le + - traefik.http.routers.minio-console.service=minio-console + - traefik.http.services.minio-console.loadbalancer.server.port=9001 + logging: *logging + + # --------------------------------------------------------------------------- + # Einmaliger Job: legt den Bucket an und setzt ihn auf privat. + # --------------------------------------------------------------------------- + minio-init: + image: minio/mc:latest + container_name: florian-minio-init + depends_on: + minio: + condition: service_healthy + entrypoint: /bin/sh + command: + - -c + - | + mc alias set florian http://minio:9000 "$${MINIO_ROOT_USER}" "$${MINIO_ROOT_PASSWORD}" && + mc mb --ignore-existing florian/"$${S3_BUCKET}" && + mc anonymous set none florian/"$${S3_BUCKET}" && + mc version enable florian/"$${S3_BUCKET}" && + echo "Bucket $${S3_BUCKET} bereit." + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + S3_BUCKET: ${S3_BUCKET:-florian} + networks: + - intern + restart: "no" + logging: *logging + + # --------------------------------------------------------------------------- + # Gotenberg. Rendert HTML zu PDF (Prüfprotokolle, Berichte). + # --------------------------------------------------------------------------- + gotenberg: + image: gotenberg/gotenberg:8 + container_name: florian-gotenberg + restart: unless-stopped + command: + - gotenberg + - --chromium-disable-javascript=false + - --chromium-allow-list=file:///tmp/.* + - --api-timeout=${GOTENBERG_TIMEOUT:-30}s + environment: + TZ: ${TZ:-Europe/Berlin} + networks: + - intern + logging: *logging + + # --------------------------------------------------------------------------- + # Die Anwendung selbst (Next.js, Standalone-Build). + # Führt vor dem Start die ausstehenden Prisma-Migrationen aus. + # --------------------------------------------------------------------------- + app: + build: + context: . + dockerfile: docker/Dockerfile + target: runner + image: florian-app:${IMAGE_TAG:-latest} + container_name: florian-app + restart: unless-stopped + environment: + <<: *app-env + PORT: "3000" + HOSTNAME: "0.0.0.0" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + minio: + condition: service_healthy + networks: + - intern + - edge + labels: + - traefik.enable=true + - traefik.http.routers.app.rule=Host(`${APP_DOMAIN}`) + - traefik.http.routers.app.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure} + - traefik.http.routers.app.tls.certresolver=le + - traefik.http.services.app.loadbalancer.server.port=3000 + logging: *logging + + # --------------------------------------------------------------------------- + # Hintergrundjobs: Fristenberechnung, Erinnerungen, PDF-Erzeugung, Exporte. + # Gleiches Image wie die App, anderer Einstiegspunkt. + # --------------------------------------------------------------------------- + worker: + build: + context: . + dockerfile: docker/Dockerfile + target: runner + image: florian-app:${IMAGE_TAG:-latest} + container_name: florian-worker + restart: unless-stopped + command: ["node", "--enable-source-maps", "dist/worker/index.js"] + environment: + <<: *app-env + ROLLE: worker + healthcheck: + test: ["CMD", "node", "-e", "process.exit(0)"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + app: + condition: service_started + networks: + - intern + logging: *logging + +volumes: + postgres-data: + redis-data: + minio-data: + traefik-acme: + +networks: + # Nur was von außen erreichbar sein muss, hängt hier drin. + edge: + name: florian-edge + # Datenbank, Redis, Gotenberg: intern, kein Zugang von außen. + intern: + name: florian-intern + internal: false diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..596bae7 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,111 @@ +# ============================================================================= +# FLORIAN — Mehrstufiger Build. +# deps : Abhängigkeiten installieren (wird gecacht) +# builder : Prisma-Client erzeugen, Next.js bauen, Worker transpilieren +# runner : schlankes Laufzeit-Image, non-root, mit Healthcheck +# ============================================================================= + +ARG NODE_VERSION=22.20.0 + +# ----------------------------------------------------------------------------- +# Stufe 1: Abhängigkeiten +# ----------------------------------------------------------------------------- +FROM node:${NODE_VERSION}-bookworm-slim AS deps +WORKDIR /app + +# openssl wird von Prisma benötigt, ca-certificates für ausgehendes TLS. +RUN apt-get update \ + && apt-get install -y --no-install-recommends openssl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +ENV NEXT_TELEMETRY_DISABLED=1 +COPY package.json package-lock.json* .npmrc ./ +RUN npm ci --include=dev + +# ----------------------------------------------------------------------------- +# Stufe 2: Build +# ----------------------------------------------------------------------------- +FROM node:${NODE_VERSION}-bookworm-slim AS builder +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends openssl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Prisma-Client aus dem Schema erzeugen — ohne Datenbankverbindung. +RUN npx prisma generate + +# Der Next.js-Build braucht formal gültige Variablen, verbindet sich aber nicht. +# Die echten Werte kommen zur Laufzeit aus der Umgebung. +RUN DATABASE_URL="postgresql://build:build@localhost:5432/build?schema=public" \ + DIRECT_DATABASE_URL="postgresql://build:build@localhost:5432/build?schema=public" \ + AUTH_SECRET="build_time_platzhalter_mindestens_32_zeichen" \ + KIOSK_DEVICE_SECRET="build_time_platzhalter" \ + SKIP_ENV_VALIDATION=1 \ + npm run build + +# Worker separat nach CommonJS transpilieren, damit im Runtime-Image kein +# TypeScript-Toolchain mehr nötig ist. +RUN npx esbuild src/worker/index.ts \ + --bundle \ + --platform=node \ + --target=node22 \ + --format=cjs \ + --packages=external \ + --sourcemap \ + --outfile=dist/worker/index.js + +# ----------------------------------------------------------------------------- +# Stufe 3: Laufzeit +# ----------------------------------------------------------------------------- +FROM node:${NODE_VERSION}-bookworm-slim AS runner +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends openssl ca-certificates tini \ + && rm -rf /var/lib/apt/lists/* + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 +ENV TZ=Europe/Berlin + +# Eigener, unprivilegierter Benutzer. Läuft NICHT als root. +RUN groupadd --system --gid 1001 florian \ + && useradd --system --uid 1001 --gid florian --home /app --shell /usr/sbin/nologin florian + +# Standalone-Ausgabe von Next.js: enthält nur die tatsächlich benötigten Module. +COPY --from=builder --chown=florian:florian /app/.next/standalone ./ +COPY --from=builder --chown=florian:florian /app/.next/static ./.next/static +COPY --from=builder --chown=florian:florian /app/public ./public + +# Worker-Bundle und dessen Laufzeitabhängigkeiten. +COPY --from=builder --chown=florian:florian /app/dist ./dist + +# Prisma: Schema und Migrationen fürs Deployment, plus erzeugter Client. +COPY --from=builder --chown=florian:florian /app/prisma ./prisma +COPY --from=builder --chown=florian:florian /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder --chown=florian:florian /app/node_modules/@prisma ./node_modules/@prisma +COPY --from=builder --chown=florian:florian /app/node_modules/prisma ./node_modules/prisma + +COPY --chown=florian:florian docker/entrypoint.sh /usr/local/bin/entrypoint.sh +COPY --chown=florian:florian docker/healthcheck.js /app/healthcheck.js +RUN chmod +x /usr/local/bin/entrypoint.sh + +USER florian +EXPOSE 3000 + +# Prüft den Endpunkt /api/health — der fragt Datenbank und Redis mit ab. +HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \ + CMD node /app/healthcheck.js || exit 1 + +# tini als PID 1, damit Signale sauber durchgereicht werden. +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"] +CMD ["node", "server.js"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..3cd4ea7 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# ============================================================================= +# Einstiegspunkt für App- und Worker-Container. +# Die App wendet vor dem Start ausstehende Migrationen an, der Worker nicht — +# sonst würden zwei Prozesse gleichzeitig migrieren. +# ============================================================================= +set -eu + +log() { + echo "[florian-entrypoint] $*" +} + +if [ "${ROLLE:-app}" = "app" ] && [ "${SKIP_MIGRATIONS:-false}" != "true" ]; then + log "Wende ausstehende Datenbankmigrationen an ..." + if node node_modules/prisma/build/index.js migrate deploy; then + log "Migrationen abgeschlossen." + else + log "FEHLER: Migrationen fehlgeschlagen. Container wird beendet." + exit 1 + fi +else + log "Rolle '${ROLLE:-app}' — Migrationen werden übersprungen." +fi + +log "Starte: $*" +exec "$@" diff --git a/docker/healthcheck.js b/docker/healthcheck.js new file mode 100644 index 0000000..c320f33 --- /dev/null +++ b/docker/healthcheck.js @@ -0,0 +1,25 @@ +// Healthcheck für den Docker-HEALTHCHECK. Bewusst ohne Abhängigkeiten, +// damit er auch dann läuft, wenn die Anwendung selbst nicht startet. +const http = require("node:http"); + +const port = process.env.PORT || "3000"; + +const anfrage = http.request( + { + host: "127.0.0.1", + port, + path: "/api/health", + method: "GET", + timeout: 4000, + }, + (antwort) => { + process.exit(antwort.statusCode === 200 ? 0 : 1); + }, +); + +anfrage.on("error", () => process.exit(1)); +anfrage.on("timeout", () => { + anfrage.destroy(); + process.exit(1); +}); +anfrage.end(); diff --git a/docker/postgres/10-init-app-role.sh b/docker/postgres/10-init-app-role.sh new file mode 100644 index 0000000..5090fb5 --- /dev/null +++ b/docker/postgres/10-init-app-role.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# ============================================================================= +# Läuft EINMALIG beim allerersten Start des Postgres-Containers (leeres +# Datenverzeichnis). Richtet ein: +# - die benötigten Erweiterungen (PostGIS, pgcrypto, citext, unaccent, pg_trgm) +# - den Anwendungsbenutzer "florian_app" OHNE BYPASSRLS +# - Vorgabeprivilegien, damit künftige Tabellen automatisch nutzbar sind +# +# Wichtig zum Verständnis der Mandantentrennung: +# Der Eigentümer der Tabellen (POSTGRES_USER) umgeht Row Level Security — +# das ist gewollt, damit Migrationen und das Seed-Skript arbeiten können. +# Die Anwendung verbindet sich ausschließlich als "florian_app"; für den +# greift RLS, und ohne gesetztes app.current_organisation sieht er nichts. +# ============================================================================= +set -euo pipefail + +APP_ROLE="florian_app" +APP_PASSWORD="${POSTGRES_APP_PASSWORD:?POSTGRES_APP_PASSWORD muss gesetzt sein}" +# Einfache Anführungszeichen im Passwort für SQL verdoppeln. +APP_PASSWORD_SQL="${APP_PASSWORD//\'/\'\'}" + +echo "[florian-init] Richte Erweiterungen und Anwendungsrolle ein ..." + +# Erweiterungen. Reines SQL, keine Variablen nötig. +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-'EOSQL' + CREATE EXTENSION IF NOT EXISTS postgis; -- Koordinaten, Einsatzorte + CREATE EXTENSION IF NOT EXISTS pgcrypto; -- gen_random_uuid(), digest() + CREATE EXTENSION IF NOT EXISTS citext; -- E-Mail ohne Gross/Klein + CREATE EXTENSION IF NOT EXISTS unaccent; -- Suche ueber Umlaute hinweg + CREATE EXTENSION IF NOT EXISTS pg_trgm; -- unscharfe Namenssuche +EOSQL + +# Anwendungsrolle anlegen. Bewusst OHNE DO-Block: psql ersetzt seine Variablen +# nicht innerhalb dollar-quotierter Zeichenketten. Stattdessen erzeugt die +# Abfrage den CREATE-Befehl als Text, den \gexec dann ausführt — und zwar nur, +# wenn die Rolle noch nicht existiert. +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + SELECT format( + 'CREATE ROLE %I LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS', + '${APP_ROLE}', '${APP_PASSWORD_SQL}' + ) + WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${APP_ROLE}') + \gexec + + ALTER ROLE "${APP_ROLE}" PASSWORD '${APP_PASSWORD_SQL}'; + ALTER ROLE "${APP_ROLE}" SET timezone TO 'Europe/Berlin'; +EOSQL + +# Rechtevergabe. Hier funktionieren psql-Variablen, weil kein DO-Block im Spiel +# ist. +psql -v ON_ERROR_STOP=1 \ + --username "$POSTGRES_USER" \ + --dbname "$POSTGRES_DB" \ + -v app_role="$APP_ROLE" \ + -v datenbank="$POSTGRES_DB" \ + -v owner="$POSTGRES_USER" <<-'EOSQL' + GRANT CONNECT ON DATABASE :"datenbank" TO :"app_role"; + GRANT USAGE ON SCHEMA public TO :"app_role"; + + -- Rechte auf alles, was heute schon existiert (bei Erstinstallation leer). + GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO :"app_role"; + GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO :"app_role"; + + -- Und auf alles, was der Eigentuemer kuenftig anlegt (Prisma-Migrationen). + ALTER DEFAULT PRIVILEGES FOR ROLE :"owner" IN SCHEMA public + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO :"app_role"; + ALTER DEFAULT PRIVILEGES FOR ROLE :"owner" IN SCHEMA public + GRANT USAGE, SELECT ON SEQUENCES TO :"app_role"; + ALTER DEFAULT PRIVILEGES FOR ROLE :"owner" IN SCHEMA public + GRANT EXECUTE ON FUNCTIONS TO :"app_role"; + + -- Die Anwendung darf keine Tabellen anlegen oder loeschen. + REVOKE CREATE ON SCHEMA public FROM PUBLIC; + REVOKE CREATE ON SCHEMA public FROM :"app_role"; + + ALTER DATABASE :"datenbank" SET timezone TO 'Europe/Berlin'; +EOSQL + +echo "[florian-init] Anwendungsrolle '$APP_ROLE' eingerichtet (RLS wirksam)." diff --git a/docs/ANLEITUNG.md b/docs/ANLEITUNG.md new file mode 100644 index 0000000..7d65380 --- /dev/null +++ b/docs/ANLEITUNG.md @@ -0,0 +1,258 @@ +# Anleitung + +Bedienung von FLORIAN, Stand Phase 0 (Version 0.1.0). Die Bilder zeigen die +Beispielwehr aus dem Seed und werden mit `npm run screenshots` neu erzeugt. + +> Was hier noch fehlt — Mitglieder, Geräte, Prüfungen — kommt in den +> folgenden Phasen dazu. Der Stand ist bewusst schmal: Phase 0 baut das +> Fundament, nicht die Fachmodule. Siehe [ROADMAP.md](ROADMAP.md). + +--- + +## 1. Anmelden + +Aufgerufen wird FLORIAN im Browser unter der Adresse, die der Administrator +eingerichtet hat — in der Entwicklung `http://localhost:3100`. + +![Anmeldeseite](screenshots/01-anmeldung-light.png) + +Die Anmeldeseite zeigt genau die Wege, die in dieser Installation eingerichtet +sind: + +| Weg | Wann | +|---|---| +| **Anmeldung über authentik** | Der Regelweg, sobald authentik eingerichtet ist. Ein Klick, keine Passworteingabe in FLORIAN. | +| **E-Mail und Passwort** | Beim Erststart, und wenn authentik gerade nicht erreichbar ist. | +| **Terminal im Gerätehaus** | Am Wandtablet, mit PIN oder Ausweiskarte. Eigene Oberfläche, kommt in Phase 4. | + +Beim allerersten Start gibt es genau ein Konto: das aus `SEED_ADMIN_EMAIL` und +`SEED_ADMIN_PASSWORT` in der `.env`. Vorgabe: + +``` +admin@beispielwehr.de +Feuerwehr2026 +``` + +**Dieses Passwort gehört nach dem ersten Login geändert.** + +### Wenn die Anmeldung nicht klappt + +Nach mehreren Fehlversuchen wird das Konto vorübergehend gesperrt — voreingestellt +nach fünf Versuchen für fünf Minuten (`KIOSK_MAX_FEHLVERSUCHE`, +`KIOSK_SPERRDAUER`). Die Meldung nennt nicht, ob die E-Mail-Adresse oder das +Passwort falsch war; das ist Absicht und verhindert, dass sich vorhandene +Konten erraten lassen. + +--- + +## 2. Die Übersicht + +Nach der Anmeldung steht die Übersicht der eigenen Wehr. + +![Übersicht](screenshots/02-uebersicht-light.png) + +Von oben nach unten: + +**Kopfzeile.** Name der Wehr und darunter, wer angemeldet ist und mit welchen +Rollen. Wer mehrere Rollen trägt — der Gerätewart ist oft auch Ausbilder — +sieht alle. + +**Sicherheitshinweis.** Der gelbe Kasten bleibt stehen, solange die Wehr die +Anwendung nicht ausdrücklich freigegeben hat. Er ist keine Formalität: +Atemschutzüberwachung und Alarmmonitor sind sicherheitsrelevant, und bis zur +Freigabe bleibt die analoge Überwachung führend. + +**Kennzahlen.** Vier Kacheln mit aktiven Mitgliedern, Fahrzeugen, Geräten und +fälligen Prüfungen. Sie zählen nur die eigene Wehr — was eine andere Wehr auf +derselben Installation hat, ist nicht sichtbar und nicht abfragbar. + +**Stand des Aufbaus.** Welche Phasen fertig sind und was als Nächstes kommt. + +### Die Seitenleiste + +Links stehen alle Module. Was noch nicht existiert, ist ausgegraut und trägt +rechts sein Phasenkürzel — `P2` heißt „kommt in Phase 2". + +Was jemand überhaupt sieht, hängt an seinen Rechten. Ein einfaches Mitglied +sieht die Mitgliederliste, aber nicht das Protokoll. Wichtig zum Verständnis: +das Ausblenden ist nur Bequemlichkeit. Der eigentliche Schutz sitzt auf dem +Server — wer eine Adresse direkt aufruft, für die ihm das Recht fehlt, bekommt +trotzdem nichts. + +--- + +## 3. Helle und dunkle Darstellung + +Oben rechts wird zwischen hell und dunkel umgeschaltet. Voreingestellt ist die +Einstellung des Betriebssystems. + +![Übersicht in dunkler Darstellung](screenshots/02-uebersicht-dark.png) + +Die dunkle Darstellung ist bewusst kein invertiertes Schwarz, sondern ein +warmes Grau. Wer abends vom Fahrzeug hereinkommt und ans Terminal tritt, soll +nicht geblendet werden. + +--- + +## 4. Rollen und was sie dürfen + +Neun Rollen, jede mit einem festen Satz Rechte. Die Zuordnung steht zentral in +`src/lib/authz.ts` und wird beim Seed in die Datenbank gespiegelt — eine Wehr +kann sie also nachträglich zuschneiden, ohne dass jemand Code ändert. + +| Rolle | Darf im Kern | +|---|---| +| **Administrator** | alles, auch über Wehren hinweg | +| **Kommandant** | alles innerhalb der eigenen Wehr, einschließlich Rollen vergeben und Protokoll einsehen | +| **Gerätewart** | Fahrzeuge und Geräte anlegen, ändern, aussondern; Prüfungen planen und durchführen | +| **Atemschutzgerätewart** | zusätzlich Atemschutzgeräte, Träger und die Überwachung nach FwDV 7 | +| **Kassier** | Buchungen und Kassenabschluss (Modul noch nicht geplant, siehe Roadmap) | +| **Schriftführer** | Mitglieder pflegen, Einsätze und Berichte erfassen | +| **Ausbilder** | Übungen planen, Teilnahme und Qualifikationen erfassen | +| **Mitglied** | eigene Daten, Listen lesen | +| **Terminal** | nur anmelden und erfassen — nie verwalten | + +Zwei Dinge sind bewusst so gebaut: + +**Der Gerätewart kommt nicht an die Kasse, der Kassier nicht an die Geräte.** +Trennung nach Verantwortung, nicht nach Bequemlichkeit. + +**Das Terminal darf fast nichts.** Wer am Wandtablet arbeitet, meldet sich +zusätzlich mit PIN oder Karte an und handelt dann unter seiner eigenen Person. +Das Terminal selbst ist nur die Tür. + +--- + +## 5. Anmeldung über authentik einrichten + +authentik läuft **getrennt** von FLORIAN und ist nicht Teil dieses Stacks. + +### In authentik + +1. Neuen Provider anlegen, Typ **OAuth2/OpenID Provider** +2. Client-Typ: **Confidential** +3. Redirect-URI genau so eintragen: + ``` + https:///api/auth/callback/authentik + ``` +4. Scopes: `openid`, `profile`, `email` +5. Für die Rollenvorbelegung zusätzlich ein Scope-Mapping, das die Gruppen des + Benutzers als Claim `groups` ausliefert +6. Eine Application anlegen und mit dem Provider verbinden. Der Slug der + Application ist der letzte Teil der Issuer-URL. + +### In der `.env` + +```bash +AUTH_OIDC_ENABLED=true +AUTH_OIDC_ISSUER=https://auth.beispiel.de/application/o/florian/ +AUTH_OIDC_CLIENT_ID= +AUTH_OIDC_CLIENT_SECRET= +AUTH_OIDC_GROUP_CLAIM=groups +AUTH_OIDC_GROUP_MAPPING=feuerwehr-kommandant:KOMMANDANT,feuerwehr-geraetewart:GERAETEWART +AUTH_OIDC_AUTO_PROVISION=true +AUTH_OIDC_STANDARD_ORGANISATION=ff-musterhausen +``` + +Danach die Anwendung neu starten. Auf der Anmeldeseite erscheint der Knopf für +authentik. + +### Wie die Rollenvorbelegung wirkt + +Die Zuordnung greift **nur beim allerersten Login** einer Person. Danach ist +die in FLORIAN gepflegte Rolle führend, und authentik überschreibt sie nicht +mehr. Wer also einem Mitglied in FLORIAN die Rolle „Gerätewart" gibt, behält +sie — auch wenn die authentik-Gruppe etwas anderes sagt. + +Steht `AUTH_OIDC_AUTO_PROVISION=false`, muss die Person vorher in FLORIAN +angelegt sein. Verknüpft wird über die E-Mail-Adresse. + +### Wenn authentik ausfällt + +Solange `AUTH_LOCAL_ENABLED=true` steht, bleibt der Passwort-Login als +Rückfallweg erreichbar. Für den Wirkbetrieb ist es sinnvoll, mindestens ein +Konto mit Passwort zu behalten — sonst sperrt ein Ausfall von authentik die +ganze Wehr aus. + +--- + +## 6. Terminal-Anmeldung im Gerätehaus + +Die Oberfläche dazu kommt in Phase 4; der Anmeldeweg steht bereits. + +Am Terminal wird zweifach geprüft: + +1. **Das Gerät** weist sich mit `KIOSK_DEVICE_SECRET` aus. Ohne diesen Wert + nimmt der Server keine PIN und keine Karte an. +2. **Die Person** meldet sich mit einer sechsstelligen PIN (zusammen mit ihrer + Mitgliedsnummer) oder mit einer RFID-Karte an. + +Beides wird nur als Hash gespeichert — weder die PIN noch die Kartenkennung +liegen im Klartext in der Datenbank. + +PINs, die sich zu leicht raten lassen, weist FLORIAN ab: lauter gleiche +Ziffern und fortlaufende Folgen wie `123456`. + +Aus dem Seed lässt sich das ausprobieren: Mitgliedsnummer `0001` (Andreas +Huber), PIN `471203`. + +--- + +## 7. Was protokolliert wird + +Jede schreibende Änderung erzeugt einen Eintrag mit Zeitpunkt, Person, +betroffenem Datensatz und den geänderten Feldern im Vorher-Nachher-Vergleich. +Auch jede Anmeldung — erfolgreich wie gescheitert. + +Zwei Eigenschaften sind wichtig: + +**Der Log lässt sich nicht ändern.** Nicht über die Oberfläche, nicht über die +Datenbank, auch nicht vom Administrator. Die Datenbank selbst verhindert es. + +**Er lässt sich nur im Aufbewahrungslauf kürzen.** Nach der eingestellten +Frist (`AUDIT_AUFBEWAHRUNG_TAGE`, Vorgabe 10 Jahre) sichert der +Hintergrundprozess die alten Einträge und entfernt sie danach. Das ist der +einzige Weg, auf dem etwas aus dem Log verschwindet. + +**Passwörter, PINs und Kartenkennungen stehen nie im Log** — sie werden vor +dem Schreiben herausgenommen. + +Einsehbar ist der Log mit dem Recht `system.auditlog`, das Kommandant und +Administrator haben. Die Ansicht dazu kommt in Phase 1. + +--- + +## 8. Wenn etwas nicht geht + +| Beobachtung | Wahrscheinliche Ursache | Was hilft | +|---|---|---| +| Die Anwendung startet nicht und meldet fehlende Variablen | `.env` unvollständig | Die Meldung nennt jede fehlende Variable. Erklärungen dazu stehen in `.env.example`. | +| Anmeldung schlägt immer fehl, obwohl das Passwort stimmt | Konto nach Fehlversuchen gesperrt | Fünf Minuten warten, oder `gesperrtBis` in der Datenbank zurücksetzen | +| Listen sind leer, obwohl Daten da sind | Kein Mandantenkontext gesetzt | Das ist der Schutzmechanismus. Der Zugriff muss über `fuerOrganisation()` laufen. | +| Dateien lassen sich nicht hochladen | MinIO nicht erreichbar oder Bucket fehlt | `http://localhost:9011` öffnen; `docker compose logs minio-init` | +| Der Knopf für authentik fehlt | Issuer, Client-ID oder Secret nicht gesetzt | Alle drei sind nötig, sonst wird der Weg ausgeblendet | +| `docker compose up` startet Traefik mit | Override-Datei wurde nicht mitgeladen | In Produktion `-f docker-compose.yml` angeben | + +Zustand aller Randsysteme auf einen Blick: + +``` +http://localhost:3100/api/health +``` + +Antwortet mit `200` und `"status": "ok"`, wenn Datenbank, Objektspeicher und +Warteschlange erreichbar sind — sonst mit `503` und der Angabe, welcher Dienst +fehlt. + +--- + +## Bilder erneuern + +```bash +docker compose up -d +npm run dev +npm run screenshots +``` + +Die Bilder entstehen in `docs/screenshots/`, jeweils in heller und dunkler +Darstellung, und werden mit eingecheckt — die Anleitung soll auch dann lesbar +sein, wenn gerade kein Stack läuft. diff --git a/docs/AUFWANDSSCHAETZUNG.md b/docs/AUFWANDSSCHAETZUNG.md new file mode 100644 index 0000000..aa36ea5 --- /dev/null +++ b/docs/AUFWANDSSCHAETZUNG.md @@ -0,0 +1,237 @@ +# Aufwandsschätzung + +Schätzung je Phase, daneben der tatsächliche Aufwand aus dem +[Devlog](DEVLOG.md). Stand: 3. September 2026. + +## Wie hier gerechnet wird + +Es stehen bewusst **zwei** Größen nebeneinander, weil sie Verschiedenes +aussagen: + +| Größe | Bedeutung | +|---|---| +| **Sitzungszeit** | Wie lange Claude tatsächlich gearbeitet hat. Das ist die Zahl, die im Devlog steht, und die Grundlage für die Planung der nächsten Sitzung. | +| **Vergleichsaufwand** | Was eine einzelne Person mit diesem Stack für dasselbe Ergebnis gebraucht hätte, in Personentagen zu 8 Stunden. Das ist die Zahl, die gegenüber der Wehr etwas bedeutet — sie beantwortet „was wäre das sonst wert gewesen". | + +Der Vergleichsaufwand ist eine **Schätzung**, keine Messung. Er unterstellt +jemanden, der den Stack kennt, die Feuerwehr-Fachlichkeit aber erst klären +muss — also Recherche zu DGUV-Fristen, FwDV 3 und FwDV 7 einschließt. + +Faktoren, die in die Schätzung eingehen: + +- **Fachlichkeit** — wie viel muss geklärt werden, bevor gebaut werden kann? +- **Sicherheitsbezug** — Atemschutz und Prüfnachweise brauchen mehr Tests und + mehr Sorgfalt als eine Adressliste. +- **Fremdsysteme** — Gotenberg, Nominatim, tileserver-gl, Ollama, RFID-Leser. + Jedes ist eine eigene Fehlerquelle. +- **Offline und Echtzeit** — die teuersten Anforderungen im ganzen Projekt. + +## Übersicht + +| Phase | Inhalt | Vergleichsaufwand | Sitzungszeit geschätzt | Sitzungszeit tatsächlich | +|---|---|---:|---:|---:| +| 0 | Fundament | 12–16 PT | — | **1:00 h** | +| 1 | Stammdaten | 10–14 PT | 2,5–3,5 h | offen | +| 2 | Wartung und Prüfung | 18–24 PT | 4,5–6 h | offen | +| 3 | Einsätze und Übungen | 12–16 PT | 3–4 h | offen | +| 4 | Kiosk-PWA mit Offline | 16–22 PT | 4–5,5 h | offen | +| 5 | Atemschutz (FwDV 7) | 14–18 PT | 3,5–4,5 h | offen | +| 6 | Kartenmodule | 14–18 PT | 3,5–4,5 h | offen | +| 7 | Benachrichtigungen und Termine | 10–13 PT | 2,5–3,5 h | offen | +| 8 | Lokale KI | 9–12 PT | 2,5–3 h | offen | +| | **Summe** | **115–153 PT** | **26–34 h** | | + +115–153 Personentage entsprechen etwa **6 bis 8 Monaten Vollzeitarbeit** einer +einzelnen Person. Zum Vergleich: eine kommerzielle Feuerwehrverwaltung kostet +je nach Umfang 500 bis 2.500 Euro im Jahr, dauerhaft, mit den Daten beim +Anbieter. + +--- + +## Phase 0 — Fundament + +**Vergleichsaufwand: 12–16 PT** · **Tatsächlich: 1:00 h Sitzungszeit** + +| Baustein | PT | +|---|---:| +| Monorepo, Werkzeuge, TypeScript, Tailwind, shadcn | 1,5 | +| Docker Compose, Dockerfile, Traefik, Healthchecks | 2,5 | +| Prisma-Schema der Basismodelle | 2,0 | +| Row Level Security samt Rollen- und Rechtekonzept in Postgres | 2,5 | +| Auth.js mit OIDC, lokalem Login und Terminal-Anmeldung | 3,0 | +| storage, audit, warteschlange, Integrationsschnittstellen | 2,0 | +| Basis-Layout, Anmeldeseite, Übersicht, Dark Mode | 1,5 | +| Seed, Vitest, Playwright, Dokumentation | 1,5 | + +Das Teuerste war nicht der sichtbare Teil, sondern die Mandantentrennung. RLS +so aufzusetzen, dass Migrationen, Seed und Anmeldung funktionieren und die +Anwendung trotzdem nichts Fremdes sieht, verlangt zwei getrennte +Datenbankzugänge und ein sauberes Verständnis davon, wann welcher greift. Wer +das nachträglich einbaut, zahlt ein Vielfaches. + +## Phase 1 — Stammdaten + +**Vergleichsaufwand: 10–14 PT** + +| Baustein | PT | +|---|---:| +| Modelle: Standort, Geraeteraum, Geraetetyp, Qualifikation | 1,5 | +| Fahrzeug und Geraet um DIN-Typisierung und Lebensdauer erweitern | 1,5 | +| Tabellen mit Sortierung, Filter, Volltextsuche, Spaltenauswahl | 3,0 | +| QR-Code je Gerät, Etikettenbogen als PDF (65 Etiketten, DIN A4) | 1,5 | +| QR-Scan im Browser über die Kamera | 1,0 | +| CSV-/Excel-Import mit Spaltenzuordnung und Vorschau | 2,5 | +| Gerätehistorie aus dem Audit-Log | 1,0 | +| Massenbearbeitung und Tastaturbedienung | 1,0 | + +Der Import ist der Posten, der erfahrungsgemäß aus dem Ruder läuft. Jede Wehr +bringt eine andere Excel-Datei mit, und die Vorschau vor dem Übernehmen ist +kein Komfort, sondern die Bedingung dafür, dass die Wehr dem Ergebnis traut. + +## Phase 2 — Wartung und Prüfung + +**Vergleichsaufwand: 18–24 PT** — die aufwendigste Phase + +| Baustein | PT | +|---|---:| +| Modelle: Vorlage, Checklistenpunkt, Termin, Prüfung, Ergebnis, Mangel, Rückruf | 3,0 | +| Fristen-Dashboard mit Gruppierungen | 2,5 | +| Nächtlicher BullMQ-Job: Termine erzeugen, Status setzen, erinnern | 2,5 | +| Prüfungs-Wizard mit Fotoupload je Checklistenpunkt | 3,5 | +| Unterschrift auf Canvas, Touch und Maus | 1,5 | +| PDF-Protokoll über Gotenberg mit Fotos und Signatur | 3,0 | +| Unveränderlichkeit und Korrekturprotokolle mit Verweis | 1,5 | +| Lebensdauerüberwachung und Aussonderungsvorschlag | 1,5 | +| Rückrufe, externe Wartung, Mängel-Workflow | 2,5 | + +Hier entsteht der eigentliche Wert: die Software wird zum Nachweis gegenüber +der Unfallkasse. Das heißt aber auch, dass jeder Fehler teuer ist. Die +Unveränderlichkeit abgeschlossener Protokolle ist der Kern — und der Grund, +warum der Audit-Log schon in Phase 0 append-only ist. + +## Phase 3 — Einsätze und Übungen + +**Vergleichsaufwand: 12–16 PT** + +| Baustein | PT | +|---|---:| +| Modelle: Einsatz, Einsatzfahrzeug, Einsatzkraft, Material, Lagemeldung, Übung | 2,0 | +| Schnellerfassung mit Nachtragen | 1,5 | +| Geocoding über self-hosted Nominatim mit Rückfallweg | 1,5 | +| Personalauswahl nach Verfügbarkeit und Qualifikation | 2,0 | +| Stärkeberechnung nach FwDV 3 | 1,0 | +| Einsatzbericht als PDF, Jahresexport für die Gemeinde | 2,0 | +| Statistik: Kategorien, Eintreffzeiten, Beteiligung, Auslastung | 2,5 | +| Externes Portal mit signiertem QR-Link, ohne Login, rate-limited | 2,0 | + +Das externe Portal für Fremdwehren ist sicherheitsrelevant: ein Zugang ohne +Anmeldung, der zeitlich begrenzt und mengenbegrenzt sein muss. Der Aufwand +steckt weniger in der Funktion als in der Absicherung. + +## Phase 4 — Kiosk-PWA + +**Vergleichsaufwand: 16–22 PT** + +| Baustein | PT | +|---|---:| +| Eigenes Kiosk-Layout, 64px-Ziele, hoher Kontrast, kein Hover-Zwang | 2,0 | +| Anmeldung per RFID (WebHID), PIN und QR vom Handy | 2,5 | +| Wizards: Wartung, Fahrtenbuch, Mangel, Inventur, Wäsche, Anwesenheit | 4,0 | +| Hallen-Monitor mit DWD-Wetterwarnung | 2,0 | +| Service Worker, App-Shell und Stammdaten offline | 3,0 | +| IndexedDB-Outbox mit Idempotenzschlüssel und Synchronisation | 4,0 | +| Konfliktbehandlung und Konfliktliste für den Administrator | 2,0 | +| Playwright-Tests für den Offline-Fall | 1,5 | + +Offline-Fähigkeit mit schreibenden Aktionen ist die technisch schwierigste +Anforderung des ganzen Projekts. Eine Outbox ist schnell gebaut; dass sie +unter Verbindungsabbruch, Doppelklick und parallelem Zugriff von zwei +Terminals nichts verliert und nichts doppelt schreibt, ist die eigentliche +Arbeit. Die Idempotenzschlüssel sind dafür der Schlüssel, nicht die Synchronisation. + +## Phase 5 — Atemschutz nach FwDV 7 + +**Vergleichsaufwand: 14–18 PT** + +| Baustein | PT | +|---|---:| +| Modelle: Atemschutzeinsatz, Trupp, Druckmessung, G26, Belastungsübung | 2,0 | +| Truppanlage per RFID, Eingangsdruck | 1,5 | +| Serverseitig autoritative Timer, Socket.IO-Verteilung | 3,5 | +| Rückzugsdruck-Berechnung, Ampel, akustischer Alarm | 2,0 | +| Erinnerung zur Druckabfrage im Intervall | 1,0 | +| Mayday-Knopf mit Protokolleintrag | 1,0 | +| Lückenloses PDF-Protokoll am Einsatz | 1,5 | +| G26.3-Fristen, Belastungsübungen, Tauglichkeitsampel | 2,0 | +| Tests: Reload und Verbindungsabbruch überstehen | 2,0 | + +Der Aufwand liegt nicht in der Funktion, sondern im Beweis, dass sie hält. Ein +Timer, der einen Reload überlebt, ist Pflicht — deshalb muss die Zeit +serverseitig geführt werden und der Browser nur anzeigen. Diese Phase bekommt +den höchsten Testanteil im Projekt. + +## Phase 6 — Kartenmodule + +**Vergleichsaufwand: 14–18 PT** + +| Baustein | PT | +|---|---:| +| MapLibre GL, tileserver-gl, MBTiles fürs Ausrückegebiet | 2,5 | +| Hydranten mit PostGIS, Cluster, Filter, Umkreissuche | 3,0 | +| Import aus GeoJSON und Stadtwerke-CSV | 1,5 | +| Objekte und Feuerwehrpläne nach DIN 14095, Volltextsuche | 3,0 | +| Lagekarte mit taktischen Zeichen nach FwDV 102 als SVG-Set | 3,5 | +| Echtzeit-Sync der Lagekarte zwischen Geräten | 2,0 | +| Wasserförderung: Höhenprofil, Druckverlust, Verstärkerpumpen | 2,5 | + +Das SVG-Set für die taktischen Zeichen ist Fleißarbeit, aber unvermeidbar — +fertige Sets sind selten frei verwendbar. Die Wasserförderungsberechnung ist +Physik mit klaren Formeln und deshalb gut testbar. + +## Phase 7 — Benachrichtigungen und Termine + +**Vergleichsaufwand: 10–13 PT** + +| Baustein | PT | +|---|---:| +| Kanalabstraktion: E-Mail, Telegram, ntfy, Web-Push | 2,5 | +| Vorlagen mit Platzhaltern, je Ereignis und Rolle | 2,0 | +| Abonnements je Person | 1,0 | +| Kalender mit Serienterminen und Selbstanmeldung | 3,0 | +| CalDAV-/ICS-Feed je Person mit Token | 1,5 | +| Digitaler Dienstausweis mit signiertem QR und Prüfseite | 2,0 | + +Das Grundgerüst der Kanalabstraktion steht bereits aus Phase 0 +(`lib/integrations/`), das spart hier etwa einen Personentag. + +## Phase 8 — Lokale KI + +**Vergleichsaufwand: 9–12 PT** + +| Baustein | PT | +|---|---:| +| `lib/ai/` mit OpenAI-kompatiblem Client, abschaltbar | 1,0 | +| Diktat: Aufnahme im Browser, faster-whisper, Transkript | 2,5 | +| Berichtsassistent mit Übernehmen/Verwerfen | 2,0 | +| Wiki mit pgvector, semantische Suche, RAG mit Quellenangabe | 3,5 | +| Belegerfassung über ein Vision-Modell, immer bestätigen | 2,0 | + +Der Client aus Phase 0 (`lib/integrations/llm.ts`) ist bereits vorhanden. Die +Kennzeichnungspflicht — jede KI-Ausgabe ist als solche erkennbar und muss +bestätigt werden, bevor sie in ein Dokument mit Nachweischarakter gelangt — +kostet wenig Zeit, ist aber nicht verhandelbar. + +--- + +## Was hier nicht drin ist + +**Kassenmodul.** Die Rolle `KASSIER` ist in Phase 0 gefordert und angelegt, +die Rechte `kasse.lesen`, `kasse.bearbeiten` und `kasse.abschluss` stehen +bereit. Ein Kassenmodul kommt in keiner der Phasen 0 bis 8 vor. Geschätzter +Vergleichsaufwand, falls gewünscht: **8–11 PT** für Buchungen, Beiträge, +Rechnungen, Kassenabschluss und Jahresauswertung. Siehe +[ROADMAP.md](ROADMAP.md). + +**Betrieb und Einführung.** Datenübernahme aus der Altsoftware, Schulung der +Wehr, Sicherungskonzept, Monitoring: nach Erfahrung noch einmal 15 bis 25 +Prozent des Entwicklungsaufwands. diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md new file mode 100644 index 0000000..fa11e9e --- /dev/null +++ b/docs/DEVLOG.md @@ -0,0 +1,133 @@ +# Devlog + +Entwicklungstagebuch mit Zeiterfassung. Jeder Arbeitsabschnitt bekommt einen +Eintrag: wann, wie lange, was entstanden ist, und was dabei schiefging. + +Die aufgewendete Zeit wandert nach jedem Abschnitt in die Spalte +„tatsächlich" der [Aufwandsschätzung](AUFWANDSSCHAETZUNG.md). + +## Zeitübersicht + +| Phase | Sitzungszeit | Sitzungen | abgeschlossen | +|---|---:|---:|---| +| Phase 0 — Fundament | 1:00 h | 1 | 03.09.2026 | +| **Summe** | **1:00 h** | **1** | | + +--- + +## 2026-09-02 / 03 · Phase 0 — Fundament + +**23:13 – 00:13 · 1:00 h · Version 0.1.0** + +### Was entstanden ist + +46 Quelldateien, rund 6.100 Zeilen. Im Einzelnen: + +| Bereich | Inhalt | +|---|---| +| Betrieb | `docker-compose.yml` (7 Dienste), Entwicklungs-Override, mehrstufiges Dockerfile, Postgres-Initialisierung, `.env.example` mit Kommentar zu jeder Variable | +| Datenmodell | Prisma-Schema mit 11 Tabellen, 3 Migrationen | +| Kern | `env`, `db`, `authz`, `storage`, `audit`, `auth`, `passwort`, `warteschlange`, 3 Integrationsschnittstellen | +| Server | tRPC v11 mit drei Prozedurarten, Kontext, Wurzel-Router | +| Oberfläche | Layout, Seitenleiste, Anmeldeseite, Übersicht, 5 shadcn-Bausteine, Farbwelt in OKLCH | +| Daten | Seed: 1 Wehr, 9 Rollen, 38 Rechte, 16 Personen, 3 Fahrzeuge, 40 Geräte | +| Tests | 32 Vitest-Tests, 10 Playwright-Tests | +| Doku | README, Anleitung, Roadmap, Aufwandsschätzung, Versionierung, Changelog | + +### Entscheidungen + +**Zwei Datenbankverbindungen statt einer.** Der erste Entwurf hatte einen +einzigen Prisma-Client. Der ist beim ersten Anmeldeversuch aufgelaufen: die +Anwendung verbindet sich als `florian_app`, für den Row Level Security greift +— und die Anmeldung muss eine E-Mail-Adresse über alle Wehren hinweg suchen, +bevor überhaupt feststeht, zu welcher Wehr sie gehört. Ohne gesetzten +Mandantenkontext liefert RLS null Zeilen, also fand der Login nie jemanden. + +Die Lösung ist eine saubere Trennung: `prismaApp` für alle Fachdaten (RLS +aktiv), `prismaSystem` für die wenigen Vorgänge, die vor der +Mandantenauflösung stattfinden. Der Name ist bewusst unbequem, damit auffällt, +wenn ihn jemand im falschen Zusammenhang benutzt. + +**Kein FORCE ROW LEVEL SECURITY.** Der Tabelleneigentümer umgeht RLS. Das ist +gewollt: Migrationen und Seed müssen über alle Mandanten hinweg arbeiten +können. Mit `FORCE` wäre der Seed nicht mehr lauffähig gewesen. + +**Audit-Log: append-only mit genau einer Ausnahme.** Der erste Trigger hat +`UPDATE` und `DELETE` ausnahmslos gesperrt. Das ist beim Aufräumen der +Testdaten aufgefallen — eine Organisation ließ sich nicht mehr löschen, weil +die Kaskade auf den Audit-Log traf. + +Das ist kein Testproblem, sondern ein echtes: nach +`AUDIT_AUFBEWAHRUNG_TAGE` muss der Worker alte Einträge exportieren und dann +entfernen, sonst wächst die Tabelle unbegrenzt und das Löschkonzept nach DSGVO +lässt sich nicht umsetzen. Die zweite Migration führt deshalb einen +ausdrücklichen Schalter ein: `SET LOCAL app.audit_bereinigung = 'an'` erlaubt +`DELETE` für die laufende Transaktion. `UPDATE` bleibt unter allen Umständen +gesperrt — ein Eintrag darf verschwinden, nachdem er gesichert wurde, aber +niemals verfälscht werden. + +**Rollen aus authentik nur als Vorbelegung.** Auf Nachfrage entschieden: der +Gruppen-Claim setzt die Rolle beim ersten Login, danach ist die in FLORIAN +gepflegte Rolle führend. Umgesetzt über das Feld `ausOidcGruppe` an +`PersonRolle` und eine Prüfung, die nur bei leerer Rollenliste greift. + +**Terminal mit zwei Schranken.** Erst weist sich das Gerät mit +`KIOSK_DEVICE_SECRET` aus, dann die Person mit PIN oder Karte. Eine erratene +sechsstellige PIN nützt ohne das Gerätegeheimnis nichts. + +### Wo es gehakt hat + +| Problem | Ursache | Lösung | +|---|---|---| +| Postgres startete nicht | `de_DE.UTF-8` gibt es im Debian-Basisimage nicht | ICU-Locale: `--locale-provider=icu --icu-locale=de-DE --locale=C.UTF-8` | +| Init-Skript brach mit Syntaxfehler ab | psql ersetzt seine Variablen nicht innerhalb dollar-quotierter Blöcke | `DO $$`-Block durch `SELECT format(...) \gexec` ersetzt | +| Anmeldung schlug immer fehl | siehe oben: RLS blockierte die E-Mail-Suche | zweite Datenbankverbindung | +| `npm install` ließ Installationsskripte aus | npm 11.17 verlangt eine ausdrückliche Freigabe | `npm approve-scripts` für Prisma, esbuild und drei weitere | +| Aufräumen der Testdaten scheiterte | Audit-Trigger blockierte die Kaskade | Aufbewahrungsschalter (siehe oben) | +| Zwei E2E-Tests rot | Next.js hängt einen eigenen Routen-Ansager mit `role="alert"` in die Seite; `CardTitle` ist ein `div`, keine Überschrift | Fehlermeldung über `data-testid` angesprochen, Anmeldetitel zu einer echten `h1` gemacht | + +### Abweichung von der Vorgabe + +**`Berechtigung` trägt kein `organisationId`.** Die Architekturregel verlangt +es für jede Tabelle. Diese Tabelle ist aber kein Mandantendatum, sondern der +systemweite Katalog dessen, was die Software überhaupt kann — für alle Wehren +identisch. Mandantenbezogen wird es erst über `RolleBerechtigung`, und die +trägt `organisationId`. Die Tabelle ist für alle lesbar und für die +Anwendungsrolle nicht beschreibbar. Die Ausnahme steht im Schema und in der +Migration kommentiert. + +**Phasenzuordnung in der Seitenleiste korrigiert.** Beim Schreiben der Roadmap +ist aufgefallen, dass ich die Module den falschen Phasen zugeordnet hatte +(Atemschutz als Phase 3, Einsätze als Phase 4). Nach `Phase-3.md` bis +`Phase-8.md` gilt: 3 Einsätze, 4 Kiosk, 5 Atemschutz, 6 Karten, 7 +Benachrichtigungen, 8 KI. Seitenleiste, Kommentare in `authz.ts` und die +Übersicht sind entsprechend berichtigt. + +**Kassenmodul kommt in keiner Phase vor.** Die Rolle `KASSIER` ist gefordert +und angelegt, ein Modul dazu gibt es in `Phase-0.md` bis `Phase-8.md` nicht. +Die Rechte stehen im Katalog, es gibt aber keinen Navigationseintrag. Offene +Entscheidung, siehe [ROADMAP.md](ROADMAP.md). + +### Nachweise + +- `npm run typecheck` — ohne Ausgabe +- `npm run test` — 32 Tests grün, davon 8 gegen eine echte Datenbank +- `npm run test:e2e` — 10 Tests grün (Desktop und Tablet) +- `npm run build` — 6 Routen, 103 kB gemeinsames JavaScript +- `docker compose up -d` — Postgres, Redis, MinIO, Gotenberg gesund + +--- + +## Vorlage für neue Einträge + +```markdown +## JJJJ-MM-TT · Phase N — Titel + +**HH:MM – HH:MM · X:XX h · Version 0.N.0** + +### Was entstanden ist +### Entscheidungen +### Wo es gehakt hat +### Abweichung von der Vorgabe +### Nachweise +``` diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..fe0c5a6 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,209 @@ +# Roadmap + +Was FLORIAN wann kann. Die Phasen kommen aus `Phase-0.md` bis `Phase-8.md` und +werden **einzeln** abgearbeitet — nie zwei in einem Schritt. + +Aufwände je Phase: [AUFWANDSSCHAETZUNG.md](AUFWANDSSCHAETZUNG.md). +Versionszuordnung: [VERSIONIERUNG.md](VERSIONIERUNG.md). + +## Stand + +``` +Phase 0 ████████████████████ fertig 0.1.0 +Phase 1 ░░░░░░░░░░░░░░░░░░░░ offen 0.2.0 +Phase 2 ░░░░░░░░░░░░░░░░░░░░ offen 0.3.0 +Phase 3 ░░░░░░░░░░░░░░░░░░░░ offen 0.4.0 +Phase 4 ░░░░░░░░░░░░░░░░░░░░ offen 0.5.0 +Phase 5 ░░░░░░░░░░░░░░░░░░░░ offen 0.6.0 +Phase 6 ░░░░░░░░░░░░░░░░░░░░ offen 0.7.0 +Phase 7 ░░░░░░░░░░░░░░░░░░░░ offen 0.8.0 +Phase 8 ░░░░░░░░░░░░░░░░░░░░ offen 0.9.0 +Freigabe ░░░░░░░░░░░░░░░░░░░░ offen 1.0.0 +``` + +--- + +## Phase 0 — Fundament · fertig + +Alles, worauf der Rest aufsetzt. + +- Monorepo, Docker Compose mit Traefik, PostgreSQL 17 + PostGIS, Redis, MinIO, + Gotenberg +- Mandantentrennung über Row Level Security, geprüft gegen eine echte Datenbank +- Append-only-Audit-Log mit kontrolliertem Aufbewahrungslauf +- Anmeldung: OIDC gegen authentik, lokaler Passwort-Login, Terminal per + PIN und RFID +- 9 Rollen, 38 Einzelrechte, zentral in `lib/authz.ts` +- Dateiablage in MinIO, Zugriff nur über `lib/storage.ts` +- Basis-Layout mit Seitenleiste und dunkler Darstellung +- Seed mit einer vollständigen Beispielwehr + +## Phase 1 — Stammdaten · als Nächstes + +Die Wehr trägt ein, was sie hat. + +- Standort, Geraeteraum, Geraetetyp, Qualifikationstyp, Qualifikation +- Fahrzeug um Fahrgestellnummer, DIN-Typ und TÜV-Termin erweitern +- Geraet um Lebensdauerkategorie und die Zuordnung + „Standort ODER Fahrzeug + Geräteraum" erweitern +- Tabellen mit Sortierung, Filter, Volltextsuche und Spaltenauswahl +- QR-Code je Gerät, Etikettenbogen als PDF (DIN A4, 65 Etiketten) +- QR-Scan über die Kamera öffnet direkt die Gerätedetailseite +- CSV-/Excel-Import mit Spaltenzuordnung und Vorschau +- Gerätehistorie aus dem Audit-Log +- Qualifikationen mit Ablaufwarnung auf der Übersicht + +**Danach kann die Wehr:** ihren Bestand vollständig führen und jedes Gerät per +QR-Code am Regal wiederfinden. + +## Phase 2 — Wartung und Prüfung + +Das wichtigste Modul. Ab hier ersetzt FLORIAN den Ordner. + +- Wartungsvorlagen mit Checklisten, Intervall und Vorwarnzeit +- Fristen-Dashboard: überfällig / diese Woche / diesen Monat / nächste 90 Tage +- Nächtlicher Job erzeugt Termine, setzt Status, erinnert bei T-30, T-7 und T-0 +- Prüfungs-Wizard mit Foto je Checklistenpunkt, Zeiterfassung und Unterschrift +- PDF-Prüfprotokoll über Gotenberg, unveränderlich in MinIO +- Lebensdauerüberwachung mit Aussonderungsvorschlag +- Rückrufe markieren betroffene Geräte automatisch +- Mängel-Workflow: melden → zugewiesen → in Arbeit → behoben + +**Danach kann die Wehr:** einen DGUV-Nachweis führen, der einer Prüfung durch +die Unfallkasse standhält. + +## Phase 3 — Einsätze und Übungen + +- Einsatz mit allen Zeitstempeln: alarmiert, ausgerückt, eingetroffen, + Einsatzende, eingerückt, einsatzbereit +- Schnellerfassung: Stichwort, Adresse, Fahrzeuge — Rest später +- Geocoding über self-hosted Nominatim +- Personalauswahl mit Vorschlag nach Verfügbarkeit und Qualifikation +- Stärkeberechnung nach FwDV 3 +- Einsatzbericht als PDF, Jahresexport für die Gemeinde +- Statistik: Kategorien, Eintreffzeiten, Beteiligung, Fahrzeugauslastung +- Externes Portal für Fremdwehren über signierten QR-Link, ohne Anmeldung + +**Danach kann die Wehr:** ihre Jahresstatistik auf Knopfdruck erzeugen. + +## Phase 4 — Kiosk-PWA + +Für die Wandtablets in der Fahrzeughalle. + +- Eigenes Layout: große Kacheln, mindestens 64px, mit Handschuhen bedienbar +- Anmeldung per RFID, PIN oder QR vom Handy, Auto-Logout nach 90 Sekunden +- Wizards: Wartung, Fahrtenbuch, Mangel melden, Inventur, Wäsche, Anwesenheit +- Hallen-Monitor: Vollbild mit Terminen, überfälligen Prüfungen, + Fahrzeugstatus, Wetterwarnung +- Offline-Betrieb mit IndexedDB-Outbox und Synchronisation bei Reconnect + +**Danach kann die Wehr:** einen Mangel in unter 30 Sekunden melden, ohne den +Rechner im Büro einzuschalten — auch wenn das Netz gerade weg ist. + +## Phase 5 — Atemschutz nach FwDV 7 + +Sicherheitsrelevant. Jede Ansicht zeigt dauerhaft den Hinweis, dass die +analoge Überwachung führend bleibt — abschaltbar erst über eine Option, die +eine dokumentierte Freigabe durch den Kommandanten voraussetzt. + +- Truppanlage per RFID, Eingangsdruck erfassen +- Serverseitig autoritative Timer, per Socket.IO verteilt +- Rückzugsdruck-Berechnung mit Ampel und akustischem Alarm +- Erinnerung zur Druckabfrage im konfigurierbaren Intervall +- Mayday-Knopf je Trupp +- Lückenloses PDF-Protokoll am Einsatz +- G26.3-Fristen, Belastungsübungen, Tauglichkeitsübersicht + +**Danach kann die Wehr:** die Überwachung digital mitführen — parallel zur +analogen, bis die Wehr ausdrücklich anders entscheidet. + +## Phase 6 — Kartenmodule + +- MapLibre GL mit selbst gehosteten Kacheln fürs Ausrückegebiet +- Hydranten mit Typ, Nennweite, Durchfluss, letzter Prüfung; Import aus + GeoJSON und Stadtwerke-CSV +- Objekte und Feuerwehrpläne nach DIN 14095, vom Einsatz aus erreichbar +- Lagekarte mit taktischen Zeichen nach FwDV 102, Echtzeit-Sync +- Wasserförderung: Druckverlust, Verstärkerpumpen, druckbares Schema + +**Danach kann die Wehr:** auf der Anfahrt sehen, wo der nächste Hydrant liegt +und was ihn hergibt. + +## Phase 7 — Benachrichtigungen und Termine + +- Kanäle: E-Mail, Telegram, ntfy, Web-Push. WhatsApp bewusst nicht. +- Vorlagen mit Platzhaltern, je Ereignis und Rolle konfigurierbar +- Kalender: Dienste, Übungen, Lehrgänge, Arbeitsdienste; Serientermine, + Selbstanmeldung mit Teilnehmerlimit, Anwesenheitsliste zum Ausdrucken +- CalDAV-/ICS-Feed je Person +- Digitaler Dienstausweis mit signiertem QR und Prüfseite + +### Warum kein WhatsApp + +Die WhatsApp Business API kostet je Konversation und verlangt einen +zugelassenen Anbieter dazwischen — laufende Kosten für eine Wehr, die keinen +Etat dafür hat. Der schwerere Grund ist der Datenschutz: die Nummern aller +Mitglieder gingen an Meta, was sich mit einer Anwendung, die bewusst im +Gerätehaus steht, nicht verträgt. Telegram, ntfy und Web-Push decken denselben +Bedarf ab, ntfy sogar vollständig selbst gehostet. + +## Phase 8 — Lokale KI + +Alles gegen Ollama, kein externer Anbieter, komplett abschaltbar. + +- Diktat: Aufnahme im Browser → faster-whisper → Transkript im Berichtsfeld +- Berichtsassistent: aus Stichpunkten einen Entwurf, immer als Vorschlag +- Wiki mit pgvector: semantische Suche mit Quellenangabe +- Belegerfassung: Foto einer Quittung → Vorschlag für Betrag, Datum, Händler + +Jede KI-Ausgabe ist gekennzeichnet und muss bestätigt werden, bevor sie in ein +Dokument mit Nachweischarakter gelangt. + +## 1.0.0 — Freigabe + +Kein neues Modul, sondern eine Entscheidung: die Wehr setzt FLORIAN im +Echtbetrieb ein. Voraussetzungen: + +- Datenübernahme aus der bisherigen Verwaltung abgeschlossen und abgeglichen +- Sicherungskonzept steht und ist einmal wiederhergestellt worden +- Schulung von Kommandant, Gerätewart und Atemschutzgerätewart erfolgt +- Löschkonzept nach DSGVO abgenommen +- Der Hinweis zur analogen Atemschutzüberwachung wird bewusst und + dokumentiert abgeschaltet — oder bewusst stehen gelassen + +--- + +## Offene Punkte + +### Kassenmodul fehlt in der Phasenplanung + +Phase 0 verlangt die Rolle `KASSIER`, und sie ist angelegt — mit den Rechten +`kasse.lesen`, `kasse.bearbeiten` und `kasse.abschluss`. Ein Kassenmodul kommt +in keiner der Phasen 0 bis 8 vor. Die Rolle hätte also dauerhaft Rechte ohne +zugehörige Oberfläche. + +Zwei Wege: + +1. **Phase 9 ergänzen** — Buchungen, Beiträge, Rechnungen, Kassenabschluss, + Jahresauswertung. Geschätzt 8–11 PT. +2. **Rolle belassen, Modul streichen** — der Kassier nutzt weiterhin seine + bisherige Lösung, die Rechte bleiben als Platzhalter stehen. + +Solange das nicht entschieden ist, bleiben die Rechte im Katalog, aber es gibt +keinen Navigationseintrag dafür. + +### Weitere Kandidaten für später + +| Idee | Nutzen | grob | +|---|---|---:| +| Fahrtenbuch als eigenes Modul | derzeit nur als Kiosk-Wizard in Phase 4 vorgesehen | 3–4 PT | +| Kleiderkammer und Wäscheverwaltung | in Phase 4 nur als Wizard, ohne Bestandsführung | 4–6 PT | +| Jugendfeuerwehr mit eigener Abzeichenverwaltung | eigene Fachlichkeit | 5–7 PT | +| Schnittstelle zur Integrierten Leitstelle | Alarmdaten automatisch übernehmen | 6–10 PT, stark abhängig von der ILS | +| Mehrere Wehren je Installation | technisch bereits möglich, Oberfläche fehlt | 4–6 PT | + +Die Mandantenfähigkeit ist der Punkt, der ohne Zusatzaufwand am weitesten +trägt: das Datenmodell und RLS können bereits mehrere Wehren. Was fehlt, ist +eine Oberfläche zum Umschalten und ein Konzept für gemeindeübergreifende +Auswertungen. Für einen Landkreis, der mehrere Wehren betreiben will, wäre das +der nächste sinnvolle Schritt. diff --git a/docs/VERSIONIERUNG.md b/docs/VERSIONIERUNG.md new file mode 100644 index 0000000..db1c3a5 --- /dev/null +++ b/docs/VERSIONIERUNG.md @@ -0,0 +1,117 @@ +# Versionierung + +## Versionsnummern + +FLORIAN folgt [Semantic Versioning](https://semver.org/lang/de/) in der Form +`MAJOR.MINOR.PATCH`. + +Solange die Hauptversion `0` ist, gilt die Software als im Aufbau: es gibt +keine Zusage, dass eine Schnittstelle stabil bleibt. + +| Stelle | wird erhöht, wenn | +|---|---| +| `MAJOR` | Bestehendes bricht — Datenmodell, REST-API unter `/api/v1` oder Konfiguration ändern sich so, dass eine bestehende Installation Handarbeit braucht. Ab `1.0.0`. | +| `MINOR` | Eine Phase ist abgeschlossen oder ein Funktionsbereich kommt hinzu. | +| `PATCH` | Fehlerbehebungen, Textkorrekturen, Abhängigkeits-Updates — alles ohne neue Funktion. | + +### Zuordnung Phase zu Version + +Jede abgeschlossene Phase bekommt eine Minor-Version. Das macht aus der +Versionsnummer eine Aussage über den Funktionsumfang. + +| Version | Phase | +|---|---| +| `0.1.0` | Phase 0 — Fundament | +| `0.2.0` | Phase 1 — Stammdaten | +| `0.3.0` | Phase 2 — Wartung und Prüfung | +| `0.4.0` | Phase 3 — Einsätze und Übungen | +| `0.5.0` | Phase 4 — Kiosk-PWA | +| `0.6.0` | Phase 5 — Atemschutz | +| `0.7.0` | Phase 6 — Kartenmodule | +| `0.8.0` | Phase 7 — Benachrichtigungen und Termine | +| `0.9.0` | Phase 8 — Lokale KI | +| `1.0.0` | Freigabe für den Wirkbetrieb durch die Wehr | + +`1.0.0` ist bewusst an die **Freigabe** gebunden, nicht an Phase 8. Erst wenn +eine Wehr die Software im Echtbetrieb einsetzt, darf die analoge Überwachung +zurücktreten — und erst dann ist eine Zusage über Stabilität ehrlich. + +## Wo die Version steht + +- `package.json` → `version` ist die Wahrheit. +- `CHANGELOG.md` bekommt je Version einen eigenen Abschnitt. +- Git-Tag in der Form `v0.1.0`. + +```bash +npm version minor -m "Version %s — Phase 1 abgeschlossen" +git push --follow-tags +``` + +## Changelog + +Format nach [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), auf +Deutsch. Überschriften je Abschnitt: + +- **Hinzugefügt** — neue Funktionen +- **Geändert** — Verhalten, das anders geworden ist +- **Veraltet** — bleibt vorerst, verschwindet aber +- **Entfernt** — ist weg +- **Behoben** — Fehlerbehebungen +- **Sicherheit** — alles, was Sicherheitsbezug hat + +Einträge beschreiben, was sich für die **Wehr** ändert, nicht welche Datei +angefasst wurde. Also: „Prüfprotokolle lassen sich als PDF ablegen" statt +„PdfService ergänzt". + +Was noch nicht veröffentlicht ist, sammelt sich unter `[Unveröffentlicht]` und +bekommt beim Setzen des Tags die Versionsnummer und das Datum. + +## Branches + +| Branch | Zweck | +|---|---| +| `main` | immer lauffähig. Tests grün, Build durch. | +| `feat/-` | neue Funktion, `` ist die Issue-Nummer | +| `fix/-` | Fehlerbehebung | +| `chore/` | Aufräumen, Abhängigkeiten, Werkzeuge | + +Nie direkt auf `main` committen. Jede Änderung läuft über einen Branch und +einen Pull Request. + +## Commits + +Deutsch, Imperativ, erste Zeile höchstens 72 Zeichen. Die letzte Zeile +verweist auf das Issue: + +``` +Prüfprotokoll als PDF über Gotenberg erzeugen + +Das Protokoll wird nach dem Abschluss unveränderlich in MinIO abgelegt. +Korrekturen erzeugen ein neues Protokoll mit Verweis auf das ursprüngliche. + +Refs #42 +``` + +Der Text des Pull Requests enthält `Closes #42`. + +## Datenbankmigrationen + +- Eine Migration wird nach dem Anwenden **nie** verändert. Ein Fehler wird mit + einer neuen Migration behoben. +- Der Ordnername sagt, worum es geht: `20260902000100_rls_policies`. +- Jede Migration beginnt mit einem Kommentarblock, der erklärt **warum** sie + existiert — nicht nur was sie tut. +- Migrationen, die Daten verlieren können, werden im Changelog unter + **Sicherheit** oder **Geändert** ausdrücklich genannt. + +## Was vor einem Tag erledigt sein muss + +1. `npm run typecheck` ohne Ausgabe +2. `npm run test` grün +3. `npm run test:e2e` grün +4. `npm run build` durch +5. `docker compose -f docker-compose.yml up -d` startet sauber +6. Changelog-Abschnitt geschrieben +7. Devlog-Eintrag mit der aufgewendeten Zeit ergänzt +8. Aufwandsschätzung um den Ist-Wert ergänzt +9. Bildschirmfotos neu erzeugt, falls sich die Oberfläche geändert hat diff --git a/docs/screenshots/01-anmeldung-dark.png b/docs/screenshots/01-anmeldung-dark.png new file mode 100644 index 0000000..1d50f72 Binary files /dev/null and b/docs/screenshots/01-anmeldung-dark.png differ diff --git a/docs/screenshots/01-anmeldung-light.png b/docs/screenshots/01-anmeldung-light.png new file mode 100644 index 0000000..d181c8e Binary files /dev/null and b/docs/screenshots/01-anmeldung-light.png differ diff --git a/docs/screenshots/02-uebersicht-dark.png b/docs/screenshots/02-uebersicht-dark.png new file mode 100644 index 0000000..23f89dd Binary files /dev/null and b/docs/screenshots/02-uebersicht-dark.png differ diff --git a/docs/screenshots/02-uebersicht-light.png b/docs/screenshots/02-uebersicht-light.png new file mode 100644 index 0000000..b345608 Binary files /dev/null and b/docs/screenshots/02-uebersicht-light.png differ diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..7ca3a3b --- /dev/null +++ b/next.config.ts @@ -0,0 +1,27 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + // Standalone-Build für ein schlankes Runtime-Image im Dockerfile. + output: "standalone", + reactStrictMode: true, + // Keine externen CDNs: Bilder kommen ausschließlich aus MinIO bzw. lokal. + images: { remotePatterns: [] }, + serverExternalPackages: ["@prisma/client", "bcryptjs", "minio", "bullmq"], + eslint: { ignoreDuringBuilds: true }, + async headers() { + return [ + { + source: "/:path*", + headers: [ + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "X-Frame-Options", value: "DENY" }, + { key: "Referrer-Policy", value: "no-referrer" }, + // Keine Telemetrie, keine externen Fonts/CDNs. + { key: "Permissions-Policy", value: "geolocation=(self), camera=(self), microphone=()" }, + ], + }, + ]; + }, +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..631fafb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,12557 @@ +{ + "name": "florian", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "florian", + "version": "0.1.0", + "dependencies": { + "@prisma/client": "^6.16.2", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@tanstack/react-query": "^5.90.2", + "@trpc/client": "^11.6.0", + "@trpc/react-query": "^11.6.0", + "@trpc/server": "^11.6.0", + "bcryptjs": "^3.0.2", + "bullmq": "^5.61.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "ioredis": "^5.8.1", + "lucide-react": "^0.544.0", + "minio": "^8.0.6", + "next": "^15.5.4", + "next-auth": "^5.0.0-beta.29", + "next-themes": "^0.4.6", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "server-only": "^0.0.1", + "superjson": "^2.2.6", + "tailwind-merge": "^3.3.1", + "zod": "^3.25.76" + }, + "devDependencies": { + "@playwright/test": "^1.55.1", + "@tailwindcss/postcss": "^4.1.13", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@types/node": "^22.18.6", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.3", + "dotenv": "^17.4.2", + "esbuild": "^0.25.10", + "eslint": "^9.36.0", + "eslint-config-next": "^15.5.4", + "jsdom": "^27.0.0", + "prisma": "^6.16.2", + "tailwindcss": "^4.1.13", + "tsx": "^4.20.6", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.2", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.2.4" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@auth/core": { + "version": "0.41.3", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.3.tgz", + "integrity": "sha512-sJ3JMHHkXMD3aOjopv7mOBTO1Ocw4b0fAEXJBz6k7YHLpYQI6C40jCUPc5fNvUKxXRXNE1/sRISA15UrwWJBTw==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^7.0.7 || ^8.0.5" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz", + "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.12.tgz", + "integrity": "sha512-3vLQK+dXxhBMR2Wx99PTCifE+vHtW2ndZWyla8yK813ev6oGhyn8Lja8jCyGAWTJ+LEYZK7EVtJxrDj8ztevJw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "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/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz", + "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==", + "license": "MIT" + }, + "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/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@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.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "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/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@next/env": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.25.tgz", + "integrity": "sha512-42h1lLr07vl4gawALP1hsgRZjHB1xYa58JfUfHwr0f7jG/zhPakh5GHkADHXOC9ZxUvlQFOPIrp7s6qX4DezPQ==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.25.tgz", + "integrity": "sha512-dAzOqZQCAOgIq5yQpsuMBZcwxK0AtzGqHMQpxNWYLQlvf79rsP3SDwdGPNQC4ySaMSihOQXMO/VXubQ3JWW+3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.25.tgz", + "integrity": "sha512-w+RR0v/QuApnWEjRGm1z6gcObKwGMb5YPA7V3bzBEVSBpMFUXprer0tS27UxjUcEnqbhL7Zuzohej79B6rYmBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.25.tgz", + "integrity": "sha512-QiGGBUSakt8S1H4Lt9Ehsh6Ja87axiBnQQgysOObvCbI7iUfJnRGntF1P64S4/ijuHFnSB8KLsEddkY3nN26uw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.25.tgz", + "integrity": "sha512-ehLos/66zo0d/mJCU5u96a/VDcr01aaUrX0o/i16UdInxz8qPTCDSxGtjk/Lps1sIr1RJFdiX3hxc0fxJo+cPA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.25.tgz", + "integrity": "sha512-ZVMrqLiJ7DiChgmbkQwFtdhAnUkSH/4p7tB29QY+giATb0Q/XGHNRSKAb/B8XGDHRUaA67NepOW5W8u3ZRJBAA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.25.tgz", + "integrity": "sha512-UOewtDGkTMJTiODrEdeLZ50yGb59xCZSriNpXkfPMxRRgwDkGc7i8mLWqV5076wEdb+Ca/XN7MhJyM3CupyNyQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.25.tgz", + "integrity": "sha512-UBHwA8AhkCZgtRfU1aJpunuAJe/6gZv6jDESQe4p5MjTb5V0YEeJBWCdNqx15Vj3x+5jmauRfeMJSjfQj9HGFQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.25.tgz", + "integrity": "sha512-QcFcPRr16djk5IqK5+e8O80eZfgWzIvVBXfitIq0tQ/uc+eyfdoZ0NmKc0cnbIJyfVwREapKuG97YcxWA9gcpA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.25.tgz", + "integrity": "sha512-zREeykps3ndWr9egJgvJKqVkkDuaw6Zrrg23cYBos0ygydFkAWYU4+PaPVwXzP1eAYQJe53ShSK45iDM529BOg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodable/entities": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "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/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@playwright/test": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@prisma/client": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", + "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", + "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.21.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", + "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", + "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.3", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", + "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", + "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "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-avatar": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.6.tgz", + "integrity": "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.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-collection": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.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-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "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.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", + "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.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "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": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.24.tgz", + "integrity": "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "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-guards": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", + "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.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.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-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.15.tgz", + "integrity": "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "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.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.24.tgz", + "integrity": "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.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-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.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-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.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-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.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-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.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": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.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-separator": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz", + "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "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.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "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.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "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-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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.16.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz", + "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.102.8.tgz", + "integrity": "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.102.8.tgz", + "integrity": "sha512-TYBea4OuXWD7MhaSHq069TWbFe7rcwWN6kzT7JF0OKi1K6c1gTv2IzD6A6ExJsCMozdkqBWeuIUZmu4KQg0O5A==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.102.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@trpc/client": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@trpc/client/-/client-11.18.0.tgz", + "integrity": "sha512-wOqeg3Fvl25V1ZisQhUD3K8G60ZJDlSGJNSyeXrLH24xAo5w6GSR2Kzb1cSNY9Y+IQ2YZvYGZstBU+V/ulo/ow==", + "funding": [ + "https://trpc.io/sponsor" + ], + "license": "MIT", + "bin": { + "intent": "bin/intent.js" + }, + "peerDependencies": { + "@trpc/server": "11.18.0", + "typescript": ">=5.7.2" + } + }, + "node_modules/@trpc/react-query": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@trpc/react-query/-/react-query-11.18.0.tgz", + "integrity": "sha512-C1+Wwm2pCeUJucI+bnFpxGYjNuvV+ko1BC1T9tUxBVdrhHRCdn9ubxdevdLSAa49XRJRJiZnSuzl3Ys/yvs1vg==", + "funding": [ + "https://trpc.io/sponsor" + ], + "license": "MIT", + "peerDependencies": { + "@tanstack/react-query": "^5.80.3", + "@trpc/client": "11.18.0", + "@trpc/server": "11.18.0", + "react": ">=18.2.0", + "typescript": ">=5.7.2" + } + }, + "node_modules/@trpc/server": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@trpc/server/-/server-11.18.0.tgz", + "integrity": "sha512-JAvXOuNTxgXjIDfQaOvDq1j66LMNfDJUH1IU7Slfn8EvRv2EkH6ehu3A7zpYhjO0syHHiYg77v2lG2JFJgvw7Q==", + "funding": [ + "https://trpc.io/sponsor" + ], + "license": "MIT", + "bin": { + "intent": "bin/intent.js" + }, + "peerDependencies": { + "typescript": ">=5.7.2" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/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": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.69.0.tgz", + "integrity": "sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/type-utils": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.69.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz", + "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.69.0.tgz", + "integrity": "sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.69.0.tgz", + "integrity": "sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.69.0", + "@typescript-eslint/types": "^8.69.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.69.0.tgz", + "integrity": "sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.69.0.tgz", + "integrity": "sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.69.0.tgz", + "integrity": "sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.69.0.tgz", + "integrity": "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.69.0.tgz", + "integrity": "sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.69.0", + "@typescript-eslint/tsconfig-utils": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.69.0.tgz", + "integrity": "sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.69.0.tgz", + "integrity": "sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "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/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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", + "peer": true, + "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/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "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.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "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.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/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "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": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/async-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/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.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "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.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/block-stream2": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz", + "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", + "license": "MIT", + "dependencies": { + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "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/browser-or-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", + "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", + "license": "MIT" + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "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.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bullmq": { + "version": "5.81.4", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.81.4.tgz", + "integrity": "sha512-n+WHSzz20KooBGoJyASre6oJNz/p5f1IJRRN2ibD+NWPQaNpNQmDrwYuPO+bsXrQeM1MQzUxXGbdjmyOFKP2xQ==", + "license": "MIT", + "dependencies": { + "cron-parser": "4.9.0", + "ioredis": "5.11.1", + "msgpackr": "2.0.5", + "node-abort-controller": "3.1.1", + "semver": "7.8.5", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "redis": ">=5.0.0" + }, + "peerDependenciesMeta": { + "redis": { + "optional": true + } + } + }, + "node_modules/bullmq/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "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/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "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/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/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/cluster-key-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", + "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "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/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.1.0.tgz", + "integrity": "sha512-ufbM3smX/Jbnpk5wcQjzd1MgBpzmqfNETUAyZNrGwU9foRlyHoGzMMBBCRzEhQLBjZfFDE1W2ufPXX2vdWkV8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "deprecated": "v4 is no longer maintained, upgrade to v5", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "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/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/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/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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/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/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "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/effect": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", + "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.420", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz", + "integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==", + "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/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "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-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "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.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "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", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "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.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "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": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "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.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.25.tgz", + "integrity": "sha512-hB1oClZzRO3vMAeMmBMch0FHNJ4irH74LvQWDEMHkJtG/Us9+SAsg1f5Pvcw9jz4tc8wSGnvbmF050BRAAIbBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "15.5.25", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "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.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "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-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-jsx-a11y/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/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.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "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 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/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/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.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/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exsolve": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", + "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "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.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-builder": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", + "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.1.tgz", + "integrity": "sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^3.0.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.2", + "xml-naming": "^0.3.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/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/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "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/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "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.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob-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/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "ISC" + }, + "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.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "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/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/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/ioredis": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz", + "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.10.0", + "cluster-key-slot": "1.1.1", + "debug": "4.4.3", + "denque": "2.1.0", + "redis-errors": "1.2.0", + "redis-parser": "3.0.0", + "standard-as-callback": "2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ipaddr.js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "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-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-bun-module/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "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-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "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-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-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "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-unsafe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz", + "integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "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/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.10", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.10.tgz", + "integrity": "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.544.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz", + "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "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/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/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/minio": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/minio/-/minio-8.0.7.tgz", + "integrity": "sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.4", + "block-stream2": "^2.1.0", + "browser-or-node": "^2.1.1", + "buffer-crc32": "^1.0.0", + "eventemitter3": "^5.0.1", + "fast-xml-parser": "^5.3.4", + "ipaddr.js": "^2.0.1", + "lodash": "^4.17.21", + "mime-types": "^2.1.35", + "query-string": "^7.1.3", + "stream-json": "^1.8.0", + "through2": "^4.0.2", + "xml2js": "^0.5.0 || ^0.6.2" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.5.tgz", + "integrity": "sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.4" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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": "15.5.25", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.25.tgz", + "integrity": "sha512-OMWNulIIqKM2ykvC2qMjIt0IoavB4UB2SCs4iXJ6z6847FvyH8jBmBWcvrF5iuhTu8Przh20Fo/aoszIdqx4PA==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.25", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.25", + "@next/swc-darwin-x64": "15.5.25", + "@next/swc-linux-arm64-gnu": "15.5.25", + "@next/swc-linux-arm64-musl": "15.5.25", + "@next/swc-linux-x64-gnu": "15.5.25", + "@next/swc-linux-x64-musl": "15.5.25", + "@next/swc-win32-arm64-msvc": "15.5.25", + "@next/swc-win32-x64-msvc": "15.5.25", + "sharp": "^0.34.3 || ^0.35.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-auth": { + "version": "5.0.0-beta.32", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.32.tgz", + "integrity": "sha512-CGlChIEWZ6LltNVxrE5yiySMID+Idpmry47JYA5lLwgD8Sx02a8M65VL0TWVz9nbnOioS/tCW/rP/0+mE7Qp4Q==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.3" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", + "nodemailer": "^7.0.7 || ^8.0.5", + "react": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "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-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.9.tgz", + "integrity": "sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.2.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/oauth4webapi": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.7.tgz", + "integrity": "sha512-4RxcKxXjuItDFZ20RRPf4YTw3kpeXJyCgJFxVzJ068A7PNJ18st2Dg90tlC1LkSDS0GecroagCLHYEIVUhCAkw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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-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/ohash": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", + "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", + "devOptional": true, + "license": "MIT" + }, + "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.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "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/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "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-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.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/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/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.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "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.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, + "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/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", + "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.3", + "@prisma/engines": "6.19.3" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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/prop-types/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/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/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/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.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/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-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/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "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/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, + "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/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "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/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/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.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.1.0.tgz", + "integrity": "sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "get-intrinsic": "^1.3.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.4", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.1" + }, + "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.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "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-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-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/strnum": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.2.tgz", + "integrity": "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "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 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "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/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "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.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.11", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.11.tgz", + "integrity": "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.11" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.11", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.11.tgz", + "integrity": "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==", + "dev": true, + "license": "MIT" + }, + "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/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "deprecated": "unmaintained", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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/tsconfig-paths/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/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/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/tsx/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/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "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/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.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "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==", + "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.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/vite/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/vite/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/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "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.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "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/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "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" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c4294ee --- /dev/null +++ b/package.json @@ -0,0 +1,88 @@ +{ + "name": "florian", + "version": "0.1.0", + "private": true, + "description": "FLORIAN — Feuerwehr-Verwaltungsplattform", + "engines": { + "node": ">=22" + }, + "scripts": { + "dev": "next dev -p 3100", + "build": "next build", + "start": "next start -p 3100", + "worker": "tsx src/worker/index.ts", + "worker:dev": "tsx watch src/worker/index.ts", + "lint": "next lint", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "playwright test", + "db:generate": "prisma generate", + "db:migrate": "prisma migrate deploy", + "db:migrate:dev": "prisma migrate dev", + "db:push": "prisma db push", + "db:seed": "tsx prisma/seed.ts", + "db:studio": "prisma studio", + "screenshots": "tsx scripts/screenshots.ts" + }, + "dependencies": { + "@prisma/client": "^6.16.2", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@tanstack/react-query": "^5.90.2", + "@trpc/client": "^11.6.0", + "@trpc/react-query": "^11.6.0", + "@trpc/server": "^11.6.0", + "bcryptjs": "^3.0.2", + "bullmq": "^5.61.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "ioredis": "^5.8.1", + "lucide-react": "^0.544.0", + "minio": "^8.0.6", + "next": "^15.5.4", + "next-auth": "^5.0.0-beta.29", + "next-themes": "^0.4.6", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "server-only": "^0.0.1", + "superjson": "^2.2.6", + "tailwind-merge": "^3.3.1", + "zod": "^3.25.76" + }, + "devDependencies": { + "@playwright/test": "^1.55.1", + "@tailwindcss/postcss": "^4.1.13", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@types/node": "^22.18.6", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.3", + "dotenv": "^17.4.2", + "esbuild": "^0.25.10", + "eslint": "^9.36.0", + "eslint-config-next": "^15.5.4", + "jsdom": "^27.0.0", + "prisma": "^6.16.2", + "tailwindcss": "^4.1.13", + "tsx": "^4.20.6", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.2", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.2.4" + }, + "allowScripts": { + "prisma": true, + "@prisma/client": true, + "@prisma/engines": true, + "esbuild": true, + "unrs-resolver": true, + "msgpackr-extract": true + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..7d9e94a --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,49 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * End-to-End-Tests. + * + * Voraussetzung: der Stack läuft (docker compose up -d) und die Datenbank ist + * migriert und geseedet. Der Entwicklungsserver wird bei Bedarf selbst + * gestartet — ein bereits laufender wird wiederverwendet. + */ +const BASIS_URL = process.env.E2E_BASIS_URL ?? "http://localhost:3100"; + +export default defineConfig({ + testDir: "./tests/e2e", + fullyParallel: true, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI ? [["list"], ["html", { open: "never" }]] : [["list"]], + timeout: 30_000, + expect: { timeout: 10_000 }, + + use: { + baseURL: BASIS_URL, + trace: "on-first-retry", + screenshot: "only-on-failure", + video: "retain-on-failure", + // Die Oberfläche ist durchgängig deutsch — auch der Browser soll es sein. + locale: "de-DE", + timezoneId: "Europe/Berlin", + }, + + projects: [ + { name: "chromium", use: { ...devices["Desktop Chrome"] } }, + // Am Terminal im Gerätehaus steht ein Touchgerät — deshalb mitgetestet. + // Bewusst mit Chromium statt WebKit: das Terminal läuft auf einem + // Android-Tablet im Kiosk-Browser, nicht auf einem iPad. + { + name: "tablet", + use: { ...devices["Galaxy Tab S4 landscape"], browserName: "chromium" }, + }, + ], + + webServer: { + command: "npm run dev", + url: BASIS_URL, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, +}); diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..66a0bfd --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,14 @@ +// Prisma-Konfiguration. Ersetzt den veralteten "prisma"-Block in package.json. +// Anders als früher lädt Prisma die .env hier nicht mehr von selbst — deshalb +// der ausdrückliche dotenv-Aufruf. +import path from "node:path"; +import "dotenv/config"; +import { defineConfig } from "prisma/config"; + +export default defineConfig({ + schema: path.join("prisma", "schema.prisma"), + migrations: { + path: path.join("prisma", "migrations"), + seed: "tsx prisma/seed.ts", + }, +}); diff --git a/prisma/migrations/20260902000000_init/migration.sql b/prisma/migrations/20260902000000_init/migration.sql new file mode 100644 index 0000000..e03a479 --- /dev/null +++ b/prisma/migrations/20260902000000_init/migration.sql @@ -0,0 +1,409 @@ +-- CreateSchema +CREATE SCHEMA IF NOT EXISTS "public"; + +-- CreateExtension +CREATE EXTENSION IF NOT EXISTS "citext"; + +-- CreateExtension +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; + +-- CreateExtension +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +-- CreateExtension +CREATE EXTENSION IF NOT EXISTS "postgis"; + +-- CreateExtension +CREATE EXTENSION IF NOT EXISTS "unaccent"; + +-- CreateEnum +CREATE TYPE "RolleSchluessel" AS ENUM ('SUPERADMIN', 'KOMMANDANT', 'GERAETEWART', 'ATEMSCHUTZGERAETEWART', 'KASSIER', 'SCHRIFTFUEHRER', 'AUSBILDER', 'MITGLIED', 'KIOSK'); + +-- CreateEnum +CREATE TYPE "AusweisTokenTyp" AS ENUM ('PIN', 'RFID'); + +-- CreateEnum +CREATE TYPE "AnmeldeArt" AS ENUM ('LOKAL', 'OIDC', 'KIOSK_PIN', 'KIOSK_RFID', 'SYSTEM'); + +-- CreateEnum +CREATE TYPE "AuditAktion" AS ENUM ('ERSTELLT', 'GEAENDERT', 'GELOESCHT', 'WIEDERHERGESTELLT', 'ANGEMELDET', 'ABGEMELDET', 'ANMELDUNG_FEHLGESCHLAGEN', 'EXPORTIERT', 'DATEI_HOCHGELADEN', 'DATEI_ABGERUFEN', 'BERECHTIGUNG_GEAENDERT'); + +-- CreateEnum +CREATE TYPE "AkteurTyp" AS ENUM ('PERSON', 'SYSTEM', 'WORKER', 'KIOSK', 'API'); + +-- CreateEnum +CREATE TYPE "GeraetStatus" AS ENUM ('EINSATZBEREIT', 'PRUEFUNG_FAELLIG', 'GESPERRT', 'AUSGESONDERT', 'IN_REPARATUR'); + +-- CreateEnum +CREATE TYPE "FahrzeugArt" AS ENUM ('LOESCHFAHRZEUG', 'DREHLEITER', 'RUESTWAGEN', 'GERAETEWAGEN', 'MANNSCHAFTSTRANSPORT', 'EINSATZLEITWAGEN', 'WECHSELLADER', 'ANHAENGER', 'SONSTIGES'); + +-- CreateEnum +CREATE TYPE "Dienstgrad" AS ENUM ('FEUERWEHRANWAERTER', 'FEUERWEHRMANN', 'OBERFEUERWEHRMANN', 'HAUPTFEUERWEHRMANN', 'LOESCHMEISTER', 'OBERLOESCHMEISTER', 'HAUPTLOESCHMEISTER', 'BRANDMEISTER', 'OBERBRANDMEISTER', 'HAUPTBRANDMEISTER', 'KEIN'); + +-- CreateTable +CREATE TABLE "organisation" ( + "id" UUID NOT NULL, + "slug" VARCHAR(64) NOT NULL, + "name" VARCHAR(200) NOT NULL, + "kurzname" VARCHAR(60) NOT NULL, + "strasse" VARCHAR(200), + "plz" VARCHAR(10), + "ort" VARCHAR(120), + "landkreis" VARCHAR(120), + "bundesland" VARCHAR(60) NOT NULL DEFAULT 'Bayern', + "telefon" VARCHAR(40), + "email" VARCHAR(200), + "ilsKennung" VARCHAR(40), + "funkPraefix" VARCHAR(80), + "zeitzone" VARCHAR(60) NOT NULL DEFAULT 'Europe/Berlin', + "aktiv" BOOLEAN NOT NULL DEFAULT true, + "erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "geaendertAm" TIMESTAMPTZ(3) NOT NULL, + "geloeschtAm" TIMESTAMPTZ(3), + + CONSTRAINT "organisation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "person" ( + "id" UUID NOT NULL, + "organisationId" UUID NOT NULL, + "vorname" VARCHAR(100) NOT NULL, + "nachname" VARCHAR(100) NOT NULL, + "mitgliedsnummer" VARCHAR(30), + "geburtsdatum" DATE, + "dienstgrad" "Dienstgrad" NOT NULL DEFAULT 'KEIN', + "email" VARCHAR(200), + "telefon" VARCHAR(40), + "mobil" VARCHAR(40), + "strasse" VARCHAR(200), + "plz" VARCHAR(10), + "ort" VARCHAR(120), + "eintrittsdatum" DATE, + "austrittsdatum" DATE, + "abteilung" VARCHAR(60), + "passwortHash" VARCHAR(120), + "passwortGeaendertAm" TIMESTAMPTZ(3), + "oidcSubject" VARCHAR(255), + "oidcGruppen" TEXT[] DEFAULT ARRAY[]::TEXT[], + "letzteAnmeldung" TIMESTAMPTZ(3), + "fehlversuche" INTEGER NOT NULL DEFAULT 0, + "gesperrtBis" TIMESTAMPTZ(3), + "aktiv" BOOLEAN NOT NULL DEFAULT true, + "avatarDateiId" UUID, + "erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "geaendertAm" TIMESTAMPTZ(3) NOT NULL, + "geloeschtAm" TIMESTAMPTZ(3), + + CONSTRAINT "person_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rolle" ( + "id" UUID NOT NULL, + "organisationId" UUID NOT NULL, + "schluessel" "RolleSchluessel" NOT NULL, + "name" VARCHAR(100) NOT NULL, + "beschreibung" VARCHAR(500), + "systemRolle" BOOLEAN NOT NULL DEFAULT false, + "erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "geaendertAm" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "rolle_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "person_rolle" ( + "id" UUID NOT NULL, + "organisationId" UUID NOT NULL, + "personId" UUID NOT NULL, + "rolleId" UUID NOT NULL, + "zugewiesenAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "zugewiesenVonId" UUID, + "gueltigBis" TIMESTAMPTZ(3), + "ausOidcGruppe" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "person_rolle_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "berechtigung" ( + "id" UUID NOT NULL, + "schluessel" VARCHAR(80) NOT NULL, + "bereich" VARCHAR(60) NOT NULL, + "bezeichnung" VARCHAR(160) NOT NULL, + "beschreibung" VARCHAR(500), + "erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "berechtigung_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rolle_berechtigung" ( + "id" UUID NOT NULL, + "organisationId" UUID NOT NULL, + "rolleId" UUID NOT NULL, + "berechtigungId" UUID NOT NULL, + "gewaehrt" BOOLEAN NOT NULL DEFAULT true, + "erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "rolle_berechtigung_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ausweis_token" ( + "id" UUID NOT NULL, + "organisationId" UUID NOT NULL, + "personId" UUID NOT NULL, + "typ" "AusweisTokenTyp" NOT NULL, + "tokenHash" VARCHAR(120) NOT NULL, + "kennzeichen" VARCHAR(20), + "bezeichnung" VARCHAR(120), + "aktiv" BOOLEAN NOT NULL DEFAULT true, + "gueltigAb" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "gueltigBis" TIMESTAMPTZ(3), + "letzteNutzung" TIMESTAMPTZ(3), + "fehlversuche" INTEGER NOT NULL DEFAULT 0, + "gesperrtBis" TIMESTAMPTZ(3), + "erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "geaendertAm" TIMESTAMPTZ(3) NOT NULL, + "geloeschtAm" TIMESTAMPTZ(3), + + CONSTRAINT "ausweis_token_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "audit_log" ( + "id" UUID NOT NULL, + "organisationId" UUID NOT NULL, + "zeitpunkt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "akteurTyp" "AkteurTyp" NOT NULL DEFAULT 'PERSON', + "akteurPersonId" UUID, + "akteurName" VARCHAR(200), + "anmeldeArt" "AnmeldeArt", + "aktion" "AuditAktion" NOT NULL, + "entitaet" VARCHAR(80) NOT NULL, + "entitaetId" UUID, + "beschreibung" VARCHAR(500), + "vorher" JSONB, + "nachher" JSONB, + "kontext" JSONB, + "ipAdresse" INET, + "userAgent" VARCHAR(400), + + CONSTRAINT "audit_log_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "datei" ( + "id" UUID NOT NULL, + "organisationId" UUID NOT NULL, + "bucket" VARCHAR(80) NOT NULL, + "objektSchluessel" VARCHAR(500) NOT NULL, + "dateiname" VARCHAR(300) NOT NULL, + "mimeTyp" VARCHAR(160) NOT NULL, + "groesseBytes" BIGINT NOT NULL, + "pruefsumme" VARCHAR(64), + "entitaet" VARCHAR(80), + "entitaetId" UUID, + "kategorie" VARCHAR(60), + "hochgeladenVonId" UUID, + "erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "geaendertAm" TIMESTAMPTZ(3) NOT NULL, + "geloeschtAm" TIMESTAMPTZ(3), + + CONSTRAINT "datei_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "fahrzeug" ( + "id" UUID NOT NULL, + "organisationId" UUID NOT NULL, + "funkrufname" VARCHAR(120) NOT NULL, + "bezeichnung" VARCHAR(120) NOT NULL, + "art" "FahrzeugArt" NOT NULL DEFAULT 'LOESCHFAHRZEUG', + "kennzeichen" VARCHAR(20), + "hersteller" VARCHAR(120), + "baujahr" INTEGER, + "tuevBis" DATE, + "aktiv" BOOLEAN NOT NULL DEFAULT true, + "erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "geaendertAm" TIMESTAMPTZ(3) NOT NULL, + "geloeschtAm" TIMESTAMPTZ(3), + + CONSTRAINT "fahrzeug_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "geraet" ( + "id" UUID NOT NULL, + "organisationId" UUID NOT NULL, + "inventarnummer" VARCHAR(60) NOT NULL, + "bezeichnung" VARCHAR(160) NOT NULL, + "hersteller" VARCHAR(120), + "seriennummer" VARCHAR(120), + "herstellungsdatum" DATE, + "indienststellung" DATE, + "status" "GeraetStatus" NOT NULL DEFAULT 'EINSATZBEREIT', + "pruefintervallMonate" INTEGER, + "naechstePruefung" DATE, + "lebensdauerJahre" INTEGER, + "fahrzeugId" UUID, + "lagerort" VARCHAR(160), + "erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "geaendertAm" TIMESTAMPTZ(3) NOT NULL, + "geloeschtAm" TIMESTAMPTZ(3), + + CONSTRAINT "geraet_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "organisation_slug_key" ON "organisation"("slug"); + +-- CreateIndex +CREATE INDEX "organisation_aktiv_idx" ON "organisation"("aktiv"); + +-- CreateIndex +CREATE INDEX "person_organisationId_nachname_vorname_idx" ON "person"("organisationId", "nachname", "vorname"); + +-- CreateIndex +CREATE INDEX "person_organisationId_aktiv_idx" ON "person"("organisationId", "aktiv"); + +-- CreateIndex +CREATE INDEX "person_oidcSubject_idx" ON "person"("oidcSubject"); + +-- CreateIndex +CREATE UNIQUE INDEX "person_organisationId_email_key" ON "person"("organisationId", "email"); + +-- CreateIndex +CREATE UNIQUE INDEX "person_organisationId_oidcSubject_key" ON "person"("organisationId", "oidcSubject"); + +-- CreateIndex +CREATE UNIQUE INDEX "person_organisationId_mitgliedsnummer_key" ON "person"("organisationId", "mitgliedsnummer"); + +-- CreateIndex +CREATE INDEX "rolle_organisationId_idx" ON "rolle"("organisationId"); + +-- CreateIndex +CREATE UNIQUE INDEX "rolle_organisationId_schluessel_key" ON "rolle"("organisationId", "schluessel"); + +-- CreateIndex +CREATE INDEX "person_rolle_organisationId_personId_idx" ON "person_rolle"("organisationId", "personId"); + +-- CreateIndex +CREATE INDEX "person_rolle_organisationId_rolleId_idx" ON "person_rolle"("organisationId", "rolleId"); + +-- CreateIndex +CREATE UNIQUE INDEX "person_rolle_personId_rolleId_key" ON "person_rolle"("personId", "rolleId"); + +-- CreateIndex +CREATE UNIQUE INDEX "berechtigung_schluessel_key" ON "berechtigung"("schluessel"); + +-- CreateIndex +CREATE INDEX "berechtigung_bereich_idx" ON "berechtigung"("bereich"); + +-- CreateIndex +CREATE INDEX "rolle_berechtigung_organisationId_rolleId_idx" ON "rolle_berechtigung"("organisationId", "rolleId"); + +-- CreateIndex +CREATE UNIQUE INDEX "rolle_berechtigung_rolleId_berechtigungId_key" ON "rolle_berechtigung"("rolleId", "berechtigungId"); + +-- CreateIndex +CREATE INDEX "ausweis_token_organisationId_personId_idx" ON "ausweis_token"("organisationId", "personId"); + +-- CreateIndex +CREATE INDEX "ausweis_token_organisationId_typ_aktiv_idx" ON "ausweis_token"("organisationId", "typ", "aktiv"); + +-- CreateIndex +CREATE INDEX "audit_log_organisationId_zeitpunkt_idx" ON "audit_log"("organisationId", "zeitpunkt" DESC); + +-- CreateIndex +CREATE INDEX "audit_log_organisationId_entitaet_entitaetId_idx" ON "audit_log"("organisationId", "entitaet", "entitaetId"); + +-- CreateIndex +CREATE INDEX "audit_log_organisationId_akteurPersonId_idx" ON "audit_log"("organisationId", "akteurPersonId"); + +-- CreateIndex +CREATE INDEX "datei_organisationId_entitaet_entitaetId_idx" ON "datei"("organisationId", "entitaet", "entitaetId"); + +-- CreateIndex +CREATE INDEX "datei_organisationId_kategorie_idx" ON "datei"("organisationId", "kategorie"); + +-- CreateIndex +CREATE UNIQUE INDEX "datei_bucket_objektSchluessel_key" ON "datei"("bucket", "objektSchluessel"); + +-- CreateIndex +CREATE INDEX "fahrzeug_organisationId_aktiv_idx" ON "fahrzeug"("organisationId", "aktiv"); + +-- CreateIndex +CREATE UNIQUE INDEX "fahrzeug_organisationId_funkrufname_key" ON "fahrzeug"("organisationId", "funkrufname"); + +-- CreateIndex +CREATE INDEX "geraet_organisationId_status_idx" ON "geraet"("organisationId", "status"); + +-- CreateIndex +CREATE INDEX "geraet_organisationId_naechstePruefung_idx" ON "geraet"("organisationId", "naechstePruefung"); + +-- CreateIndex +CREATE INDEX "geraet_organisationId_fahrzeugId_idx" ON "geraet"("organisationId", "fahrzeugId"); + +-- CreateIndex +CREATE UNIQUE INDEX "geraet_organisationId_inventarnummer_key" ON "geraet"("organisationId", "inventarnummer"); + +-- AddForeignKey +ALTER TABLE "person" ADD CONSTRAINT "person_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "person" ADD CONSTRAINT "person_avatarDateiId_fkey" FOREIGN KEY ("avatarDateiId") REFERENCES "datei"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rolle" ADD CONSTRAINT "rolle_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "person_rolle" ADD CONSTRAINT "person_rolle_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "person_rolle" ADD CONSTRAINT "person_rolle_personId_fkey" FOREIGN KEY ("personId") REFERENCES "person"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "person_rolle" ADD CONSTRAINT "person_rolle_rolleId_fkey" FOREIGN KEY ("rolleId") REFERENCES "rolle"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "person_rolle" ADD CONSTRAINT "person_rolle_zugewiesenVonId_fkey" FOREIGN KEY ("zugewiesenVonId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rolle_berechtigung" ADD CONSTRAINT "rolle_berechtigung_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rolle_berechtigung" ADD CONSTRAINT "rolle_berechtigung_rolleId_fkey" FOREIGN KEY ("rolleId") REFERENCES "rolle"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "rolle_berechtigung" ADD CONSTRAINT "rolle_berechtigung_berechtigungId_fkey" FOREIGN KEY ("berechtigungId") REFERENCES "berechtigung"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ausweis_token" ADD CONSTRAINT "ausweis_token_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ausweis_token" ADD CONSTRAINT "ausweis_token_personId_fkey" FOREIGN KEY ("personId") REFERENCES "person"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_akteurPersonId_fkey" FOREIGN KEY ("akteurPersonId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "datei" ADD CONSTRAINT "datei_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "datei" ADD CONSTRAINT "datei_hochgeladenVonId_fkey" FOREIGN KEY ("hochgeladenVonId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "fahrzeug" ADD CONSTRAINT "fahrzeug_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "geraet" ADD CONSTRAINT "geraet_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "geraet" ADD CONSTRAINT "geraet_fahrzeugId_fkey" FOREIGN KEY ("fahrzeugId") REFERENCES "fahrzeug"("id") ON DELETE SET NULL ON UPDATE CASCADE; + diff --git a/prisma/migrations/20260902000100_rls_policies/migration.sql b/prisma/migrations/20260902000100_rls_policies/migration.sql new file mode 100644 index 0000000..aa148fb --- /dev/null +++ b/prisma/migrations/20260902000100_rls_policies/migration.sql @@ -0,0 +1,156 @@ +-- ============================================================================= +-- Row Level Security: Mandantentrennung auf Datenbankebene. +-- +-- Warum auf Datenbankebene und nicht nur im Code? +-- Weil ein vergessener WHERE-Filter sonst Daten einer fremden Wehr ausliefert. +-- Mit RLS ist der schlimmste Fall ein leeres Ergebnis, nicht ein Datenleck. +-- +-- Funktionsweise: +-- Die Anwendung verbindet sich als "florian_app" (NOBYPASSRLS) und setzt zu +-- Beginn jeder Transaktion +-- SET LOCAL app.current_organisation = ''; +-- Siehe src/lib/db.ts. Ohne gesetzten Wert liefert jede Abfrage null Zeilen. +-- +-- Der Eigentümer der Tabellen (POSTGRES_USER) umgeht RLS. Das ist gewollt: +-- Migrationen und das Seed-Skript müssen über alle Mandanten hinweg +-- arbeiten. Deshalb steht hier bewusst kein FORCE ROW LEVEL SECURITY. +-- ============================================================================= + +-- ----------------------------------------------------------------------------- +-- Hilfsfunktionen +-- ----------------------------------------------------------------------------- + +-- Liest die aktive Organisation aus der Sitzung. Ist nichts gesetzt oder steht +-- dort kein gültiges UUID, kommt NULL zurück — und NULL vergleicht sich mit +-- nichts, die Abfrage liefert also keine Zeilen. +CREATE OR REPLACE FUNCTION aktuelle_organisation() +RETURNS uuid +LANGUAGE plpgsql +STABLE +AS $$ +DECLARE + wert text; +BEGIN + wert := current_setting('app.current_organisation', true); + IF wert IS NULL OR wert = '' THEN + RETURN NULL; + END IF; + RETURN wert::uuid; +EXCEPTION + WHEN invalid_text_representation THEN + RETURN NULL; +END; +$$; + +COMMENT ON FUNCTION aktuelle_organisation() IS + 'Aktive Organisation der Sitzung, gesetzt per SET LOCAL app.current_organisation.'; + +-- Verhindert Änderungen am Audit-Log — auch durch den Tabelleneigentümer. +CREATE OR REPLACE FUNCTION audit_log_unveraenderlich() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + RAISE EXCEPTION + 'Der Audit-Log ist unveraenderlich. % ist auf audit_log nicht zulaessig.', + TG_OP + USING ERRCODE = 'insufficient_privilege'; +END; +$$; + +-- ----------------------------------------------------------------------------- +-- Mandantenpflichtige Tabellen +-- ----------------------------------------------------------------------------- + +-- Die Organisation selbst: hier ist die id die Mandantenspalte. +ALTER TABLE "organisation" ENABLE ROW LEVEL SECURITY; + +CREATE POLICY organisation_mandant ON "organisation" + FOR ALL + USING ("id" = aktuelle_organisation()) + WITH CHECK ("id" = aktuelle_organisation()); + +-- Alle übrigen Tabellen hängen über organisationId am Mandanten. +DO $$ +DECLARE + tabelle text; +BEGIN + FOREACH tabelle IN ARRAY ARRAY[ + 'person', + 'rolle', + 'person_rolle', + 'rolle_berechtigung', + 'ausweis_token', + 'datei', + 'fahrzeug', + 'geraet' + ] + LOOP + EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', tabelle); + EXECUTE format( + 'CREATE POLICY %I ON %I FOR ALL ' + 'USING ("organisationId" = aktuelle_organisation()) ' + 'WITH CHECK ("organisationId" = aktuelle_organisation())', + tabelle || '_mandant', tabelle + ); + END LOOP; +END; +$$; + +-- ----------------------------------------------------------------------------- +-- Audit-Log: mandantengetrennt UND append-only +-- Bewusst nur SELECT und INSERT — es gibt keine Policy für UPDATE und DELETE, +-- die Anwendungsrolle kann also gar nicht ändern oder löschen. +-- ----------------------------------------------------------------------------- +ALTER TABLE "audit_log" ENABLE ROW LEVEL SECURITY; + +CREATE POLICY audit_log_lesen ON "audit_log" + FOR SELECT + USING ("organisationId" = aktuelle_organisation()); + +CREATE POLICY audit_log_schreiben ON "audit_log" + FOR INSERT + WITH CHECK ("organisationId" = aktuelle_organisation()); + +CREATE TRIGGER audit_log_kein_update + BEFORE UPDATE ON "audit_log" + FOR EACH ROW EXECUTE FUNCTION audit_log_unveraenderlich(); + +CREATE TRIGGER audit_log_kein_delete + BEFORE DELETE ON "audit_log" + FOR EACH ROW EXECUTE FUNCTION audit_log_unveraenderlich(); + +COMMENT ON TABLE "audit_log" IS + 'Append-only. Aenderungen und Loeschungen sind per Trigger gesperrt. ' + 'Die Aufbewahrungsfrist raeumt der Worker ab, nachdem er exportiert hat.'; + +-- ----------------------------------------------------------------------------- +-- Berechtigungskatalog: systemweit, kein Mandantenbezug. +-- Er beschreibt, was die Software kann — nicht, was eine Wehr besitzt. +-- Deshalb kein RLS, aber nur Lesezugriff für die Anwendung. +-- ----------------------------------------------------------------------------- +COMMENT ON TABLE "berechtigung" IS + 'Systemweiter Katalog der Einzelrechte. Bewusst ohne organisationId: ' + 'mandantenbezogen wird es erst ueber rolle_berechtigung.'; + +-- ----------------------------------------------------------------------------- +-- Rechte für die Anwendungsrolle. Existiert die Rolle nicht (z. B. bei einer +-- Entwicklungsdatenbank ohne den Init-Container), wird der Block übersprungen. +-- ----------------------------------------------------------------------------- +DO $$ +BEGIN + IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'florian_app') THEN + EXECUTE 'GRANT USAGE ON SCHEMA public TO florian_app'; + EXECUTE 'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO florian_app'; + EXECUTE 'GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO florian_app'; + EXECUTE 'GRANT EXECUTE ON FUNCTION aktuelle_organisation() TO florian_app'; + + -- Der Katalog ist für die Anwendung nur lesbar. + EXECUTE 'REVOKE INSERT, UPDATE, DELETE ON "berechtigung" FROM florian_app'; + -- Der Audit-Log ist für die Anwendung nur schreib- und lesbar. + EXECUTE 'REVOKE UPDATE, DELETE ON "audit_log" FROM florian_app'; + ELSE + RAISE NOTICE 'Rolle florian_app nicht vorhanden — Rechtevergabe uebersprungen.'; + END IF; +END; +$$; diff --git a/prisma/migrations/20260902000200_audit_bereinigung/migration.sql b/prisma/migrations/20260902000200_audit_bereinigung/migration.sql new file mode 100644 index 0000000..aa26e9c --- /dev/null +++ b/prisma/migrations/20260902000200_audit_bereinigung/migration.sql @@ -0,0 +1,60 @@ +-- ============================================================================= +-- Kontrollierte Ausnahme vom Änderungsverbot des Audit-Logs. +-- +-- Ausgangslage: Der Trigger aus 20260902000100_rls_policies verbietet UPDATE +-- und DELETE auf audit_log. Das ist richtig — nachträglich verändern soll den +-- Log niemand. +-- +-- Es gibt aber genau einen Vorgang, der löschen können muss: der +-- Aufbewahrungsjob des Workers. Nach AUDIT_AUFBEWAHRUNG_TAGE exportiert er die +-- alten Einträge nach MinIO und räumt sie danach ab. Ohne diese Möglichkeit +-- wächst die Tabelle unbegrenzt, und das Löschkonzept nach DSGVO ließe sich +-- nicht umsetzen. +-- +-- Der Ausweg ist ein ausdrücklicher Schalter, den nur der Eigentümer-Zugang +-- setzen kann und der nur für die laufende Transaktion gilt: +-- +-- BEGIN; +-- SELECT set_config('app.audit_bereinigung', 'an', true); +-- DELETE FROM audit_log WHERE zeitpunkt < now() - interval '10 years'; +-- COMMIT; +-- +-- UPDATE bleibt unter allen Umständen verboten. Ein Eintrag kann verschwinden, +-- nachdem er gesichert wurde — verfälscht werden kann er nie. +-- ============================================================================= + +CREATE OR REPLACE FUNCTION audit_log_unveraenderlich() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + -- Änderungen sind ausnahmslos gesperrt. + IF TG_OP = 'UPDATE' THEN + RAISE EXCEPTION + 'Der Audit-Log ist unveraenderlich. UPDATE ist auf audit_log nicht zulaessig.' + USING ERRCODE = 'insufficient_privilege'; + END IF; + + -- Löschen nur im ausdrücklich gekennzeichneten Aufbewahrungslauf. + IF TG_OP = 'DELETE' THEN + IF coalesce(current_setting('app.audit_bereinigung', true), '') = 'an' THEN + RETURN OLD; + END IF; + + RAISE EXCEPTION + 'Der Audit-Log ist unveraenderlich. DELETE ist nur im Aufbewahrungslauf zulaessig ' + '(SET LOCAL app.audit_bereinigung = ''an'').' + USING ERRCODE = 'insufficient_privilege'; + END IF; + + RETURN NULL; +END; +$$; + +COMMENT ON FUNCTION audit_log_unveraenderlich() IS + 'Sperrt UPDATE auf audit_log dauerhaft und DELETE ausserhalb des Aufbewahrungslaufs.'; + +-- Die Anwendungsrolle darf den Schalter gar nicht erst setzen: sie hat kein +-- DELETE-Recht auf audit_log (siehe vorherige Migration). Der Schalter wirkt +-- also nur fuer den Eigentuemer-Zugang, den ausschliesslich Worker und +-- Migrationen benutzen. diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..7b0a624 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,528 @@ +// ============================================================================= +// FLORIAN — Datenmodell, Phase 0 (Fundament) +// +// Grundregeln, die für JEDES Modell gelten: +// 1. Mandantentrennung: jede Tabelle trägt organisationId. Postgres Row Level +// Security setzt das durch (siehe migrations/.../rls). Ausnahme ist der +// systemweite Berechtigungskatalog, siehe Kommentar dort. +// 2. Jede schreibende Operation erzeugt einen AuditLog-Eintrag (lib/audit.ts). +// 3. Prüf-, Wartungs- und Buchungsdaten werden nie hart gelöscht: +// geloeschtAm setzen statt DELETE. +// 4. Alle Zeitstempel in UTC speichern, Anzeige in Europe/Berlin. +// +// Bezeichner sind englisch, wo es Technik ist, und deutsch, wo es Fachlichkeit +// ist — so wie es in der Wehr auch gesprochen wird. +// ============================================================================= + +generator client { + provider = "prisma-client-js" + binaryTargets = ["native", "linux-musl-openssl-3.0.x", "debian-openssl-3.0.x"] + previewFeatures = ["postgresqlExtensions"] +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + directUrl = env("DIRECT_DATABASE_URL") + extensions = [postgis, pgcrypto, citext, unaccent, pg_trgm(map: "pg_trgm")] +} + +// ============================================================================= +// Aufzählungen +// ============================================================================= + +/// Rollen im Sinne der Feuerwehrorganisation. Die Berechtigungen dahinter +/// stehen in lib/authz.ts und werden beim Seed in die Datenbank gespiegelt. +enum RolleSchluessel { + SUPERADMIN + KOMMANDANT + GERAETEWART + ATEMSCHUTZGERAETEWART + KASSIER + SCHRIFTFUEHRER + AUSBILDER + MITGLIED + KIOSK +} + +/// Art des Ausweises, mit dem sich jemand am Terminal im Gerätehaus anmeldet. +enum AusweisTokenTyp { + PIN + RFID +} + +/// Wie sich jemand angemeldet hat — landet im Audit-Log. +enum AnmeldeArt { + LOKAL + OIDC + KIOSK_PIN + KIOSK_RFID + SYSTEM +} + +/// Aktionen im Audit-Log. +enum AuditAktion { + ERSTELLT + GEAENDERT + GELOESCHT + WIEDERHERGESTELLT + ANGEMELDET + ABGEMELDET + ANMELDUNG_FEHLGESCHLAGEN + EXPORTIERT + DATEI_HOCHGELADEN + DATEI_ABGERUFEN + BERECHTIGUNG_GEAENDERT +} + +/// Wer die Aktion ausgelöst hat. +enum AkteurTyp { + PERSON + SYSTEM + WORKER + KIOSK + API +} + +/// Betriebsstatus eines Geräts nach DGUV-Logik. +enum GeraetStatus { + EINSATZBEREIT + PRUEFUNG_FAELLIG + GESPERRT + AUSGESONDERT + IN_REPARATUR +} + +/// Grobe Fahrzeugkategorie. Die vollständige DIN-Typisierung kommt in Phase 1. +enum FahrzeugArt { + LOESCHFAHRZEUG + DREHLEITER + RUESTWAGEN + GERAETEWAGEN + MANNSCHAFTSTRANSPORT + EINSATZLEITWAGEN + WECHSELLADER + ANHAENGER + SONSTIGES +} + +/// Dienstgrad nach bayerischer Feuerwehr-Dienstgradverordnung (Auswahl). +enum Dienstgrad { + FEUERWEHRANWAERTER + FEUERWEHRMANN + OBERFEUERWEHRMANN + HAUPTFEUERWEHRMANN + LOESCHMEISTER + OBERLOESCHMEISTER + HAUPTLOESCHMEISTER + BRANDMEISTER + OBERBRANDMEISTER + HAUPTBRANDMEISTER + KEIN +} + +// ============================================================================= +// Mandant +// ============================================================================= + +/// Eine Feuerwehr. Oberste Klammer — an ihr hängt alles andere. +model Organisation { + id String @id @default(uuid()) @db.Uuid + /// Kurzform für URLs und Konfiguration, z. B. "ff-musterhausen". + slug String @unique @db.VarChar(64) + name String @db.VarChar(200) + kurzname String @db.VarChar(60) + + strasse String? @db.VarChar(200) + plz String? @db.VarChar(10) + ort String? @db.VarChar(120) + landkreis String? @db.VarChar(120) + bundesland String @default("Bayern") @db.VarChar(60) + telefon String? @db.VarChar(40) + email String? @db.VarChar(200) + /// Kennung im Einsatzleitsystem der Integrierten Leitstelle. + ilsKennung String? @db.VarChar(40) + /// Funkrufname-Präfix der Wehr, z. B. "Florian Musterhausen". + funkPraefix String? @db.VarChar(80) + + zeitzone String @default("Europe/Berlin") @db.VarChar(60) + aktiv Boolean @default(true) + + erstelltAm DateTime @default(now()) @db.Timestamptz(3) + geaendertAm DateTime @updatedAt @db.Timestamptz(3) + geloeschtAm DateTime? @db.Timestamptz(3) + + personen Person[] + rollen Rolle[] + personRollen PersonRolle[] + rolleRechte RolleBerechtigung[] + ausweisTokens AusweisToken[] + auditEintraege AuditLog[] + dateien Datei[] + fahrzeuge Fahrzeug[] + geraete Geraet[] + + @@index([aktiv]) + @@map("organisation") +} + +// ============================================================================= +// Personen und Rechte +// ============================================================================= + +/// Ein Mitglied der Wehr. Gleichzeitig der Anmeldebenutzer — es gibt bewusst +/// keine getrennte "User"-Tabelle, damit Dienstgrad, Qualifikation und Zugang +/// nicht auseinanderlaufen. +model Person { + id String @id @default(uuid()) @db.Uuid + organisationId String @db.Uuid + + vorname String @db.VarChar(100) + nachname String @db.VarChar(100) + /// Fortlaufende Mitgliedsnummer innerhalb der Wehr. + mitgliedsnummer String? @db.VarChar(30) + geburtsdatum DateTime? @db.Date + dienstgrad Dienstgrad @default(KEIN) + + email String? @db.VarChar(200) + telefon String? @db.VarChar(40) + mobil String? @db.VarChar(40) + + strasse String? @db.VarChar(200) + plz String? @db.VarChar(10) + ort String? @db.VarChar(120) + + eintrittsdatum DateTime? @db.Date + austrittsdatum DateTime? @db.Date + /// Aktiver Dienst, passiv, Jugendfeuerwehr, Ehrenmitglied. + abteilung String? @db.VarChar(60) + + /// bcrypt-Hash für den lokalen Login. Null, wenn nur OIDC genutzt wird. + passwortHash String? @db.VarChar(120) + passwortGeaendertAm DateTime? @db.Timestamptz(3) + /// "sub"-Claim aus authentik. Verknüpft die Person mit dem OIDC-Konto. + oidcSubject String? @db.VarChar(255) + /// Gruppen aus dem letzten OIDC-Login — nur zur Nachvollziehbarkeit. + oidcGruppen String[] @default([]) + + letzteAnmeldung DateTime? @db.Timestamptz(3) + fehlversuche Int @default(0) + gesperrtBis DateTime? @db.Timestamptz(3) + aktiv Boolean @default(true) + + /// Profilbild. Liegt wie alles andere in MinIO. + avatarDateiId String? @db.Uuid + + erstelltAm DateTime @default(now()) @db.Timestamptz(3) + geaendertAm DateTime @updatedAt @db.Timestamptz(3) + geloeschtAm DateTime? @db.Timestamptz(3) + + organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade) + avatar Datei? @relation("PersonAvatar", fields: [avatarDateiId], references: [id], onDelete: SetNull) + rollen PersonRolle[] @relation("PersonRollenInhaber") + vergebeneRollen PersonRolle[] @relation("PersonRollenVergeber") + ausweisTokens AusweisToken[] + auditEintraege AuditLog[] + hochgeladeneDateien Datei[] @relation("DateiHochgeladenVon") + + /// E-Mail und OIDC-Subject sind je Wehr eindeutig, nicht global — dieselbe + /// Person kann in zwei Wehren Mitglied sein. + @@unique([organisationId, email]) + @@unique([organisationId, oidcSubject]) + @@unique([organisationId, mitgliedsnummer]) + @@index([organisationId, nachname, vorname]) + @@index([organisationId, aktiv]) + @@index([oidcSubject]) + @@map("person") +} + +/// Eine Rolle innerhalb einer Wehr. Die Systemrollen legt der Seed an; eigene +/// Rollen kann der Kommandant ergänzen. +model Rolle { + id String @id @default(uuid()) @db.Uuid + organisationId String @db.Uuid + + schluessel RolleSchluessel + name String @db.VarChar(100) + beschreibung String? @db.VarChar(500) + /// Systemrollen dürfen nicht gelöscht werden. + systemRolle Boolean @default(false) + + erstelltAm DateTime @default(now()) @db.Timestamptz(3) + geaendertAm DateTime @updatedAt @db.Timestamptz(3) + + organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade) + personen PersonRolle[] + rechte RolleBerechtigung[] + + @@unique([organisationId, schluessel]) + @@index([organisationId]) + @@map("rolle") +} + +/// Zuweisung Person zu Rolle. Eine Person kann mehrere Rollen tragen — der +/// Gerätewart ist meist auch aktives Mitglied. +model PersonRolle { + id String @id @default(uuid()) @db.Uuid + organisationId String @db.Uuid + personId String @db.Uuid + rolleId String @db.Uuid + + zugewiesenAm DateTime @default(now()) @db.Timestamptz(3) + zugewiesenVonId String? @db.Uuid + /// Befristete Zuweisung, z. B. Vertretung während Urlaub. + gueltigBis DateTime? @db.Timestamptz(3) + /// true, wenn die Rolle aus einer authentik-Gruppe vorbelegt wurde. Sobald + /// jemand sie in FLORIAN ändert, steht hier false und OIDC überschreibt nicht + /// mehr. + ausOidcGruppe Boolean @default(false) + + organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade) + person Person @relation("PersonRollenInhaber", fields: [personId], references: [id], onDelete: Cascade) + rolle Rolle @relation(fields: [rolleId], references: [id], onDelete: Cascade) + zugewiesenVon Person? @relation("PersonRollenVergeber", fields: [zugewiesenVonId], references: [id], onDelete: SetNull) + + @@unique([personId, rolleId]) + @@index([organisationId, personId]) + @@index([organisationId, rolleId]) + @@map("person_rolle") +} + +/// Systemweiter Katalog der Einzelrechte, z. B. "geraet.bearbeiten". +/// +/// BEWUSSTE AUSNAHME von der organisationId-Regel: das hier sind keine +/// Mandantendaten, sondern eine Liste dessen, was die Software überhaupt kann. +/// Der Katalog wird vom Seed aus lib/authz.ts befüllt und ist für alle Wehren +/// identisch. Mandantenbezogen wird es erst durch RolleBerechtigung, und die +/// trägt organisationId. Die Tabelle ist deshalb für alle lesbar und für die +/// Anwendungsrolle nicht beschreibbar. +model Berechtigung { + id String @id @default(uuid()) @db.Uuid + schluessel String @unique @db.VarChar(80) + /// Fachbereich für die Gruppierung in der Oberfläche. + bereich String @db.VarChar(60) + bezeichnung String @db.VarChar(160) + beschreibung String? @db.VarChar(500) + + erstelltAm DateTime @default(now()) @db.Timestamptz(3) + + rollen RolleBerechtigung[] + + @@index([bereich]) + @@map("berechtigung") +} + +/// Welche Rolle welches Recht hat — je Wehr getrennt, damit eine Wehr dem +/// Gerätewart mehr erlauben kann als eine andere. +model RolleBerechtigung { + id String @id @default(uuid()) @db.Uuid + organisationId String @db.Uuid + rolleId String @db.Uuid + berechtigungId String @db.Uuid + + /// false setzt ein Recht gezielt ab, das die Rolle sonst hätte. + gewaehrt Boolean @default(true) + + erstelltAm DateTime @default(now()) @db.Timestamptz(3) + + organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade) + rolle Rolle @relation(fields: [rolleId], references: [id], onDelete: Cascade) + berechtigung Berechtigung @relation(fields: [berechtigungId], references: [id], onDelete: Cascade) + + @@unique([rolleId, berechtigungId]) + @@index([organisationId, rolleId]) + @@map("rolle_berechtigung") +} + +/// Anmeldemittel für das Terminal im Gerätehaus: PIN oder RFID-Karte. +/// Gespeichert wird immer nur ein Hash, nie die PIN oder die UID im Klartext. +model AusweisToken { + id String @id @default(uuid()) @db.Uuid + organisationId String @db.Uuid + personId String @db.Uuid + + typ AusweisTokenTyp + /// bcrypt-Hash der PIN bzw. der RFID-UID. + tokenHash String @db.VarChar(120) + /// Letzte vier Stellen der Karten-UID, damit man Karten unterscheiden kann. + kennzeichen String? @db.VarChar(20) + bezeichnung String? @db.VarChar(120) + + aktiv Boolean @default(true) + gueltigAb DateTime @default(now()) @db.Timestamptz(3) + gueltigBis DateTime? @db.Timestamptz(3) + letzteNutzung DateTime? @db.Timestamptz(3) + fehlversuche Int @default(0) + gesperrtBis DateTime? @db.Timestamptz(3) + + erstelltAm DateTime @default(now()) @db.Timestamptz(3) + geaendertAm DateTime @updatedAt @db.Timestamptz(3) + geloeschtAm DateTime? @db.Timestamptz(3) + + organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade) + person Person @relation(fields: [personId], references: [id], onDelete: Cascade) + + /// Je Person und Art höchstens ein aktives Mittel dieser Bezeichnung. + @@index([organisationId, personId]) + @@index([organisationId, typ, aktiv]) + @@map("ausweis_token") +} + +// ============================================================================= +// Nachvollziehbarkeit +// ============================================================================= + +/// Append-only. Es gibt keine UPDATE- oder DELETE-Policy für die Anwendung; +/// zusätzlich verhindert ein Trigger Änderungen auch für den Eigentümer. +model AuditLog { + id String @id @default(uuid()) @db.Uuid + organisationId String @db.Uuid + + zeitpunkt DateTime @default(now()) @db.Timestamptz(3) + + akteurTyp AkteurTyp @default(PERSON) + akteurPersonId String? @db.Uuid + /// Klarname zum Zeitpunkt der Aktion — bleibt lesbar, auch wenn die Person + /// später anonymisiert wird. + akteurName String? @db.VarChar(200) + anmeldeArt AnmeldeArt? + + aktion AuditAktion + /// Modellname, z. B. "Geraet". + entitaet String @db.VarChar(80) + entitaetId String? @db.Uuid + /// Menschenlesbare Kurzfassung für die Oberfläche. + beschreibung String? @db.VarChar(500) + + vorher Json? @db.JsonB + nachher Json? @db.JsonB + /// Zusatzangaben: Anfrage-ID, Endpunkt, betroffene Felder. + kontext Json? @db.JsonB + + ipAdresse String? @db.Inet + userAgent String? @db.VarChar(400) + + organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade) + akteur Person? @relation(fields: [akteurPersonId], references: [id], onDelete: SetNull) + + @@index([organisationId, zeitpunkt(sort: Desc)]) + @@index([organisationId, entitaet, entitaetId]) + @@index([organisationId, akteurPersonId]) + @@map("audit_log") +} + +// ============================================================================= +// Dateien +// ============================================================================= + +/// Metadaten zu einem Objekt in MinIO. Der Inhalt liegt NIE im Dateisystem und +/// nie in der Datenbank — hier steht nur, wo er zu finden ist. +model Datei { + id String @id @default(uuid()) @db.Uuid + organisationId String @db.Uuid + + bucket String @db.VarChar(80) + /// Objektschlüssel in MinIO, z. B. "ff-musterhausen/geraet//foto.jpg". + objektSchluessel String @db.VarChar(500) + + dateiname String @db.VarChar(300) + mimeTyp String @db.VarChar(160) + groesseBytes BigInt + /// SHA-256 des Inhalts. Erkennt Dubletten und belegt Unverändertheit. + pruefsumme String? @db.VarChar(64) + + /// Woran die Datei hängt (z. B. "Geraet") und an welchem Datensatz. + entitaet String? @db.VarChar(80) + entitaetId String? @db.Uuid + /// Freie Einordnung: "pruefprotokoll", "foto", "rechnung", "export". + kategorie String? @db.VarChar(60) + + hochgeladenVonId String? @db.Uuid + erstelltAm DateTime @default(now()) @db.Timestamptz(3) + geaendertAm DateTime @updatedAt @db.Timestamptz(3) + /// Soft-Delete: das Objekt in MinIO räumt erst der Aufbewahrungs-Job ab. + geloeschtAm DateTime? @db.Timestamptz(3) + + organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade) + hochgeladenVon Person? @relation("DateiHochgeladenVon", fields: [hochgeladenVonId], references: [id], onDelete: SetNull) + alsAvatarGenutzt Person[] @relation("PersonAvatar") + + @@unique([bucket, objektSchluessel]) + @@index([organisationId, entitaet, entitaetId]) + @@index([organisationId, kategorie]) + @@map("datei") +} + +// ============================================================================= +// Vorgriff auf Phase 1 +// Diese beiden Modelle sind bewusst schmal gehalten. Sie existieren, damit das +// Seed-Skript eine realistische Wehr aufbauen kann und die Mandantentrennung +// an echten Daten prüfbar ist. Phase 1 erweitert sie um Funkrufname-Systematik, +// DIN-Typisierung, Geräteräume, Prüfintervalle und Lebensdauerkategorien. +// ============================================================================= + +/// Ein Einsatzfahrzeug der Wehr. +model Fahrzeug { + id String @id @default(uuid()) @db.Uuid + organisationId String @db.Uuid + + /// Funkrufname, z. B. "Florian Musterhausen 40/1". + funkrufname String @db.VarChar(120) + bezeichnung String @db.VarChar(120) + art FahrzeugArt @default(LOESCHFAHRZEUG) + kennzeichen String? @db.VarChar(20) + hersteller String? @db.VarChar(120) + baujahr Int? + /// Nächste Hauptuntersuchung. + tuevBis DateTime? @db.Date + aktiv Boolean @default(true) + + erstelltAm DateTime @default(now()) @db.Timestamptz(3) + geaendertAm DateTime @updatedAt @db.Timestamptz(3) + geloeschtAm DateTime? @db.Timestamptz(3) + + organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade) + geraete Geraet[] + + @@unique([organisationId, funkrufname]) + @@index([organisationId, aktiv]) + @@map("fahrzeug") +} + +/// Ein prüfpflichtiges Gerät — Pressluftatmer, Schlauch, Leiter, Motorsäge. +model Geraet { + id String @id @default(uuid()) @db.Uuid + organisationId String @db.Uuid + + inventarnummer String @db.VarChar(60) + bezeichnung String @db.VarChar(160) + hersteller String? @db.VarChar(120) + seriennummer String? @db.VarChar(120) + + herstellungsdatum DateTime? @db.Date + indienststellung DateTime? @db.Date + + status GeraetStatus @default(EINSATZBEREIT) + /// Prüfintervall in Monaten aus Vorschrift oder Herstellerangabe. + pruefintervallMonate Int? + naechstePruefung DateTime? @db.Date + /// Maximale Nutzungsdauer in Jahren (Helme, Seile, Schutzkleidung). + lebensdauerJahre Int? + + /// Verlastet auf einem Fahrzeug — oder im Gerätehaus, dann null. + fahrzeugId String? @db.Uuid + lagerort String? @db.VarChar(160) + + erstelltAm DateTime @default(now()) @db.Timestamptz(3) + geaendertAm DateTime @updatedAt @db.Timestamptz(3) + geloeschtAm DateTime? @db.Timestamptz(3) + + organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade) + fahrzeug Fahrzeug? @relation(fields: [fahrzeugId], references: [id], onDelete: SetNull) + + @@unique([organisationId, inventarnummer]) + @@index([organisationId, status]) + @@index([organisationId, naechstePruefung]) + @@index([organisationId, fahrzeugId]) + @@map("geraet") +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..f4022ac --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,595 @@ +/** + * Seed — legt eine vollständige Beispielwehr an. + * + * Inhalt: Berechtigungskatalog, 9 Systemrollen, 15 Personen mit Rollen, + * 3 Fahrzeuge, 40 Geräte, PIN und RFID-Ausweise für das Terminal. + * + * Das Skript läuft über DIRECT_DATABASE_URL, also mit dem Eigentümer-Zugang, + * der Row Level Security umgehen darf. Anders ginge es nicht: der Seed legt + * die Organisation ja erst an, deren Kontext RLS bräuchte. + * + * Es ist idempotent — mehrfaches Ausführen ändert nichts an bereits + * vorhandenen Daten. + * + * npm run db:seed + */ +import "dotenv/config"; +import { + Dienstgrad, + FahrzeugArt, + GeraetStatus, + PrismaClient, + RolleSchluessel, + AusweisTokenTyp, +} from "@prisma/client"; +import bcrypt from "bcryptjs"; + +const prisma = new PrismaClient({ + datasources: { + db: { url: process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL ?? "" }, + }, +}); + +const RUNDEN = Number(process.env.AUTH_BCRYPT_ROUNDS ?? 12); +const ORGANISATION_SLUG = "ff-musterhausen"; + +function log(nachricht: string): void { + console.info(`[seed] ${nachricht}`); +} + +/** Deterministisches Datum, damit zwei Läufe dieselben Daten erzeugen. */ +function datum(jahr: number, monat: number, tag: number): Date { + return new Date(Date.UTC(jahr, monat - 1, tag, 12, 0, 0)); +} + +/** Verschiebt ein Datum um Monate — für Prüffristen. */ +function plusMonate(basis: Date, monate: number): Date { + const neu = new Date(basis); + neu.setUTCMonth(neu.getUTCMonth() + monate); + return neu; +} + +// ----------------------------------------------------------------------------- +// Berechtigungskatalog +// ----------------------------------------------------------------------------- + +/** + * Der Katalog wird aus lib/authz.ts gespiegelt. Der Import geht bewusst über + * einen relativen Pfad, weil das Seed-Skript außerhalb des Next.js-Alias läuft. + */ +async function katalogAbgleichen(): Promise> { + const { RECHTE } = await import("../src/lib/authz"); + + const karte = new Map(); + + for (const [schluessel, angaben] of Object.entries(RECHTE)) { + const eintrag = await prisma.berechtigung.upsert({ + where: { schluessel }, + update: { bereich: angaben.bereich, bezeichnung: angaben.bezeichnung }, + create: { + schluessel, + bereich: angaben.bereich, + bezeichnung: angaben.bezeichnung, + }, + }); + karte.set(schluessel, eintrag.id); + } + + log(`Berechtigungskatalog: ${karte.size} Einzelrechte`); + return karte; +} + +// ----------------------------------------------------------------------------- +// Organisation und Rollen +// ----------------------------------------------------------------------------- + +async function organisationAnlegen() { + const organisation = await prisma.organisation.upsert({ + where: { slug: ORGANISATION_SLUG }, + update: {}, + create: { + slug: ORGANISATION_SLUG, + name: "Freiwillige Feuerwehr Musterhausen", + kurzname: "FF Musterhausen", + strasse: "Hauptstraße 12", + plz: "85354", + ort: "Musterhausen", + landkreis: "Freising", + bundesland: "Bayern", + telefon: "08161 123456", + email: "kommandant@ff-musterhausen.de", + ilsKennung: "FS-MUS-01", + funkPraefix: "Florian Musterhausen", + }, + }); + + log(`Organisation: ${organisation.name} (${organisation.id})`); + return organisation; +} + +async function rollenAnlegen(organisationId: string, katalog: Map) { + const { ROLLEN, ROLLEN_BEZEICHNUNG, ROLLEN_BESCHREIBUNG, ROLLEN_RECHTE, ALLE_RECHTE } = + await import("../src/lib/authz"); + + const rollenKarte = new Map(); + + for (const schluessel of ROLLEN) { + const rolle = await prisma.rolle.upsert({ + where: { + organisationId_schluessel: { + organisationId, + schluessel: schluessel as RolleSchluessel, + }, + }, + update: { + name: ROLLEN_BEZEICHNUNG[schluessel], + beschreibung: ROLLEN_BESCHREIBUNG[schluessel], + }, + create: { + organisationId, + schluessel: schluessel as RolleSchluessel, + name: ROLLEN_BEZEICHNUNG[schluessel], + beschreibung: ROLLEN_BESCHREIBUNG[schluessel], + systemRolle: true, + }, + }); + rollenKarte.set(schluessel, rolle.id); + + // Rechte der Rolle spiegeln — der Administrator bekommt alles. + const rechte = + schluessel === "SUPERADMIN" + ? ALLE_RECHTE + : (ROLLEN_RECHTE[schluessel as Exclude] ?? []); + + for (const recht of rechte) { + const berechtigungId = katalog.get(recht); + if (!berechtigungId) continue; + + await prisma.rolleBerechtigung.upsert({ + where: { rolleId_berechtigungId: { rolleId: rolle.id, berechtigungId } }, + update: { gewaehrt: true }, + create: { organisationId, rolleId: rolle.id, berechtigungId, gewaehrt: true }, + }); + } + } + + log(`Rollen: ${rollenKarte.size} Systemrollen samt Rechtezuordnung`); + return rollenKarte; +} + +// ----------------------------------------------------------------------------- +// Personen +// ----------------------------------------------------------------------------- + +interface PersonVorlage { + vorname: string; + nachname: string; + dienstgrad: Dienstgrad; + rollen: RolleSchluessel[]; + eintritt: Date; + abteilung: string; + /** PIN fürs Terminal. Nur im Seed im Klartext, in der Datenbank gehasht. */ + pin?: string; + /** RFID-Kennung der Ausweiskarte. */ + rfid?: string; +} + +const PERSONEN: PersonVorlage[] = [ + { + vorname: "Andreas", + nachname: "Huber", + dienstgrad: Dienstgrad.HAUPTBRANDMEISTER, + rollen: [RolleSchluessel.KOMMANDANT], + eintritt: datum(1998, 4, 1), + abteilung: "Aktive", + pin: "471203", + rfid: "04A3B7C1D2", + }, + { + vorname: "Bernhard", + nachname: "Maier", + dienstgrad: Dienstgrad.OBERBRANDMEISTER, + rollen: [RolleSchluessel.KOMMANDANT], + eintritt: datum(2001, 9, 15), + abteilung: "Aktive", + pin: "938157", + }, + { + vorname: "Christian", + nachname: "Bauer", + dienstgrad: Dienstgrad.HAUPTLOESCHMEISTER, + rollen: [RolleSchluessel.GERAETEWART], + eintritt: datum(2005, 3, 1), + abteilung: "Aktive", + pin: "620394", + rfid: "04B8C2D3E4", + }, + { + vorname: "Daniela", + nachname: "Schmid", + dienstgrad: Dienstgrad.OBERLOESCHMEISTER, + rollen: [RolleSchluessel.ATEMSCHUTZGERAETEWART], + eintritt: datum(2008, 7, 1), + abteilung: "Aktive", + pin: "184926", + rfid: "04C9D3E4F5", + }, + { + vorname: "Elisabeth", + nachname: "Wagner", + dienstgrad: Dienstgrad.LOESCHMEISTER, + rollen: [RolleSchluessel.KASSIER], + eintritt: datum(2010, 1, 15), + abteilung: "Aktive", + pin: "573081", + }, + { + vorname: "Franz", + nachname: "Fischer", + dienstgrad: Dienstgrad.LOESCHMEISTER, + rollen: [RolleSchluessel.SCHRIFTFUEHRER], + eintritt: datum(2009, 5, 20), + abteilung: "Aktive", + pin: "306159", + }, + { + vorname: "Gabriele", + nachname: "Weber", + dienstgrad: Dienstgrad.BRANDMEISTER, + rollen: [RolleSchluessel.AUSBILDER], + eintritt: datum(2004, 11, 1), + abteilung: "Aktive", + pin: "849265", + }, + { + vorname: "Hans", + nachname: "Becker", + dienstgrad: Dienstgrad.HAUPTFEUERWEHRMANN, + rollen: [RolleSchluessel.GERAETEWART, RolleSchluessel.AUSBILDER], + eintritt: datum(2012, 2, 1), + abteilung: "Aktive", + pin: "715384", + }, + { + vorname: "Ingrid", + nachname: "Hofmann", + dienstgrad: Dienstgrad.OBERFEUERWEHRMANN, + rollen: [RolleSchluessel.MITGLIED], + eintritt: datum(2015, 6, 1), + abteilung: "Aktive", + }, + { + vorname: "Josef", + nachname: "Schneider", + dienstgrad: Dienstgrad.OBERFEUERWEHRMANN, + rollen: [RolleSchluessel.MITGLIED], + eintritt: datum(2016, 4, 1), + abteilung: "Aktive", + }, + { + vorname: "Katharina", + nachname: "Koch", + dienstgrad: Dienstgrad.FEUERWEHRMANN, + rollen: [RolleSchluessel.MITGLIED], + eintritt: datum(2019, 9, 1), + abteilung: "Aktive", + }, + { + vorname: "Ludwig", + nachname: "Richter", + dienstgrad: Dienstgrad.FEUERWEHRMANN, + rollen: [RolleSchluessel.MITGLIED], + eintritt: datum(2020, 3, 1), + abteilung: "Aktive", + }, + { + vorname: "Martina", + nachname: "Klein", + dienstgrad: Dienstgrad.FEUERWEHRANWAERTER, + rollen: [RolleSchluessel.MITGLIED], + eintritt: datum(2024, 1, 15), + abteilung: "Aktive", + }, + { + vorname: "Nikolaus", + nachname: "Wolf", + dienstgrad: Dienstgrad.FEUERWEHRANWAERTER, + rollen: [RolleSchluessel.MITGLIED], + eintritt: datum(2025, 2, 1), + abteilung: "Jugendfeuerwehr", + }, + { + vorname: "Otto", + nachname: "Neumann", + dienstgrad: Dienstgrad.HAUPTBRANDMEISTER, + rollen: [RolleSchluessel.MITGLIED], + eintritt: datum(1975, 5, 1), + abteilung: "Passive", + }, +]; + +async function personenAnlegen( + organisationId: string, + rollenKarte: Map, +): Promise { + let nummer = 1; + + for (const vorlage of PERSONEN) { + const mitgliedsnummer = String(nummer).padStart(4, "0"); + const email = `${vorlage.vorname}.${vorlage.nachname}` + .toLowerCase() + .replace(/ä/g, "ae") + .replace(/ö/g, "oe") + .replace(/ü/g, "ue") + .replace(/ß/g, "ss") + "@ff-musterhausen.de"; + + const person = await prisma.person.upsert({ + where: { organisationId_mitgliedsnummer: { organisationId, mitgliedsnummer } }, + update: {}, + create: { + organisationId, + mitgliedsnummer, + vorname: vorlage.vorname, + nachname: vorlage.nachname, + dienstgrad: vorlage.dienstgrad, + email, + telefon: `08161 ${100000 + nummer * 7}`, + strasse: `Beispielweg ${nummer}`, + plz: "85354", + ort: "Musterhausen", + eintrittsdatum: vorlage.eintritt, + abteilung: vorlage.abteilung, + aktiv: vorlage.abteilung !== "Passive", + }, + }); + + for (const schluessel of vorlage.rollen) { + const rolleId = rollenKarte.get(schluessel); + if (!rolleId) continue; + await prisma.personRolle.upsert({ + where: { personId_rolleId: { personId: person.id, rolleId } }, + update: {}, + create: { organisationId, personId: person.id, rolleId }, + }); + } + + // Terminal-Ausweise: PIN und, wo vorhanden, RFID-Karte. + if (vorlage.pin) { + const vorhanden = await prisma.ausweisToken.findFirst({ + where: { organisationId, personId: person.id, typ: AusweisTokenTyp.PIN }, + }); + if (!vorhanden) { + await prisma.ausweisToken.create({ + data: { + organisationId, + personId: person.id, + typ: AusweisTokenTyp.PIN, + tokenHash: await bcrypt.hash(vorlage.pin, RUNDEN), + bezeichnung: "Terminal-PIN", + }, + }); + } + } + + if (vorlage.rfid) { + const vorhanden = await prisma.ausweisToken.findFirst({ + where: { organisationId, personId: person.id, typ: AusweisTokenTyp.RFID }, + }); + if (!vorhanden) { + await prisma.ausweisToken.create({ + data: { + organisationId, + personId: person.id, + typ: AusweisTokenTyp.RFID, + tokenHash: await bcrypt.hash(vorlage.rfid, RUNDEN), + kennzeichen: vorlage.rfid.slice(-4), + bezeichnung: "Ausweiskarte", + }, + }); + } + } + + nummer += 1; + } + + log(`Personen: ${PERSONEN.length} Mitglieder samt Rollen und Terminal-Ausweisen`); +} + +/** + * Der erste Administrator. Anders als die Mitglieder hat er ein Passwort für + * den lokalen Login — damit kommt man beim Erststart überhaupt hinein, bevor + * authentik eingerichtet ist. + */ +async function adminAnlegen(organisationId: string, rollenKarte: Map) { + const email = (process.env.SEED_ADMIN_EMAIL ?? "admin@beispielwehr.de").toLowerCase(); + const passwort = process.env.SEED_ADMIN_PASSWORT ?? "Feuerwehr2026"; + + const admin = await prisma.person.upsert({ + where: { organisationId_email: { organisationId, email } }, + update: {}, + create: { + organisationId, + mitgliedsnummer: "0000", + vorname: "System", + nachname: "Administrator", + email, + dienstgrad: Dienstgrad.KEIN, + passwortHash: await bcrypt.hash(passwort, RUNDEN), + passwortGeaendertAm: new Date(), + abteilung: "Verwaltung", + }, + }); + + const rolleId = rollenKarte.get(RolleSchluessel.SUPERADMIN); + if (rolleId) { + await prisma.personRolle.upsert({ + where: { personId_rolleId: { personId: admin.id, rolleId } }, + update: {}, + create: { organisationId, personId: admin.id, rolleId }, + }); + } + + log(`Administrator: ${email} (Passwort aus SEED_ADMIN_PASSWORT)`); + return admin; +} + +// ----------------------------------------------------------------------------- +// Fahrzeuge und Geräte +// ----------------------------------------------------------------------------- + +const FAHRZEUGE = [ + { + funkrufname: "Florian Musterhausen 40/1", + bezeichnung: "LF 20 (Löschgruppenfahrzeug)", + art: FahrzeugArt.LOESCHFAHRZEUG, + kennzeichen: "FS-MU 401", + hersteller: "MAN / Rosenbauer", + baujahr: 2018, + tuevBis: datum(2027, 5, 31), + }, + { + funkrufname: "Florian Musterhausen 42/1", + bezeichnung: "TSF-W (Tragkraftspritzenfahrzeug Wasser)", + art: FahrzeugArt.LOESCHFAHRZEUG, + kennzeichen: "FS-MU 421", + hersteller: "Mercedes-Benz / Ziegler", + baujahr: 2011, + tuevBis: datum(2026, 11, 30), + }, + { + funkrufname: "Florian Musterhausen 11/1", + bezeichnung: "MTW (Mannschaftstransportwagen)", + art: FahrzeugArt.MANNSCHAFTSTRANSPORT, + kennzeichen: "FS-MU 111", + hersteller: "Volkswagen", + baujahr: 2021, + tuevBis: datum(2027, 3, 31), + }, +]; + +/** + * 40 Geräte mit realistischen Prüfintervallen. Die Fristen sind bewusst + * gemischt: einige liegen in der Vergangenheit, damit der Status + * "Prüfung fällig" in der Oberfläche auch sichtbar ist. + */ +const GERAETE_VORLAGEN: Array<{ + bezeichnung: string; + hersteller: string; + anzahl: number; + intervallMonate: number; + lebensdauerJahre?: number; + aufFahrzeug?: number; + lagerort?: string; +}> = [ + { bezeichnung: "Pressluftatmer PA 94", hersteller: "Dräger", anzahl: 4, intervallMonate: 12, lebensdauerJahre: 20, aufFahrzeug: 0 }, + { bezeichnung: "Atemschutzmaske FPS 7000", hersteller: "Dräger", anzahl: 6, intervallMonate: 6, lebensdauerJahre: 10, lagerort: "Atemschutzwerkstatt" }, + { bezeichnung: "Atemluftflasche 6,8 l / 300 bar", hersteller: "Luxfer", anzahl: 8, intervallMonate: 60, lebensdauerJahre: 20, aufFahrzeug: 0 }, + { bezeichnung: "Feuerwehrhelm F220", hersteller: "Schuberth", anzahl: 6, intervallMonate: 12, lebensdauerJahre: 15, lagerort: "Umkleide" }, + { bezeichnung: "Feuerwehrleine 30 m", hersteller: "Gustav Bauer", anzahl: 3, intervallMonate: 12, lebensdauerJahre: 8, aufFahrzeug: 0 }, + { bezeichnung: "Steckleiterteil", hersteller: "Iturri", anzahl: 4, intervallMonate: 12, lebensdauerJahre: 25, aufFahrzeug: 0 }, + { bezeichnung: "Tragkraftspritze PFPN 10-1000", hersteller: "Rosenbauer", anzahl: 1, intervallMonate: 12, aufFahrzeug: 1 }, + { bezeichnung: "Hydraulischer Rettungssatz", hersteller: "Weber Rescue", anzahl: 1, intervallMonate: 12, aufFahrzeug: 0 }, + { bezeichnung: "Motorkettensäge MS 261", hersteller: "Stihl", anzahl: 2, intervallMonate: 12, aufFahrzeug: 0 }, + { bezeichnung: "Stromerzeuger 8 kVA", hersteller: "Endress", anzahl: 1, intervallMonate: 12, aufFahrzeug: 0 }, + { bezeichnung: "Wärmebildkamera", hersteller: "Dräger", anzahl: 1, intervallMonate: 12, aufFahrzeug: 0 }, + { bezeichnung: "Handsprechfunkgerät TPH900", hersteller: "Airbus", anzahl: 3, intervallMonate: 24, lagerort: "Funkraum" }, +]; + +async function fahrzeugeUndGeraeteAnlegen(organisationId: string): Promise { + const fahrzeugIds: string[] = []; + + for (const vorlage of FAHRZEUGE) { + const fahrzeug = await prisma.fahrzeug.upsert({ + where: { + organisationId_funkrufname: { organisationId, funkrufname: vorlage.funkrufname }, + }, + update: {}, + create: { organisationId, ...vorlage }, + }); + fahrzeugIds.push(fahrzeug.id); + } + + log(`Fahrzeuge: ${fahrzeugIds.length}`); + + const heute = new Date(); + let laufend = 1; + let angelegt = 0; + + for (const vorlage of GERAETE_VORLAGEN) { + for (let index = 0; index < vorlage.anzahl; index += 1) { + const inventarnummer = `INV-${String(laufend).padStart(4, "0")}`; + + // Ein Drittel der Geräte bekommt eine bereits überschrittene Frist, + // damit die Statusanzeige nicht nur grün ist. + const ueberfaellig = laufend % 3 === 0; + const indienststellung = datum(2015 + (laufend % 9), ((laufend % 12) + 1), 15); + const naechstePruefung = ueberfaellig + ? plusMonate(heute, -(1 + (laufend % 5))) + : plusMonate(heute, 1 + (laufend % vorlage.intervallMonate || 1)); + + const status = ueberfaellig + ? GeraetStatus.PRUEFUNG_FAELLIG + : laufend % 17 === 0 + ? GeraetStatus.GESPERRT + : GeraetStatus.EINSATZBEREIT; + + await prisma.geraet.upsert({ + where: { organisationId_inventarnummer: { organisationId, inventarnummer } }, + update: {}, + create: { + organisationId, + inventarnummer, + bezeichnung: vorlage.bezeichnung, + hersteller: vorlage.hersteller, + seriennummer: `SN-${vorlage.hersteller.slice(0, 3).toUpperCase()}-${100000 + laufend}`, + herstellungsdatum: datum(indienststellung.getUTCFullYear() - 1, 6, 1), + indienststellung, + status, + pruefintervallMonate: vorlage.intervallMonate, + naechstePruefung, + lebensdauerJahre: vorlage.lebensdauerJahre ?? null, + fahrzeugId: + vorlage.aufFahrzeug !== undefined ? (fahrzeugIds[vorlage.aufFahrzeug] ?? null) : null, + lagerort: vorlage.lagerort ?? null, + }, + }); + + laufend += 1; + angelegt += 1; + } + } + + log(`Geräte: ${angelegt}`); +} + +// ----------------------------------------------------------------------------- +// Ablauf +// ----------------------------------------------------------------------------- + +async function main(): Promise { + log("Starte ..."); + + const katalog = await katalogAbgleichen(); + const organisation = await organisationAnlegen(); + const rollen = await rollenAnlegen(organisation.id, katalog); + await adminAnlegen(organisation.id, rollen); + await personenAnlegen(organisation.id, rollen); + await fahrzeugeUndGeraeteAnlegen(organisation.id); + + log("Fertig."); + log(""); + log("Anmeldung für den Erststart:"); + log(` E-Mail: ${process.env.SEED_ADMIN_EMAIL ?? "admin@beispielwehr.de"}`); + log(` Passwort: ${process.env.SEED_ADMIN_PASSWORT ?? "Feuerwehr2026"}`); + log(""); + log("Terminal-PIN zum Ausprobieren (Mitgliedsnummer 0001, Andreas Huber): 471203"); +} + +main() + .catch((fehler) => { + console.error("[seed] Abbruch:", fehler); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/scripts/screenshots.ts b/scripts/screenshots.ts new file mode 100644 index 0000000..2d0aa61 --- /dev/null +++ b/scripts/screenshots.ts @@ -0,0 +1,100 @@ +/** + * Erzeugt die Bilder für docs/ANLEITUNG.md. + * + * Läuft gegen die laufende Anwendung, meldet sich mit dem Seed-Administrator an + * und legt die Bilder unter docs/screenshots ab — jeweils in heller und dunkler + * Darstellung, damit die Anleitung zu beiden Einstellungen passt. + * + * docker compose up -d + * npm run dev + * npm run screenshots + * + * Die Bilder werden mit eingecheckt: die Anleitung soll auch dann lesbar sein, + * wenn gerade kein Stack läuft. + */ +import "dotenv/config"; +import { mkdir } from "node:fs/promises"; +import path from "node:path"; +import { chromium, type Page } from "@playwright/test"; + +const BASIS = process.env.E2E_BASIS_URL ?? "http://localhost:3100"; +const ZIEL = path.join(process.cwd(), "docs", "screenshots"); + +const ADMIN_EMAIL = process.env.SEED_ADMIN_EMAIL ?? "admin@beispielwehr.de"; +const ADMIN_PASSWORT = process.env.SEED_ADMIN_PASSWORT ?? "Feuerwehr2026"; + +interface Aufnahme { + name: string; + pfad: string; + beschreibung: string; + /** true, wenn vorher angemeldet werden muss. */ + angemeldet: boolean; +} + +const AUFNAHMEN: Aufnahme[] = [ + { + name: "01-anmeldung", + pfad: "/anmelden", + beschreibung: "Anmeldeseite mit lokalem Login", + angemeldet: false, + }, + { + name: "02-uebersicht", + pfad: "/", + beschreibung: "Übersicht nach der Anmeldung", + angemeldet: true, + }, +]; + +async function anmelden(seite: Page): Promise { + await seite.goto(`${BASIS}/anmelden`, { waitUntil: "networkidle" }); + await seite.getByLabel("E-Mail-Adresse").fill(ADMIN_EMAIL); + await seite.getByLabel("Passwort").fill(ADMIN_PASSWORT); + await seite.getByRole("button", { name: "Anmelden" }).click(); + await seite.waitForURL(`${BASIS}/`, { timeout: 20_000 }); + await seite.waitForLoadState("networkidle"); +} + +async function main(): Promise { + await mkdir(ZIEL, { recursive: true }); + + const browser = await chromium.launch(); + + for (const darstellung of ["light", "dark"] as const) { + const kontext = await browser.newContext({ + viewport: { width: 1440, height: 900 }, + deviceScaleFactor: 2, + locale: "de-DE", + timezoneId: "Europe/Berlin", + colorScheme: darstellung, + }); + + const seite = await kontext.newPage(); + let istAngemeldet = false; + + for (const aufnahme of AUFNAHMEN) { + if (aufnahme.angemeldet && !istAngemeldet) { + await anmelden(seite); + istAngemeldet = true; + } + + await seite.goto(`${BASIS}${aufnahme.pfad}`, { waitUntil: "networkidle" }); + // Kurz warten, damit Übergänge und Symbole fertig gezeichnet sind. + await seite.waitForTimeout(400); + + const datei = path.join(ZIEL, `${aufnahme.name}-${darstellung}.png`); + await seite.screenshot({ path: datei, fullPage: true }); + console.info(`[screenshots] ${aufnahme.beschreibung} (${darstellung}) -> ${datei}`); + } + + await kontext.close(); + } + + await browser.close(); + console.info("[screenshots] Fertig."); +} + +main().catch((fehler) => { + console.error("[screenshots] Abbruch:", fehler); + process.exit(1); +}); diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx new file mode 100644 index 0000000..f70bc37 --- /dev/null +++ b/src/app/(app)/layout.tsx @@ -0,0 +1,42 @@ +import { redirect } from "next/navigation"; +import { aktuelleSitzung } from "@/lib/auth"; +import { fuerOrganisation } from "@/lib/db"; +import { rechteVon } from "@/lib/authz"; +import { Seitenleiste } from "@/components/layout/seitenleiste"; +import { ThemeToggle } from "@/components/layout/theme-toggle"; + +/** + * Layout für alles, was eine Anmeldung verlangt. Wer nicht angemeldet ist, + * landet auf der Anmeldeseite — geprüft wird serverseitig, nicht im Browser. + */ +export default async function AngemeldetLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + const sitzung = await aktuelleSitzung(); + if (!sitzung) redirect("/anmelden"); + + const organisation = await fuerOrganisation(sitzung.organisationId, (db) => + db.organisation.findUnique({ + where: { id: sitzung.organisationId }, + select: { name: true, kurzname: true }, + }), + ); + + return ( +
+ + +
+
+ +
+
{children}
+
+
+ ); +} diff --git a/src/app/(app)/page.tsx b/src/app/(app)/page.tsx new file mode 100644 index 0000000..bb3767a --- /dev/null +++ b/src/app/(app)/page.tsx @@ -0,0 +1,121 @@ +import { AlertTriangle, Truck, Users, Wrench } from "lucide-react"; +import { aktuelleSitzung } from "@/lib/auth"; +import { fuerOrganisation } from "@/lib/db"; +import { ROLLEN_BEZEICHNUNG } from "@/lib/authz"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { alsDatumZeit } from "@/lib/utils"; + +export const metadata = { title: "Übersicht" }; + +/** Startseite nach der Anmeldung. */ +export default async function UebersichtSeite() { + const sitzung = await aktuelleSitzung(); + if (!sitzung) return null; + + const daten = await fuerOrganisation(sitzung.organisationId, async (db) => { + const [organisation, mitglieder, fahrzeuge, geraete, pruefungFaellig, gesperrt] = + await Promise.all([ + db.organisation.findUnique({ where: { id: sitzung.organisationId } }), + db.person.count({ where: { aktiv: true, geloeschtAm: null } }), + db.fahrzeug.count({ where: { aktiv: true, geloeschtAm: null } }), + db.geraet.count({ where: { geloeschtAm: null } }), + db.geraet.count({ where: { status: "PRUEFUNG_FAELLIG", geloeschtAm: null } }), + db.geraet.count({ where: { status: "GESPERRT", geloeschtAm: null } }), + ]); + return { organisation, mitglieder, fahrzeuge, geraete, pruefungFaellig, gesperrt }; + }); + + const kacheln = [ + { titel: "Aktive Mitglieder", wert: daten.mitglieder, symbol: Users }, + { titel: "Fahrzeuge", wert: daten.fahrzeuge, symbol: Truck }, + { titel: "Geräte", wert: daten.geraete, symbol: Wrench }, + { titel: "Prüfung fällig", wert: daten.pruefungFaellig, symbol: AlertTriangle }, + ]; + + return ( +
+
+

+ {daten.organisation?.name ?? "Feuerwehr"} +

+

+ Angemeldet als {sitzung.name} —{" "} + {sitzung.rollen.map((rolle) => ROLLEN_BEZEICHNUNG[rolle]).join(", ")} +

+
+ + {/* + Sicherheitshinweis nach den Projektleitplanken. Er bleibt stehen, bis + die Wehr die Anwendung ausdrücklich freigegeben hat. + */} +
+ +
+

Diese Installation ist noch nicht freigegeben.

+

+ Atemschutzüberwachung und Alarmmonitor sind sicherheitsrelevant. Solange die + Wehr das System nicht ausdrücklich freigegeben hat, bleibt die analoge + Überwachung führend. FLORIAN ersetzt sie nicht. +

+
+
+ +
+ {kacheln.map((kachel) => { + const Symbol = kachel.symbol; + return ( + + + + {kachel.titel} + + + + +

{kachel.wert}

+
+
+ ); + })} +
+ + + + Stand des Aufbaus + + Phase 0 ist abgeschlossen: Fundament, Mandantentrennung, Anmeldung, + Dateiablage und Protokollierung stehen. Die Fachmodule kommen Phase für + Phase dazu. + + + +
+ Phase 0 — Fundament + Phase 1 — Stammdaten + Phase 2 — Wartung und Prüfung + Phase 3 — Einsätze + Phase 4 — Terminal + Phase 5 — Atemschutz +
+
+
+
Gesperrte Geräte
+
{daten.gesperrt}
+
+
+
Zeitpunkt
+
{alsDatumZeit(new Date())}
+
+
+
+
+
+ ); +} diff --git a/src/app/anmelden/formular.tsx b/src/app/anmelden/formular.tsx new file mode 100644 index 0000000..fa4ffd2 --- /dev/null +++ b/src/app/anmelden/formular.tsx @@ -0,0 +1,151 @@ +"use client"; + +import * as React from "react"; +import { signIn } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { KeyRound, LogIn } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export interface AnmeldeFormularProps { + oidcVerfuegbar: boolean; + oidcName: string; + lokalVerfuegbar: boolean; + fehler?: string; + weiterZu?: string; +} + +/** Wandelt die Fehlerkennungen von Auth.js in verständliches Deutsch. */ +function fehlertext(kennung?: string): string | null { + if (!kennung) return null; + switch (kennung) { + case "CredentialsSignin": + return "E-Mail-Adresse oder Passwort stimmt nicht. Nach mehreren Fehlversuchen wird das Konto vorübergehend gesperrt."; + case "AccessDenied": + return "Für dieses Konto gibt es in FLORIAN keinen Zugang. Bitte beim Kommandanten melden."; + case "OAuthAccountNotLinked": + return "Dieses Konto ist bereits mit einem anderen Anmeldeweg verknüpft."; + case "Configuration": + return "Die Anmeldung ist nicht vollständig eingerichtet. Bitte in der Serverkonfiguration prüfen."; + default: + return "Die Anmeldung ist fehlgeschlagen. Bitte erneut versuchen."; + } +} + +export function AnmeldeFormular({ + oidcVerfuegbar, + oidcName, + lokalVerfuegbar, + fehler, + weiterZu, +}: AnmeldeFormularProps) { + const router = useRouter(); + const [laeuft, setLaeuft] = React.useState(false); + const [meldung, setMeldung] = React.useState(fehlertext(fehler)); + + async function lokalAnmelden(ereignis: React.FormEvent) { + ereignis.preventDefault(); + setMeldung(null); + setLaeuft(true); + + const daten = new FormData(ereignis.currentTarget); + const ergebnis = await signIn("passwort", { + email: String(daten.get("email") ?? ""), + passwort: String(daten.get("passwort") ?? ""), + redirect: false, + }); + + setLaeuft(false); + + if (ergebnis?.error) { + setMeldung(fehlertext("CredentialsSignin")); + return; + } + router.push(weiterZu ?? "/"); + router.refresh(); + } + + return ( +
+ {meldung && ( +

+ {meldung} +

+ )} + + {oidcVerfuegbar && ( + + )} + + {oidcVerfuegbar && lokalVerfuegbar && ( +
+ + + oder + + +
+ )} + + {lokalVerfuegbar && ( +
+
+ + +
+ +
+ + +
+ + +
+ )} + + {!oidcVerfuegbar && !lokalVerfuegbar && ( +

+ Es ist kein Anmeldeweg eingerichtet. Bitte in der Serverkonfiguration + AUTH_LOCAL_ENABLED oder AUTH_OIDC_ENABLED setzen. +

+ )} + +

+ Am Terminal im Gerätehaus wird stattdessen mit PIN oder Ausweiskarte + angemeldet. +

+
+ ); +} diff --git a/src/app/anmelden/page.tsx b/src/app/anmelden/page.tsx new file mode 100644 index 0000000..8669b90 --- /dev/null +++ b/src/app/anmelden/page.tsx @@ -0,0 +1,53 @@ +import { redirect } from "next/navigation"; +import { Flame } from "lucide-react"; +import { aktuelleSitzung } from "@/lib/auth"; +import { env, oidcVerfuegbar } from "@/lib/env"; +import { Card, CardContent, CardDescription, CardHeader } from "@/components/ui/card"; +import { AnmeldeFormular } from "@/app/anmelden/formular"; + +export const metadata = { title: "Anmelden" }; + +/** + * Anmeldeseite. Zeigt genau die Wege, die in dieser Installation eingerichtet + * sind: OIDC gegen authentik, lokaler Login, oder beides. + */ +export default async function AnmeldenSeite({ + searchParams, +}: { + searchParams: Promise<{ error?: string; callbackUrl?: string }>; +}) { + const sitzung = await aktuelleSitzung(); + if (sitzung) redirect("/"); + + const { error, callbackUrl } = await searchParams; + + return ( +
+ + +
+ +
+ {/* Echte h1: die Seite braucht eine Ueberschrift, die + Hilfsmittel und Tests ansteuern koennen. */} +

+ FLORIAN +

+ Feuerwehrverwaltung +
+
+
+ + + + +
+
+ ); +} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..6fd0f23 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,8 @@ +import { handlers } from "@/lib/auth"; + +/** + * Auth.js-Endpunkte. Die Rückleitung von authentik landet hier unter + * /api/auth/callback/authentik — genau diese Adresse muss im authentik-Provider + * als Redirect-URI eingetragen sein. + */ +export const { GET, POST } = handlers; diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..338a592 --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import { datenbankErreichbar } from "@/lib/db"; +import { speicherErreichbar } from "@/lib/storage"; +import { warteschlangeErreichbar } from "@/lib/warteschlange"; + +/** + * Erreichbarkeitsprüfung für Docker, Traefik und die Überwachung. + * 200, wenn Datenbank, Objektspeicher und Warteschlange antworten — sonst 503. + * Bewusst ohne personenbezogene Angaben, damit der Endpunkt offen sein kann. + */ +export const dynamic = "force-dynamic"; +export const revalidate = 0; + +export async function GET() { + const beginn = Date.now(); + + const [datenbank, speicher, warteschlange] = await Promise.all([ + datenbankErreichbar(), + speicherErreichbar(), + warteschlangeErreichbar(), + ]); + + const gesund = datenbank && speicher && warteschlange; + + return NextResponse.json( + { + status: gesund ? "ok" : "gestoert", + dienste: { datenbank, speicher, warteschlange }, + dauerMs: Date.now() - beginn, + zeitpunkt: new Date().toISOString(), + }, + { + status: gesund ? 200 : 503, + headers: { "Cache-Control": "no-store" }, + }, + ); +} diff --git a/src/app/api/trpc/[trpc]/route.ts b/src/app/api/trpc/[trpc]/route.ts new file mode 100644 index 0000000..79add83 --- /dev/null +++ b/src/app/api/trpc/[trpc]/route.ts @@ -0,0 +1,22 @@ +import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; +import { appRouter } from "@/server/routers/_app"; +import { kontextErzeugen } from "@/server/context"; +import { env } from "@/lib/env"; + +/** Einstiegspunkt für alle internen tRPC-Aufrufe. */ +function verarbeiten(anfrage: Request) { + return fetchRequestHandler({ + endpoint: "/api/trpc", + req: anfrage, + router: appRouter, + createContext: () => kontextErzeugen({ req: anfrage }), + onError: + env.NODE_ENV === "development" + ? ({ path, error }) => { + console.error(`[trpc] Fehler in ${path ?? ""}:`, error.message); + } + : undefined, + }); +} + +export { verarbeiten as GET, verarbeiten as POST }; diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..c143f41 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,170 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +/* + Farbwelt FLORIAN. + Grundton ist das Rot der Feuerwehr, bewusst entsättigt — eine Oberfläche, in + der jemand acht Stunden Prüfungen dokumentiert, darf nicht schreien. Signalrot + bleibt für das reserviert, was wirklich Alarm ist: gesperrte Geräte, + überzogene Fristen, laufende Atemschutzüberwachung. + + Keine Google Fonts, keine externen CDNs — es werden ausschließlich Schriften + benutzt, die auf dem Gerät vorhanden sind. +*/ + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + + /* Statusfarben der Gerätezustände — überall dieselben. */ + --color-einsatzbereit: var(--einsatzbereit); + --color-pruefung-faellig: var(--pruefung-faellig); + --color-gesperrt: var(--gesperrt); + --color-ausgesondert: var(--ausgesondert); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + + --font-sans: "Inter", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", sans-serif; + --font-mono: "JetBrains Mono", "Cascadia Code", Consolas, "Courier New", monospace; +} + +:root { + --radius: 0.625rem; + + --background: oklch(0.99 0.002 60); + --foreground: oklch(0.21 0.01 40); + --card: oklch(1 0 0); + --card-foreground: oklch(0.21 0.01 40); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.21 0.01 40); + + /* Feuerwehrrot, für Flächen nutzbar gedämpft. */ + --primary: oklch(0.52 0.19 27); + --primary-foreground: oklch(0.99 0.005 60); + + --secondary: oklch(0.96 0.005 60); + --secondary-foreground: oklch(0.28 0.015 40); + --muted: oklch(0.96 0.005 60); + --muted-foreground: oklch(0.53 0.012 45); + --accent: oklch(0.95 0.012 40); + --accent-foreground: oklch(0.28 0.015 40); + + --destructive: oklch(0.58 0.23 27); + --destructive-foreground: oklch(0.99 0.005 60); + + --border: oklch(0.91 0.005 60); + --input: oklch(0.91 0.005 60); + --ring: oklch(0.52 0.19 27); + + --einsatzbereit: oklch(0.62 0.15 150); + --pruefung-faellig: oklch(0.72 0.16 75); + --gesperrt: oklch(0.58 0.23 27); + --ausgesondert: oklch(0.6 0.01 60); +} + +/* + Dunkle Darstellung. Nicht bloß invertiert: im Gerätehaus wird abends und + nachts gearbeitet, deshalb ein warmes, kontrastarmes Grau statt Schwarz — + das blendet nicht, wenn man vom Fahrzeug hereinkommt. +*/ +.dark { + --background: oklch(0.17 0.008 40); + --foreground: oklch(0.94 0.004 60); + --card: oklch(0.21 0.009 40); + --card-foreground: oklch(0.94 0.004 60); + --popover: oklch(0.21 0.009 40); + --popover-foreground: oklch(0.94 0.004 60); + + --primary: oklch(0.63 0.19 27); + --primary-foreground: oklch(0.98 0.005 60); + + --secondary: oklch(0.26 0.01 40); + --secondary-foreground: oklch(0.94 0.004 60); + --muted: oklch(0.26 0.01 40); + --muted-foreground: oklch(0.68 0.01 50); + --accent: oklch(0.29 0.012 40); + --accent-foreground: oklch(0.94 0.004 60); + + --destructive: oklch(0.65 0.21 27); + --destructive-foreground: oklch(0.98 0.005 60); + + --border: oklch(0.29 0.01 40); + --input: oklch(0.29 0.01 40); + --ring: oklch(0.63 0.19 27); + + --einsatzbereit: oklch(0.68 0.15 150); + --pruefung-faellig: oklch(0.78 0.15 75); + --gesperrt: oklch(0.65 0.21 27); + --ausgesondert: oklch(0.55 0.01 60); +} + +@layer base { + * { + border-color: var(--border); + } + + body { + background-color: var(--background); + color: var(--foreground); + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + } + + /* Tastaturbedienung muss sichtbar sein — am Terminal wird auch getabbt. */ + :focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; + } + + /* Zahlen in Tabellen sollen untereinander stehen. */ + table, + .tabellenzahlen { + font-variant-numeric: tabular-nums; + } +} + +/* Am Kiosk-Terminal wird mit dem Finger bedient: größere Ziele, keine Auswahl. */ +.kiosk { + --radius: 0.875rem; + user-select: none; +} + +.kiosk button, +.kiosk [role="button"] { + min-height: 3.5rem; + font-size: 1.125rem; +} + +@media print { + .nicht-drucken { + display: none !important; + } + + body { + background: white; + color: black; + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..5537171 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,45 @@ +import type { Metadata, Viewport } from "next"; +import "./globals.css"; +import { ThemeProvider } from "@/components/layout/theme-provider"; + +export const metadata: Metadata = { + title: { + default: "FLORIAN — Feuerwehrverwaltung", + template: "%s · FLORIAN", + }, + description: + "Verwaltungsplattform für Freiwillige Feuerwehren: Mitglieder, Fahrzeuge, Geräte, Prüfungen, Atemschutz, Einsätze.", + applicationName: "FLORIAN", + // Keine Suchmaschinen: die Installation steht im Gerätehaus, nicht im Netz. + robots: { index: false, follow: false }, +}; + +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + // Am Terminal soll nicht versehentlich gezoomt werden. + maximumScale: 5, + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "#ffffff" }, + { media: "(prefers-color-scheme: dark)", color: "#1c1a19" }, + ], +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + + + {children} + + + + ); +} diff --git a/src/components/layout/seitenleiste.tsx b/src/components/layout/seitenleiste.tsx new file mode 100644 index 0000000..c622fa8 --- /dev/null +++ b/src/components/layout/seitenleiste.tsx @@ -0,0 +1,216 @@ +"use client"; + +import * as React from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { + Album, + BookOpenCheck, + CalendarDays, + ClipboardCheck, + Flame, + Gauge, + Map, + Menu, + MonitorSmartphone, + Siren, + Truck, + Users, + Wrench, + X, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import type { Recht, Rolle } from "@/lib/authz"; + +/** + * Hauptnavigation. + * + * Welche Einträge jemand sieht, entscheidet NICHT diese Komponente. Sie bekommt + * die Rechte der Sitzung übergeben und blendet danach aus — die Prüfung selbst + * bleibt in lib/authz.ts, wie es die Projektregeln verlangen. Ein ausgeblendeter + * Eintrag ist Bequemlichkeit, kein Schutz; der Schutz sitzt in der tRPC-Schicht. + * + * Die Phasenangaben entsprechen den Dateien Phase-0.md bis Phase-8.md. + */ + +interface Eintrag { + titel: string; + pfad: string; + symbol: React.ComponentType<{ className?: string }>; + /** Sichtbar, wenn eines dieser Rechte vorliegt. Leer = für alle. */ + rechte?: Recht[]; + /** Ab welcher Phase es die Ansicht wirklich gibt. */ + abPhase?: number; +} + +const EINTRAEGE: Eintrag[] = [ + { titel: "Übersicht", pfad: "/", symbol: Gauge }, + { titel: "Mitglieder", pfad: "/mitglieder", symbol: Users, rechte: ["person.lesen"], abPhase: 1 }, + { titel: "Fahrzeuge", pfad: "/fahrzeuge", symbol: Truck, rechte: ["fahrzeug.lesen"], abPhase: 1 }, + { titel: "Geräte", pfad: "/geraete", symbol: Wrench, rechte: ["geraet.lesen"], abPhase: 1 }, + { + titel: "Qualifikationen", + pfad: "/qualifikationen", + symbol: BookOpenCheck, + rechte: ["qualifikation.lesen"], + abPhase: 1, + }, + { + titel: "Prüfungen", + pfad: "/pruefungen", + symbol: ClipboardCheck, + rechte: ["pruefung.lesen"], + abPhase: 2, + }, + { titel: "Einsätze", pfad: "/einsaetze", symbol: Flame, rechte: ["einsatz.lesen"], abPhase: 3 }, + { + titel: "Terminal", + pfad: "/kiosk", + symbol: MonitorSmartphone, + rechte: ["kiosk.erfassen"], + abPhase: 4, + }, + { + titel: "Atemschutz", + pfad: "/atemschutz", + symbol: Siren, + rechte: ["atemschutz.lesen"], + abPhase: 5, + }, + { titel: "Karte", pfad: "/karte", symbol: Map, rechte: ["einsatz.lesen"], abPhase: 6 }, + { + titel: "Termine", + pfad: "/termine", + symbol: CalendarDays, + rechte: ["ausbildung.lesen"], + abPhase: 7, + }, + { + titel: "Protokoll", + pfad: "/protokoll", + symbol: Album, + rechte: ["system.auditlog"], + abPhase: 1, + }, +]; + +/** In Phase 0 gibt es nur die Übersicht — alles andere ist noch Ausblick. */ +const AKTUELLE_PHASE = 0; + +export interface SeitenleisteProps { + rechte: Recht[]; + rollen: Rolle[]; + organisationName: string; + personName: string; +} + +export function Seitenleiste({ rechte, organisationName, personName }: SeitenleisteProps) { + const pfad = usePathname(); + const [offen, setOffen] = React.useState(false); + const rechteMenge = React.useMemo(() => new Set(rechte), [rechte]); + + const sichtbar = EINTRAEGE.filter( + (eintrag) => !eintrag.rechte || eintrag.rechte.some((recht) => rechteMenge.has(recht)), + ); + + return ( + <> + {/* Auf schmalen Geräten aufklappbar. */} + + + + + {offen && ( + + ); +} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..7839898 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,41 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +/** + * Statusabzeichen. Die Gerätezustände haben feste Farben, die überall gleich + * bedeuten — grün einsatzbereit, gelb Prüfung fällig, rot gesperrt, grau + * ausgesondert. Wer die Farbe kennt, muss den Text nicht lesen. + */ +const badgeVarianten = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors", + { + variants: { + variant: { + default: + "border-transparent bg-[var(--primary)] text-[var(--primary-foreground)]", + secondary: + "border-transparent bg-[var(--secondary)] text-[var(--secondary-foreground)]", + outline: "text-[var(--foreground)] border-[var(--border)]", + einsatzbereit: + "border-transparent bg-[var(--einsatzbereit)]/15 text-[var(--einsatzbereit)]", + pruefungFaellig: + "border-transparent bg-[var(--pruefung-faellig)]/20 text-[var(--pruefung-faellig)]", + gesperrt: "border-transparent bg-[var(--gesperrt)]/15 text-[var(--gesperrt)]", + ausgesondert: + "border-transparent bg-[var(--ausgesondert)]/15 text-[var(--ausgesondert)]", + }, + }, + defaultVariants: { variant: "default" }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return
; +} + +export { Badge, badgeVarianten }; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..b6ccdb0 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,55 @@ +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 buttonVarianten = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--background)] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-[var(--primary)] text-[var(--primary-foreground)] hover:opacity-90 shadow-sm", + destructive: + "bg-[var(--destructive)] text-[var(--destructive-foreground)] hover:opacity-90 shadow-sm", + outline: + "border border-[var(--border)] bg-[var(--background)] hover:bg-[var(--accent)] hover:text-[var(--accent-foreground)]", + secondary: + "bg-[var(--secondary)] text-[var(--secondary-foreground)] hover:opacity-80", + ghost: "hover:bg-[var(--accent)] hover:text-[var(--accent-foreground)]", + link: "text-[var(--primary)] underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-11 rounded-md px-8", + /** Für das Terminal im Gerätehaus — mit Handschuhen bedienbar. */ + kiosk: "h-16 rounded-xl px-8 text-lg", + icon: "h-9 w-9", + }, + }, + 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 Komponente = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { Button, buttonVarianten }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..96efaf7 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,61 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardFooter.displayName = "CardFooter"; + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..8e96cac --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => ( + + ), +); +Input.displayName = "Input"; + +export { Input }; diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..7e93689 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,22 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cn } from "@/lib/utils"; + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/src/lib/audit.ts b/src/lib/audit.ts new file mode 100644 index 0000000..c79189c --- /dev/null +++ b/src/lib/audit.ts @@ -0,0 +1,238 @@ +import "server-only"; +import { AkteurTyp, AnmeldeArt, AuditAktion, Prisma } from "@prisma/client"; +import { fuerOrganisation, prismaSystem, type MandantenClient } from "@/lib/db"; +import type { Sitzung } from "@/lib/authz"; + +/** + * Audit-Log. + * + * Regel aus CLAUDE.md: Jede schreibende Operation erzeugt einen Eintrag mit + * Akteur, Zeitpunkt, Entität und Vorher/Nachher als JSONB. Die Tabelle ist + * append-only — auf Datenbankebene per Trigger abgesichert, nicht nur hier. + * + * Zwei Aufrufarten: + * protokolliere(...) — eigene Transaktion, für Einzelaktionen + * protokolliereIn(db, ...) — innerhalb einer bestehenden Transaktion, damit + * Änderung und Protokoll gemeinsam gültig werden + * oder gemeinsam zurückgerollt. + * + * Der zweite Weg ist der richtige, sobald es um Daten geht, die wirklich + * zusammengehören. + */ + +export { AuditAktion, AkteurTyp, AnmeldeArt }; + +/** Felder, die niemals ins Protokoll wandern. */ +const GEHEIME_FELDER = new Set([ + "passwortHash", + "passwort", + "tokenHash", + "pin", + "secret", + "clientSecret", + "accessToken", + "refreshToken", +]); + +/** + * Entfernt Geheimnisse und wandelt Werte, die JSONB nicht kennt (Date, BigInt), + * in etwas Speicherbares um. + */ +export function fuerProtokoll(wert: unknown, tiefe = 0): Prisma.InputJsonValue | null { + if (wert === null || wert === undefined) return null; + if (tiefe > 6) return "[zu tief verschachtelt]"; + + if (wert instanceof Date) return wert.toISOString(); + if (typeof wert === "bigint") return wert.toString(); + if (typeof wert === "string" || typeof wert === "number" || typeof wert === "boolean") { + return wert; + } + + if (Array.isArray(wert)) { + return wert.map((eintrag) => fuerProtokoll(eintrag, tiefe + 1) ?? null); + } + + if (typeof wert === "object") { + const ergebnis: Record = {}; + for (const [schluessel, inhalt] of Object.entries(wert as Record)) { + ergebnis[schluessel] = GEHEIME_FELDER.has(schluessel) + ? "[entfernt]" + : fuerProtokoll(inhalt, tiefe + 1); + } + return ergebnis as Prisma.InputJsonValue; + } + + return String(wert); +} + +/** + * Nur die tatsächlich geänderten Felder — sonst bläht jeder Speichervorgang das + * Protokoll mit unveränderten Werten auf. + */ +export function unterschied( + vorher: Record | null | undefined, + nachher: Record | null | undefined, +): { vorher: Prisma.InputJsonValue | null; nachher: Prisma.InputJsonValue | null; felder: string[] } { + if (!vorher) return { vorher: null, nachher: fuerProtokoll(nachher), felder: Object.keys(nachher ?? {}) }; + if (!nachher) return { vorher: fuerProtokoll(vorher), nachher: null, felder: Object.keys(vorher) }; + + const felder: string[] = []; + const vorherGefiltert: Record = {}; + const nachherGefiltert: Record = {}; + + for (const schluessel of new Set([...Object.keys(vorher), ...Object.keys(nachher)])) { + const alt = vorher[schluessel]; + const neu = nachher[schluessel]; + if (JSON.stringify(alt) === JSON.stringify(neu)) continue; + felder.push(schluessel); + vorherGefiltert[schluessel] = alt; + nachherGefiltert[schluessel] = neu; + } + + return { + vorher: fuerProtokoll(vorherGefiltert), + nachher: fuerProtokoll(nachherGefiltert), + felder, + }; +} + +export interface ProtokollEingabe { + organisationId: string; + aktion: AuditAktion; + /** Modellname, z. B. "Geraet". */ + entitaet: string; + entitaetId?: string | null; + beschreibung?: string; + + /** Wer war es. Entweder eine Sitzung übergeben oder Akteur einzeln setzen. */ + sitzung?: Sitzung | null; + akteurTyp?: AkteurTyp; + akteurPersonId?: string | null; + akteurName?: string | null; + anmeldeArt?: AnmeldeArt | null; + + vorher?: Record | null; + nachher?: Record | null; + kontext?: Record | null; + + ipAdresse?: string | null; + userAgent?: string | null; +} + +function datensatzBauen(eingabe: ProtokollEingabe): Prisma.AuditLogUncheckedCreateInput { + const abweichung = unterschied(eingabe.vorher, eingabe.nachher); + + const kontext = { + ...(eingabe.kontext ?? {}), + ...(abweichung.felder.length > 0 ? { geaenderteFelder: abweichung.felder } : {}), + }; + + return { + organisationId: eingabe.organisationId, + aktion: eingabe.aktion, + entitaet: eingabe.entitaet, + entitaetId: eingabe.entitaetId ?? null, + beschreibung: eingabe.beschreibung ?? null, + akteurTyp: eingabe.akteurTyp ?? (eingabe.sitzung ? AkteurTyp.PERSON : AkteurTyp.SYSTEM), + akteurPersonId: eingabe.akteurPersonId ?? eingabe.sitzung?.personId ?? null, + akteurName: eingabe.akteurName ?? eingabe.sitzung?.name ?? null, + anmeldeArt: + eingabe.anmeldeArt ?? + (eingabe.sitzung ? (eingabe.sitzung.anmeldeArt as AnmeldeArt) : null), + vorher: abweichung.vorher ?? Prisma.JsonNull, + nachher: abweichung.nachher ?? Prisma.JsonNull, + kontext: Object.keys(kontext).length > 0 ? (fuerProtokoll(kontext) as Prisma.InputJsonValue) : Prisma.JsonNull, + ipAdresse: eingabe.ipAdresse ?? null, + userAgent: eingabe.userAgent?.slice(0, 400) ?? null, + }; +} + +/** + * Schreibt den Eintrag in eine BESTEHENDE Transaktion. So werden Änderung und + * Protokoll gemeinsam wirksam. + * + * @example + * await fuerOrganisation(orgId, async (db) => { + * const vorher = await db.geraet.findUniqueOrThrow({ where: { id } }); + * const nachher = await db.geraet.update({ where: { id }, data }); + * await protokolliereIn(db, { + * organisationId: orgId, aktion: AuditAktion.GEAENDERT, + * entitaet: "Geraet", entitaetId: id, sitzung, vorher, nachher, + * }); + * }); + */ +export async function protokolliereIn( + db: MandantenClient, + eingabe: ProtokollEingabe, +): Promise { + await db.auditLog.create({ data: datensatzBauen(eingabe) }); +} + +/** + * Schreibt den Eintrag in einer eigenen Transaktion. Für Vorgänge ohne + * begleitende Datenänderung — Anmeldungen, Exporte, Dateiabrufe. + */ +export async function protokolliere(eingabe: ProtokollEingabe): Promise { + await fuerOrganisation(eingabe.organisationId, async (db) => { + await protokolliereIn(db, eingabe); + }); +} + +/** + * Anmeldevorgänge. Die laufen, bevor ein Mandantenkontext feststeht, deshalb + * schreibt diese Funktion ausnahmsweise über den Eigentümer-Zugang. + * Fehlschläge dürfen den Anmeldeweg nicht blockieren — deshalb nur geloggt. + */ +export async function protokolliereAnmeldung(eingabe: { + organisationId: string; + personId?: string | null; + name?: string | null; + anmeldeArt: AnmeldeArt; + erfolgreich: boolean; + grund?: string; + ipAdresse?: string | null; + userAgent?: string | null; +}): Promise { + try { + await prismaSystem.auditLog.create({ + data: datensatzBauen({ + organisationId: eingabe.organisationId, + aktion: eingabe.erfolgreich + ? AuditAktion.ANGEMELDET + : AuditAktion.ANMELDUNG_FEHLGESCHLAGEN, + entitaet: "Person", + entitaetId: eingabe.personId ?? null, + akteurTyp: eingabe.anmeldeArt === AnmeldeArt.SYSTEM ? AkteurTyp.SYSTEM : AkteurTyp.PERSON, + akteurPersonId: eingabe.personId ?? null, + akteurName: eingabe.name ?? null, + anmeldeArt: eingabe.anmeldeArt, + beschreibung: eingabe.erfolgreich + ? `Anmeldung über ${eingabe.anmeldeArt}` + : `Anmeldung fehlgeschlagen: ${eingabe.grund ?? "unbekannt"}`, + ipAdresse: eingabe.ipAdresse, + userAgent: eingabe.userAgent, + }), + }); + } catch (fehler) { + console.error("[audit] Anmeldung konnte nicht protokolliert werden:", fehler); + } +} + +/** Die letzten Einträge zu einem Datensatz — für die Verlaufsansicht. */ +export async function verlauf( + organisationId: string, + entitaet: string, + entitaetId: string, + anzahl = 50, +) { + return fuerOrganisation(organisationId, (db) => + db.auditLog.findMany({ + where: { entitaet, entitaetId }, + orderBy: { zeitpunkt: "desc" }, + take: anzahl, + include: { + akteur: { select: { id: true, vorname: true, nachname: true } }, + }, + }), + ); +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..6f7f332 --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,613 @@ +import NextAuth, { type NextAuthConfig } from "next-auth"; +import Credentials from "next-auth/providers/credentials"; +import type { Provider } from "next-auth/providers"; +import { AnmeldeArt, type RolleSchluessel } from "@prisma/client"; +import { z } from "zod"; +import { env, oidcGruppenZuordnung, oidcVerfuegbar } from "@/lib/env"; +import { prismaSystem, fuerOrganisation } from "@/lib/db"; +import { pruefen } from "@/lib/passwort"; +import { rfidNormalisieren } from "@/lib/passwort"; +import { protokolliereAnmeldung } from "@/lib/audit"; +import { alsRolle, type Rolle, type Sitzung } from "@/lib/authz"; + +/** + * Anmeldung — drei Wege, ein Sitzungsbegriff. + * + * 1. OIDC gegen authentik — der Regelweg im laufenden Betrieb. + * 2. E-Mail und Passwort — Rückfalloption für den Erststart und für den + * Fall, dass authentik gerade nicht erreichbar ist. + * 3. Terminal (PIN/RFID) — für das Kiosk-Gerät im Gerätehaus. + * + * Alle drei führen zu derselben Sitzung (siehe lib/authz.ts). Die Sitzung liegt + * als signiertes JWT im Cookie — keine Sitzungstabelle, damit ein Neustart der + * Anwendung niemanden abmeldet. + * + * Die Rollen stehen im Token. Damit eine Rollenänderung nicht erst beim + * nächsten Login greift, werden sie bei jedem Zugriff nachgeladen, wenn das + * Token älter als fünf Minuten ist. + */ + +const ROLLEN_NACHLADEN_NACH_MS = 5 * 60 * 1000; + +// ----------------------------------------------------------------------------- +// Hilfsfunktionen +// ----------------------------------------------------------------------------- + +/** Lädt Person samt Rollen. Läuft vor der Mandantenauflösung, daher ohne RLS. */ +async function personMitRollen(personId: string) { + return prismaSystem.person.findFirst({ + where: { id: personId, aktiv: true, geloeschtAm: null }, + include: { + rollen: { + where: { OR: [{ gueltigBis: null }, { gueltigBis: { gt: new Date() } }] }, + include: { rolle: true }, + }, + organisation: { select: { id: true, slug: true, name: true, aktiv: true } }, + }, + }); +} + +function rollenAus( + eintraege: Array<{ rolle: { schluessel: RolleSchluessel } }>, +): Rolle[] { + const rollen = eintraege + .map((eintrag) => alsRolle(eintrag.rolle.schluessel)) + .filter((rolle): rolle is Rolle => rolle !== null); + // Wer gar keine Rolle hat, ist trotzdem Mitglied — sonst sieht er nicht + // einmal die eigenen Daten. + return rollen.length > 0 ? rollen : ["MITGLIED"]; +} + +/** Ist das Konto gerade wegen zu vieler Fehlversuche gesperrt? */ +function istGesperrt(person: { gesperrtBis: Date | null }): boolean { + return person.gesperrtBis !== null && person.gesperrtBis > new Date(); +} + +/** Fehlversuch zählen und ab der Grenze sperren. */ +async function fehlversuchVermerken(personId: string): Promise { + const person = await prismaSystem.person.findUnique({ + where: { id: personId }, + select: { fehlversuche: true }, + }); + const neu = (person?.fehlversuche ?? 0) + 1; + await prismaSystem.person.update({ + where: { id: personId }, + data: { + fehlversuche: neu, + gesperrtBis: + neu >= env.KIOSK_MAX_FEHLVERSUCHE + ? new Date(Date.now() + env.KIOSK_SPERRDAUER * 1000) + : null, + }, + }); +} + +async function anmeldungVermerken(personId: string): Promise { + await prismaSystem.person.update({ + where: { id: personId }, + data: { letzteAnmeldung: new Date(), fehlversuche: 0, gesperrtBis: null }, + }); +} + +// ----------------------------------------------------------------------------- +// Anbieter +// ----------------------------------------------------------------------------- + +const lokaleAnmeldung = z.object({ + email: z.string().email("Bitte eine gültige E-Mail-Adresse eingeben."), + passwort: z.string().min(1, "Bitte das Passwort eingeben."), +}); + +const terminalAnmeldung = z.object({ + geraeteSchluessel: z.string().min(1), + organisationSlug: z.string().min(1), + art: z.enum(["PIN", "RFID"]), + /** Bei PIN zusätzlich die Mitgliedsnummer, bei RFID nur die Kennung. */ + kennung: z.string().min(1), + geheimnis: z.string().min(1), +}); + +/** E-Mail und Passwort — der Rückfallweg. */ +const passwortAnbieter = Credentials({ + id: "passwort", + name: "E-Mail und Passwort", + credentials: { + email: { label: "E-Mail", type: "email" }, + passwort: { label: "Passwort", type: "password" }, + }, + async authorize(eingabe) { + const geprueft = lokaleAnmeldung.safeParse(eingabe); + if (!geprueft.success) return null; + + const { email, passwort } = geprueft.data; + + // Die E-Mail-Suche geht bewusst über alle Wehren — zu diesem Zeitpunkt ist + // der Mandant noch nicht bekannt. + const person = await prismaSystem.person.findFirst({ + where: { email: email.toLowerCase(), aktiv: true, geloeschtAm: null }, + include: { + rollen: { include: { rolle: true } }, + organisation: { select: { id: true, aktiv: true } }, + }, + }); + + if (!person || !person.organisation.aktiv) { + // Trotzdem hashen, damit "Konto gibt es nicht" zeitlich nicht auffällt. + await pruefen(passwort, null); + return null; + } + + if (istGesperrt(person)) { + await protokolliereAnmeldung({ + organisationId: person.organisationId, + personId: person.id, + name: `${person.vorname} ${person.nachname}`, + anmeldeArt: AnmeldeArt.LOKAL, + erfolgreich: false, + grund: "Konto vorübergehend gesperrt", + }); + return null; + } + + const stimmt = await pruefen(passwort, person.passwortHash); + if (!stimmt) { + await fehlversuchVermerken(person.id); + await protokolliereAnmeldung({ + organisationId: person.organisationId, + personId: person.id, + name: `${person.vorname} ${person.nachname}`, + anmeldeArt: AnmeldeArt.LOKAL, + erfolgreich: false, + grund: "Falsches Passwort", + }); + return null; + } + + await anmeldungVermerken(person.id); + await protokolliereAnmeldung({ + organisationId: person.organisationId, + personId: person.id, + name: `${person.vorname} ${person.nachname}`, + anmeldeArt: AnmeldeArt.LOKAL, + erfolgreich: true, + }); + + return { + id: person.id, + name: `${person.vorname} ${person.nachname}`, + email: person.email ?? undefined, + organisationId: person.organisationId, + rollen: rollenAus(person.rollen), + anmeldeArt: "LOKAL" as const, + }; + }, +}); + +/** + * Terminal im Gerätehaus. Zwei Schranken: erst weist sich das GERÄT mit + * KIOSK_DEVICE_SECRET aus, dann die PERSON mit PIN oder RFID-Karte. Ohne die + * erste Schranke nützt eine erratene PIN nichts. + */ +const terminalAnbieter = Credentials({ + id: "terminal", + name: "Terminal im Gerätehaus", + credentials: { + geraeteSchluessel: { label: "Gerät", type: "text" }, + organisationSlug: { label: "Wehr", type: "text" }, + art: { label: "Art", type: "text" }, + kennung: { label: "Kennung", type: "text" }, + geheimnis: { label: "PIN oder Kartenkennung", type: "password" }, + }, + async authorize(eingabe) { + if (!env.KIOSK_ENABLED) return null; + + const geprueft = terminalAnmeldung.safeParse(eingabe); + if (!geprueft.success) return null; + + const { geraeteSchluessel, organisationSlug, art, kennung, geheimnis } = geprueft.data; + + // Erste Schranke: Ist das ein Terminal, das wir kennen? + if (geraeteSchluessel !== env.KIOSK_DEVICE_SECRET) return null; + + const organisation = await prismaSystem.organisation.findFirst({ + where: { slug: organisationSlug, aktiv: true, geloeschtAm: null }, + select: { id: true }, + }); + if (!organisation) return null; + + // Zweite Schranke: die Person. Bei PIN identifiziert die Mitgliedsnummer, + // bei RFID die Kartenkennung. + const tokens = await fuerOrganisation(organisation.id, (db) => + db.ausweisToken.findMany({ + where: { + typ: art, + aktiv: true, + geloeschtAm: null, + gueltigAb: { lte: new Date() }, + OR: [{ gueltigBis: null }, { gueltigBis: { gt: new Date() } }], + ...(art === "PIN" + ? { person: { mitgliedsnummer: kennung, aktiv: true, geloeschtAm: null } } + : { kennzeichen: rfidNormalisieren(kennung).slice(-4) }), + }, + include: { + person: { + include: { rollen: { include: { rolle: true } } }, + }, + }, + }), + ); + + const vergleichswert = art === "RFID" ? rfidNormalisieren(geheimnis) : geheimnis; + + for (const token of tokens) { + if (token.gesperrtBis && token.gesperrtBis > new Date()) continue; + if (!(await pruefen(vergleichswert, token.tokenHash))) continue; + + await fuerOrganisation(organisation.id, (db) => + db.ausweisToken.update({ + where: { id: token.id }, + data: { letzteNutzung: new Date(), fehlversuche: 0, gesperrtBis: null }, + }), + ); + + const anmeldeArt = art === "PIN" ? AnmeldeArt.KIOSK_PIN : AnmeldeArt.KIOSK_RFID; + await protokolliereAnmeldung({ + organisationId: organisation.id, + personId: token.person.id, + name: `${token.person.vorname} ${token.person.nachname}`, + anmeldeArt, + erfolgreich: true, + }); + + return { + id: token.person.id, + name: `${token.person.vorname} ${token.person.nachname}`, + email: token.person.email ?? undefined, + organisationId: organisation.id, + rollen: rollenAus(token.person.rollen), + anmeldeArt: art === "PIN" ? ("KIOSK_PIN" as const) : ("KIOSK_RFID" as const), + }; + } + + // Fehlversuche werden auf allen in Frage kommenden Tokens gezählt. + await fuerOrganisation(organisation.id, async (db) => { + for (const token of tokens) { + const neu = token.fehlversuche + 1; + await db.ausweisToken.update({ + where: { id: token.id }, + data: { + fehlversuche: neu, + gesperrtBis: + neu >= env.KIOSK_MAX_FEHLVERSUCHE + ? new Date(Date.now() + env.KIOSK_SPERRDAUER * 1000) + : null, + }, + }); + } + }); + + await protokolliereAnmeldung({ + organisationId: organisation.id, + anmeldeArt: art === "PIN" ? AnmeldeArt.KIOSK_PIN : AnmeldeArt.KIOSK_RFID, + erfolgreich: false, + grund: `Terminal-Anmeldung fehlgeschlagen (${art})`, + }); + + return null; + }, +}); + +/** + * OIDC gegen authentik. Wird nur eingehängt, wenn Issuer, Client-ID und Secret + * gesetzt sind — sonst bliebe ein Knopf stehen, der ins Leere führt. + */ +function authentikAnbieter(): Provider { + return { + id: "authentik", + name: env.AUTH_OIDC_NAME, + type: "oidc", + issuer: env.AUTH_OIDC_ISSUER, + clientId: env.AUTH_OIDC_CLIENT_ID, + clientSecret: env.AUTH_OIDC_CLIENT_SECRET, + authorization: { params: { scope: "openid email profile" } }, + checks: ["pkce", "state"], + }; +} + +// ----------------------------------------------------------------------------- +// OIDC: Person finden oder anlegen, Rollen aus Gruppen vorbelegen +// ----------------------------------------------------------------------------- + +interface OidcProfil { + sub: string; + email?: string; + name?: string; + given_name?: string; + family_name?: string; + [claim: string]: unknown; +} + +/** + * Verknüpft ein authentik-Konto mit einer Person. Reihenfolge: + * 1. bereits verknüpft (oidcSubject) + * 2. E-Mail-Adresse stimmt überein -> verknüpfen + * 3. AUTH_OIDC_AUTO_PROVISION -> neue Person anlegen + * Sonst: Anmeldung abgelehnt. + */ +async function personAusOidc(profil: OidcProfil): Promise<{ + personId: string; + organisationId: string; + name: string; + rollen: Rolle[]; +} | null> { + const email = profil.email?.toLowerCase(); + + const vorhanden = await prismaSystem.person.findFirst({ + where: { + OR: [{ oidcSubject: profil.sub }, ...(email ? [{ email }] : [])], + geloeschtAm: null, + }, + include: { + rollen: { include: { rolle: true } }, + organisation: { select: { id: true, aktiv: true } }, + }, + }); + + const gruppen = gruppenAusProfil(profil); + + if (vorhanden) { + if (!vorhanden.aktiv || !vorhanden.organisation.aktiv) return null; + + await prismaSystem.person.update({ + where: { id: vorhanden.id }, + data: { + oidcSubject: profil.sub, + oidcGruppen: gruppen, + letzteAnmeldung: new Date(), + fehlversuche: 0, + gesperrtBis: null, + }, + }); + + // Gruppen wirken nur als Vorbelegung: Rollen, die jemand in FLORIAN von + // Hand vergeben hat, bleiben unangetastet. + await gruppenRollenVorbelegen(vorhanden.organisationId, vorhanden.id, gruppen); + + const aktualisiert = await personMitRollen(vorhanden.id); + if (!aktualisiert) return null; + + return { + personId: aktualisiert.id, + organisationId: aktualisiert.organisationId, + name: `${aktualisiert.vorname} ${aktualisiert.nachname}`, + rollen: rollenAus(aktualisiert.rollen), + }; + } + + if (!env.AUTH_OIDC_AUTO_PROVISION) return null; + + const slug = env.AUTH_OIDC_STANDARD_ORGANISATION; + if (!slug) { + console.error( + "[auth] AUTH_OIDC_AUTO_PROVISION=true, aber AUTH_OIDC_STANDARD_ORGANISATION fehlt.", + ); + return null; + } + + const organisation = await prismaSystem.organisation.findFirst({ + where: { slug, aktiv: true, geloeschtAm: null }, + select: { id: true }, + }); + if (!organisation) return null; + + const vorname = profil.given_name ?? profil.name?.split(" ")[0] ?? "Unbekannt"; + const nachname = + profil.family_name ?? profil.name?.split(" ").slice(1).join(" ") ?? "Unbekannt"; + + const neu = await fuerOrganisation(organisation.id, (db) => + db.person.create({ + data: { + organisationId: organisation.id, + vorname, + nachname, + email: email ?? null, + oidcSubject: profil.sub, + oidcGruppen: gruppen, + letzteAnmeldung: new Date(), + }, + }), + ); + + await gruppenRollenVorbelegen(organisation.id, neu.id, gruppen); + + const mitRollen = await personMitRollen(neu.id); + return { + personId: neu.id, + organisationId: organisation.id, + name: `${vorname} ${nachname}`, + rollen: mitRollen ? rollenAus(mitRollen.rollen) : ["MITGLIED"], + }; +} + +function gruppenAusProfil(profil: OidcProfil): string[] { + const claim = env.AUTH_OIDC_GROUP_CLAIM; + if (!claim) return []; + const wert = profil[claim]; + if (Array.isArray(wert)) return wert.map(String); + if (typeof wert === "string") return wert.split(",").map((teil) => teil.trim()); + return []; +} + +/** + * Belegt Rollen aus authentik-Gruppen vor. Bestehende Zuweisungen werden nicht + * angefasst: Was in FLORIAN von Hand gesetzt wurde, ist führend. + */ +async function gruppenRollenVorbelegen( + organisationId: string, + personId: string, + gruppen: string[], +): Promise { + if (gruppen.length === 0) return; + + const zuordnung = oidcGruppenZuordnung(); + if (zuordnung.size === 0) return; + + const gewuenscht = new Set(); + for (const gruppe of gruppen) { + const rolle = zuordnung.get(gruppe.toLowerCase()); + if (rolle) gewuenscht.add(rolle); + } + if (gewuenscht.size === 0) return; + + await fuerOrganisation(organisationId, async (db) => { + const bestehend = await db.personRolle.findMany({ + where: { personId }, + select: { rolleId: true }, + }); + // Nur beim allerersten Mal vorbelegen — danach entscheidet die Wehr. + if (bestehend.length > 0) return; + + const rollen = await db.rolle.findMany({ + where: { + organisationId, + schluessel: { in: [...gewuenscht] as RolleSchluessel[] }, + }, + select: { id: true }, + }); + + if (rollen.length === 0) return; + + await db.personRolle.createMany({ + data: rollen.map((rolle) => ({ + organisationId, + personId, + rolleId: rolle.id, + ausOidcGruppe: true, + })), + skipDuplicates: true, + }); + }); +} + +// ----------------------------------------------------------------------------- +// Auth.js-Konfiguration +// ----------------------------------------------------------------------------- + +function anbieterListe(): Provider[] { + const liste: Provider[] = []; + if (oidcVerfuegbar) liste.push(authentikAnbieter()); + if (env.AUTH_LOCAL_ENABLED) liste.push(passwortAnbieter); + if (env.KIOSK_ENABLED) liste.push(terminalAnbieter); + return liste; +} + +export const authConfig: NextAuthConfig = { + trustHost: env.AUTH_TRUST_HOST, + secret: env.AUTH_SECRET, + session: { strategy: "jwt", maxAge: env.AUTH_SESSION_MAX_AGE }, + pages: { signIn: "/anmelden", error: "/anmelden" }, + providers: anbieterListe(), + + callbacks: { + async signIn({ account, profile }) { + // Die Credentials-Anbieter haben in authorize() bereits alles geprüft. + if (account?.provider !== "authentik") return true; + if (!profile?.sub) return false; + + const ergebnis = await personAusOidc(profile as unknown as OidcProfil); + if (!ergebnis) return false; + + await protokolliereAnmeldung({ + organisationId: ergebnis.organisationId, + personId: ergebnis.personId, + name: ergebnis.name, + anmeldeArt: AnmeldeArt.OIDC, + erfolgreich: true, + }); + return true; + }, + + async jwt({ token, user, account, trigger }) { + // Erste Anmeldung über einen Credentials-Anbieter. + if (user && "organisationId" in user) { + const angemeldet = user as unknown as { + id: string; + name: string; + organisationId: string; + rollen: Rolle[]; + anmeldeArt: Sitzung["anmeldeArt"]; + }; + token.personId = angemeldet.id; + token.organisationId = angemeldet.organisationId; + token.rollen = angemeldet.rollen; + token.anmeldeArt = angemeldet.anmeldeArt; + token.geladenAm = Date.now(); + return token; + } + + // Erste Anmeldung über OIDC — hier steht das Profil im account/profile. + if (account?.provider === "authentik" && token.sub) { + const person = await prismaSystem.person.findFirst({ + where: { oidcSubject: token.sub, geloeschtAm: null }, + include: { rollen: { include: { rolle: true } } }, + }); + if (person) { + token.personId = person.id; + token.organisationId = person.organisationId; + token.rollen = rollenAus(person.rollen); + token.anmeldeArt = "OIDC"; + token.geladenAm = Date.now(); + } + return token; + } + + // Rollen nachladen, damit eine Änderung nicht bis zum nächsten Login wartet. + const veraltet = + typeof token.geladenAm !== "number" || + Date.now() - token.geladenAm > ROLLEN_NACHLADEN_NACH_MS; + + if (trigger === "update" || (veraltet && typeof token.personId === "string")) { + const person = await personMitRollen(token.personId as string); + if (!person) { + // Person deaktiviert oder gelöscht: Sitzung entwerten. + return { ...token, personId: undefined, organisationId: undefined, rollen: [] }; + } + token.rollen = rollenAus(person.rollen); + token.organisationId = person.organisationId; + token.geladenAm = Date.now(); + } + + return token; + }, + + async session({ session, token }) { + session.user = { + ...session.user, + id: (token.personId as string) ?? "", + organisationId: (token.organisationId as string) ?? "", + rollen: (token.rollen as Rolle[]) ?? [], + anmeldeArt: (token.anmeldeArt as Sitzung["anmeldeArt"]) ?? "LOKAL", + }; + return session; + }, + }, +}; + +export const { handlers, auth, signIn, signOut } = NextAuth(authConfig); + +/** + * Die Sitzung, wie der Rest der Anwendung sie erwartet. Gibt null zurück, + * wenn niemand angemeldet ist oder die Sitzung entwertet wurde. + */ +export async function aktuelleSitzung(): Promise { + const sitzung = await auth(); + if (!sitzung?.user?.id || !sitzung.user.organisationId) return null; + + return { + personId: sitzung.user.id, + organisationId: sitzung.user.organisationId, + name: sitzung.user.name ?? "Unbekannt", + rollen: sitzung.user.rollen ?? [], + anmeldeArt: sitzung.user.anmeldeArt ?? "LOKAL", + }; +} diff --git a/src/lib/authz.ts b/src/lib/authz.ts new file mode 100644 index 0000000..7eb99a1 --- /dev/null +++ b/src/lib/authz.ts @@ -0,0 +1,338 @@ +/** + * Zentrale Berechtigungslogik. + * + * Regel aus CLAUDE.md: Keine Rollenprüfung in Komponenten. Alles, was mit + * "darf jemand das?" zu tun hat, steht hier — und nur hier. + * + * Aufbau: + * Recht = ein einzelnes Können, z. B. "geraet.bearbeiten" + * Rolle = ein Bündel von Rechten, z. B. GERAETEWART + * Sitzung = wer gerade angemeldet ist, mit welchen Rollen, in welcher Wehr + * + * Die Rechte einer Rolle sind hier als Vorgabe hinterlegt. Beim Seed werden sie + * in die Tabellen berechtigung/rolle_berechtigung gespiegelt, damit eine Wehr + * sie nachträglich anpassen kann, ohne dass jemand Code ändern muss. + */ + +/** Rollen exakt wie im Prisma-Schema (enum RolleSchluessel). */ +export const ROLLEN = [ + "SUPERADMIN", + "KOMMANDANT", + "GERAETEWART", + "ATEMSCHUTZGERAETEWART", + "KASSIER", + "SCHRIFTFUEHRER", + "AUSBILDER", + "MITGLIED", + "KIOSK", +] as const; + +export type Rolle = (typeof ROLLEN)[number]; + +/** Klartext für die Oberfläche. */ +export const ROLLEN_BEZEICHNUNG: Record = { + SUPERADMIN: "Administrator", + KOMMANDANT: "Kommandant", + GERAETEWART: "Gerätewart", + ATEMSCHUTZGERAETEWART: "Atemschutzgerätewart", + KASSIER: "Kassier", + SCHRIFTFUEHRER: "Schriftführer", + AUSBILDER: "Ausbilder", + MITGLIED: "Mitglied", + KIOSK: "Terminal im Gerätehaus", +}; + +export const ROLLEN_BESCHREIBUNG: Record = { + SUPERADMIN: + "Verwaltet die Installation selbst: Wehren anlegen, Rollen zuschneiden, Systemeinstellungen.", + KOMMANDANT: + "Führt die Wehr. Sieht und bearbeitet alles innerhalb der eigenen Organisation.", + GERAETEWART: + "Verantwortet Fahrzeuge, Geräte und deren Prüfungen nach DGUV.", + ATEMSCHUTZGERAETEWART: + "Verantwortet Atemschutzgeräte, Masken und die Überwachung nach FwDV 7.", + KASSIER: "Führt die Kasse: Buchungen, Beiträge, Rechnungen, Auswertungen.", + SCHRIFTFUEHRER: + "Führt Protokolle, Berichte und die Dokumentation der Wehr.", + AUSBILDER: "Plant Übungen und Lehrgänge, führt Teilnahmenachweise.", + MITGLIED: "Sieht die eigenen Daten, Termine und Qualifikationen.", + KIOSK: + "Terminal im Gerätehaus. Darf nur anmelden, quittieren und erfassen — nie verwalten.", +}; + +// ----------------------------------------------------------------------------- +// Rechte +// ----------------------------------------------------------------------------- + +/** + * Alle Einzelrechte. Schema: . + * Neue Rechte hier ergänzen, dann `npm run db:seed` — der Seed gleicht den + * Katalog in der Datenbank ab. + */ +export const RECHTE = { + // Organisation und System + "organisation.lesen": { bereich: "Organisation", bezeichnung: "Wehrdaten ansehen" }, + "organisation.bearbeiten": { bereich: "Organisation", bezeichnung: "Wehrdaten bearbeiten" }, + "organisation.verwalten": { bereich: "Organisation", bezeichnung: "Wehren anlegen und löschen" }, + "system.einstellungen": { bereich: "System", bezeichnung: "Systemeinstellungen ändern" }, + "system.auditlog": { bereich: "System", bezeichnung: "Audit-Log einsehen" }, + + // Personen + "person.lesen": { bereich: "Personen", bezeichnung: "Mitgliederliste ansehen" }, + "person.eigene": { bereich: "Personen", bezeichnung: "Eigene Daten ansehen und pflegen" }, + "person.bearbeiten": { bereich: "Personen", bezeichnung: "Mitglieder anlegen und bearbeiten" }, + "person.loeschen": { bereich: "Personen", bezeichnung: "Mitglieder austragen" }, + "person.rollen": { bereich: "Personen", bezeichnung: "Rollen zuweisen" }, + "person.ausweis": { bereich: "Personen", bezeichnung: "PIN und RFID-Ausweise verwalten" }, + "person.export": { bereich: "Personen", bezeichnung: "Personendaten exportieren (DSGVO)" }, + + // Fahrzeuge und Geräte + "fahrzeug.lesen": { bereich: "Fahrzeuge", bezeichnung: "Fahrzeuge ansehen" }, + "fahrzeug.bearbeiten": { bereich: "Fahrzeuge", bezeichnung: "Fahrzeuge anlegen und bearbeiten" }, + "geraet.lesen": { bereich: "Geräte", bezeichnung: "Geräte ansehen" }, + "geraet.bearbeiten": { bereich: "Geräte", bezeichnung: "Geräte anlegen und bearbeiten" }, + "geraet.aussondern": { bereich: "Geräte", bezeichnung: "Geräte aussondern oder sperren" }, + + // Prüfungen und Wartung (Phase 2) + "pruefung.lesen": { bereich: "Prüfungen", bezeichnung: "Prüfungen und Fristen ansehen" }, + "pruefung.durchfuehren": { bereich: "Prüfungen", bezeichnung: "Prüfungen durchführen und dokumentieren" }, + "pruefung.planen": { bereich: "Prüfungen", bezeichnung: "Prüftermine und Vorlagen planen" }, + + // Atemschutz nach FwDV 7 (Phase 5) + "atemschutz.lesen": { bereich: "Atemschutz", bezeichnung: "Atemschutzdaten ansehen" }, + "atemschutz.bearbeiten": { bereich: "Atemschutz", bezeichnung: "Atemschutzgeräte und Träger verwalten" }, + "atemschutz.ueberwachung": { bereich: "Atemschutz", bezeichnung: "Atemschutzüberwachung führen" }, + + // Einsätze und Übungen (Phase 3) + "einsatz.lesen": { bereich: "Einsätze", bezeichnung: "Einsätze ansehen" }, + "einsatz.bearbeiten": { bereich: "Einsätze", bezeichnung: "Einsätze erfassen und bearbeiten" }, + "einsatz.bericht": { bereich: "Einsätze", bezeichnung: "Einsatzberichte erstellen" }, + + // Ausbildung und Qualifikationen (Phase 1 und 3) + "ausbildung.lesen": { bereich: "Ausbildung", bezeichnung: "Übungen und Lehrgänge ansehen" }, + "ausbildung.bearbeiten": { bereich: "Ausbildung", bezeichnung: "Übungen planen und Teilnahme erfassen" }, + "qualifikation.lesen": { bereich: "Ausbildung", bezeichnung: "Qualifikationen ansehen" }, + "qualifikation.bearbeiten": { bereich: "Ausbildung", bezeichnung: "Qualifikationen pflegen" }, + + // Kasse — die Rolle KASSIER ist in Phase 0 gefordert, ein Kassenmodul ist + // in Phase-0.md bis Phase-8.md aber nicht vorgesehen. Die Rechte stehen hier + // bereits bereit, damit die Rolle nicht leer bleibt. Siehe docs/ROADMAP.md. + "kasse.lesen": { bereich: "Kasse", bezeichnung: "Buchungen und Auswertungen ansehen" }, + "kasse.bearbeiten": { bereich: "Kasse", bezeichnung: "Buchungen erfassen" }, + "kasse.abschluss": { bereich: "Kasse", bezeichnung: "Kassenabschluss durchführen" }, + + // Dateien + "datei.lesen": { bereich: "Dateien", bezeichnung: "Dateien herunterladen" }, + "datei.hochladen": { bereich: "Dateien", bezeichnung: "Dateien hochladen" }, + "datei.loeschen": { bereich: "Dateien", bezeichnung: "Dateien löschen" }, + + // Terminal im Gerätehaus (Phase 4) + "kiosk.anmelden": { bereich: "Terminal", bezeichnung: "Am Terminal anmelden" }, + "kiosk.erfassen": { bereich: "Terminal", bezeichnung: "Am Terminal Erfassungen vornehmen" }, +} as const; + +export type Recht = keyof typeof RECHTE; + +export const ALLE_RECHTE = Object.keys(RECHTE) as Recht[]; + +// ----------------------------------------------------------------------------- +// Rolle zu Recht +// ----------------------------------------------------------------------------- + +/** Rechte, die jede angemeldete Person hat. */ +const GRUNDRECHTE: Recht[] = [ + "person.eigene", + "organisation.lesen", + "person.lesen", + "fahrzeug.lesen", + "geraet.lesen", + "pruefung.lesen", + "einsatz.lesen", + "ausbildung.lesen", + "qualifikation.lesen", + "datei.lesen", +]; + +/** + * Vorgabe je Rolle. SUPERADMIN steht bewusst nicht drin — der bekommt in + * hatRecht() pauschal alles, damit ein neu ergänztes Recht nicht versehentlich + * am Administrator vorbeigeht. + */ +export const ROLLEN_RECHTE: Record, Recht[]> = { + KOMMANDANT: [ + ...GRUNDRECHTE, + "organisation.bearbeiten", + "system.auditlog", + "person.bearbeiten", + "person.loeschen", + "person.rollen", + "person.ausweis", + "person.export", + "fahrzeug.bearbeiten", + "geraet.bearbeiten", + "geraet.aussondern", + "pruefung.planen", + "pruefung.durchfuehren", + "atemschutz.lesen", + "atemschutz.bearbeiten", + "atemschutz.ueberwachung", + "einsatz.bearbeiten", + "einsatz.bericht", + "ausbildung.bearbeiten", + "qualifikation.bearbeiten", + "kasse.lesen", + "datei.hochladen", + "datei.loeschen", + ], + + GERAETEWART: [ + ...GRUNDRECHTE, + "fahrzeug.bearbeiten", + "geraet.bearbeiten", + "geraet.aussondern", + "pruefung.planen", + "pruefung.durchfuehren", + "datei.hochladen", + ], + + ATEMSCHUTZGERAETEWART: [ + ...GRUNDRECHTE, + "geraet.bearbeiten", + "pruefung.planen", + "pruefung.durchfuehren", + "atemschutz.lesen", + "atemschutz.bearbeiten", + "atemschutz.ueberwachung", + "datei.hochladen", + ], + + KASSIER: [...GRUNDRECHTE, "kasse.lesen", "kasse.bearbeiten", "kasse.abschluss", "datei.hochladen"], + + SCHRIFTFUEHRER: [ + ...GRUNDRECHTE, + "person.bearbeiten", + "einsatz.bearbeiten", + "einsatz.bericht", + "ausbildung.bearbeiten", + "datei.hochladen", + ], + + AUSBILDER: [ + ...GRUNDRECHTE, + "ausbildung.bearbeiten", + "qualifikation.bearbeiten", + "datei.hochladen", + ], + + MITGLIED: [...GRUNDRECHTE], + + // Das Terminal darf bewusst fast nichts. Wer daran arbeitet, meldet sich + // zusätzlich mit PIN oder Ausweis an und handelt dann unter seiner Person. + KIOSK: ["kiosk.anmelden", "kiosk.erfassen", "organisation.lesen"], +}; + +// ----------------------------------------------------------------------------- +// Sitzung und Prüffunktionen +// ----------------------------------------------------------------------------- + +/** Was die Anwendung über den gerade Angemeldeten weiß. */ +export interface Sitzung { + personId: string; + organisationId: string; + name: string; + rollen: Rolle[]; + /** Wie die Anmeldung zustande kam — relevant fürs Audit und für den Kiosk. */ + anmeldeArt: "LOKAL" | "OIDC" | "KIOSK_PIN" | "KIOSK_RFID" | "SYSTEM"; +} + +/** Fehler, den die tRPC-Schicht in ein FORBIDDEN übersetzt. */ +export class KeinZugriffFehler extends Error { + constructor( + public readonly benoetigtesRecht: Recht | Recht[], + nachricht?: string, + ) { + const rechte = Array.isArray(benoetigtesRecht) + ? benoetigtesRecht.join(", ") + : benoetigtesRecht; + super(nachricht ?? `Fehlende Berechtigung: ${rechte}`); + this.name = "KeinZugriffFehler"; + } +} + +/** Alle Rechte, die sich aus den Rollen einer Sitzung ergeben. */ +export function rechteVon(rollen: readonly Rolle[]): Set { + if (rollen.includes("SUPERADMIN")) return new Set(ALLE_RECHTE); + + const gesammelt = new Set(); + for (const rolle of rollen) { + if (rolle === "SUPERADMIN") continue; + for (const recht of ROLLEN_RECHTE[rolle] ?? []) gesammelt.add(recht); + } + return gesammelt; +} + +/** Hat die Sitzung das Recht? Der Administrator hat immer alles. */ +export function hatRecht(sitzung: Sitzung | null, recht: Recht): boolean { + if (!sitzung) return false; + if (sitzung.rollen.includes("SUPERADMIN")) return true; + return rechteVon(sitzung.rollen).has(recht); +} + +/** Mindestens eines der Rechte. */ +export function hatEinesDerRechte(sitzung: Sitzung | null, rechte: Recht[]): boolean { + return rechte.some((recht) => hatRecht(sitzung, recht)); +} + +/** Alle genannten Rechte. */ +export function hatAlleRechte(sitzung: Sitzung | null, rechte: Recht[]): boolean { + return rechte.every((recht) => hatRecht(sitzung, recht)); +} + +/** Wirft, wenn das Recht fehlt. Für Server-Actions und tRPC-Prozeduren. */ +export function verlangeRecht(sitzung: Sitzung | null, recht: Recht): asserts sitzung is Sitzung { + if (!hatRecht(sitzung, recht)) throw new KeinZugriffFehler(recht); +} + +/** Wirft, wenn keines der Rechte vorliegt. */ +export function verlangeEinesDerRechte( + sitzung: Sitzung | null, + rechte: Recht[], +): asserts sitzung is Sitzung { + if (!hatEinesDerRechte(sitzung, rechte)) throw new KeinZugriffFehler(rechte); +} + +/** Hat die Sitzung eine dieser Rollen? Nur für Anzeigezwecke gedacht. */ +export function hatRolle(sitzung: Sitzung | null, ...rollen: Rolle[]): boolean { + if (!sitzung) return false; + return rollen.some((rolle) => sitzung.rollen.includes(rolle)); +} + +/** + * Darf die Sitzung auf diesen Datensatz zugreifen? Ergänzt die Mandanten- + * trennung der Datenbank um eine Prüfung im Code — doppelt hält besser, und + * die Fehlermeldung ist verständlicher als ein leeres Ergebnis. + */ +export function gehoertZurOrganisation( + sitzung: Sitzung | null, + datensatz: { organisationId: string } | null | undefined, +): boolean { + if (!sitzung || !datensatz) return false; + return sitzung.organisationId === datensatz.organisationId; +} + +/** + * Die eigenen Daten darf jeder sehen, auch ohne person.lesen. Andere nur mit + * dem entsprechenden Recht. + */ +export function darfPersonSehen(sitzung: Sitzung | null, personId: string): boolean { + if (!sitzung) return false; + if (sitzung.personId === personId) return true; + return hatRecht(sitzung, "person.lesen"); +} + +/** Wandelt einen beliebigen Text in eine gültige Rolle um, sonst null. */ +export function alsRolle(wert: string | null | undefined): Rolle | null { + if (!wert) return null; + const gross = wert.trim().toUpperCase(); + return (ROLLEN as readonly string[]).includes(gross) ? (gross as Rolle) : null; +} diff --git a/src/lib/db.ts b/src/lib/db.ts new file mode 100644 index 0000000..c52fd6d --- /dev/null +++ b/src/lib/db.ts @@ -0,0 +1,146 @@ +import { PrismaClient } from "@prisma/client"; +import { env } from "@/lib/env"; + +/** + * Datenbankzugriff mit Mandantenkontext. + * + * Es gibt ZWEI Verbindungen, und der Unterschied ist der Kern der + * Mandantentrennung: + * + * prismaApp — verbindet sich als "florian_app". Für diese Rolle ist Row + * Level Security aktiv. Ohne gesetztes + * app.current_organisation liefert jede Abfrage null Zeilen. + * Das ist die Verbindung, über die die Anwendung arbeitet. + * + * prismaSystem — verbindet sich als Eigentümer (DIRECT_DATABASE_URL) und + * umgeht RLS. Das ist nötig für genau die Vorgänge, die VOR + * der Mandantenauflösung stattfinden: die Anmeldung muss eine + * E-Mail-Adresse über alle Wehren hinweg suchen können, weil + * zu dem Zeitpunkt noch niemand weiß, zu welcher Wehr sie + * gehört. Ebenso Wartungsjobs des Workers. + * + * Zugriff auf Fachdaten läuft ausschließlich über fuerOrganisation(). Wer + * prismaSystem in einem Router benutzt, umgeht die Trennung — deshalb heißt die + * Hilfsfunktion dafür ohneMandantenfilter() und verlangt eine Begründung. + */ + +const globalerCache = globalThis as unknown as { + prismaApp?: PrismaClient; + prismaSystem?: PrismaClient; +}; + +function protokollStufen() { + return env.NODE_ENV === "development" + ? ([ + { emit: "stdout", level: "warn" }, + { emit: "stdout", level: "error" }, + ] as const) + : ([{ emit: "stdout", level: "error" }] as const); +} + +function clientAnlegen(url: string): PrismaClient { + return new PrismaClient({ + datasources: { db: { url } }, + log: [...protokollStufen()], + errorFormat: env.NODE_ENV === "development" ? "pretty" : "minimal", + }); +} + +/** + * Anwendungsverbindung mit aktiver Mandantentrennung. + * Nicht direkt verwenden — immer über fuerOrganisation(). + */ +export const prismaApp: PrismaClient = + globalerCache.prismaApp ?? clientAnlegen(env.DATABASE_URL); + +/** + * Eigentümerverbindung ohne Mandantentrennung. + * Nur für Anmeldung, Health-Checks und Wartungsjobs. + */ +export const prismaSystem: PrismaClient = + globalerCache.prismaSystem ?? + clientAnlegen(env.DIRECT_DATABASE_URL ?? env.DATABASE_URL); + +// In der Entwicklung zwischenlagern, damit Hot Reload keine neue +// Verbindungsschar aufmacht. +if (env.NODE_ENV !== "production") { + globalerCache.prismaApp = prismaApp; + globalerCache.prismaSystem = prismaSystem; +} + +/** Der auf eine Wehr eingeschränkte Client, den fuerOrganisation übergibt. */ +export type MandantenClient = Omit< + PrismaClient, + "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends" +>; + +/** Prüft grob, ob es überhaupt ein UUID sein kann, bevor es ins SQL geht. */ +const UUID_MUSTER = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** + * Führt fn innerhalb einer Transaktion aus, in der app.current_organisation + * gesetzt ist. SET LOCAL gilt nur bis zum Ende der Transaktion — danach ist der + * Kontext wieder leer, auch wenn die Verbindung in den Pool zurückgeht. + * + * @example + * const personen = await fuerOrganisation(orgId, (db) => + * db.person.findMany({ where: { aktiv: true } }), + * ); + */ +export async function fuerOrganisation( + organisationId: string, + fn: (db: MandantenClient) => Promise, + optionen: { timeoutMs?: number; maxWaitMs?: number } = {}, +): Promise { + if (!UUID_MUSTER.test(organisationId)) { + throw new Error( + `Ungültige Organisations-Kennung: ${organisationId}. Erwartet wird ein UUID.`, + ); + } + + return prismaApp.$transaction( + async (tx) => { + // Parametrisiert, damit kein Wert ungeprüft ins SQL wandert. + // set_config mit is_local=true entspricht SET LOCAL. + await tx.$executeRaw`SELECT set_config('app.current_organisation', ${organisationId}, true)`; + return fn(tx as unknown as MandantenClient); + }, + { + maxWait: optionen.maxWaitMs ?? 5_000, + timeout: optionen.timeoutMs ?? 15_000, + }, + ); +} + +/** + * Zugriff ohne Mandantenfilter — ausdrücklich benannt, damit man beim Lesen + * sofort sieht, dass hier die Trennung umgangen wird. + * + * Zulässig nur für: Anmeldung (E-Mail-Suche über alle Wehren), Health-Checks, + * Wartungsjobs des Workers. Jede Verwendung gehört begründet. + */ +export async function ohneMandantenfilter( + grund: string, + fn: (db: PrismaClient) => Promise, +): Promise { + if (env.NODE_ENV === "development") { + console.warn(`[db] Zugriff ohne Mandantenfilter: ${grund}`); + } + return fn(prismaSystem); +} + +/** Einfache Erreichbarkeitsprüfung für /api/health. */ +export async function datenbankErreichbar(): Promise { + try { + await prismaApp.$queryRaw`SELECT 1`; + return true; + } catch { + return false; + } +} + +/** Verbindungen schließen — für den Worker-Abbruch und für Tests. */ +export async function verbindungenTrennen(): Promise { + await Promise.allSettled([prismaApp.$disconnect(), prismaSystem.$disconnect()]); +} diff --git a/src/lib/env.ts b/src/lib/env.ts new file mode 100644 index 0000000..c80c922 --- /dev/null +++ b/src/lib/env.ts @@ -0,0 +1,173 @@ +import { z } from "zod"; + +/** + * Zentrale Umgebungsvalidierung. + * + * Fehlt oder taugt eine Variable nicht, bricht der Start hier ab — mit einer + * Meldung, die sagt welche. Das ist deutlich angenehmer als ein Dienst, der + * anläuft und beim ersten Datei-Upload merkt, dass MinIO nie konfiguriert war. + * + * Beim Container-Build ist keine echte Umgebung vorhanden; dort setzt das + * Dockerfile SKIP_ENV_VALIDATION=1. + */ + +/** + * Akzeptiert "true"/"1"/"ja" als wahr — Compose liefert alles als Text. + * Der Standardwert wird innerhalb der Umwandlung gesetzt, nicht per .default(): + * .default() erwartet den Eingabetyp (string), nicht das Ergebnis (boolean). + */ +const boolAusText = (standard: boolean) => + z + .string() + .optional() + .transform((wert) => { + if (wert === undefined || wert.trim() === "") return standard; + return ["true", "1", "ja", "yes", "on"].includes(wert.trim().toLowerCase()); + }); + +const zahl = (standard: number) => + z + .string() + .optional() + .transform((wert) => (wert === undefined || wert === "" ? standard : Number(wert))) + .pipe(z.number().int().positive()); + +const schema = z.object({ + NODE_ENV: z.enum(["development", "production", "test"]).default("development"), + TZ: z.string().default("Europe/Berlin"), + APP_URL: z.string().url().default("http://localhost:3000"), + + // Datenbank + DATABASE_URL: z.string().url(), + DIRECT_DATABASE_URL: z.string().url().optional(), + + // Redis / Warteschlangen + REDIS_URL: z.string().url(), + WORKER_CONCURRENCY: zahl(5), + + // Objektspeicher + S3_ENDPOINT: z.string().min(1), + S3_PORT: zahl(9000), + S3_USE_SSL: boolAusText(false), + S3_ACCESS_KEY: z.string().min(1), + S3_SECRET_KEY: z.string().min(1), + S3_REGION: z.string().default("eu-central-1"), + S3_BUCKET: z.string().min(1).default("florian"), + S3_PUBLIC_URL: z.string().url().optional(), + S3_URL_TTL: zahl(900), + + // PDF-Erzeugung + GOTENBERG_URL: z.string().url().optional(), + GOTENBERG_TIMEOUT: zahl(30), + + // Sitzungen + AUTH_SECRET: z.string().min(32, "AUTH_SECRET muss mindestens 32 Zeichen haben"), + AUTH_TRUST_HOST: boolAusText(true), + AUTH_SESSION_MAX_AGE: zahl(28_800), + + // OIDC gegen authentik + AUTH_OIDC_ENABLED: boolAusText(false), + AUTH_OIDC_ISSUER: z.string().optional(), + AUTH_OIDC_CLIENT_ID: z.string().optional(), + AUTH_OIDC_CLIENT_SECRET: z.string().optional(), + AUTH_OIDC_NAME: z.string().default("authentik"), + AUTH_OIDC_GROUP_CLAIM: z.string().default("groups"), + AUTH_OIDC_GROUP_MAPPING: z.string().default(""), + AUTH_OIDC_AUTO_PROVISION: boolAusText(true), + AUTH_OIDC_STANDARD_ORGANISATION: z.string().optional(), + + // Lokaler Login + AUTH_LOCAL_ENABLED: boolAusText(true), + AUTH_BCRYPT_ROUNDS: zahl(12), + + // Kiosk + KIOSK_ENABLED: boolAusText(true), + KIOSK_DEVICE_SECRET: z.string().min(8), + KIOSK_PIN_LAENGE: zahl(6), + KIOSK_MAX_FEHLVERSUCHE: zahl(5), + KIOSK_SPERRDAUER: zahl(300), + KIOSK_IDLE_TIMEOUT: zahl(120), + + // Integrationen (alle optional und abschaltbar) + MAIL_ENABLED: boolAusText(false), + MAIL_HOST: z.string().optional(), + MAIL_PORT: zahl(587), + MAIL_SECURE: boolAusText(false), + MAIL_USER: z.string().optional(), + MAIL_PASSWORD: z.string().optional(), + MAIL_FROM: z.string().optional(), + + TELEGRAM_ENABLED: boolAusText(false), + TELEGRAM_BOT_TOKEN: z.string().optional(), + TELEGRAM_CHAT_ID: z.string().optional(), + + LLM_ENABLED: boolAusText(false), + LLM_BASE_URL: z.string().optional(), + LLM_API_KEY: z.string().optional(), + LLM_MODELL: z.string().optional(), + + // Aufbewahrung + AUFBEWAHRUNG_JAHRE: zahl(10), + AUDIT_AUFBEWAHRUNG_TAGE: zahl(3650), + PERSON_ANONYMISIERUNG_TAGE: zahl(365), +}); + +export type Umgebung = z.infer; + +/** Ersatzwerte für Build und Tests — nie für den echten Betrieb. */ +const platzhalter: Record = { + DATABASE_URL: "postgresql://build:build@localhost:5432/build?schema=public", + REDIS_URL: "redis://localhost:6379", + S3_ENDPOINT: "localhost", + S3_ACCESS_KEY: "build", + S3_SECRET_KEY: "build", + AUTH_SECRET: "build_time_platzhalter_mindestens_32_zeichen", + KIOSK_DEVICE_SECRET: "build_time_platzhalter", +}; + +function lesen(): Umgebung { + const roh = + process.env.SKIP_ENV_VALIDATION === "1" || process.env.NODE_ENV === "test" + ? { ...platzhalter, ...process.env } + : process.env; + + const ergebnis = schema.safeParse(roh); + + if (!ergebnis.success) { + const zeilen = ergebnis.error.issues.map( + (problem) => ` - ${problem.path.join(".")}: ${problem.message}`, + ); + throw new Error( + [ + "Die Umgebungskonfiguration ist unvollständig oder fehlerhaft:", + ...zeilen, + "", + "Vorlage mit Erklärungen zu jeder Variable: .env.example", + ].join("\n"), + ); + } + + return ergebnis.data; +} + +export const env: Umgebung = lesen(); + +/** true, wenn die Anmeldung über authentik vollständig konfiguriert ist. */ +export const oidcVerfuegbar = + env.AUTH_OIDC_ENABLED && + Boolean(env.AUTH_OIDC_ISSUER) && + Boolean(env.AUTH_OIDC_CLIENT_ID) && + Boolean(env.AUTH_OIDC_CLIENT_SECRET); + +/** + * Zuordnung authentik-Gruppe zu FLORIAN-Rolle aus AUTH_OIDC_GROUP_MAPPING. + * Format: "gruppe:ROLLE,gruppe2:ROLLE2". + */ +export function oidcGruppenZuordnung(): Map { + const karte = new Map(); + for (const eintrag of env.AUTH_OIDC_GROUP_MAPPING.split(",")) { + const [gruppe, rolle] = eintrag.split(":").map((teil) => teil?.trim()); + if (gruppe && rolle) karte.set(gruppe.toLowerCase(), rolle.toUpperCase()); + } + return karte; +} diff --git a/src/lib/integrations/llm.ts b/src/lib/integrations/llm.ts new file mode 100644 index 0000000..0f26d94 --- /dev/null +++ b/src/lib/integrations/llm.ts @@ -0,0 +1,106 @@ +import "server-only"; +import { env } from "@/lib/env"; + +/** + * Sprachmodell-Anbindung hinter einer Schnittstelle. + * + * Vorgesehen für: Einsatzberichte aus Stichpunkten formulieren, Prüfmängel + * zusammenfassen, Protokolle vorstrukturieren. Immer als Vorschlag, nie als + * Entscheidung — und immer mit einem Menschen, der gegenzeichnet. + * + * Es wird ausschließlich gegen einen OpenAI-kompatiblen Endpunkt gesprochen, + * der selbst betrieben werden kann (Ollama, vLLM, llama.cpp). Kein Cloud-Zwang, + * keine Daten, die das Gerätehaus verlassen müssen. + */ + +export interface LlmAnfrage { + systemAnweisung: string; + benutzerEingabe: string; + maxToken?: number; + temperatur?: number; +} + +export interface LlmAntwort { + erfolgreich: boolean; + text?: string; + fehler?: string; +} + +export interface LlmDienst { + vervollstaendigen(anfrage: LlmAnfrage): Promise; + verfuegbar(): boolean; +} + +/** Standard, solange LLM_ENABLED=false: liefert eine klare Absage. */ +class AbgeschaltetesLlm implements LlmDienst { + verfuegbar(): boolean { + return false; + } + + async vervollstaendigen(): Promise { + return { + erfolgreich: false, + fehler: "Die Textunterstützung ist in dieser Installation abgeschaltet (LLM_ENABLED=false).", + }; + } +} + +class OpenAiKompatiblesLlm implements LlmDienst { + verfuegbar(): boolean { + return Boolean(env.LLM_BASE_URL && env.LLM_MODELL); + } + + async vervollstaendigen(anfrage: LlmAnfrage): Promise { + if (!this.verfuegbar()) { + return { erfolgreich: false, fehler: "LLM ist nicht vollständig konfiguriert." }; + } + + try { + const antwort = await fetch(`${env.LLM_BASE_URL}/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(env.LLM_API_KEY ? { Authorization: `Bearer ${env.LLM_API_KEY}` } : {}), + }, + body: JSON.stringify({ + model: env.LLM_MODELL, + messages: [ + { role: "system", content: anfrage.systemAnweisung }, + { role: "user", content: anfrage.benutzerEingabe }, + ], + max_tokens: anfrage.maxToken ?? 1024, + temperature: anfrage.temperatur ?? 0.3, + }), + }); + + if (!antwort.ok) { + return { erfolgreich: false, fehler: `Endpunkt antwortete mit ${antwort.status}` }; + } + + const daten = (await antwort.json()) as { + choices?: Array<{ message?: { content?: string } }>; + }; + const text = daten.choices?.[0]?.message?.content; + + return text + ? { erfolgreich: true, text } + : { erfolgreich: false, fehler: "Leere Antwort vom Endpunkt." }; + } catch (fehler) { + return { erfolgreich: false, fehler: String(fehler) }; + } + } +} + +let cache: LlmDienst | null = null; + +export function llm(): LlmDienst { + if (!cache) { + cache = env.LLM_ENABLED ? new OpenAiKompatiblesLlm() : new AbgeschaltetesLlm(); + } + return cache; +} + +/** Nur für Tests. */ +export function _llmErsetzen(ersatz: LlmDienst | null): void { + cache = ersatz; +} diff --git a/src/lib/integrations/mail.ts b/src/lib/integrations/mail.ts new file mode 100644 index 0000000..0afdd1e --- /dev/null +++ b/src/lib/integrations/mail.ts @@ -0,0 +1,75 @@ +import "server-only"; +import { env } from "@/lib/env"; + +/** + * E-Mail-Versand hinter einer Schnittstelle. + * + * Regel aus CLAUDE.md: externe Dienste hinter Interfaces in lib/integrations/, + * damit sie im Test mockbar und im Betrieb abschaltbar sind. Ist MAIL_ENABLED + * nicht gesetzt, landet die Nachricht im Log statt beim Empfänger — für + * Entwicklung und Testbetrieb genau richtig. + * + * Die tatsächliche SMTP-Anbindung kommt in Phase 7 (Benachrichtigungen). Bis + * dahin steht hier bewusst nur der Vertrag. + */ + +export interface Nachricht { + an: string | string[]; + betreff: string; + text: string; + html?: string; + antwortAn?: string; + anhaenge?: Array<{ dateiname: string; inhalt: Buffer; mimeTyp: string }>; +} + +export interface MailVersand { + senden(nachricht: Nachricht): Promise<{ erfolgreich: boolean; kennung?: string; fehler?: string }>; + verfuegbar(): boolean; +} + +/** Schreibt statt zu senden. Standard, solange MAIL_ENABLED=false. */ +class ProtokollVersand implements MailVersand { + verfuegbar(): boolean { + return false; + } + + async senden(nachricht: Nachricht) { + const empfaenger = Array.isArray(nachricht.an) ? nachricht.an.join(", ") : nachricht.an; + console.info( + `[mail] (nicht versendet, MAIL_ENABLED=false) an=${empfaenger} betreff="${nachricht.betreff}"`, + ); + return { erfolgreich: true, kennung: "protokoll" }; + } +} + +/** Platzhalter für den echten SMTP-Versand aus Phase 7. */ +class SmtpVersand implements MailVersand { + verfuegbar(): boolean { + return Boolean(env.MAIL_HOST && env.MAIL_FROM); + } + + async senden(nachricht: Nachricht) { + const empfaenger = Array.isArray(nachricht.an) ? nachricht.an.join(", ") : nachricht.an; + console.warn( + `[mail] SMTP-Versand ist erst ab Phase 7 umgesetzt. an=${empfaenger} betreff="${nachricht.betreff}"`, + ); + return { + erfolgreich: false, + fehler: "SMTP-Versand noch nicht umgesetzt (Phase 7).", + }; + } +} + +let versandCache: MailVersand | null = null; + +export function mail(): MailVersand { + if (!versandCache) { + versandCache = env.MAIL_ENABLED ? new SmtpVersand() : new ProtokollVersand(); + } + return versandCache; +} + +/** Nur für Tests: eigene Umsetzung unterschieben. */ +export function _mailErsetzen(ersatz: MailVersand | null): void { + versandCache = ersatz; +} diff --git a/src/lib/integrations/telegram.ts b/src/lib/integrations/telegram.ts new file mode 100644 index 0000000..8079e75 --- /dev/null +++ b/src/lib/integrations/telegram.ts @@ -0,0 +1,77 @@ +import "server-only"; +import { env } from "@/lib/env"; + +/** + * Telegram-Benachrichtigungen hinter einer Schnittstelle — abschaltbar und + * mockbar. Gedacht für Erinnerungen an fällige Prüfungen und, ab Phase 4, + * optionale Alarmhinweise. + * + * ACHTUNG: Telegram ist ein externer Dienst. Es dürfen ausschließlich + * Hinweise ohne personenbezogene Daten hinausgehen ("3 Prüfungen fällig"), + * niemals Namen, Adressen oder Einsatzdetails. + */ + +export interface TelegramVersand { + senden(text: string, chatId?: string): Promise<{ erfolgreich: boolean; fehler?: string }>; + verfuegbar(): boolean; +} + +class ProtokollTelegram implements TelegramVersand { + verfuegbar(): boolean { + return false; + } + + async senden(text: string) { + console.info(`[telegram] (nicht versendet, TELEGRAM_ENABLED=false) ${text}`); + return { erfolgreich: true }; + } +} + +class EchtesTelegram implements TelegramVersand { + verfuegbar(): boolean { + return Boolean(env.TELEGRAM_BOT_TOKEN && env.TELEGRAM_CHAT_ID); + } + + async senden(text: string, chatId?: string) { + if (!this.verfuegbar()) { + return { erfolgreich: false, fehler: "Telegram ist nicht vollständig konfiguriert." }; + } + + try { + const antwort = await fetch( + `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + chat_id: chatId ?? env.TELEGRAM_CHAT_ID, + text, + parse_mode: "HTML", + disable_web_page_preview: true, + }), + }, + ); + + if (!antwort.ok) { + return { erfolgreich: false, fehler: `Telegram antwortete mit ${antwort.status}` }; + } + return { erfolgreich: true }; + } catch (fehler) { + return { erfolgreich: false, fehler: String(fehler) }; + } + } +} + +let cache: TelegramVersand | null = null; + +export function telegram(): TelegramVersand { + if (!cache) { + cache = env.TELEGRAM_ENABLED ? new EchtesTelegram() : new ProtokollTelegram(); + } + return cache; +} + +/** Nur für Tests. */ +export function _telegramErsetzen(ersatz: TelegramVersand | null): void { + cache = ersatz; +} diff --git a/src/lib/passwort.ts b/src/lib/passwort.ts new file mode 100644 index 0000000..031a554 --- /dev/null +++ b/src/lib/passwort.ts @@ -0,0 +1,66 @@ +import "server-only"; +import bcrypt from "bcryptjs"; +import { env } from "@/lib/env"; + +/** + * Alles, was gehasht wird: Passwörter, Terminal-PINs, RFID-Kennungen. + * Immer bcrypt, immer mit dem Kostenfaktor aus AUTH_BCRYPT_ROUNDS, und + * Vergleiche immer über bcrypt.compare — nie per Zeichenkettenvergleich. + */ + +export async function hashen(klartext: string): Promise { + return bcrypt.hash(klartext, env.AUTH_BCRYPT_ROUNDS); +} + +export async function pruefen(klartext: string, hash: string | null | undefined): Promise { + if (!hash) { + // Trotzdem einen Vergleich durchführen, damit ein fehlender Hash zeitlich + // nicht von einem falschen Passwort zu unterscheiden ist. + await bcrypt.compare(klartext, "$2a$12$ungueltigerhashfuerzeitgleichheit000000000000000000000"); + return false; + } + return bcrypt.compare(klartext, hash); +} + +/** + * Mindestanforderungen an ein Passwort. Bewusst schlicht gehalten: Länge + * schlägt Sonderzeichen, und eine Wehr soll sich nicht an Regeln abarbeiten. + */ +export function passwortPruefen(passwort: string): { gueltig: boolean; meldung?: string } { + if (passwort.length < 10) { + return { gueltig: false, meldung: "Das Passwort muss mindestens 10 Zeichen lang sein." }; + } + if (passwort.length > 200) { + return { gueltig: false, meldung: "Das Passwort darf höchstens 200 Zeichen lang sein." }; + } + if (/^\d+$/.test(passwort)) { + return { gueltig: false, meldung: "Das Passwort darf nicht nur aus Ziffern bestehen." }; + } + return { gueltig: true }; +} + +/** Prüft eine Terminal-PIN gegen die konfigurierte Länge. */ +export function pinPruefen(pin: string): { gueltig: boolean; meldung?: string } { + const laenge = env.KIOSK_PIN_LAENGE; + if (!new RegExp(`^\\d{${laenge}}$`).test(pin)) { + return { gueltig: false, meldung: `Die PIN muss aus genau ${laenge} Ziffern bestehen.` }; + } + if (/^(\d)\1+$/.test(pin)) { + return { gueltig: false, meldung: "Die PIN darf nicht aus lauter gleichen Ziffern bestehen." }; + } + const ziffern = pin.split("").map(Number); + const aufsteigend = ziffern.every((z, i) => i === 0 || z === (ziffern[i - 1] ?? 0) + 1); + const absteigend = ziffern.every((z, i) => i === 0 || z === (ziffern[i - 1] ?? 0) - 1); + if (aufsteigend || absteigend) { + return { gueltig: false, meldung: "Die PIN darf keine fortlaufende Ziffernfolge sein." }; + } + return { gueltig: true }; +} + +/** + * Normalisiert eine RFID-/NFC-Kennung, bevor sie gehasht wird. Lesegeräte + * liefern die UID mal mit Doppelpunkten, mal klein-, mal großgeschrieben. + */ +export function rfidNormalisieren(uid: string): string { + return uid.replace(/[\s:-]/g, "").toUpperCase(); +} diff --git a/src/lib/storage.ts b/src/lib/storage.ts new file mode 100644 index 0000000..a44e918 --- /dev/null +++ b/src/lib/storage.ts @@ -0,0 +1,209 @@ +import "server-only"; +import { createHash, randomUUID } from "node:crypto"; +import { Readable } from "node:stream"; +import { Client as MinioClient } from "minio"; +import { env } from "@/lib/env"; + +/** + * Der einzige Weg zu Dateien. + * + * Regel aus CLAUDE.md: Dateizugriff ausschließlich über lib/storage.ts. Kein + * fs.readFile, kein Pfad im Dateisystem, keine Blobs in der Datenbank. Alles + * liegt in MinIO — dadurch überlebt eine Datei den Neuaufbau des Containers, + * lässt sich versionieren und getrennt sichern. + * + * Objektschlüssel folgen immer demselben Muster: + * ///- + * Der Slug vorn macht Sicherungen und Löschungen je Wehr einfach. + */ + +let clientCache: MinioClient | null = null; + +function client(): MinioClient { + if (!clientCache) { + clientCache = new MinioClient({ + endPoint: env.S3_ENDPOINT, + port: env.S3_PORT, + useSSL: env.S3_USE_SSL, + accessKey: env.S3_ACCESS_KEY, + secretKey: env.S3_SECRET_KEY, + region: env.S3_REGION, + }); + } + return clientCache; +} + +export const BUCKET = env.S3_BUCKET; + +/** Zeichen, die in Objektschlüsseln Ärger machen, werden ersetzt. */ +function dateinameBereinigen(name: string): string { + return name + .normalize("NFKD") + .replace(/[̀-ͯ]/g, "") + .replace(/ä/gi, "ae") + .replace(/ö/gi, "oe") + .replace(/ü/gi, "ue") + .replace(/ß/g, "ss") + .replace(/[^a-zA-Z0-9._-]/g, "_") + .replace(/_{2,}/g, "_") + .slice(0, 180); +} + +export interface SchluesselTeile { + organisationSlug: string; + /** Modellname, z. B. "Geraet". Wird kleingeschrieben. */ + entitaet: string; + entitaetId?: string; + dateiname: string; +} + +/** Baut einen eindeutigen Objektschlüssel nach dem oben beschriebenen Muster. */ +export function objektSchluessel({ + organisationSlug, + entitaet, + entitaetId, + dateiname, +}: SchluesselTeile): string { + const teile = [ + organisationSlug, + entitaet.toLowerCase(), + entitaetId ?? "allgemein", + `${randomUUID()}-${dateinameBereinigen(dateiname)}`, + ]; + return teile.join("/"); +} + +export interface HochladenErgebnis { + bucket: string; + objektSchluessel: string; + groesseBytes: number; + /** SHA-256 des Inhalts — erkennt Dubletten, belegt Unverändertheit. */ + pruefsumme: string; + etag: string; +} + +/** + * Legt einen Puffer in MinIO ab und gibt zurück, was in die Datei-Tabelle + * gehört. Der Aufrufer schreibt den Datensatz und den Audit-Eintrag. + */ +export async function hochladen( + schluessel: string, + inhalt: Buffer, + mimeTyp: string, + metadaten: Record = {}, +): Promise { + await bucketSicherstellen(); + + const pruefsumme = createHash("sha256").update(inhalt).digest("hex"); + + const ergebnis = await client().putObject(BUCKET, schluessel, inhalt, inhalt.length, { + "Content-Type": mimeTyp, + // MinIO nimmt eigene Angaben nur mit diesem Präfix an. + "x-amz-meta-pruefsumme": pruefsumme, + ...Object.fromEntries( + Object.entries(metadaten).map(([schlssel, wert]) => [`x-amz-meta-${schlssel}`, wert]), + ), + }); + + return { + bucket: BUCKET, + objektSchluessel: schluessel, + groesseBytes: inhalt.length, + pruefsumme, + etag: ergebnis.etag, + }; +} + +/** Lädt ein Objekt vollständig in den Speicher. Für große Dateien streamen. */ +export async function herunterladen(schluessel: string): Promise { + const strom = await client().getObject(BUCKET, schluessel); + const stuecke: Buffer[] = []; + for await (const stueck of strom) { + stuecke.push(Buffer.isBuffer(stueck) ? stueck : Buffer.from(stueck)); + } + return Buffer.concat(stuecke); +} + +/** Objekt als Strom — für Downloads, die durch die Anwendung durchgereicht werden. */ +export async function alsStrom(schluessel: string): Promise { + return client().getObject(BUCKET, schluessel); +} + +/** + * Zeitlich begrenzter Download-Link. Damit muss die Datei nicht durch die + * Anwendung laufen — der Browser holt sie direkt bei MinIO ab. + * Der Link gilt S3_URL_TTL Sekunden und ist danach wertlos. + */ +export async function downloadLink( + schluessel: string, + dateiname?: string, + gueltigkeitSekunden = env.S3_URL_TTL, +): Promise { + const kopfzeilen: Record = {}; + if (dateiname) { + kopfzeilen["response-content-disposition"] = + `attachment; filename="${dateinameBereinigen(dateiname)}"`; + } + return client().presignedGetObject(BUCKET, schluessel, gueltigkeitSekunden, kopfzeilen); +} + +/** + * Zeitlich begrenzter Upload-Link. Der Browser lädt direkt zu MinIO hoch, + * ohne dass große Dateien durch die Anwendung müssen. + */ +export async function uploadLink( + schluessel: string, + gueltigkeitSekunden = env.S3_URL_TTL, +): Promise { + await bucketSicherstellen(); + return client().presignedPutObject(BUCKET, schluessel, gueltigkeitSekunden); +} + +/** Löscht ein Objekt endgültig. Nur für den Aufbewahrungs-Job des Workers. */ +export async function endgueltigLoeschen(schluessel: string): Promise { + await client().removeObject(BUCKET, schluessel); +} + +/** Gibt es das Objekt und wie groß ist es? */ +export async function objektInfo(schluessel: string) { + try { + const status = await client().statObject(BUCKET, schluessel); + return { + vorhanden: true as const, + groesseBytes: status.size, + geaendertAm: status.lastModified, + mimeTyp: status.metaData?.["content-type"] ?? "application/octet-stream", + etag: status.etag, + }; + } catch { + return { vorhanden: false as const }; + } +} + +/** Kopiert ein Objekt, z. B. beim Duplizieren einer Prüfvorlage. */ +export async function kopieren(vonSchluessel: string, nachSchluessel: string): Promise { + await client().copyObject(BUCKET, nachSchluessel, `/${BUCKET}/${vonSchluessel}`); +} + +/** Legt den Bucket an, falls er fehlt. Idempotent, deshalb überall aufrufbar. */ +export async function bucketSicherstellen(): Promise { + const vorhanden = await client().bucketExists(BUCKET).catch(() => false); + if (!vorhanden) { + await client().makeBucket(BUCKET, env.S3_REGION); + } +} + +/** Erreichbarkeitsprüfung für /api/health. */ +export async function speicherErreichbar(): Promise { + try { + await client().bucketExists(BUCKET); + return true; + } catch { + return false; + } +} + +/** Nur für Tests: setzt den zwischengespeicherten Client zurück. */ +export function _clientZuruecksetzen(): void { + clientCache = null; +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..1f6a554 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,104 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; +import { formatDistanceToNow } from "date-fns"; +import { de } from "date-fns/locale"; +import { formatInTimeZone } from "date-fns-tz"; + +/** Tailwind-Klassen zusammenführen, spätere gewinnen. */ +export function cn(...eingaben: ClassValue[]) { + return twMerge(clsx(eingaben)); +} + +/** + * Alle Anzeigen in Europe/Berlin. Gespeichert wird in UTC — die Umrechnung + * passiert ausschließlich hier, damit sie nirgends vergessen wird. + */ +export const ZEITZONE = "Europe/Berlin"; + +function alsDate(wert: Date | string | number): Date { + return wert instanceof Date ? wert : new Date(wert); +} + +/** 02.09.2026 */ +export function alsDatum(wert: Date | string | number | null | undefined): string { + if (!wert) return "—"; + return formatInTimeZone(alsDate(wert), ZEITZONE, "dd.MM.yyyy", { locale: de }); +} + +/** 02.09.2026, 14:35 */ +export function alsDatumZeit(wert: Date | string | number | null | undefined): string { + if (!wert) return "—"; + return formatInTimeZone(alsDate(wert), ZEITZONE, "dd.MM.yyyy, HH:mm", { locale: de }); +} + +/** 14:35 */ +export function alsUhrzeit(wert: Date | string | number | null | undefined): string { + if (!wert) return "—"; + return formatInTimeZone(alsDate(wert), ZEITZONE, "HH:mm", { locale: de }); +} + +/** "vor 3 Tagen" */ +export function alsRelativeZeit(wert: Date | string | number | null | undefined): string { + if (!wert) return "—"; + return formatDistanceToNow(alsDate(wert), { addSuffix: true, locale: de }); +} + +/** Nachname, Vorname — so stehen Mitglieder in jeder Liste. */ +export function personName( + person: { vorname: string; nachname: string } | null | undefined, +): string { + if (!person) return "—"; + return `${person.nachname}, ${person.vorname}`; +} + +/** Vorname Nachname — für Anreden und Überschriften. */ +export function personAnrede( + person: { vorname: string; nachname: string } | null | undefined, +): string { + if (!person) return "—"; + return `${person.vorname} ${person.nachname}`; +} + +/** Initialen für den Platzhalter im Profilbild. */ +export function initialen( + person: { vorname: string; nachname: string } | null | undefined, +): string { + if (!person) return "??"; + return `${person.vorname.charAt(0)}${person.nachname.charAt(0)}`.toUpperCase(); +} + +/** 1,4 MB — deutsche Schreibweise mit Komma. */ +export function alsDateigroesse(bytes: number | bigint | null | undefined): string { + if (bytes === null || bytes === undefined) return "—"; + const zahl = typeof bytes === "bigint" ? Number(bytes) : bytes; + if (zahl < 1024) return `${zahl} B`; + const einheiten = ["kB", "MB", "GB", "TB"]; + let wert = zahl / 1024; + let index = 0; + while (wert >= 1024 && index < einheiten.length - 1) { + wert /= 1024; + index += 1; + } + return `${wert.toLocaleString("de-DE", { maximumFractionDigits: 1 })} ${einheiten[index]}`; +} + +/** 1.234,50 € */ +export function alsEuro(betrag: number | null | undefined): string { + if (betrag === null || betrag === undefined) return "—"; + return betrag.toLocaleString("de-DE", { style: "currency", currency: "EUR" }); +} + +/** Erzeugt aus einem Namen einen URL-tauglichen Slug. */ +export function alsSlug(text: string): string { + return text + .toLowerCase() + .replace(/ä/g, "ae") + .replace(/ö/g, "oe") + .replace(/ü/g, "ue") + .replace(/ß/g, "ss") + .normalize("NFKD") + .replace(/[̀-ͯ]/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 64); +} diff --git a/src/lib/warteschlange.ts b/src/lib/warteschlange.ts new file mode 100644 index 0000000..40124b8 --- /dev/null +++ b/src/lib/warteschlange.ts @@ -0,0 +1,109 @@ +import "server-only"; +import { Queue, type JobsOptions } from "bullmq"; +import IORedis from "ioredis"; +import { env } from "@/lib/env"; + +/** + * Warteschlangen für Hintergrundarbeit. + * + * Was hier hineingehört: alles, was länger dauert als eine Anfrage dauern darf + * oder nach Plan laufen soll — Fristenberechnung, Erinnerungen, PDF-Erzeugung, + * Exporte, Aufräumjobs. Der Worker in src/worker/index.ts arbeitet sie ab. + * + * Die Anwendung schreibt nur hinein, der Worker liest. So kann man den Worker + * neu starten, ohne dass die Oberfläche stehen bleibt. + */ + +export const WARTESCHLANGE = "florian"; + +/** Bekannte Auftragsarten. Ab Phase 2 kommen die fachlichen Jobs dazu. */ +export const AUFTRAG = { + /** Prüft alle Geräte auf fällige Fristen und setzt den Status. */ + FRISTEN_PRUEFEN: "fristen.pruefen", + /** Verschickt Erinnerungen an fällige Prüfungen. */ + ERINNERUNG_SENDEN: "erinnerung.senden", + /** Erzeugt ein PDF über Gotenberg. */ + PDF_ERZEUGEN: "pdf.erzeugen", + /** Räumt abgelaufene Daten nach den Aufbewahrungsfristen ab. */ + AUFBEWAHRUNG_PRUEFEN: "aufbewahrung.pruefen", +} as const; + +export type AuftragsArt = (typeof AUFTRAG)[keyof typeof AUFTRAG]; + +/** Jeder Auftrag trägt die Wehr mit, für die er läuft. */ +export interface AuftragsDaten { + organisationId: string; + [feld: string]: unknown; +} + +let verbindungCache: IORedis | null = null; +let warteschlangeCache: Queue | null = null; + +export function redis(): IORedis { + if (!verbindungCache) { + verbindungCache = new IORedis(env.REDIS_URL, { + // BullMQ verlangt null, sonst bricht es Aufträge bei Netzhängern ab. + maxRetriesPerRequest: null, + enableReadyCheck: false, + }); + verbindungCache.on("error", (fehler) => { + console.error("[redis] Verbindungsfehler:", fehler.message); + }); + } + return verbindungCache; +} + +export function warteschlange(): Queue { + if (!warteschlangeCache) { + warteschlangeCache = new Queue(WARTESCHLANGE, { + connection: redis(), + defaultJobOptions: { + attempts: 3, + backoff: { type: "exponential", delay: 5_000 }, + removeOnComplete: { age: 3600, count: 500 }, + removeOnFail: { age: 7 * 24 * 3600 }, + }, + }); + } + return warteschlangeCache; +} + +/** Stellt einen Auftrag ein. */ +export async function beauftrage( + art: AuftragsArt, + daten: AuftragsDaten, + optionen: JobsOptions = {}, +): Promise { + const auftrag = await warteschlange().add(art, daten, optionen); + return auftrag.id; +} + +/** Wiederkehrender Auftrag nach Zeitplan (cron-Syntax, Europe/Berlin). */ +export async function beauftrageWiederkehrend( + art: AuftragsArt, + daten: AuftragsDaten, + zeitplan: string, +): Promise { + await warteschlange().add(art, daten, { + repeat: { pattern: zeitplan, tz: "Europe/Berlin" }, + jobId: `${art}:${daten.organisationId}`, + }); +} + +/** Erreichbarkeitsprüfung für /api/health. */ +export async function warteschlangeErreichbar(): Promise { + try { + const antwort = await redis().ping(); + return antwort === "PONG"; + } catch { + return false; + } +} + +/** Verbindungen sauber schließen — für Tests und den Worker-Abbruch. */ +export async function verbindungenSchliessen(): Promise { + await warteschlangeCache?.close(); + verbindungCache?.disconnect(); + warteschlangeCache = null; + verbindungCache = null; +} diff --git a/src/server/context.ts b/src/server/context.ts new file mode 100644 index 0000000..e6b3d77 --- /dev/null +++ b/src/server/context.ts @@ -0,0 +1,32 @@ +import { aktuelleSitzung } from "@/lib/auth"; +import type { Sitzung } from "@/lib/authz"; + +/** + * Kontext jeder tRPC-Anfrage. Enthält bewusst nur, was für JEDE Prozedur + * sinnvoll ist — die Sitzung und die Angaben zur Anfrage fürs Audit. + * Der mandantengebundene Datenbankzugriff kommt in der angemeldet-Middleware + * dazu, damit er ohne Sitzung gar nicht erst existiert. + */ +export interface Kontext { + sitzung: Sitzung | null; + ipAdresse: string | null; + userAgent: string | null; +} + +/** Ermittelt die IP hinter Traefik aus den Weiterleitungs-Kopfzeilen. */ +function ipErmitteln(kopfzeilen: Headers): string | null { + const weitergeleitet = kopfzeilen.get("x-forwarded-for"); + if (weitergeleitet) { + const erste = weitergeleitet.split(",")[0]?.trim(); + if (erste) return erste; + } + return kopfzeilen.get("x-real-ip"); +} + +export async function kontextErzeugen({ req }: { req: Request }): Promise { + return { + sitzung: await aktuelleSitzung(), + ipAdresse: ipErmitteln(req.headers), + userAgent: req.headers.get("user-agent"), + }; +} diff --git a/src/server/routers/_app.ts b/src/server/routers/_app.ts new file mode 100644 index 0000000..e586783 --- /dev/null +++ b/src/server/routers/_app.ts @@ -0,0 +1,106 @@ +import { z } from "zod"; +import { router, oeffentlich, angemeldet, mitRecht } from "@/server/trpc"; +import { ROLLEN_BEZEICHNUNG, rechteVon } from "@/lib/authz"; +import { datenbankErreichbar } from "@/lib/db"; +import { speicherErreichbar } from "@/lib/storage"; + +/** + * Wurzel-Router. Ab Phase 1 kommen hier die Fachbereiche als Unter-Router + * dazu (stammdaten, wartung, atemschutz, einsatz, ausbildung, kasse). + * Phase 0 liefert nur, was zum Prüfen des Fundaments nötig ist. + */ + +const systemRouter = router({ + /** Erreichbarkeit der Randsysteme. Ohne Anmeldung abrufbar. */ + status: oeffentlich.query(async () => { + const [datenbank, speicher] = await Promise.all([ + datenbankErreichbar(), + speicherErreichbar(), + ]); + return { + datenbank, + speicher, + zeitpunkt: new Date(), + zeitzone: "Europe/Berlin", + }; + }), +}); + +const sitzungRouter = router({ + /** Wer bin ich, welcher Wehr gehöre ich an, was darf ich? */ + ich: angemeldet.query(async ({ ctx }) => { + const person = await ctx.db((db) => + db.person.findUnique({ + where: { id: ctx.sitzung.personId }, + select: { + id: true, + vorname: true, + nachname: true, + email: true, + dienstgrad: true, + mitgliedsnummer: true, + letzteAnmeldung: true, + organisation: { select: { id: true, name: true, kurzname: true, slug: true } }, + }, + }), + ); + + return { + person, + rollen: ctx.sitzung.rollen, + rollenBezeichnung: ctx.sitzung.rollen.map((rolle) => ROLLEN_BEZEICHNUNG[rolle]), + rechte: [...rechteVon(ctx.sitzung.rollen)], + anmeldeArt: ctx.sitzung.anmeldeArt, + }; + }), +}); + +const organisationRouter = router({ + /** Stammdaten der eigenen Wehr. */ + eigene: angemeldet.query(async ({ ctx }) => + ctx.db((db) => + db.organisation.findUnique({ + where: { id: ctx.sitzung.organisationId }, + }), + ), + ), + + /** Kennzahlen für die Startseite. */ + kennzahlen: angemeldet.query(async ({ ctx }) => + ctx.db(async (db) => { + const [mitglieder, fahrzeuge, geraete, pruefungFaellig] = await Promise.all([ + db.person.count({ where: { aktiv: true, geloeschtAm: null } }), + db.fahrzeug.count({ where: { aktiv: true, geloeschtAm: null } }), + db.geraet.count({ where: { geloeschtAm: null } }), + db.geraet.count({ where: { status: "PRUEFUNG_FAELLIG", geloeschtAm: null } }), + ]); + return { mitglieder, fahrzeuge, geraete, pruefungFaellig }; + }), + ), +}); + +const auditRouter = router({ + /** Die letzten Einträge im Protokoll — nur mit dem entsprechenden Recht. */ + letzte: mitRecht("system.auditlog") + .input(z.object({ anzahl: z.number().int().min(1).max(200).default(50) }).optional()) + .query(async ({ ctx, input }) => + ctx.db((db) => + db.auditLog.findMany({ + orderBy: { zeitpunkt: "desc" }, + take: input?.anzahl ?? 50, + include: { + akteur: { select: { id: true, vorname: true, nachname: true } }, + }, + }), + ), + ), +}); + +export const appRouter = router({ + system: systemRouter, + sitzung: sitzungRouter, + organisation: organisationRouter, + audit: auditRouter, +}); + +export type AppRouter = typeof appRouter; diff --git a/src/server/trpc.ts b/src/server/trpc.ts new file mode 100644 index 0000000..5c4ff06 --- /dev/null +++ b/src/server/trpc.ts @@ -0,0 +1,99 @@ +import { initTRPC, TRPCError } from "@trpc/server"; +import superjson from "superjson"; +import { ZodError } from "zod"; +import type { Kontext } from "@/server/context"; +import { KeinZugriffFehler, hatRecht, type Recht } from "@/lib/authz"; +import { fuerOrganisation } from "@/lib/db"; + +/** + * tRPC-Grundgerüst. + * + * Drei Arten von Prozeduren: + * oeffentlich — ohne Anmeldung erreichbar (Health, Anmeldestatus) + * angemeldet — verlangt eine Sitzung; stellt db bereit, der bereits auf + * die Wehr des Angemeldeten eingeschränkt ist + * mitRecht(...) — zusätzlich ein bestimmtes Recht aus lib/authz.ts + * + * Wichtig: In den Prozeduren steht ctx.db für den mandantengebundenen Zugriff. + * Wer prismaSystem direkt benutzt, umgeht die Trennung — das soll auffallen. + */ + +const t = initTRPC.context().create({ + // superjson erhaelt Date, BigInt und Decimal auf dem Weg zum Browser. + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + // Feldfehler aus Zod direkt an die Oberfläche durchreichen. + zodFehler: + error.cause instanceof ZodError ? error.cause.flatten().fieldErrors : null, + }, + }; + }, +}); + +export const router = t.router; +export const middleware = t.middleware; +export const createCallerFactory = t.createCallerFactory; + +/** Ohne Anmeldung erreichbar. */ +export const oeffentlich = t.procedure; + +/** Übersetzt KeinZugriffFehler in ein sauberes tRPC-FORBIDDEN. */ +const fehlerUebersetzen = middleware(async ({ next }) => { + try { + return await next(); + } catch (fehler) { + if (fehler instanceof KeinZugriffFehler) { + throw new TRPCError({ code: "FORBIDDEN", message: fehler.message, cause: fehler }); + } + throw fehler; + } +}); + +/** Verlangt eine Sitzung und stellt den mandantengebundenen Zugriff bereit. */ +const angemeldetMiddleware = middleware(async ({ ctx, next }) => { + if (!ctx.sitzung) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Bitte zuerst anmelden.", + }); + } + + const sitzung = ctx.sitzung; + + return next({ + ctx: { + ...ctx, + sitzung, + /** + * Führt eine Abfrage im Mandantenkontext der Sitzung aus. + * @example const personen = await ctx.db((db) => db.person.findMany()); + */ + db: (fn: Parameters>[1]) => + fuerOrganisation(sitzung.organisationId, fn), + }, + }); +}); + +export const angemeldet = t.procedure.use(fehlerUebersetzen).use(angemeldetMiddleware); + +/** + * Prozedur, die ein bestimmtes Recht verlangt. + * @example export const geraeteAendern = mitRecht("geraet.bearbeiten").mutation(...) + */ +export function mitRecht(recht: Recht) { + return angemeldet.use( + middleware(async ({ ctx, next }) => { + if (!hatRecht(ctx.sitzung, recht)) { + throw new TRPCError({ + code: "FORBIDDEN", + message: `Für diesen Vorgang fehlt die Berechtigung "${recht}".`, + }); + } + return next({ ctx }); + }), + ); +} diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts new file mode 100644 index 0000000..19da6d3 --- /dev/null +++ b/src/types/next-auth.d.ts @@ -0,0 +1,38 @@ +import type { Rolle, Sitzung } from "@/lib/authz"; + +/** + * Erweitert die Typen von Auth.js um das, was FLORIAN zusätzlich in der + * Sitzung führt: Person, Wehr, Rollen und die Art der Anmeldung. + */ +declare module "next-auth" { + interface Session { + user: { + id: string; + name?: string | null; + email?: string | null; + image?: string | null; + organisationId: string; + rollen: Rolle[]; + anmeldeArt: Sitzung["anmeldeArt"]; + }; + } + + interface User { + organisationId?: string; + rollen?: Rolle[]; + anmeldeArt?: Sitzung["anmeldeArt"]; + } +} + +declare module "next-auth/jwt" { + interface JWT { + personId?: string; + organisationId?: string; + rollen?: Rolle[]; + anmeldeArt?: Sitzung["anmeldeArt"]; + /** Zeitpunkt, zu dem die Rollen zuletzt aus der Datenbank kamen. */ + geladenAm?: number; + } +} + +export {}; diff --git a/src/worker/index.ts b/src/worker/index.ts new file mode 100644 index 0000000..f3a1258 --- /dev/null +++ b/src/worker/index.ts @@ -0,0 +1,94 @@ +import { Worker, type Job } from "bullmq"; +import { env } from "@/lib/env"; +import { AUFTRAG, WARTESCHLANGE, redis, type AuftragsDaten } from "@/lib/warteschlange"; +import { verbindungenTrennen } from "@/lib/db"; + +/** + * Hintergrundprozess. + * + * Läuft als eigener Container neben der Anwendung und arbeitet die + * Warteschlange ab. In Phase 0 sind die Auftragsarten angelegt, aber fachlich + * noch leer — die Fristenberechnung kommt in Phase 2, die Erinnerungen in + * Phase 7. Was hier schon steht, ist das Gerüst: Aufnahme, Protokollierung, + * Fehlerbehandlung und ein sauberes Herunterfahren. + */ + +function protokoll(nachricht: string, ...weiteres: unknown[]): void { + console.info(`[worker] ${nachricht}`, ...weiteres); +} + +async function fristenPruefen(auftrag: Job): Promise<{ geprueft: number }> { + protokoll(`Fristen prüfen für Organisation ${auftrag.data.organisationId}`); + // Ab Phase 2: Geräte mit überschrittener Frist auf PRUEFUNG_FAELLIG setzen. + return { geprueft: 0 }; +} + +async function erinnerungSenden(auftrag: Job): Promise<{ versendet: number }> { + protokoll(`Erinnerungen für Organisation ${auftrag.data.organisationId}`); + // Ab Phase 7: Mail und Telegram über lib/integrations. + return { versendet: 0 }; +} + +async function pdfErzeugen(auftrag: Job): Promise<{ erzeugt: boolean }> { + protokoll(`PDF erzeugen für Organisation ${auftrag.data.organisationId}`); + // Ab Phase 2: HTML an Gotenberg, Ergebnis nach MinIO über lib/storage. + return { erzeugt: false }; +} + +async function aufbewahrungPruefen(auftrag: Job): Promise<{ entfernt: number }> { + protokoll(`Aufbewahrungsfristen prüfen für Organisation ${auftrag.data.organisationId}`); + // Ab Phase 8: Audit-Log exportieren und danach abräumen, + // ausgetretene Mitglieder nach PERSON_ANONYMISIERUNG_TAGE anonymisieren. + return { entfernt: 0 }; +} + +const bearbeiter: Record) => Promise> = { + [AUFTRAG.FRISTEN_PRUEFEN]: fristenPruefen, + [AUFTRAG.ERINNERUNG_SENDEN]: erinnerungSenden, + [AUFTRAG.PDF_ERZEUGEN]: pdfErzeugen, + [AUFTRAG.AUFBEWAHRUNG_PRUEFEN]: aufbewahrungPruefen, +}; + +const worker = new Worker( + WARTESCHLANGE, + async (auftrag) => { + const bearbeiten = bearbeiter[auftrag.name]; + if (!bearbeiten) { + throw new Error(`Unbekannte Auftragsart: ${auftrag.name}`); + } + const beginn = Date.now(); + const ergebnis = await bearbeiten(auftrag); + protokoll(`${auftrag.name} erledigt in ${Date.now() - beginn} ms`); + return ergebnis; + }, + { + connection: redis(), + concurrency: env.WORKER_CONCURRENCY, + }, +); + +worker.on("failed", (auftrag, fehler) => { + console.error( + `[worker] Auftrag ${auftrag?.name ?? "?"} (${auftrag?.id ?? "?"}) fehlgeschlagen:`, + fehler.message, + ); +}); + +worker.on("ready", () => { + protokoll( + `bereit — Warteschlange "${WARTESCHLANGE}", ${env.WORKER_CONCURRENCY} parallele Aufträge`, + ); +}); + +/** Laufende Aufträge zu Ende bringen, bevor der Prozess endet. */ +async function herunterfahren(signal: string): Promise { + protokoll(`${signal} empfangen — fahre herunter ...`); + await worker.close(); + await verbindungenTrennen(); + redis().disconnect(); + protokoll("beendet."); + process.exit(0); +} + +process.on("SIGTERM", () => void herunterfahren("SIGTERM")); +process.on("SIGINT", () => void herunterfahren("SIGINT")); diff --git a/tests/e2e/anmeldung.spec.ts b/tests/e2e/anmeldung.spec.ts new file mode 100644 index 0000000..9c401c8 --- /dev/null +++ b/tests/e2e/anmeldung.spec.ts @@ -0,0 +1,96 @@ +import { expect, test } from "@playwright/test"; + +/** + * Beispiel-E2E-Test für Phase 0. + * + * Geprüft wird der Weg, ohne den nichts anderes funktioniert: jemand ruft die + * Anwendung auf, wird zur Anmeldung geschickt, meldet sich an und landet auf + * der Übersicht seiner Wehr. + * + * Voraussetzung: Stack läuft, Datenbank ist migriert und geseedet. + * docker compose up -d && npm run db:migrate && npm run db:seed + */ + +const ADMIN_EMAIL = process.env.SEED_ADMIN_EMAIL ?? "admin@beispielwehr.de"; +const ADMIN_PASSWORT = process.env.SEED_ADMIN_PASSWORT ?? "Feuerwehr2026"; + +test.describe("Anmeldung", () => { + test("schickt nicht angemeldete Aufrufe zur Anmeldeseite", async ({ page }) => { + await page.goto("/"); + await expect(page).toHaveURL(/\/anmelden/); + await expect(page.getByRole("heading", { name: "FLORIAN" })).toBeVisible(); + }); + + test("weist eine falsche Kombination ab und nennt den Grund auf Deutsch", async ({ page }) => { + await page.goto("/anmelden"); + + await page.getByLabel("E-Mail-Adresse").fill(ADMIN_EMAIL); + await page.getByLabel("Passwort").fill("definitiv-das-falsche-passwort"); + await page.getByRole("button", { name: "Anmelden" }).click(); + + // Nicht ueber die Rolle "alert" suchen: Next.js haengt einen eigenen + // Routen-Ansager mit derselben Rolle in die Seite. + const meldung = page.getByTestId("anmeldefehler"); + await expect(meldung).toBeVisible(); + await expect(meldung).toContainText("stimmt nicht"); + // Und niemand ist hineingekommen. + await expect(page).toHaveURL(/\/anmelden/); + }); + + test("lässt den Administrator hinein und zeigt die Übersicht seiner Wehr", async ({ page }) => { + await page.goto("/anmelden"); + + await page.getByLabel("E-Mail-Adresse").fill(ADMIN_EMAIL); + await page.getByLabel("Passwort").fill(ADMIN_PASSWORT); + await page.getByRole("button", { name: "Anmelden" }).click(); + + await page.waitForURL("/"); + + // Die Wehr aus dem Seed. + await expect( + page.getByRole("heading", { name: "Freiwillige Feuerwehr Musterhausen" }), + ).toBeVisible(); + + // Die Kennzahlen aus dem Seed: 15 Mitglieder, 3 Fahrzeuge, 40 Geräte. + await expect(page.getByText("Aktive Mitglieder")).toBeVisible(); + await expect(page.getByText("Fahrzeuge", { exact: true }).first()).toBeVisible(); + await expect(page.getByText("Geräte", { exact: true }).first()).toBeVisible(); + + // Der Sicherheitshinweis muss stehen, solange nichts freigegeben ist. + await expect(page.getByRole("note")).toContainText("analoge Überwachung"); + }); + + test("zeigt die Navigation und markiert künftige Module als noch nicht verfügbar", async ({ + page, + }) => { + await page.goto("/anmelden"); + await page.getByLabel("E-Mail-Adresse").fill(ADMIN_EMAIL); + await page.getByLabel("Passwort").fill(ADMIN_PASSWORT); + await page.getByRole("button", { name: "Anmelden" }).click(); + await page.waitForURL("/"); + + const navigation = page.getByRole("navigation", { name: "Hauptnavigation" }); + await expect(navigation.getByRole("link", { name: "Übersicht" })).toBeVisible(); + + // Atemschutz kommt erst in Phase 3 — der Eintrag ist sichtbar, aber kein Link. + await expect(navigation.getByText("Atemschutz")).toBeVisible(); + await expect(navigation.getByRole("link", { name: "Atemschutz" })).toHaveCount(0); + }); +}); + +test.describe("Erreichbarkeit", () => { + test("meldet den Zustand der Randsysteme unter /api/health", async ({ request }) => { + const antwort = await request.get("/api/health"); + expect(antwort.status()).toBe(200); + + const daten = (await antwort.json()) as { + status: string; + dienste: { datenbank: boolean; speicher: boolean; warteschlange: boolean }; + }; + + expect(daten.status).toBe("ok"); + expect(daten.dienste.datenbank).toBe(true); + expect(daten.dienste.speicher).toBe(true); + expect(daten.dienste.warteschlange).toBe(true); + }); +}); diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..b39b2f1 --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,32 @@ +import "dotenv/config"; + +/** + * Wird vor jedem Testlauf ausgeführt. + * + * Setzt eine vollständige, aber unschädliche Umgebung, damit lib/env.ts + * durchläuft, ohne dass eine echte .env vorhanden sein muss. + * + * Ist eine .env vorhanden, wird sie oben geladen — dann laufen auch die Tests + * mit, die eine echte Datenbank brauchen (Mandantentrennung). Ohne .env greifen + * die Platzhalter, und diese Tests überspringen sich selbst. + */ + +// NODE_ENV ist in den Node-Typen schreibgeschützt, zur Laufzeit aber setzbar. +const umgebung = process.env as Record; + +umgebung.NODE_ENV = "test"; +umgebung.TZ = "Europe/Berlin"; +umgebung.SKIP_ENV_VALIDATION = "1"; + +umgebung.APP_URL ??= "http://localhost:3100"; +umgebung.DATABASE_URL ??= "postgresql://test:test@localhost:5442/test?schema=public"; +umgebung.DIRECT_DATABASE_URL ??= umgebung.DATABASE_URL; +umgebung.REDIS_URL ??= "redis://localhost:6389"; +umgebung.S3_ENDPOINT ??= "localhost"; +umgebung.S3_ACCESS_KEY ??= "test"; +umgebung.S3_SECRET_KEY ??= "test"; +umgebung.S3_BUCKET ??= "test"; +umgebung.AUTH_SECRET ??= "testgeheimnis_mindestens_32_zeichen_lang_ok"; +umgebung.KIOSK_DEVICE_SECRET ??= "testgeraetegeheimnis"; +// bcrypt mit weniger Runden — im Test zählt Tempo, nicht Härte. +umgebung.AUTH_BCRYPT_ROUNDS ??= "4"; diff --git a/tests/unit/authz.test.ts b/tests/unit/authz.test.ts new file mode 100644 index 0000000..d5e1db2 --- /dev/null +++ b/tests/unit/authz.test.ts @@ -0,0 +1,197 @@ +import { describe, expect, it } from "vitest"; +import { + ALLE_RECHTE, + KeinZugriffFehler, + ROLLEN, + ROLLEN_BEZEICHNUNG, + ROLLEN_RECHTE, + alsRolle, + darfPersonSehen, + gehoertZurOrganisation, + hatAlleRechte, + hatEinesDerRechte, + hatRecht, + rechteVon, + verlangeRecht, + type Sitzung, +} from "@/lib/authz"; + +/** + * Beispieltest für Phase 0. + * + * Geprüft wird das, was am teuersten schiefgeht: dass der Gerätewart nicht in + * die Kasse kommt, dass der Administrator wirklich alles darf, und dass das + * Terminal nur das Nötigste kann. + */ + +function sitzung(rollen: Sitzung["rollen"], organisationId = "org-1"): Sitzung { + return { + personId: "person-1", + organisationId, + name: "Testperson", + rollen, + anmeldeArt: "LOKAL", + }; +} + +describe("Rollenkatalog", () => { + it("hat für jede Rolle eine deutsche Bezeichnung", () => { + for (const rolle of ROLLEN) { + expect(ROLLEN_BEZEICHNUNG[rolle]).toBeTruthy(); + } + }); + + it("vergibt in ROLLEN_RECHTE nur Rechte, die es im Katalog gibt", () => { + for (const [rolle, rechte] of Object.entries(ROLLEN_RECHTE)) { + for (const recht of rechte) { + expect(ALLE_RECHTE, `${rolle} verweist auf unbekanntes Recht ${recht}`).toContain( + recht, + ); + } + } + }); +}); + +describe("hatRecht", () => { + it("gibt dem Administrator ausnahmslos alles", () => { + const admin = sitzung(["SUPERADMIN"]); + for (const recht of ALLE_RECHTE) { + expect(hatRecht(admin, recht)).toBe(true); + } + }); + + it("lässt den Gerätewart Geräte bearbeiten", () => { + expect(hatRecht(sitzung(["GERAETEWART"]), "geraet.bearbeiten")).toBe(true); + expect(hatRecht(sitzung(["GERAETEWART"]), "pruefung.durchfuehren")).toBe(true); + }); + + it("lässt den Gerätewart NICHT an die Kasse", () => { + expect(hatRecht(sitzung(["GERAETEWART"]), "kasse.bearbeiten")).toBe(false); + expect(hatRecht(sitzung(["GERAETEWART"]), "kasse.abschluss")).toBe(false); + }); + + it("lässt den Kassier nicht an Geräte und den Atemschutz", () => { + expect(hatRecht(sitzung(["KASSIER"]), "geraet.bearbeiten")).toBe(false); + expect(hatRecht(sitzung(["KASSIER"]), "atemschutz.ueberwachung")).toBe(false); + }); + + it("gibt dem einfachen Mitglied nur Lese- und eigene Rechte", () => { + const mitglied = sitzung(["MITGLIED"]); + expect(hatRecht(mitglied, "person.eigene")).toBe(true); + expect(hatRecht(mitglied, "geraet.lesen")).toBe(true); + expect(hatRecht(mitglied, "person.bearbeiten")).toBe(false); + expect(hatRecht(mitglied, "geraet.bearbeiten")).toBe(false); + }); + + it("beschränkt das Terminal auf Anmelden und Erfassen", () => { + const kiosk = sitzung(["KIOSK"]); + expect(hatRecht(kiosk, "kiosk.anmelden")).toBe(true); + expect(hatRecht(kiosk, "kiosk.erfassen")).toBe(true); + expect(hatRecht(kiosk, "person.bearbeiten")).toBe(false); + expect(hatRecht(kiosk, "system.einstellungen")).toBe(false); + expect(hatRecht(kiosk, "kasse.lesen")).toBe(false); + }); + + it("verweigert ohne Sitzung alles", () => { + for (const recht of ALLE_RECHTE) { + expect(hatRecht(null, recht)).toBe(false); + } + }); + + it("addiert die Rechte mehrerer Rollen", () => { + const doppelrolle = sitzung(["GERAETEWART", "KASSIER"]); + expect(hatRecht(doppelrolle, "geraet.bearbeiten")).toBe(true); + expect(hatRecht(doppelrolle, "kasse.bearbeiten")).toBe(true); + }); +}); + +describe("rechteVon", () => { + it("liefert für den Administrator den vollständigen Katalog", () => { + expect(rechteVon(["SUPERADMIN"]).size).toBe(ALLE_RECHTE.length); + }); + + it("liefert für das Terminal deutlich weniger als für den Kommandanten", () => { + expect(rechteVon(["KIOSK"]).size).toBeLessThan(rechteVon(["KOMMANDANT"]).size); + }); +}); + +describe("hatEinesDerRechte / hatAlleRechte", () => { + const geraetewart = sitzung(["GERAETEWART"]); + + it("genügt ein Treffer bei hatEinesDerRechte", () => { + expect(hatEinesDerRechte(geraetewart, ["kasse.bearbeiten", "geraet.bearbeiten"])).toBe( + true, + ); + }); + + it("verlangt hatAlleRechte jeden einzelnen Treffer", () => { + expect(hatAlleRechte(geraetewart, ["kasse.bearbeiten", "geraet.bearbeiten"])).toBe(false); + expect(hatAlleRechte(geraetewart, ["geraet.lesen", "geraet.bearbeiten"])).toBe(true); + }); +}); + +describe("verlangeRecht", () => { + it("lässt durch, wenn das Recht vorliegt", () => { + expect(() => verlangeRecht(sitzung(["KOMMANDANT"]), "person.bearbeiten")).not.toThrow(); + }); + + it("wirft KeinZugriffFehler, wenn das Recht fehlt", () => { + expect(() => verlangeRecht(sitzung(["MITGLIED"]), "person.bearbeiten")).toThrow( + KeinZugriffFehler, + ); + }); + + it("nennt im Fehler das fehlende Recht", () => { + try { + verlangeRecht(sitzung(["MITGLIED"]), "kasse.abschluss"); + expect.unreachable("hätte werfen müssen"); + } catch (fehler) { + expect(fehler).toBeInstanceOf(KeinZugriffFehler); + expect((fehler as Error).message).toContain("kasse.abschluss"); + } + }); +}); + +describe("Mandantentrennung", () => { + it("erkennt Datensätze der eigenen Wehr", () => { + expect(gehoertZurOrganisation(sitzung(["MITGLIED"], "org-1"), { organisationId: "org-1" })).toBe( + true, + ); + }); + + it("weist Datensätze einer fremden Wehr ab — auch dem Administrator", () => { + expect( + gehoertZurOrganisation(sitzung(["SUPERADMIN"], "org-1"), { organisationId: "org-2" }), + ).toBe(false); + }); + + it("weist null-Datensätze ab", () => { + expect(gehoertZurOrganisation(sitzung(["KOMMANDANT"]), null)).toBe(false); + }); +}); + +describe("darfPersonSehen", () => { + it("lässt jeden die eigenen Daten sehen, auch ohne person.lesen", () => { + const kiosk = sitzung(["KIOSK"]); + expect(hatRecht(kiosk, "person.lesen")).toBe(false); + expect(darfPersonSehen(kiosk, kiosk.personId)).toBe(true); + }); + + it("verlangt für fremde Personen das Recht person.lesen", () => { + expect(darfPersonSehen(sitzung(["KIOSK"]), "andere-person")).toBe(false); + expect(darfPersonSehen(sitzung(["MITGLIED"]), "andere-person")).toBe(true); + }); +}); + +describe("alsRolle", () => { + it("erkennt Rollen unabhängig von Groß- und Kleinschreibung", () => { + expect(alsRolle("kommandant")).toBe("KOMMANDANT"); + expect(alsRolle(" GeraeteWart ")).toBe("GERAETEWART"); + }); + + it("gibt bei Unsinn null zurück", () => { + expect(alsRolle("oberbrandmeister")).toBeNull(); + expect(alsRolle("")).toBeNull(); + expect(alsRolle(null)).toBeNull(); + }); +}); diff --git a/tests/unit/mandantentrennung.test.ts b/tests/unit/mandantentrennung.test.ts new file mode 100644 index 0000000..96ed678 --- /dev/null +++ b/tests/unit/mandantentrennung.test.ts @@ -0,0 +1,226 @@ +import { PrismaClient } from "@prisma/client"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +/** + * Prüft die Mandantentrennung dort, wo sie zählt: in der Datenbank. + * + * Der Test legt zwei Wehren an und stellt sicher, dass die Anwendungsrolle + * (florian_app, NOBYPASSRLS) nur die Wehr sieht, die per + * app.current_organisation gesetzt ist — und ohne gesetzten Wert gar nichts. + * + * Er braucht eine laufende Datenbank. Ist keine erreichbar, wird er + * übersprungen statt fehlzuschlagen, damit "npm run test" auch ohne Docker + * durchläuft. + */ + +const APP_URL = process.env.DATABASE_URL ?? ""; +const SYSTEM_URL = process.env.DIRECT_DATABASE_URL ?? APP_URL; + +const app = new PrismaClient({ datasources: { db: { url: APP_URL } } }); +const system = new PrismaClient({ datasources: { db: { url: SYSTEM_URL } } }); + +let erreichbar = false; +let orgA = ""; +let orgB = ""; + +const SLUG_A = "test-rls-wehr-a"; +const SLUG_B = "test-rls-wehr-b"; + +beforeAll(async () => { + try { + await system.$queryRaw`SELECT 1`; + erreichbar = true; + } catch { + return; + } + + const a = await system.organisation.upsert({ + where: { slug: SLUG_A }, + update: {}, + create: { slug: SLUG_A, name: "Testwehr A", kurzname: "TW A" }, + }); + const b = await system.organisation.upsert({ + where: { slug: SLUG_B }, + update: {}, + create: { slug: SLUG_B, name: "Testwehr B", kurzname: "TW B" }, + }); + + orgA = a.id; + orgB = b.id; + + await system.person.upsert({ + where: { organisationId_mitgliedsnummer: { organisationId: orgA, mitgliedsnummer: "T001" } }, + update: {}, + create: { + organisationId: orgA, + mitgliedsnummer: "T001", + vorname: "Anna", + nachname: "AusWehrA", + }, + }); + + await system.person.upsert({ + where: { organisationId_mitgliedsnummer: { organisationId: orgB, mitgliedsnummer: "T001" } }, + update: {}, + create: { + organisationId: orgB, + mitgliedsnummer: "T001", + vorname: "Bertram", + nachname: "AusWehrB", + }, + }); +}); + +afterAll(async () => { + if (erreichbar) { + // Aufräumen über die Systemverbindung. Der Schalter app.audit_bereinigung + // ist nötig, weil die Kaskade auch Audit-Einträge mitnimmt und die sonst + // vom Trigger geschützt sind. + await system.$transaction(async (tx) => { + await tx.$executeRaw`SELECT set_config('app.audit_bereinigung', 'an', true)`; + await tx.organisation.deleteMany({ where: { slug: { in: [SLUG_A, SLUG_B] } } }); + }); + } + await Promise.allSettled([app.$disconnect(), system.$disconnect()]); +}); + +/** Führt eine Abfrage über die Anwendungsrolle im Kontext einer Wehr aus. */ +async function imKontext( + organisationId: string | null, + fn: (db: PrismaClient) => Promise, +): Promise { + return app.$transaction(async (tx) => { + if (organisationId) { + await tx.$executeRaw`SELECT set_config('app.current_organisation', ${organisationId}, true)`; + } + return fn(tx as unknown as PrismaClient); + }); +} + +describe.runIf(process.env.DATABASE_URL)("Row Level Security", () => { + it("liefert ohne gesetzten Mandantenkontext keine Zeilen", async ({ skip }) => { + if (!erreichbar) return skip(); + + const personen = await imKontext(null, (db) => db.person.findMany()); + expect(personen).toHaveLength(0); + + const organisationen = await imKontext(null, (db) => db.organisation.findMany()); + expect(organisationen).toHaveLength(0); + }); + + it("zeigt im Kontext von Wehr A nur Personen aus Wehr A", async ({ skip }) => { + if (!erreichbar) return skip(); + + const personen = await imKontext(orgA, (db) => db.person.findMany()); + expect(personen.length).toBeGreaterThan(0); + expect(personen.every((person) => person.organisationId === orgA)).toBe(true); + expect(personen.some((person) => person.nachname === "AusWehrB")).toBe(false); + }); + + it("zeigt im Kontext von Wehr B nur Personen aus Wehr B", async ({ skip }) => { + if (!erreichbar) return skip(); + + const personen = await imKontext(orgB, (db) => db.person.findMany()); + expect(personen.every((person) => person.organisationId === orgB)).toBe(true); + expect(personen.some((person) => person.nachname === "AusWehrA")).toBe(false); + }); + + it("findet einen Datensatz der fremden Wehr auch bei gezielter Abfrage nicht", async ({ + skip, + }) => { + if (!erreichbar) return skip(); + + const fremde = await system.person.findFirst({ where: { organisationId: orgB } }); + expect(fremde).not.toBeNull(); + + // Dieselbe ID, aber im Kontext der anderen Wehr: RLS blendet sie aus. + const versuch = await imKontext(orgA, (db) => + db.person.findUnique({ where: { id: fremde!.id } }), + ); + expect(versuch).toBeNull(); + }); + + it("lässt keinen Datensatz für eine fremde Wehr anlegen", async ({ skip }) => { + if (!erreichbar) return skip(); + + await expect( + imKontext(orgA, (db) => + db.person.create({ + data: { + organisationId: orgB, + mitgliedsnummer: "T999", + vorname: "Schmuggel", + nachname: "Versuch", + }, + }), + ), + ).rejects.toThrow(); + }); + + it("hält den Audit-Log unveränderlich", async ({ skip }) => { + if (!erreichbar) return skip(); + + const eintrag = await system.auditLog.create({ + data: { + organisationId: orgA, + aktion: "ERSTELLT", + entitaet: "Test", + beschreibung: "Testeintrag für die Unveränderlichkeit", + }, + }); + + // Selbst über die Systemverbindung greift der Trigger. + await expect( + system.auditLog.update({ + where: { id: eintrag.id }, + data: { beschreibung: "nachträglich geändert" }, + }), + ).rejects.toThrow(); + + await expect(system.auditLog.delete({ where: { id: eintrag.id } })).rejects.toThrow(); + }); + + it("erlaubt das Löschen nur im gekennzeichneten Aufbewahrungslauf", async ({ skip }) => { + if (!erreichbar) return skip(); + + const eintrag = await system.auditLog.create({ + data: { + organisationId: orgA, + aktion: "ERSTELLT", + entitaet: "Test", + beschreibung: "Eintrag für den Aufbewahrungslauf", + }, + }); + + await system.$transaction(async (tx) => { + await tx.$executeRaw`SELECT set_config('app.audit_bereinigung', 'an', true)`; + await tx.auditLog.delete({ where: { id: eintrag.id } }); + }); + + const weg = await system.auditLog.findUnique({ where: { id: eintrag.id } }); + expect(weg).toBeNull(); + }); + + it("lässt auch im Aufbewahrungslauf kein UPDATE zu", async ({ skip }) => { + if (!erreichbar) return skip(); + + const eintrag = await system.auditLog.create({ + data: { + organisationId: orgA, + aktion: "ERSTELLT", + entitaet: "Test", + beschreibung: "Eintrag, der nicht verfälscht werden darf", + }, + }); + + await expect( + system.$transaction(async (tx) => { + await tx.$executeRaw`SELECT set_config('app.audit_bereinigung', 'an', true)`; + await tx.auditLog.update({ + where: { id: eintrag.id }, + data: { beschreibung: "verfälscht" }, + }); + }), + ).rejects.toThrow(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8903c37 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noUncheckedIndexedAccess": 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", "tests/e2e"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..be52978 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; +import tsconfigPaths from "vite-tsconfig-paths"; + +/** + * Unit- und Integrationstests. + * Die End-to-End-Tests laufen unter tests/e2e mit Playwright und sind hier + * ausgeschlossen — sonst würde Vitest versuchen, sie mitzunehmen. + */ +export default defineConfig({ + plugins: [react(), tsconfigPaths()], + test: { + environment: "node", + globals: true, + setupFiles: ["./tests/setup.ts"], + include: ["tests/**/*.test.{ts,tsx}"], + exclude: ["node_modules", ".next", "tests/e2e/**"], + coverage: { + provider: "v8", + reportsDirectory: "./coverage", + include: ["src/lib/**", "src/server/**"], + exclude: ["**/*.d.ts"], + }, + }, +});