From a943db9c5407cad63004b0e39269aa4a8470a855 Mon Sep 17 00:00:00 2001 From: Pepe Ziberi Date: Thu, 23 Jul 2026 12:19:01 +0200 Subject: [PATCH] docs(legal)+test: Compliance-Dokumentation & Consent-Unit-Tests (v1.7.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Teil 5 des Compliance-Updates: - docs/legal/: data-processing-inventory.md, subprocessors.md, technical-organizational-measures.md, retention-and-deletion.md, incident-response.md, licenses-and-attributions.md — Ist-Zustand aus der Analyse, offene Punkte als Platzhalter - Reine Consent-Regeln nach src/lib/legal-rules.ts ausgelagert (kein Prisma) + Texte nach src/lib/legal-content.ts (7 Pflichtbestätigungen) — für Testbarkeit - Vitest eingerichtet (vitest.config.ts, test-Script); 13 Tests grün: Pflicht-Dokumenttypen, Re-Consent bei neuer Version, normale Benutzer ≠ Org-Vertreter, 7 Org-Bestätigungen, Platzhalter-Erkennung, keine erfundenen Subprozessoren Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/legal/data-processing-inventory.md | 23 + docs/legal/incident-response.md | 44 + docs/legal/licenses-and-attributions.md | 45 + docs/legal/retention-and-deletion.md | 37 + docs/legal/subprocessors.md | 33 + .../technical-organizational-measures.md | 59 + package-lock.json | 1138 ++++++++++++++++- package.json | 7 +- src/components/legal/legal-consent-gate.tsx | 11 +- src/config/__tests__/legal-config.test.ts | 32 + src/lib/__tests__/legal-rules.test.ts | 63 + src/lib/legal-content.ts | 14 + src/lib/legal-rules.ts | 44 + src/lib/legal.ts | 39 +- vitest.config.ts | 14 + 15 files changed, 1534 insertions(+), 69 deletions(-) create mode 100644 docs/legal/data-processing-inventory.md create mode 100644 docs/legal/incident-response.md create mode 100644 docs/legal/licenses-and-attributions.md create mode 100644 docs/legal/retention-and-deletion.md create mode 100644 docs/legal/subprocessors.md create mode 100644 docs/legal/technical-organizational-measures.md create mode 100644 src/config/__tests__/legal-config.test.ts create mode 100644 src/lib/__tests__/legal-rules.test.ts create mode 100644 src/lib/legal-content.ts create mode 100644 src/lib/legal-rules.ts create mode 100644 vitest.config.ts diff --git a/docs/legal/data-processing-inventory.md b/docs/legal/data-processing-inventory.md new file mode 100644 index 0000000..46c55c9 --- /dev/null +++ b/docs/legal/data-processing-inventory.md @@ -0,0 +1,23 @@ +# Dateninventar / Verzeichnis der Bearbeitungstätigkeiten — Lageplan + +Stand: 2026-07-23 · sorgfältiger Entwurf, ersetzt keine juristische Prüfung. +Abgeleitet aus dem tatsächlichen Code-Stand. Offene Fristen/Standorte als Platzhalter `[…]`. + +| Datenkategorie | Zweck | Betroffene | Herkunft | Speicherort | Empfänger/Drittanbieter | Auslandbekanntgabe | Aufbewahrung | Löschprozess | Schutzmassnahmen | +|---|---|---|---|---|---|---|---|---|---| +| Kontodaten (Name, E-Mail, Rolle) | Authentifizierung, Verwaltung | Benutzer | Eingabe bei Registrierung | PostgreSQL | — | `[SERVERSTANDORT]` | bis Kontolöschung | Selfservice/Antrag; Backups bis Zyklusende | Rollen/Mandanten, TLS, Zugriffskontrolle | +| Passwort (bcrypt-Hash) | Authentifizierung | Benutzer | Eingabe | PostgreSQL | — | `[SERVERSTANDORT]` | bis Kontolöschung | mit Konto | Einweg-Hash (bcrypt cost 12) | +| Einsatz-/Inhaltsdaten (Pläne, Journale, Zeichnungen, Koordinaten) | Kernfunktion | Org-Mitglieder, ggf. Dritte | Eingabe | PostgreSQL | — | `[SERVERSTANDORT]` | bis Löschung durch Org | Projekt-/Orglöschung; Backups | Mandantentrennung, Zugriffskontrolle | +| Hochgeladene Dateien (Logos, Planbilder, Symbole) | Darstellung | Org | Upload | MinIO | — | `[SERVERSTANDORT]` | bis Löschung | mit Projekt/Org | Zugriffsschutz, Validierung | +| Zustimmungsnachweise (Doc-Typ/Version/Zeitpunkt/Kontext) | Nachweis Einwilligung/Vertrag | Benutzer | System | PostgreSQL | — | `[SERVERSTANDORT]` | Aufbewahrung zu Nachweiszwecken `[FRIST]` | revisionssicher, nicht überschrieben | minimale Daten (keine roh-IP) | +| Protokoll-/Sicherheitsdaten | Betrieb, Sicherheit | Benutzer | System | Server-Logs | — | `[SERVERSTANDORT]` | `[FRIST]` | rotierend | Minimierung sensibler Inhalte | +| Session-Cookie `auth-token` | Anmeldung | Benutzer | System | Client-Cookie | — | — | 24 h / 14 Tage | Ablauf/Logout | httpOnly, secure (prod), sameSite=lax | +| E-Mail-Versand (Verifizierung, Reset, Kontakt, Einladung) | Kommunikation | Benutzer/Empfänger | System/Eingabe | SMTP-Anbieter | E-Mail-Anbieter | `[ABHÄNGIG]` | beim Anbieter | — | TLS, nur bei Bedarf | +| Unterstützungsbeiträge (Betrag, Name/Nachricht, payment_intent) | Zahlungsabwicklung | Spender | Eingabe/Stripe | Stripe + `SystemSetting` | Stripe | ja (möglich) | `[FRIST]` | `[PROZESS]` | Zahlungsdaten nur bei Stripe | +| Karten-/Geo-Anfragen | Karten/Adresse/Wetter | Benutzer | Client | (extern) | OSM/Esri/swisstopo/Nominatim/Open-Meteo | möglich | keine Speicherung durch App | — | nur Kachel/Koordinaten | +| localStorage (Zeichnungen, Sync-Queue, Theme, Onboarding, letztes Projekt, Cookie-Hinweis) | Funktion/Offline | Benutzer | Client | Gerät des Nutzers | — | — | bis lokal gelöscht | Browser | verbleibt lokal | + +## Offene Entscheidungen + +- Konkrete Aufbewahrungsfristen (Logs, Zustimmungen, Zahlungsdaten) → siehe `retention-and-deletion.md`. +- Serverstandorte bestätigen. diff --git a/docs/legal/incident-response.md b/docs/legal/incident-response.md new file mode 100644 index 0000000..a0c451f --- /dev/null +++ b/docs/legal/incident-response.md @@ -0,0 +1,44 @@ +# Incident-Response — Lageplan + +Stand: 2026-07-23 · sorgfältiger Entwurf. **Manueller Entscheidungsprozess** — es erfolgt KEINE +automatische Meldung an Behörden. Meldepflichten werden im Einzelfall geprüft. + +## Kontakte (auszufüllen) +- Verantwortlich (Betreiber): `[NAME]` · `[KONTAKT]` +- Datenschutzkontakt: siehe `src/config/legal.ts` (`privacyContactEmail`) +- Technischer Notfallkontakt: `[NAME/TELEFON]` +- Zuständige Aufsichtsbehörde (CH): EDÖB — `[LINK/KONTAKT]` + +## Ablauf + +1. **Erkennung** — Quelle (Monitoring, Meldung, Nutzerhinweis), Zeitpunkt und Symptome festhalten. +2. **Interne Eskalation** — Verantwortliche informieren; Vorfall im Vorfallprotokoll eröffnen. +3. **Eindämmung** — betroffene Systeme/Zugänge isolieren, ggf. Dienste temporär sperren. +4. **Beweissicherung** — Logs/Artefakte sichern, bevor Änderungen erfolgen. +5. **Bewertung betroffener Daten** — welche Datenkategorien/Personen sind betroffen? +6. **Risikobewertung** — Eintritts-/Schadenswahrscheinlichkeit für Betroffene. +7. **Information betroffener Organisationen** — ohne unangemessene Verzögerung, sachlich. +8. **Prüfung Behördenmeldung** — Meldepflicht (z.B. EDÖB) im Einzelfall prüfen und **manuell** entscheiden/dokumentieren. +9. **Passwort-/Schlüsselrotation** — Secrets, Tokens, ggf. Nutzer-Passwörter rotieren. +10. **Wiederherstellung** — aus sauberem Stand/Backup; Integrität prüfen. +11. **Nachbearbeitung** — Ursachenanalyse, Massnahmen, Lessons Learned. + +## Vorfallprotokoll (Vorlage) +| Feld | Inhalt | +|---|---| +| Vorfall-ID / Datum | | +| Entdeckt durch / wann | | +| Beschreibung | | +| Betroffene Daten/Personen | | +| Sofortmassnahmen | | +| Risikobewertung | | +| Information an Organisationen (wann/wie) | | +| Behördenmeldung (ja/nein, Begründung) | | +| Rotation durchgeführt | | +| Wiederherstellung | | +| Abschluss / Massnahmen | | + +## Vom Betreiber zu treffende Entscheidungen +- Verbindliche Kontaktliste und Erreichbarkeit. +- Melde-Schwellen und interne Fristen. +- Aufbewahrung des Vorfallprotokolls. diff --git a/docs/legal/licenses-and-attributions.md b/docs/legal/licenses-and-attributions.md new file mode 100644 index 0000000..3c8f599 --- /dev/null +++ b/docs/legal/licenses-and-attributions.md @@ -0,0 +1,45 @@ +# Lizenzen und Attributionen — Lageplan + +Stand: 2026-07-23 · sorgfältiger Entwurf. Eine Quellenangabe ersetzt NICHT automatisch eine +erforderliche Nutzungslizenz. Offene Freigabefragen sind markiert. + +## Karten & Geodaten +| Quelle | Nutzung | Lizenz/Bedingungen | Attribution | Offene Frage | +|---|---|---|---|---| +| OpenStreetMap | Basiskarte, Nominatim | ODbL (Daten) | "© OpenStreetMap-Mitwirkende" (vorhanden im Impressum/Karte) | Tile-Usage-Policy einhalten (Fair Use) | +| Esri World Imagery | Satellitenlayer | Esri-Nutzungsbedingungen | "Esri World Imagery" | ⚠️ kommerzielle/Nutzungsbedingungen prüfen | +| swisstopo (geo.admin.ch) | Landeskarte | swisstopo-Bedingungen | "© swisstopo" empfohlen | ⚠️ Attribution/Bedingungen bestätigen | +| Open-Meteo | Wind/Höhe | Open-Meteo Terms (frei, Attribution empfohlen) | "Open-Meteo" | Attribution ergänzen? | +| Open-Elevation | Höhe (Fallback) | offen/community | — | ⚠️ Bedingungen prüfen | + +## Symbole / Icons +| Quelle | Nutzung | Lizenz | Attribution | Offene Frage | +|---|---|---|---|---| +| FKS/BABS taktische Zeichen | Einsatzsymbole | `[NUTZUNGSRECHT ZU KLÄREN]` | "Bildquelle Feuerwehr Koordination Schweiz FKS" (vorhanden) | ⚠️ **Nutzungsrecht/Freigabe FKS bestätigen** | +| lucide-react | UI-Icons | ISC | — (im Code) | ok | + +## Schriftarten +| Quelle | Lizenz | Hinweis | +|---|---|---| +| Barlow (Google Fonts, via next/font) | SIL Open Font License 1.1 | build-time self-hosted, kein Laufzeit-Request zu Google | + +## Wesentliche Bibliotheken (Auszug) +| Paket | Lizenz | +|---|---| +| next / react | MIT | +| maplibre-gl | BSD-3-Clause | +| jspdf | MIT (+ Teile Apache-2.0) | +| qrcode | MIT | +| stripe (SDK) | MIT | +| bcryptjs, jose, zod, socket.io | MIT | +| @prisma/client, prisma | Apache-2.0 | + +## Bereits umgesetzte sichtbare Attributionen +- Karten-Attribution "© OpenStreetMap contributors | MapLibre" auf der Karte. +- "Taktische Symbole: Bildquelle Feuerwehr Koordination Schweiz FKS" im Footer. +- Kartendaten-Hinweis im Impressum (OpenStreetMap, Nominatim). + +## Offene Freigabefragen (Betreiber) +1. **FKS/BABS-Symbole**: Nutzungsrecht/Freigabe für die konkrete Verwendung schriftlich bestätigen. +2. Esri/swisstopo-Nutzungsbedingungen für den vorgesehenen (auch nicht-kommerziellen) Einsatz prüfen. +3. Vollständige Lizenzliste (z.B. `license-checker`) generieren und Drittlizenzen bündeln, falls verteilt. diff --git a/docs/legal/retention-and-deletion.md b/docs/legal/retention-and-deletion.md new file mode 100644 index 0000000..7a2f283 --- /dev/null +++ b/docs/legal/retention-and-deletion.md @@ -0,0 +1,37 @@ +# Aufbewahrungs- und Löschkonzept — Lageplan + +Stand: 2026-07-23 · sorgfältiger Entwurf. **Keine Fristen erfunden** — offene Fristen als `[FRIST — zu +entscheiden]`. Der Betreiber muss die markierten Entscheidungen treffen. + +| Datenkategorie | Aufbewahrung | Löschung / Prozess | +|---|---|---| +| Aktive Benutzerkonten | solange Konto besteht | Selfservice/Antrag; kaskadierende Löschung zugehöriger Daten | +| Deaktivierte Konten (`isActive=false`) | `[FRIST]` | `[PROZESS — automatische Löschung nach X?]` | +| Organisationen (Tenant) | solange aktiv | Löschung durch Org-Admin (Einstellungen) nach Berechtigungsprüfung | +| Pläne / Projekte | bis Löschung durch Org | mit Projekt-/Orglöschung (Cascade) | +| Dateiuploads (MinIO) | bis Löschung des Bezugsobjekts | mit Projekt/Org/Icon | +| Audit-/Aktionslogs (falls geführt) | `[FRIST]` | `[PROZESS]` | +| Sicherheits-/Server-Logs | `[FRIST]` | Rotation `[KONFIG]` | +| Einladungen | `[FRIST]` | nach Annahme/Ablauf | +| Passwort-Reset-Tokens | Ablauf (`resetTokenExpiry`) | mit Verwendung/Ablauf | +| E-Mail-Verifizierungs-Tokens | bis Verifizierung | mit Verifizierung | +| E-Mails (beim SMTP-Anbieter) | beim Anbieter | `[ABHÄNGIG]` | +| Zahlungsinformationen (Stripe + `SystemSetting/donations`) | `[FRIST]` (ggf. gesetzliche Aufbewahrung) | `[PROZESS]`; Zahlungsdaten liegen bei Stripe | +| Supportanfragen (Kontaktformular) | `[FRIST]` | `[PROZESS]` | +| Backups | `[BACKUP-AUFBEWAHRUNGSFRIST]` | mit Ablauf des Backup-Zyklus | +| Zustimmungshistorie (LegalAcceptance) | zu Nachweiszwecken `[FRIST]` | revisionssicher, i.d.R. nicht gelöscht solange Konto besteht | + +## Grundsätze +- Produktivdaten werden nach Abschluss des Löschvorgangs entfernt. +- Daten in Sicherungskopien können bis zum Ablauf des regulären Backup-Zyklus fortbestehen. +- Organisationsrelevante Daten werden nicht ohne Berechtigungsprüfung sofort gelöscht. +- Betroffenenrechte (Auskunft, Berichtigung, Löschung, Export) über `/konto/datenschutz` bzw. + Datenschutzkontakt; Eingang wird bestätigt. + +## Vom Betreiber zu treffende Entscheidungen +1. Aufbewahrungsfrist deaktivierter Konten und automatischer Löschlauf? +2. Log-Aufbewahrung (Server-/Sicherheitslogs) und Rotation? +3. Aufbewahrung Zahlungs-/Spendendaten (gesetzliche Pflichten?). +4. Backup-Aufbewahrungsfrist (`LEGAL_BACKUP_RETENTION`). +5. Aufbewahrung Zustimmungsnachweise nach Kontolöschung. +6. Support-/Kontaktanfragen-Frist. diff --git a/docs/legal/subprocessors.md b/docs/legal/subprocessors.md new file mode 100644 index 0000000..fa68768 --- /dev/null +++ b/docs/legal/subprocessors.md @@ -0,0 +1,33 @@ +# Unterauftragnehmer / Subprozessoren — Lageplan + +Stand: 2026-07-23 · sorgfältiger Entwurf, ersetzt keine juristische Prüfung. + +Nur tatsächlich im Code verwendete bzw. optional konfigurierbare Dienste. Nicht verwendete Dienste +sind NICHT aufgeführt. Regionen/Verträge, die nicht sicher bekannt sind, tragen Platzhalter `[…]`. + +| Anbieter | Zweck | Verarbeitete Daten | Sitz / Region | Datenschutzhinweis | Auslandbearbeitung | Vertragsstatus | +|---|---|---|---|---|---|---| +| OpenStreetMap Foundation (Tiles) | Basiskarte | Kachel-Koordinaten, IP (Client) | `[REGION — UK/EU]` | osmfoundation.org/wiki/Privacy_Policy | möglich | `[ZU PRÜFEN]` | +| Esri (ArcGIS World Imagery) | Satellitenbilder | Kachel-Koordinaten, IP (Client) | `[REGION — USA/global]` | esri.com/legal | wahrscheinlich | `[ZU PRÜFEN]` | +| swisstopo (geo.admin.ch) | Schweizer Landeskarte | Kachel-Koordinaten, IP (Client) | Schweiz | geo.admin.ch | nein | `[ZU PRÜFEN]` | +| Nominatim (OSM) | Adress-/Reverse-Geocoding | Suchbegriff/GPS, IP (Client) | `[REGION — OSMF]` | osmfoundation.org | möglich | `[ZU PRÜFEN]` | +| Open-Meteo | Wind-/Höhendaten | Koordinaten, IP (Client) | `[REGION — EU/DE]` | open-meteo.com/en/terms | möglich | `[ZU PRÜFEN]` | +| Open-Elevation | Höhendaten (Fallback) | Koordinaten, IP (Client) | `[REGION]` | open-elevation.com | möglich | `[ZU PRÜFEN]` | +| Stripe | Zahlungsabwicklung (freiwillige Beiträge) | Betrag, freiwilliger Name/Nachricht, Zahlungsdaten (direkt bei Stripe) | `[REGION — USA/EU]` | stripe.com/privacy | ja (USA möglich) | `[DPA PRÜFEN]` · nur wenn konfiguriert | +| SMTP-/E-Mail-Anbieter (konfigurierbar) | System-E-Mails | Empfängeradresse, Mailinhalt | `[ABHÄNGIG VOM ANBIETER]` | `[ANBIETER]` | abhängig | `[ZU PRÜFEN]` · nur wenn konfiguriert | +| MinIO (Objektspeicher, selbst gehostet) | Datei-/Bild-Uploads | Pläne, Logos, Symbole | `[SERVERSTANDORT]` | — (Eigenbetrieb) | `[ZU BESTÄTIGEN]` | Eigenbetrieb | +| PostgreSQL (Datenbank, selbst gehostet) | Primäre Datenhaltung | Konto-, Org-, Plan-, Journaldaten | `[SERVERSTANDORT]` | — (Eigenbetrieb) | `[ZU BESTÄTIGEN]` | Eigenbetrieb | + +## Hinweise + +- **Client-seitige Dienste** (Tiles, Nominatim, Open-Meteo/Elevation): werden direkt aus dem Browser + des Nutzers aufgerufen; dabei kann die IP-Adresse an den Anbieter gelangen. +- **Stripe** und **SMTP** sind nur aktiv, wenn in den Systemeinstellungen (DB `SystemSetting`) konfiguriert. +- **MapTiler / Mapbox / Google Maps**: NICHT genutzt (MapTiler nur in der CSP-Allowlist vorbereitet). +- **Analytics / Error-Tracking (Sentry, GA, Plausible, PostHog)**: NICHT vorhanden. + +## Offene Entscheidungen des Betreibers + +- Serverstandort für PostgreSQL, MinIO, Backups und Logs abschliessend bestätigen. +- DPA/AVV mit Stripe und dem gewählten E-Mail-Anbieter abschliessen und Status hier eintragen. +- Verarbeitungsregionen der Kartendienste konkretisieren. diff --git a/docs/legal/technical-organizational-measures.md b/docs/legal/technical-organizational-measures.md new file mode 100644 index 0000000..187954f --- /dev/null +++ b/docs/legal/technical-organizational-measures.md @@ -0,0 +1,59 @@ +# Technische und organisatorische Massnahmen (TOM) — Lageplan + +Stand: 2026-07-23 · sorgfältiger Entwurf. Ist-Zustand aus dem Code; offene Punkte klar markiert. +Legende: ✅ vorhanden · ⚠️ teilweise / zu bestätigen · ❌ offen / nicht umgesetzt. + +## Zugriff & Berechtigungen +- ✅ Mandantentrennung je Organisation (Tenant), `getProjectWithTenantCheck`. +- ✅ Rollenkonzept: SERVER_ADMIN, TENANT_ADMIN, OPERATOR, VIEWER. +- ⚠️ MFA für System-/Org-Admins: ❌ nicht vorhanden → **Empfehlung offen**. +- ✅ Admin-APIs serverseitig mit `isAdmin`/`isServerAdmin` geschützt. + +## Authentifizierung & Sessions +- ✅ Passwort-Hashing: bcrypt (cost 12), nie Klartext. +- ✅ JWT-Session (jose, HS256), Cookie `auth-token` httpOnly, secure (prod), sameSite=lax, 24 h/14 Tage. +- ✅ Passwort-Reset mit kryptographischem Token (32 Bytes), Ablauf. +- ✅ E-Mail-Verifizierung. + +## Transport & Web-Härtung +- ✅ HTTPS/TLS; HSTS (max-age 2 Jahre, preload). +- ✅ Sicherheits-Header: CSP (mit `frame-ancestors 'self'`), X-Content-Type-Options nosniff, + Referrer-Policy, Permissions-Policy, COOP, X-XSS-Protection. +- ⚠️ CSP enthält `unsafe-inline`/`unsafe-eval` (für MapLibre gelockert) → langfristig verschärfen. +- ✅ Private Routen (/app,/admin,/settings,/konto): `X-Robots-Tag: noindex` + `Cache-Control: no-store` (Middleware). +- ⚠️ Socket.io-Server (`server-custom.js`): CORS `origin:'*'` → **einschränken empfohlen**. + +## Eingaben & Uploads +- ✅ Rate-Limiting: Login, Registrierung, Passwort-Reset, Verifizierung, Kontakt, Konto-Löschung, Share-PIN. +- ⚠️ Rate-Limiter ist In-Memory (pro Instanz) → bei Mehr-Instanz-Betrieb geteilten Store nutzen. +- ⚠️ Upload-Validierung (MIME/Grösse): vorhanden für Icons/Logos/Planbilder → **Umfang bestätigen**. +- ✅ Zugriffsschutz auf Dateien (serve-Routen mit Tenant-Check). + +## Speicherung & Verschlüsselung +- ✅ Übertragung verschlüsselt (TLS). +- ⚠️ Verschlüsselung ruhender Daten (DB/MinIO/Backups): `[ZU BESTÄTIGEN]`. +- ⚠️ Secrets-Verwaltung: ENV (`NEXTAUTH_SECRET`, MinIO) + DB (`SystemSetting`: SMTP/Stripe) → **Absicherung der DB-Secrets bestätigen**. + +## Backup & Wiederherstellung +- ⚠️ Backups: `[VORHANDEN? FREQUENZ? AUFBEWAHRUNG?]` (siehe `docs/backup-und-seeding.md`). +- ❌ Restore-Tests: `[DOKUMENTIEREN]`. + +## Logging & Monitoring +- ✅ Server-Logs (console). ⚠️ Vermeidung sensibler Daten in Logs → **Review empfohlen**. +- ❌ Zentrales Monitoring/Alerting: `[OFFEN]`. + +## Betrieb & Prozesse +- ✅ Idempotente Migrationen (`prisma/migrate.js`) beim Container-Start. +- ⚠️ Dependency-/Patch-Management: `[PROZESS DOKUMENTIEREN]`. +- ⚠️ Trennung Entwicklung/Test/Produktion: `[BESTÄTIGEN]`; Umgang mit Testdaten `[BESTÄTIGEN]`. +- ✅ Löschkonzept/Datenexport: siehe `retention-and-deletion.md`; Betroffenenrechte via `/konto/datenschutz`. +- ✅ Berechtigungsentzug: Benutzer sperren (`isActive`), aus Organisation entfernen. + +## Direkt umgesetzte risikoarme Verbesserungen (dieses Update) +- Private Routen auf `noindex`/`no-store` gesetzt. +- Rechtliche Prüf-Hinweise in Exporten und Berechnungen. +- Revisionssichere Zustimmungsprotokollierung ohne roh-IP. + +## Empfohlene, noch offene Massnahmen +- MFA für Admins; CSP verschärfen; Socket.io-CORS einschränken; geteilter Rate-Limit-Store; + Verschlüsselung ruhender Daten + Backups bestätigen; Restore-Tests; Monitoring. diff --git a/package-lock.json b/package-lock.json index b4fcacc..27acfbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lageplan", - "version": "1.3.2", + "version": "1.7.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lageplan", - "version": "1.3.2", + "version": "1.7.3", "hasInstallScript": true, "dependencies": { "@dnd-kit/core": "^6.1.0", @@ -71,7 +71,8 @@ "sharp": "^0.34.5", "tailwindcss": "^3.4.1", "tsx": "^4.7.0", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^4.1.10" } }, "node_modules/@alloc/quick-lru": { @@ -192,21 +193,21 @@ "license": "MIT" }, "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, @@ -215,9 +216,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -1623,6 +1624,16 @@ "node": ">=12.4.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@prisma/client": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", @@ -3046,6 +3057,289 @@ "@react-pdf/stylesheet": "^6.1.2" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -3094,6 +3388,13 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "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==", + "dev": true, + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.18", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", @@ -3104,9 +3405,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "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, @@ -3121,6 +3422,17 @@ "dev": true, "license": "MIT" }, + "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/cors": { "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", @@ -3130,6 +3442,20 @@ "@types/node": "*" } }, + "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/geojson": { "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", @@ -3782,6 +4108,119 @@ "win32" ] }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@zxing/text-encoding": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", @@ -4089,6 +4528,16 @@ "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", @@ -4510,6 +4959,16 @@ "node": ">=10.0.0" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4657,6 +5116,13 @@ "dev": true, "license": "MIT" }, + "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/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -5178,6 +5644,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -5753,6 +6226,16 @@ "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", @@ -5772,6 +6255,16 @@ "node": ">=0.8.x" } }, + "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/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7251,6 +7744,267 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "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.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "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-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "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-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "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-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "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-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "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/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -7340,6 +8094,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, + "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/maplibre-gl": { "version": "4.7.1", "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz", @@ -7513,9 +8277,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -7847,6 +8611,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -8004,6 +8782,13 @@ "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==", + "dev": true, + "license": "MIT" + }, "node_modules/pbf": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", @@ -8082,9 +8867,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", "dev": true, "funding": [ { @@ -8102,7 +8887,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -8842,6 +9627,40 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9190,6 +10009,13 @@ "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/simple-swizzle": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", @@ -9280,6 +10106,13 @@ "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/stackblur-canvas": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", @@ -9290,6 +10123,13 @@ "node": ">=0.1.14" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "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", @@ -9757,15 +10597,32 @@ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", "license": "MIT" }, + "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.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "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.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -9775,9 +10632,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -9793,6 +10650,16 @@ "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", "license": "ISC" }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10212,6 +11079,84 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "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", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.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 + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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-compatible-readable-stream": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz", @@ -10226,6 +11171,122 @@ "node": ">= 6" } }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vt-pbf": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", @@ -10359,6 +11420,23 @@ "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", diff --git a/package.json b/package.json index 1a63c56..9071565 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lageplan", - "version": "1.7.3", + "version": "1.7.4", "description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation", "private": true, "scripts": { @@ -8,6 +8,8 @@ "build": "next build", "start": "next start", "lint": "next lint", + "test": "vitest run", + "test:watch": "vitest", "db:generate": "prisma generate", "db:push": "prisma db push", "db:migrate": "prisma migrate dev", @@ -88,7 +90,8 @@ "sharp": "^0.34.5", "tailwindcss": "^3.4.1", "tsx": "^4.7.0", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^4.1.10" }, "browserslist": [ "last 2 Chrome versions", diff --git a/src/components/legal/legal-consent-gate.tsx b/src/components/legal/legal-consent-gate.tsx index c747f73..f94b9e2 100644 --- a/src/components/legal/legal-consent-gate.tsx +++ b/src/components/legal/legal-consent-gate.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from 'react' import { usePathname } from 'next/navigation' import { useAuth } from '@/components/providers/auth-provider' import { Loader2, ExternalLink } from 'lucide-react' +import { ORG_CONFIRMATIONS } from '@/lib/legal-content' interface PendingDoc { type: string; version: string; title: string; url: string | null } @@ -15,16 +16,6 @@ const ALLOWLIST = [ '/konto', '/forgot-password', '/reset-password', ] -const ORG_CONFIRMATIONS = [ - 'Ich bestätige, dass ich berechtigt bin, für diese Organisation ein Administratorkonto einzurichten oder diese Bestätigung abzugeben.', - 'Unsere Organisation versteht, dass Lageplan ein unterstützendes Werkzeug und kein Alarmierungs-, Leitstellen- oder Einsatzleitsystem ist.', - 'Unsere Organisation prüft Lagepläne, Adressen, Symbole, Distanzen, Berechnungen und sonstige Inhalte vor der operativen Verwendung durch fachkundige Personen.', - 'Unsere Organisation hält unabhängig von Lageplan aktuelle Offline-, Notfall- und Rückfallunterlagen bereit.', - 'Unsere Organisation verwendet Lageplan nicht als einzige Informationsquelle für operative Entscheidungen.', - 'Unsere Organisation speichert ohne separate schriftliche Vereinbarung keine Patienten-, Gesundheits-, Polizei-, Passwort-, Zugangs-, Schlüsselcode- oder anderen besonders schützenswerten Daten in Lageplan.', - 'Ich habe die Nutzungsbedingungen und die Datenschutzerklärung gelesen und akzeptiere die für die Nutzung geltenden Bestimmungen.', -] - export function LegalConsentGate() { const { user, loading, logout } = useAuth() const pathname = usePathname() diff --git a/src/config/__tests__/legal-config.test.ts b/src/config/__tests__/legal-config.test.ts new file mode 100644 index 0000000..2487f0a --- /dev/null +++ b/src/config/__tests__/legal-config.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest' +import { isPlaceholder, legal } from '@/config/legal' + +describe('isPlaceholder', () => { + it('erkennt Platzhalter im Format [NAME]', () => { + expect(isPlaceholder('[POSTADRESSE]')).toBe(true) + expect(isPlaceholder('[RECHTLICHER NAME DES BETREIBERS]')).toBe(true) + }) + it('erkennt echte Werte als Nicht-Platzhalter', () => { + expect(isPlaceholder('Musterstrasse 1, 8000 Zürich')).toBe(false) + expect(isPlaceholder('app@lageplan.ch')).toBe(false) + }) + it('leere Werte gelten als Platzhalter (fehlend)', () => { + expect(isPlaceholder('')).toBe(true) + expect(isPlaceholder(null)).toBe(true) + expect(isPlaceholder(undefined)).toBe(true) + }) +}) + +describe('legal config', () => { + it('führt genau 5 Rechtsdokumenttypen', () => { + expect(Object.keys(legal.documents).sort()).toEqual( + ['dataProcessingAgreement', 'organizationDeclaration', 'privacy', 'responsibleUse', 'terms'] + ) + }) + it('listet nur real genutzte Subprozessoren (keine erfundenen wie Google Analytics)', () => { + const names = legal.processors.map(p => p.name.toLowerCase()) + expect(names.some(n => n.includes('stripe'))).toBe(true) + expect(names.some(n => n.includes('analytics'))).toBe(false) + expect(names.some(n => n.includes('mapbox'))).toBe(false) + }) +}) diff --git a/src/lib/__tests__/legal-rules.test.ts b/src/lib/__tests__/legal-rules.test.ts new file mode 100644 index 0000000..098c40e --- /dev/null +++ b/src/lib/__tests__/legal-rules.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from 'vitest' +import { requiredTypesFor, computePendingDocuments, REQUIRED_FOR_ALL, REQUIRED_FOR_ORG_ADMIN } from '@/lib/legal-rules' +import { ORG_CONFIRMATIONS } from '@/lib/legal-content' + +const active = [ + { type: 'TERMS', version: '1.0.0' }, + { type: 'PRIVACY', version: '1.0.0' }, + { type: 'ORGANIZATION_DECLARATION', version: '1.0.0' }, +] + +describe('requiredTypesFor', () => { + it('verlangt für alle Benutzer TERMS + PRIVACY', () => { + const req = requiredTypesFor(false) + expect(req.has('TERMS')).toBe(true) + expect(req.has('PRIVACY')).toBe(true) + expect(req.has('ORGANIZATION_DECLARATION')).toBe(false) + }) + + it('verlangt für Org-Admins zusätzlich die Organisationsbestätigung', () => { + const req = requiredTypesFor(true) + expect(req.has('ORGANIZATION_DECLARATION')).toBe(true) + }) +}) + +describe('computePendingDocuments', () => { + it('meldet alle Pflichtdokumente offen, wenn nichts akzeptiert wurde', () => { + const pending = computePendingDocuments(active, requiredTypesFor(true), new Set()) + expect(pending.map(d => d.type).sort()).toEqual(['ORGANIZATION_DECLARATION', 'PRIVACY', 'TERMS']) + }) + + it('meldet nichts offen, wenn aktive Versionen akzeptiert wurden', () => { + const accepted = new Set(['TERMS@1.0.0', 'PRIVACY@1.0.0', 'ORGANIZATION_DECLARATION@1.0.0']) + const pending = computePendingDocuments(active, requiredTypesFor(true), accepted) + expect(pending).toHaveLength(0) + }) + + it('löst Re-Consent aus, wenn eine neue Version veröffentlicht wird', () => { + // Nutzer hat 1.0.0 akzeptiert, aktiv ist jetzt 2.0.0 + const activeV2 = [{ type: 'TERMS', version: '2.0.0' }, { type: 'PRIVACY', version: '1.0.0' }] + const accepted = new Set(['TERMS@1.0.0', 'PRIVACY@1.0.0']) + const pending = computePendingDocuments(activeV2, requiredTypesFor(false), accepted) + expect(pending.map(d => d.type)).toEqual(['TERMS']) + expect(pending[0].version).toBe('2.0.0') + }) + + it('normale Benutzer werden nicht als Org-Vertreter behandelt (keine Org-Bestätigung verlangt)', () => { + const accepted = new Set(['TERMS@1.0.0', 'PRIVACY@1.0.0']) + const pending = computePendingDocuments(active, requiredTypesFor(false), accepted) + expect(pending).toHaveLength(0) + }) +}) + +describe('Konstanten & Inhalte', () => { + it('REQUIRED-Mengen sind korrekt', () => { + expect(REQUIRED_FOR_ALL).toEqual(['TERMS', 'PRIVACY']) + expect(REQUIRED_FOR_ORG_ADMIN).toEqual(['ORGANIZATION_DECLARATION']) + }) + + it('Organisationsbestätigung hat genau 7 Pflichtaussagen', () => { + expect(ORG_CONFIRMATIONS).toHaveLength(7) + expect(ORG_CONFIRMATIONS.every(s => s.trim().length > 0)).toBe(true) + }) +}) diff --git a/src/lib/legal-content.ts b/src/lib/legal-content.ts new file mode 100644 index 0000000..371ead3 --- /dev/null +++ b/src/lib/legal-content.ts @@ -0,0 +1,14 @@ +/** + * Reine Textinhalte für den Consent-Flow (ohne UI-Abhängigkeiten, damit testbar). + */ + +/** Die 7 einzeln zu bestätigenden Pflichtaussagen der Organisationsbestätigung. */ +export const ORG_CONFIRMATIONS: string[] = [ + 'Ich bestätige, dass ich berechtigt bin, für diese Organisation ein Administratorkonto einzurichten oder diese Bestätigung abzugeben.', + 'Unsere Organisation versteht, dass Lageplan ein unterstützendes Werkzeug und kein Alarmierungs-, Leitstellen- oder Einsatzleitsystem ist.', + 'Unsere Organisation prüft Lagepläne, Adressen, Symbole, Distanzen, Berechnungen und sonstige Inhalte vor der operativen Verwendung durch fachkundige Personen.', + 'Unsere Organisation hält unabhängig von Lageplan aktuelle Offline-, Notfall- und Rückfallunterlagen bereit.', + 'Unsere Organisation verwendet Lageplan nicht als einzige Informationsquelle für operative Entscheidungen.', + 'Unsere Organisation speichert ohne separate schriftliche Vereinbarung keine Patienten-, Gesundheits-, Polizei-, Passwort-, Zugangs-, Schlüsselcode- oder anderen besonders schützenswerten Daten in Lageplan.', + 'Ich habe die Nutzungsbedingungen und die Datenschutzerklärung gelesen und akzeptiere die für die Nutzung geltenden Bestimmungen.', +] diff --git a/src/lib/legal-rules.ts b/src/lib/legal-rules.ts new file mode 100644 index 0000000..c548ec5 --- /dev/null +++ b/src/lib/legal-rules.ts @@ -0,0 +1,44 @@ +/** + * Reine Consent-Regeln OHNE Laufzeit-Abhängigkeiten (kein Prisma) — damit unit-testbar. + */ + +export type LegalDocumentType = + | 'TERMS' + | 'PRIVACY' + | 'ORGANIZATION_DECLARATION' + | 'RESPONSIBLE_USE' + | 'DATA_PROCESSING_AGREEMENT' + +export type LegalAcceptanceContext = + | 'REGISTRATION' + | 'LOGIN_RECONSENT' + | 'ORGANIZATION_CREATION' + | 'ADMIN_ROLE_ACCEPTANCE' + | 'DONATION' + +/** Für ALLE Benutzer erforderliche Dokumente. */ +export const REQUIRED_FOR_ALL: LegalDocumentType[] = ['TERMS', 'PRIVACY'] + +/** Zusätzlich für Organisationsadministratoren erforderlich. */ +export const REQUIRED_FOR_ORG_ADMIN: LegalDocumentType[] = ['ORGANIZATION_DECLARATION'] + +/** Pflicht-Dokumenttypen für einen Benutzer. */ +export function requiredTypesFor(isOrgAdmin: boolean): Set { + return new Set([...REQUIRED_FOR_ALL, ...(isOrgAdmin ? REQUIRED_FOR_ORG_ADMIN : [])]) +} + +/** + * Kernregel: welche aktiven Pflichtdokumente sind für den Benutzer noch offen? + * @param active aktive Dokumente (eine Version je Typ) + * @param required Menge erforderlicher Dokumenttypen + * @param acceptedKeys bereits akzeptierte Schlüssel `TYPE@version` + */ +export function computePendingDocuments( + active: T[], + required: Set, + acceptedKeys: Set, +): T[] { + return active + .filter(d => required.has(d.type)) + .filter(d => !acceptedKeys.has(`${d.type}@${d.version}`)) +} diff --git a/src/lib/legal.ts b/src/lib/legal.ts index f7d836b..23a2900 100644 --- a/src/lib/legal.ts +++ b/src/lib/legal.ts @@ -1,26 +1,14 @@ import { prisma } from '@/lib/db' +import { + REQUIRED_FOR_ALL, REQUIRED_FOR_ORG_ADMIN, requiredTypesFor, computePendingDocuments, + type LegalDocumentType, type LegalAcceptanceContext, +} from '@/lib/legal-rules' -/** Dokumenttypen (mit Prisma-String-Feldern konsistent). */ -export type LegalDocumentType = - | 'TERMS' - | 'PRIVACY' - | 'ORGANIZATION_DECLARATION' - | 'RESPONSIBLE_USE' - | 'DATA_PROCESSING_AGREEMENT' - -/** Zustimmungskontexte. */ -export type LegalAcceptanceContext = - | 'REGISTRATION' - | 'LOGIN_RECONSENT' - | 'ORGANIZATION_CREATION' - | 'ADMIN_ROLE_ACCEPTANCE' - | 'DONATION' - -/** Für ALLE Benutzer erforderliche Dokumente. */ -export const REQUIRED_FOR_ALL: LegalDocumentType[] = ['TERMS', 'PRIVACY'] - -/** Zusätzlich für Organisationsadministratoren erforderlich. */ -export const REQUIRED_FOR_ORG_ADMIN: LegalDocumentType[] = ['ORGANIZATION_DECLARATION'] +// Re-Export für bestehende Importe +export { + REQUIRED_FOR_ALL, REQUIRED_FOR_ORG_ADMIN, requiredTypesFor, computePendingDocuments, +} +export type { LegalDocumentType, LegalAcceptanceContext } export interface ActiveDocument { type: string @@ -52,10 +40,7 @@ export async function getActiveDocument(type: LegalDocumentType): Promise { - const required = new Set([ - ...REQUIRED_FOR_ALL, - ...(isOrgAdmin ? REQUIRED_FOR_ORG_ADMIN : []), - ]) + const required = requiredTypesFor(isOrgAdmin) const active = (await getActiveDocuments()).filter(d => required.has(d.type)) if (active.length === 0) return [] @@ -66,8 +51,8 @@ export async function getPendingAcceptances(userId: string, isOrgAdmin: boolean) }, select: { documentType: true, documentVersion: true }, }) - const acceptedKeys = new Set(accepted.map((a: any) => `${a.documentType}@${a.documentVersion}`)) - return active.filter(d => !acceptedKeys.has(`${d.type}@${d.version}`)) + const acceptedKeys = new Set(accepted.map((a: any) => `${a.documentType}@${a.documentVersion}`)) + return computePendingDocuments(active, required, acceptedKeys) } /** True, wenn der Benutzer alle erforderlichen aktiven Dokumente akzeptiert hat. */ diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..36d6b71 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config' +import path from 'path' + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.{test,spec}.{ts,tsx}'], + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +})